Merge pull request #950 from pikasTech/fix/issue934-message-self-trim

fix: trim message-only folded progress
This commit is contained in:
Lyon
2026-06-05 23:13:11 +08:00
committed by GitHub
2 changed files with 89 additions and 2 deletions
+72
View File
@@ -1934,6 +1934,78 @@ test("cloud api trims folded progress from historical multi-turn final response
}
});
test("cloud api trims folded progress when only conversation messages preserve the clean final text (#934)", async () => {
const nowValue = "2026-06-05T15:08:00.000Z";
const projectId = "prj_hwpod_workbench";
const conversationId = "cnv_issue934_message_only_history";
const sessionId = "ses_issue934_message_only_history";
const firstTraceId = "trc_issue934_message_only_first";
const secondTraceId = "trc_issue934_message_only_second";
const threadId = "thread-issue-934-message-only";
const finalText = "当前工作区没有 `.hwlab/hwpod-spec.yaml`,也没有 `.hwlab` 目录或现成的 hwpod-spec 文件。这意味着这个 AgentRun 会话还没有绑定具体的 HWPOD。\n\n目前可用的 HWPOD**0 个**(需要先创建 spec 并绑定)。\n\n如果你有已知的 HWPOD 信息(例如节点 ID、target 或 spec 模板),可以给我,我可以帮你生成 `.hwlab/hwpod-spec.yaml`。";
const progressText = "Let me check the available HWPODs using the `hwpod` CLI tool.当前工作区还没有 `.hwlab/hwpod-spec.yaml` 文件,所以 `hwpod inspect` 无法获取可用 HWPOD 信息。 让我看看仓库里有没有现成的 spec 示例或相关配置:";
const accessController = createAccessController({
env: { HWLAB_ACCESS_CONTROL_REQUIRED: "1", HWLAB_BOOTSTRAP_ADMIN_PASSWORD: "admin-pass" },
now: () => nowValue
});
const server = createCloudApiServer({ env: { HWLAB_ACCESS_CONTROL_REQUIRED: "1" }, accessController, now: () => nowValue });
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const login = await postJson(port, "/auth/login", { username: "admin", password: "admin-pass" });
assert.equal(login.status, 200);
const ownerUserId = login.body.actor.id;
accessController.store.agentSessions.set(sessionId, {
id: sessionId,
projectId,
agentId: "hwlab-code-agent",
status: "completed",
startedAt: nowValue,
endedAt: null,
ownerUserId,
conversationId,
threadId,
lastTraceId: firstTraceId,
updatedAt: nowValue,
session: {
source: "legacy-message-only-progress-polluted-snapshot",
sessionStatus: "completed",
lastTraceId: firstTraceId,
messages: [
{ id: "msg_issue934_message_only_first_user", role: "user", title: "用户", text: "你好", status: "sent", traceId: firstTraceId, conversationId, sessionId, threadId, createdAt: nowValue },
{ id: "msg_issue934_message_only_first_agent", role: "agent", title: "Code Agent 回复", text: finalText, status: "completed", traceId: firstTraceId, conversationId, sessionId, threadId, createdAt: nowValue },
{ id: "msg_issue934_message_only_second_user", role: "user", title: "用户", text: "看看有几个hwpod可用?", status: "sent", traceId: secondTraceId, conversationId, sessionId, threadId, createdAt: nowValue },
{ id: "msg_issue934_message_only_second_agent", role: "agent", title: "Code Agent 回复", text: `${progressText}${finalText}`, status: "completed", traceId: secondTraceId, conversationId, sessionId, threadId, createdAt: nowValue }
],
messageCount: 4,
firstUserMessagePreview: "你好",
valuesRedacted: true,
secretMaterialStored: false
}
});
const direct = await getJson(port, `/v1/agent/conversations/${conversationId}?projectId=${projectId}`, login.cookie);
assert.equal(direct.status, 200);
assert.equal(direct.body.conversation.messages.length, 4);
assert.equal(direct.body.conversation.messages[1].text, finalText);
assert.equal(direct.body.conversation.messages[3].text, finalText);
assert.equal(direct.body.conversation.messages[3].traceId, secondTraceId);
assert.doesNotMatch(direct.body.conversation.messages[3].text, /Let me check||/u);
const list = await getJson(port, `/v1/agent/conversations?projectId=${projectId}`, login.cookie);
assert.equal(list.status, 200);
const listed = list.body.conversations.find((conversation) => conversation.conversationId === conversationId);
assert.ok(listed, "expected message-only legacy conversation to be listed");
assert.equal(listed.messages[3].text, finalText);
assert.doesNotMatch(listed.messages[3].text, /Let me check||/u);
} finally {
await new Promise((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
}
});
test("access controller restores AgentRun mapping by Code Agent traceId", async () => {
const accessController = createAccessController({ now: () => "2026-06-01T00:00:00.000Z" });
await accessController.recordAgentSessionOwner({
+17 -2
View File
@@ -1866,7 +1866,7 @@ function mergeAgentSessionOwnerEvidence(nextValue, existingValue) {
} else if (existingMessages?.length) {
merged.messages = existingMessages;
}
merged.messages = normalizeFinalResponseConversationMessages(merged.messages, merged.finalResponse);
merged.messages = normalizeFoldedProgressConversationMessages(normalizeFinalResponseConversationMessages(merged.messages, merged.finalResponse));
const existingChatMessages = Array.isArray(existing.chatMessages) ? existing.chatMessages : null;
const nextChatMessages = Array.isArray(next.chatMessages) ? next.chatMessages : null;
if (existingChatMessages?.length && (!nextChatMessages || nextChatMessages.length === 0)) merged.chatMessages = existingChatMessages;
@@ -1911,6 +1911,21 @@ function isFoldedProgressBeforeFinalResponse(messageText, finalText) {
if (prefix.length < 20) return false;
return /\bLet me (?:check|inspect|look)\b|\bI(?:'|)?ll (?:check|inspect|look)\b|(?:||)?(?:|||)|(?:|||)|(?:||)||hwpod inspect/iu.test(prefix);
}
function normalizeFoldedProgressConversationMessages(messages) {
if (!Array.isArray(messages) || messages.length < 2) return messages;
const cleanAgentTexts = [];
return messages.map((message) => {
if (message?.role !== "agent") return message;
const text = textOr(message.text, "");
const replacementText = cleanAgentTexts.find((candidate) => isFoldedProgressBeforeFinalResponse(text, candidate));
const normalized = replacementText ? redactConversationMessage({ ...message, text: replacementText }) : message;
const normalizedText = textOr(normalized?.text, "");
if (normalizedText.length >= 80 && !cleanAgentTexts.includes(normalizedText) && !cleanAgentTexts.some((candidate) => isFoldedProgressBeforeFinalResponse(normalizedText, candidate))) {
cleanAgentTexts.push(normalizedText);
}
return normalized;
}).filter(Boolean);
}
function mergeAgentSessionOwnerMessages(nextMessages = [], existingMessages = [], context = {}) {
const next = dedupeConversationMessages(nextMessages);
if (next.some((message) => message.role === "user")) return ensureTerminalUserMessage(next, context).slice(-50);
@@ -2380,7 +2395,7 @@ function publicAgentConversation(session) {
now: session.updatedAt,
firstUserPreview: snapshot.firstUserMessagePreview
}));
const displayMessages = normalizeFinalResponseConversationMessages(messages, snapshot.finalResponse);
const displayMessages = normalizeFoldedProgressConversationMessages(normalizeFinalResponseConversationMessages(messages, snapshot.finalResponse));
const status = resolvedConversationStatus(session.status, snapshot, displayMessages);
const lastTraceId = resolvedConversationLastTraceId(session.lastTraceId, status, displayMessages, snapshot);
return {