From 7637a43699396dcae945a41d560a3a7e6653cb2c Mon Sep 17 00:00:00 2001 From: Codex Agent Date: Fri, 5 Jun 2026 22:21:10 +0800 Subject: [PATCH] fix: preserve code agent final response text --- internal/cloud/access-control.test.ts | 196 ++++++++++++++++++ internal/cloud/access-control.ts | 24 ++- .../src/state/trace-renderer.test.ts | 21 ++ 3 files changed, 237 insertions(+), 4 deletions(-) diff --git a/internal/cloud/access-control.test.ts b/internal/cloud/access-control.test.ts index f9ef1902..5361b741 100644 --- a/internal/cloud/access-control.test.ts +++ b/internal/cloud/access-control.test.ts @@ -1664,6 +1664,202 @@ test("access controller dedupes duplicate refreshed assistant messages by id and assert.equal(conversation.messages[1].text, "最终回复"); }); +test("access controller keeps final response separate from progress assistant text (#934)", async () => { + const projectId = "prj_hwpod_workbench"; + const ownerUserId = "usr_issue934_owner_merge"; + const conversationId = "cnv_issue934_owner_merge"; + const sessionId = "ses_issue934_owner_merge"; + const traceId = "trc_issue934_owner_merge"; + const threadId = "thread-issue-934-owner-merge"; + const nowValue = "2026-06-05T13:39:48.000Z"; + const userText = "看看有几个hwpod可用?"; + const progressText = "Let me check the available HWPODs using the `hwpod` CLI tool."; + const middleText = "当前工作区还没有 `.hwlab/hwpod-spec.yaml` 文件,所以 `hwpod inspect` 无法获取可用 HWPOD 信息。"; + const finalText = "当前工作区没有 `.hwlab/hwpod-spec.yaml`,也没有 `.hwlab` 目录或现成的 hwpod-spec 文件。这意味着这个 AgentRun 会话还没有绑定具体的 HWPOD。\n\n目前可用的 HWPOD:**0 个**(需要先创建 spec 并绑定)。"; + const accessController = createAccessController({ now: () => nowValue }); + + await accessController.recordAgentSessionOwner({ + ownerUserId, + sessionId, + projectId, + agentId: "hwlab-code-agent", + status: "active", + conversationId, + threadId, + traceId, + session: { + source: "workbench-status-sync", + sessionStatus: "running", + lastTraceId: traceId, + messages: [ + { id: "msg_issue934_user", role: "user", title: "用户", text: userText, status: "sent", traceId, conversationId, sessionId, threadId, createdAt: nowValue }, + { id: "msg_issue934_agent_pending", role: "agent", title: "Code Agent 处理中", text: `${progressText}${middleText}`, status: "running", traceId, conversationId, sessionId, threadId, createdAt: nowValue } + ], + messageCount: 2, + firstUserMessagePreview: userText, + valuesRedacted: true, + secretMaterialStored: false + } + }); + + await accessController.recordAgentSessionOwner({ + ownerUserId, + sessionId, + projectId, + agentId: "hwlab-code-agent", + status: "completed", + conversationId, + threadId, + traceId, + session: { + source: "code-agent-result", + status: "completed", + sessionStatus: "completed", + lastTraceId: traceId, + finalResponse: { + text: finalText, + textChars: finalText.length, + role: "assistant", + status: "completed", + traceId, + valuesPrinted: false + }, + messages: [ + { id: "msg_issue934_agent_result", role: "agent", title: "Code Agent 回复", text: finalText, status: "completed", traceId, conversationId, sessionId, threadId, createdAt: nowValue } + ], + messageCount: 1, + firstUserMessagePreview: userText, + valuesRedacted: true, + secretMaterialStored: false + } + }); + + await accessController.recordAgentSessionOwner({ + ownerUserId, + sessionId, + projectId, + agentId: "hwlab-code-agent", + status: "completed", + conversationId, + threadId, + traceId, + session: { + source: "cloud-web-react-account-sync", + sessionStatus: "completed", + lastTraceId: traceId, + messages: [ + { id: "msg_issue934_user", role: "user", title: "用户", text: userText, status: "sent", traceId, conversationId, sessionId, threadId, createdAt: nowValue }, + { id: "msg_issue934_agent_pending", role: "agent", title: "Code Agent 回复", text: `${progressText}${middleText}${finalText}`, status: "completed", traceId, conversationId, sessionId, threadId, createdAt: nowValue } + ], + messageCount: 2, + firstUserMessagePreview: userText, + valuesRedacted: true, + secretMaterialStored: false + } + }); + + const session = await accessController.getAgentSession(sessionId); + const conversation = await accessController.visibleConversationForActor({ id: ownerUserId, role: "user" }, conversationId, projectId, { includeArchived: true }); + assert.deepEqual(session.session.messages.map((message) => message.role), ["user", "agent"]); + assert.deepEqual(conversation.messages.map((message) => message.role), ["user", "agent"]); + assert.equal(session.session.finalResponse.text, finalText); + assert.equal(session.session.messages[1].text, finalText); + assert.equal(conversation.messages[1].text, finalText); + assert.doesNotMatch(conversation.messages[1].text, /Let me check the available HWPODs/u); + assert.doesNotMatch(conversation.messages[1].text, /当前工作区还没有/u); + assert.equal(conversation.messageCount, 2); + assert.equal(conversation.firstUserMessagePreview, userText); +}); + +test("cloud api exposes final response for legacy progress-polluted snapshots (#934)", async () => { + const nowValue = "2026-06-05T13:39:48.000Z"; + const projectId = "prj_hwpod_workbench"; + const conversationId = "cnv_issue934_legacy_snapshot"; + const sessionId = "ses_issue934_legacy_snapshot"; + const traceId = "trc_issue934_legacy_final"; + const staleLastTraceId = "trc_issue934_legacy_greeting"; + const threadId = "thread-issue-934-legacy"; + const userText = "看看有几个hwpod可用?"; + const progressText = "Let me check the available HWPODs using the hwpod CLI tool."; + const middleText = "当前工作区还没有 .hwlab/hwpod-spec.yaml 文件,所以 hwpod inspect 无法获取可用 HWPOD 信息。"; + const finalText = "当前工作区没有 .hwlab/hwpod-spec.yaml,也没有 .hwlab 目录或现成的 hwpod-spec 文件。\n\n目前可用的 HWPOD:0 个。"; + 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: staleLastTraceId, + updatedAt: nowValue, + session: { + source: "legacy-progress-polluted-snapshot", + sessionStatus: "completed", + lastTraceId: staleLastTraceId, + finalResponse: { + text: finalText, + textChars: finalText.length, + role: "assistant", + status: "completed", + traceId, + valuesPrinted: false + }, + messages: [ + { id: "msg_issue934_legacy_user", role: "user", title: "用户", text: userText, status: "sent", traceId, conversationId, sessionId, threadId, createdAt: nowValue }, + { id: "msg_issue934_legacy_agent", role: "agent", title: "Code Agent 回复", text: `${progressText}${middleText}${finalText}`, status: "completed", traceId, conversationId, sessionId, threadId, createdAt: nowValue } + ], + messageCount: 2, + firstUserMessagePreview: userText, + valuesRedacted: true, + secretMaterialStored: false + } + }); + + 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 legacy conversation to be listed"); + assert.equal(listed.messages[1].text, finalText); + assert.doesNotMatch(listed.messages[1].text, /Let me check|当前工作区还没有/u); + + const direct = await getJson(port, `/v1/agent/conversations/${conversationId}?projectId=${projectId}`, login.cookie); + assert.equal(direct.status, 200); + assert.equal(direct.body.conversation.messages[1].text, finalText); + assert.doesNotMatch(direct.body.conversation.messages[1].text, /Let me check|当前工作区还没有/u); + + const workspace = await getJson(port, `/v1/workbench/workspace?projectId=${projectId}`, login.cookie); + assert.equal(workspace.status, 200); + const selected = await postJson(port, `/v1/workbench/workspace/${workspace.body.workspace.workspaceId}/select-conversation`, { + projectId, + conversationId, + sessionId, + updatedByClient: "test-suite" + }, login.cookie); + assert.equal(selected.status, 200); + assert.equal(selected.body.workspace.selectedConversation.messages[1].text, finalText); + assert.doesNotMatch(selected.body.workspace.selectedConversation.messages[1].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({ diff --git a/internal/cloud/access-control.ts b/internal/cloud/access-control.ts index a8fcd8d4..95384401 100644 --- a/internal/cloud/access-control.ts +++ b/internal/cloud/access-control.ts @@ -1866,6 +1866,7 @@ function mergeAgentSessionOwnerEvidence(nextValue, existingValue) { } else if (existingMessages?.length) { merged.messages = existingMessages; } + merged.messages = 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; @@ -1884,6 +1885,20 @@ function mergeAgentSessionOwnerEvidence(nextValue, existingValue) { } return merged; } +function normalizeFinalResponseConversationMessages(messages, finalResponse) { + const finalText = textOr(finalResponse?.text, ""); + const traceId = safeTraceIdLocal(finalResponse?.traceId); + if (!Array.isArray(messages) || !finalText || !traceId) return messages; + return messages.map((message) => { + if (message?.role !== "agent" || safeTraceIdLocal(message.traceId) !== traceId) return message; + return redactConversationMessage({ + ...message, + text: finalText, + status: textOr(finalResponse.status, message.status), + updatedAt: textOr(finalResponse.updatedAt ?? finalResponse.createdAt, message.updatedAt) + }); + }).filter(Boolean); +} function mergeAgentSessionOwnerMessages(nextMessages = [], existingMessages = [], context = {}) { const next = dedupeConversationMessages(nextMessages); if (next.some((message) => message.role === "user")) return ensureTerminalUserMessage(next, context).slice(-50); @@ -2353,8 +2368,9 @@ function publicAgentConversation(session) { now: session.updatedAt, firstUserPreview: snapshot.firstUserMessagePreview })); - const status = resolvedConversationStatus(session.status, snapshot, messages); - const lastTraceId = resolvedConversationLastTraceId(session.lastTraceId, status, messages, snapshot); + const displayMessages = normalizeFinalResponseConversationMessages(messages, snapshot.finalResponse); + const status = resolvedConversationStatus(session.status, snapshot, displayMessages); + const lastTraceId = resolvedConversationLastTraceId(session.lastTraceId, status, displayMessages, snapshot); return { conversationId: session.conversationId, sessionId: session.id, @@ -2368,8 +2384,8 @@ function publicAgentConversation(session) { startedAt: session.startedAt, endedAt: session.endedAt, session: pruneEmpty({ sessionId: session.id, threadId: session.threadId, status }), - messages, - messageCount: messages.length, + messages: displayMessages, + messageCount: displayMessages.length, firstUserMessagePreview: textOr(snapshot.firstUserMessagePreview, null), snapshot: pruneEmpty({ sessionStatus: snapshot.sessionStatus, diff --git a/web/hwlab-cloud-web/src/state/trace-renderer.test.ts b/web/hwlab-cloud-web/src/state/trace-renderer.test.ts index 4253ee6d..7c433d8f 100644 --- a/web/hwlab-cloud-web/src/state/trace-renderer.test.ts +++ b/web/hwlab-cloud-web/src/state/trace-renderer.test.ts @@ -45,6 +45,27 @@ test("web trace rows keep every assistant message and mark only the last one ter assert.doesNotMatch(JSON.stringify(rows), /backend-turn-finished|short-connection polling/u); }); +test("web trace rows do not fold progress assistant text into the final response (#934)", () => { + const events: Record[] = [ + { seq: 1, label: "agentrun:request:accepted", status: "accepted", createdAt: "2026-06-05T13:39:36.000Z" }, + { seq: 10, label: "agentrun:assistant:message", type: "assistant", status: "running", messageIndex: 1, messageCount: 3, message: "Let me check the available HWPODs using the hwpod CLI tool.", createdAt: "2026-06-05T13:39:38.000Z" }, + { seq: 20, label: "agentrun:assistant:message", type: "assistant", status: "running", messageIndex: 2, messageCount: 3, message: "当前工作区还没有 .hwlab/hwpod-spec.yaml 文件,所以 hwpod inspect 无法获取可用 HWPOD 信息。", createdAt: "2026-06-05T13:39:40.000Z" }, + { seq: 30, label: "agentrun:assistant:message", type: "assistant", status: "completed", messageIndex: 3, messageCount: 3, replyAuthority: true, final: true, terminal: true, message: "当前工作区没有 .hwlab/hwpod-spec.yaml,也没有 .hwlab 目录或现成的 hwpod-spec 文件。\n\n目前可用的 HWPOD:0 个。", createdAt: "2026-06-05T13:39:44.000Z" }, + { seq: 34, label: "agentrun:result:completed", type: "result", status: "completed", terminal: true, message: "AgentRun result is ready for HWLAB short-connection polling.", createdAt: "2026-06-05T13:39:44.280Z" } + ]; + + const rows = traceDisplayRows({ startedAt: "2026-06-05T13:39:36.000Z" }, events); + const assistantRows = rows.filter((row) => row.bodyFormat === "markdown"); + const terminalRows = assistantRows.filter((row) => row.terminal === true); + + assert.equal(assistantRows.length, 3); + assert.equal(terminalRows.length, 1); + assert.match(assistantRows[0]?.body ?? "", /Let me check/u); + assert.match(assistantRows[1]?.body ?? "", /当前工作区还没有/u); + assert.match(terminalRows[0]?.body ?? "", /目前可用的 HWPOD:0 个/u); + assert.doesNotMatch(terminalRows[0]?.body ?? "", /Let me check|当前工作区还没有/u); +}); + test("web trace rows collapse tool start/completed and clean shell command escaping", () => { const events: Record[] = [ { seq: 1, label: "agentrun:request:accepted", status: "accepted", createdAt: "2026-06-04T12:13:58.212Z" },