fix: remove legacy trace fallback paths
This commit is contained in:
@@ -719,7 +719,7 @@ test("workbench workspace terminal status sync preserves a newer selected conver
|
||||
assert.equal(restored.body.workspace.workspace.selectedConversationId, "cnv_issue808_new");
|
||||
assert.equal(restored.body.workspace.workspace.selectedAgentSessionId, "ses_issue808_new");
|
||||
assert.equal(restored.body.workspace.workspace.lastTraceId, "trc_issue808_old_done");
|
||||
assert.equal(agentRunCalls.some((call) => call.path === "/api/v1/runs/run_issue808_old/commands/cmd_issue808_old/result"), false);
|
||||
assert.ok(agentRunCalls.some((call) => call.path === "/api/v1/runs/run_issue808_old/commands/cmd_issue808_old/result"));
|
||||
|
||||
const selectedOld = await postJson(port, `/v1/workbench/workspace/${workspace.body.workspace.workspaceId}/select-conversation`, {
|
||||
projectId: "prj_hwpod_workbench",
|
||||
@@ -983,8 +983,10 @@ test("workbench workspace status repairs terminal selected conversation after ac
|
||||
}, aliceLogin.cookie);
|
||||
assert.equal(update.status, 200);
|
||||
assert.equal(update.body.workspace.activeTraceId, null);
|
||||
assert.equal(update.body.workspace.selectedConversation.status, "running");
|
||||
assert.equal(update.body.workspace.selectedConversation.messages.length, 0);
|
||||
assert.equal(update.body.workspace.selectedConversation.status, "idle");
|
||||
assert.equal(update.body.workspace.selectedConversation.messages.length, 1);
|
||||
assert.equal(update.body.workspace.selectedConversation.messages[0].status, "idle");
|
||||
assert.equal(update.body.workspace.selectedConversation.messages[0].text, "repair completed");
|
||||
|
||||
const restored = await getJson(port, "/v1/workbench/workspace?projectId=prj_hwpod_workbench", aliceLogin.cookie);
|
||||
assert.equal(restored.status, 200);
|
||||
@@ -1156,96 +1158,6 @@ test("cloud api exposes terminal conversation status when stored session status
|
||||
}
|
||||
});
|
||||
|
||||
test("cloud api repairs persisted final response fallback from terminal result", async () => {
|
||||
const codeAgentChatResults = new Map();
|
||||
const server = createCloudApiServer({
|
||||
env: {
|
||||
HWLAB_ACCESS_CONTROL_REQUIRED: "1",
|
||||
HWLAB_BOOTSTRAP_ADMIN_USERNAME: "admin",
|
||||
HWLAB_BOOTSTRAP_ADMIN_PASSWORD: "admin-pass",
|
||||
HWLAB_CODE_AGENT_ADAPTER: "agentrun-v01",
|
||||
AGENTRUN_MGR_URL: "http://127.0.0.1:9",
|
||||
HWLAB_CODE_AGENT_AGENTRUN_ALLOW_NON_K3S_URL: "1"
|
||||
},
|
||||
codeAgentChatResults,
|
||||
now: () => "2026-06-04T09:45:00.000Z"
|
||||
});
|
||||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||||
|
||||
try {
|
||||
const { port } = server.address();
|
||||
const adminLogin = await postJson(port, "/auth/login", { username: "admin", password: "admin-pass" });
|
||||
const aliceCreate = await postJson(port, "/v1/admin/users", { username: "alice-issue834-fallback", password: "alice-pass" }, adminLogin.cookie);
|
||||
assert.equal(aliceCreate.status, 201);
|
||||
const aliceLogin = await postJson(port, "/auth/login", { username: "alice-issue834-fallback", password: "alice-pass" });
|
||||
const fallbackText = "Code Agent 仍在处理,可以继续 steer 或等待 trace 完成。";
|
||||
const finalText = "**直接答:能,但只到一半。**\n\n功能已经过了,剩余风险另开 issue 跟踪。";
|
||||
codeAgentChatResults.set("trc_issue834_final_response", {
|
||||
status: "completed",
|
||||
traceId: "trc_issue834_final_response",
|
||||
conversationId: "cnv_issue834_fallback",
|
||||
sessionId: "ses_issue834_fallback",
|
||||
threadId: "thread-issue-834-fallback",
|
||||
ownerUserId: aliceCreate.body.user.id,
|
||||
reply: { role: "assistant", content: finalText, messageId: "msg_issue834_real_final" },
|
||||
agentRun: { terminalStatus: "completed" },
|
||||
session: {
|
||||
sessionId: "ses_issue834_fallback",
|
||||
conversationId: "cnv_issue834_fallback",
|
||||
threadId: "thread-issue-834-fallback",
|
||||
status: "idle"
|
||||
}
|
||||
});
|
||||
|
||||
const stored = await putJson(port, "/v1/agent/conversations/cnv_issue834_fallback", {
|
||||
projectId: "prj_hwpod_workbench",
|
||||
sessionId: "ses_issue834_fallback",
|
||||
threadId: "thread-issue-834-fallback",
|
||||
sessionStatus: "completed",
|
||||
lastTraceId: "trc_issue834_final_response",
|
||||
messages: [
|
||||
{ id: "msg_issue834_question", role: "user", title: "用户", text: "功能是过了还是没过?", status: "sent", conversationId: "cnv_issue834_fallback", sessionId: "ses_issue834_fallback", threadId: "thread-issue-834-fallback" },
|
||||
{ id: "msg_issue834_fallback", role: "agent", title: "Agent", text: fallbackText, status: "completed", traceId: "trc_issue834_final_response", conversationId: "cnv_issue834_fallback", sessionId: "ses_issue834_fallback", threadId: "thread-issue-834-fallback" }
|
||||
]
|
||||
}, aliceLogin.cookie);
|
||||
assert.equal(stored.status, 200);
|
||||
assert.equal(stored.body.conversation.messages[1].text, fallbackText);
|
||||
assert.equal(stored.body.conversation.status, "completed");
|
||||
|
||||
const list = await getJson(port, "/v1/agent/conversations?projectId=prj_hwpod_workbench", aliceLogin.cookie);
|
||||
assert.equal(list.status, 200);
|
||||
const listed = list.body.conversations.find((conversation) => conversation.conversationId === "cnv_issue834_fallback");
|
||||
assert.ok(listed, "expected fallback conversation to be listed");
|
||||
assert.equal(listed.status, "idle");
|
||||
assert.equal(listed.session.status, "idle");
|
||||
assert.equal(listed.lastTraceId, "trc_issue834_final_response");
|
||||
assert.equal(listed.messages[1].text, finalText);
|
||||
assert.equal(listed.messages[1].status, "idle");
|
||||
|
||||
const direct = await getJson(port, "/v1/agent/conversations/cnv_issue834_fallback", aliceLogin.cookie);
|
||||
assert.equal(direct.status, 200);
|
||||
assert.equal(direct.body.conversation.status, "idle");
|
||||
assert.equal(direct.body.conversation.lastTraceId, "trc_issue834_final_response");
|
||||
assert.equal(direct.body.conversation.messages[1].text, finalText);
|
||||
assert.equal(direct.body.conversation.messages[1].status, "idle");
|
||||
|
||||
const workspace = await getJson(port, "/v1/workbench/workspace?projectId=prj_hwpod_workbench", aliceLogin.cookie);
|
||||
assert.equal(workspace.status, 200);
|
||||
const selected = await postJson(port, `/v1/workbench/workspace/${workspace.body.workspace.workspaceId}/select-conversation`, {
|
||||
projectId: "prj_hwpod_workbench",
|
||||
conversationId: "cnv_issue834_fallback",
|
||||
sessionId: "ses_issue834_fallback",
|
||||
updatedByClient: "test-suite"
|
||||
}, aliceLogin.cookie);
|
||||
assert.equal(selected.status, 200);
|
||||
assert.equal(selected.body.workspace.selectedConversation.messages[1].text, finalText);
|
||||
} finally {
|
||||
await new Promise((resolve, reject) => {
|
||||
server.close((error) => (error ? reject(error) : resolve()));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test("cloud api repairs conversation snapshot when persisted result advances last trace", async () => {
|
||||
const codeAgentChatResults = new Map();
|
||||
const server = createCloudApiServer({
|
||||
@@ -1679,349 +1591,6 @@ 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("cloud api trims folded progress from historical multi-turn final response snapshots (#934)", async () => {
|
||||
const nowValue = "2026-06-05T13:39:48.000Z";
|
||||
const projectId = "prj_hwpod_workbench";
|
||||
const conversationId = "cnv_issue934_multiturn_history";
|
||||
const sessionId = "ses_issue934_multiturn_history";
|
||||
const firstTraceId = "trc_issue934_first_turn";
|
||||
const secondTraceId = "trc_issue934_second_turn";
|
||||
const threadId = "thread-issue-934-multiturn";
|
||||
const firstFinalText = "当前工作区没有 `.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 secondUserText = "看看有几个hwpod可用?";
|
||||
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-multiturn-progress-polluted-snapshot",
|
||||
sessionStatus: "completed",
|
||||
lastTraceId: firstTraceId,
|
||||
finalResponse: {
|
||||
text: firstFinalText,
|
||||
textChars: firstFinalText.length,
|
||||
role: "assistant",
|
||||
status: "completed",
|
||||
traceId: firstTraceId,
|
||||
valuesPrinted: false
|
||||
},
|
||||
messages: [
|
||||
{ id: "msg_issue934_first_user", role: "user", title: "用户", text: "你好", status: "sent", traceId: firstTraceId, conversationId, sessionId, threadId, createdAt: nowValue },
|
||||
{ id: "msg_issue934_first_agent", role: "agent", title: "Code Agent 回复", text: firstFinalText, status: "completed", traceId: firstTraceId, conversationId, sessionId, threadId, createdAt: nowValue },
|
||||
{ id: "msg_issue934_second_user", role: "user", title: "用户", text: secondUserText, status: "sent", traceId: secondTraceId, conversationId, sessionId, threadId, createdAt: nowValue },
|
||||
{ id: "msg_issue934_second_agent", role: "agent", title: "Code Agent 回复", text: `${progressText}${firstFinalText}`, 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, firstFinalText);
|
||||
assert.equal(direct.body.conversation.messages[3].text, firstFinalText);
|
||||
assert.equal(direct.body.conversation.messages[3].traceId, secondTraceId);
|
||||
assert.doesNotMatch(direct.body.conversation.messages[3].text, /Let me check|当前工作区还没有|让我看看/u);
|
||||
} finally {
|
||||
await new Promise((resolve, reject) => {
|
||||
server.close((error) => (error ? reject(error) : resolve()));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
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 whitespaceFoldedFinalText = finalText.replace(/\s+/gu, " ");
|
||||
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}${whitespaceFoldedFinalText}`, 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({
|
||||
|
||||
@@ -1365,7 +1365,7 @@ class AccessController {
|
||||
|
||||
async repairVisibleConversationIfNeeded(actor, conversation, projectId = "") {
|
||||
if (textOr(conversation?.status, "").toLowerCase() === "archived") return conversation;
|
||||
const traceId = terminalConversationFallbackRepairTraceId(conversation);
|
||||
const traceId = terminalConversationRepairTraceId(conversation);
|
||||
if (!traceId) return conversation;
|
||||
const result = await this.terminalCodeAgentResultForActor(traceId, actor);
|
||||
if (!result) return conversation;
|
||||
@@ -1898,7 +1898,7 @@ function mergeAgentSessionOwnerEvidence(nextValue, existingValue, options = {})
|
||||
} else if (existingMessages?.length) {
|
||||
merged.messages = existingMessages;
|
||||
}
|
||||
merged.messages = normalizeFoldedProgressConversationMessages(normalizeFinalResponseConversationMessages(merged.messages, merged.finalResponse));
|
||||
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;
|
||||
@@ -1994,7 +1994,7 @@ function normalizeFinalResponseConversationMessages(messages, finalResponse) {
|
||||
return messages.map((message) => {
|
||||
if (message?.role !== "agent") return message;
|
||||
const sameTrace = traceId && safeTraceIdLocal(message.traceId) === traceId;
|
||||
if (!sameTrace && !isFoldedProgressBeforeFinalResponse(message.text, finalText)) return message;
|
||||
if (!sameTrace) return message;
|
||||
return redactConversationMessage({
|
||||
...message,
|
||||
text: finalText,
|
||||
@@ -2003,35 +2003,6 @@ function normalizeFinalResponseConversationMessages(messages, finalResponse) {
|
||||
});
|
||||
}).filter(Boolean);
|
||||
}
|
||||
function isFoldedProgressBeforeFinalResponse(messageText, finalText) {
|
||||
const text = textOr(messageText, "");
|
||||
const final = textOr(finalText, "");
|
||||
if (final.length < 80 || text.length <= final.length) return false;
|
||||
const index = text.indexOf(final);
|
||||
const foldedIndex = index > 0 ? index : collapseConversationWhitespace(text).indexOf(collapseConversationWhitespace(final));
|
||||
if (foldedIndex <= 0) return false;
|
||||
const prefix = text.slice(0, Math.min(foldedIndex, text.length)).trim();
|
||||
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 collapseConversationWhitespace(value) {
|
||||
return textOr(value, "").replace(/\s+/gu, " ").trim();
|
||||
}
|
||||
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);
|
||||
@@ -2274,8 +2245,7 @@ function conversationNeedsTerminalRepair(conversation, traceId) {
|
||||
if (!traceMessage) return Boolean(latestAgent && traceId === safeTraceIdLocal(conversation.lastTraceId ?? conversation.session?.lastTraceId ?? conversation.snapshot?.lastTraceId));
|
||||
if (latestAgent?.traceId && latestAgent.traceId !== traceId && traceId === safeTraceIdLocal(conversation.lastTraceId ?? conversation.session?.lastTraceId ?? conversation.snapshot?.lastTraceId)) return true;
|
||||
const messageStatus = textOr(traceMessage.status, "").toLowerCase();
|
||||
return ["", "running", "busy", "pending", "active"].includes(messageStatus)
|
||||
|| isLegacyFinalResponseFallbackText(traceMessage.text);
|
||||
return ["", "running", "busy", "pending", "active"].includes(messageStatus);
|
||||
}
|
||||
function terminalConversationRepairTraceId(conversation) {
|
||||
if (!conversation) return "";
|
||||
@@ -2289,25 +2259,6 @@ function terminalConversationRepairTraceId(conversation) {
|
||||
}
|
||||
return "";
|
||||
}
|
||||
function terminalConversationFallbackRepairTraceId(conversation) {
|
||||
if (!conversation) return "";
|
||||
const messages = Array.isArray(conversation.messages) ? conversation.messages : [];
|
||||
const lastTraceId = safeTraceIdLocal(conversation.lastTraceId ?? conversation.session?.lastTraceId ?? conversation.snapshot?.lastTraceId);
|
||||
const lastTraceMessage = messages.find((message) => message?.traceId === lastTraceId && message?.role === "agent") ?? null;
|
||||
const latestAgent = latestAgentConversationMessage(messages);
|
||||
if (lastTraceId && latestAgent?.traceId && latestAgent.traceId !== lastTraceId) return lastTraceId;
|
||||
if (lastTraceId && latestAgent && !lastTraceMessage) return lastTraceId;
|
||||
if (lastTraceId && isLegacyFinalResponseFallbackText(lastTraceMessage?.text)) return lastTraceId;
|
||||
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
||||
const message = messages[index];
|
||||
const traceId = safeTraceIdLocal(message?.traceId);
|
||||
if (message?.role === "agent" && traceId && isLegacyFinalResponseFallbackText(message.text)) return traceId;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
function isLegacyFinalResponseFallbackText(value) {
|
||||
return textOr(value, "").trim() === "Code Agent 仍在处理,可以继续 steer 或等待 trace 完成。";
|
||||
}
|
||||
function mergeTerminalConversationMessages(existingMessages = [], result = {}, context = {}) {
|
||||
const baseMessages = dedupeConversationMessages(existingMessages);
|
||||
const traceId = safeTraceIdLocal(result.traceId ?? context.traceId);
|
||||
@@ -2392,7 +2343,6 @@ function terminalRunnerTraceForConversationMessage(result = {}, context = {}) {
|
||||
fullTraceLoaded: false,
|
||||
lastEvent,
|
||||
traceStatus: textOr(result.traceStatus, ""),
|
||||
fallback: redactTraceFallback(result.fallback),
|
||||
finalResponse: redactTraceFinalResponse(result.finalResponse),
|
||||
traceSummary: redactTraceSummary(result.traceSummary)
|
||||
});
|
||||
@@ -2423,26 +2373,11 @@ function redactConversationMessage(message) {
|
||||
fullTraceLoaded: runnerTrace.fullTraceLoaded === true,
|
||||
lastEvent: runnerTrace.lastEvent,
|
||||
traceStatus: textOr(runnerTrace.traceStatus, ""),
|
||||
fallback: redactTraceFallback(runnerTrace.fallback),
|
||||
finalResponse: redactTraceFinalResponse(runnerTrace.finalResponse),
|
||||
traceSummary: redactTraceSummary(runnerTrace.traceSummary)
|
||||
}) : undefined
|
||||
});
|
||||
}
|
||||
function redactTraceFallback(value) {
|
||||
const fallback = normalizeObject(value);
|
||||
if (Object.keys(fallback).length === 0) return undefined;
|
||||
return pruneEmpty({
|
||||
available: fallback.available === true,
|
||||
source: textOr(fallback.source, ""),
|
||||
conversationId: textOr(fallback.conversationId, ""),
|
||||
sessionId: textOr(fallback.sessionId, ""),
|
||||
threadId: textOr(fallback.threadId, ""),
|
||||
agentRun: redactAgentRunSummary(fallback.agentRun),
|
||||
finalResponse: redactTraceFinalResponse(fallback.finalResponse),
|
||||
traceSummary: redactTraceSummary(fallback.traceSummary)
|
||||
});
|
||||
}
|
||||
function redactTraceFinalResponse(value) {
|
||||
const response = normalizeObject(value);
|
||||
if (Object.keys(response).length === 0) return undefined;
|
||||
@@ -2522,7 +2457,7 @@ function publicAgentConversation(session) {
|
||||
now: session.updatedAt,
|
||||
firstUserPreview: snapshot.firstUserMessagePreview
|
||||
}));
|
||||
const displayMessages = normalizeFoldedProgressConversationMessages(normalizeFinalResponseConversationMessages(messages, snapshot.finalResponse));
|
||||
const displayMessages = normalizeFinalResponseConversationMessages(messages, snapshot.finalResponse);
|
||||
const status = resolvedConversationStatus(session.status, snapshot, displayMessages);
|
||||
const lastTraceId = resolvedConversationLastTraceId(session.lastTraceId, status, displayMessages, snapshot);
|
||||
return {
|
||||
|
||||
@@ -1624,7 +1624,7 @@ test("cloud api trace uses AgentRun command result evidence when live trace stor
|
||||
},
|
||||
traceSummary: {
|
||||
traceId: "trc_issue842_expired",
|
||||
source: "stale-agent-session-snapshot",
|
||||
source: "stale-derived-session-evidence",
|
||||
sourceEventCount: 26,
|
||||
terminalStatus: "completed",
|
||||
noiseEventCount: 4,
|
||||
@@ -1717,8 +1717,8 @@ test("cloud api trace uses AgentRun command result evidence when live trace stor
|
||||
assert.equal(body.status, "expired");
|
||||
assert.equal(body.traceStatus, "expired");
|
||||
assert.equal(body.eventCount, 0);
|
||||
assert.equal(body.fallback.available, true);
|
||||
assert.equal(body.fallback.source, "agentrun-command-result");
|
||||
assert.equal(body.terminalEvidence.available, true);
|
||||
assert.equal(body.terminalEvidence.source, "agentrun-command-result");
|
||||
assert.equal(body.finalResponse.text, commandFinalText);
|
||||
assert.equal(body.traceSummary.source, "agentrun-command-result");
|
||||
assert.equal(body.traceSummary.sourceEventCount, 0);
|
||||
@@ -1795,7 +1795,7 @@ test("cloud api trace replays an earlier AgentRun command after same-run lastSeq
|
||||
ownerRole: TEST_AGENT_ACTOR.role,
|
||||
reply: { role: "assistant", content: firstFinalText },
|
||||
finalResponse: { text: firstFinalText, textChars: firstFinalText.length, role: "assistant", status: "completed", traceId: firstTraceId, valuesPrinted: false },
|
||||
traceSummary: { traceId: firstTraceId, source: "agent-session-snapshot", sourceEventCount: 41, terminalStatus: "completed", agentRun: { runId, commandId: firstCommandId, lastSeq: 35, valuesPrinted: false }, valuesPrinted: false },
|
||||
traceSummary: { traceId: firstTraceId, source: "code-agent-derived-session-evidence", sourceEventCount: 41, terminalStatus: "completed", agentRun: { runId, commandId: firstCommandId, lastSeq: 35, valuesPrinted: false }, valuesPrinted: false },
|
||||
agentRun: { adapter: "agentrun-v01", managerUrl: `http://127.0.0.1:${agentRunPort}`, runId, commandId: firstCommandId, lastSeq: 66, terminalStatus: "completed", valuesPrinted: false },
|
||||
valuesPrinted: false
|
||||
});
|
||||
@@ -1908,7 +1908,7 @@ test("cloud api repairs historical same-session AgentRun trace after lastTraceId
|
||||
],
|
||||
agentRun: { adapter: "agentrun-v01", managerUrl: `http://127.0.0.1:${agentRunPort}`, runId, commandId: firstCommandId, backendProfile: "deepseek", terminalStatus: "completed", lastSeq: 35, valuesPrinted: false },
|
||||
finalResponse: { text: firstFinalText, textChars: firstFinalText.length, role: "assistant", status: "completed", traceId: secondTraceId, valuesPrinted: false },
|
||||
traceSummary: { traceId: secondTraceId, source: "agent-session-snapshot", sourceEventCount: 36, terminalStatus: "completed", agentRun: { runId, commandId: firstCommandId, lastSeq: 35, valuesPrinted: false }, valuesPrinted: false },
|
||||
traceSummary: { traceId: secondTraceId, source: "code-agent-derived-session-evidence", sourceEventCount: 36, terminalStatus: "completed", agentRun: { runId, commandId: firstCommandId, lastSeq: 35, valuesPrinted: false }, valuesPrinted: false },
|
||||
traceResults: {
|
||||
[secondTraceId]: {
|
||||
traceId: secondTraceId,
|
||||
@@ -2062,7 +2062,7 @@ test("cloud api result polling repairs polluted completed AgentRun memory cache
|
||||
ownerRole: TEST_AGENT_ACTOR.role,
|
||||
reply: { role: "assistant", content: firstFinalText },
|
||||
finalResponse: { text: firstFinalText, textChars: firstFinalText.length, role: "assistant", status: "completed", traceId: secondTraceId, valuesPrinted: false },
|
||||
traceSummary: { traceId: secondTraceId, source: "agent-session-snapshot", sourceEventCount: 11, terminalStatus: "completed", agentRun: { runId, commandId: firstCommandId, lastSeq: 11, valuesPrinted: false }, valuesPrinted: false },
|
||||
traceSummary: { traceId: secondTraceId, source: "code-agent-derived-session-evidence", sourceEventCount: 11, terminalStatus: "completed", agentRun: { runId, commandId: firstCommandId, lastSeq: 11, valuesPrinted: false }, valuesPrinted: false },
|
||||
agentRun: {
|
||||
adapter: "agentrun-v01",
|
||||
managerUrl: `http://127.0.0.1:${agentRunPort}`,
|
||||
|
||||
@@ -1719,7 +1719,7 @@ function codeAgentTraceSummaryEvidence(payload = {}, traceId = null, finalRespon
|
||||
const terminalEvent = [...events].reverse().find((event) => event?.terminal === true) ?? runnerTrace?.lastEvent ?? null;
|
||||
return {
|
||||
traceId,
|
||||
source: "agent-session-snapshot",
|
||||
source: "code-agent-derived-session-evidence",
|
||||
sourceEventCount: Number.isFinite(Number(runnerTrace?.eventCount)) ? Number(runnerTrace.eventCount) : events.length,
|
||||
terminalStatus: payload.agentRun?.terminalStatus ?? payload.status ?? terminalEvent?.status ?? null,
|
||||
lastEventLabel: terminalEvent?.label ?? null,
|
||||
@@ -1867,6 +1867,18 @@ export async function handleCodeAgentTraceHttp(request, response, url, options)
|
||||
recordCodeAgentConversationFact(agentRunResult, options);
|
||||
await recordCodeAgentSessionOwner({ payload: agentRunResult, params: agentRunResult, options, status: codeAgentOwnerStatusForResult(agentRunResult), preserveLastTraceId: true });
|
||||
}
|
||||
} catch (error) {
|
||||
sendJson(response, Number(error?.statusCode ?? 502), {
|
||||
error: {
|
||||
code: error?.code ?? "agentrun_trace_command_result_unavailable",
|
||||
message: error?.message ?? "AgentRun command result could not be resolved for this traceId",
|
||||
traceId,
|
||||
valuesPrinted: false
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await refreshAgentRunTrace({ traceId, result: agentRunResult, options, traceStore });
|
||||
} catch (error) {
|
||||
refreshError = error;
|
||||
@@ -1879,18 +1891,17 @@ export async function handleCodeAgentTraceHttp(request, response, url, options)
|
||||
}
|
||||
|
||||
const snapshot = traceStore.snapshot(traceId);
|
||||
sendJson(response, 200, traceSnapshotWithPersistentFallback(snapshot, agentRunResult, traceId, refreshError));
|
||||
sendJson(response, 200, traceSnapshotWithTerminalEvidence(snapshot, agentRunResult, traceId, refreshError));
|
||||
}
|
||||
|
||||
function traceSnapshotWithPersistentFallback(snapshot, persistedResult, traceId, refreshError = null) {
|
||||
const fallback = persistentTraceFallback(persistedResult, traceId);
|
||||
if (snapshot?.status !== "missing") return traceSnapshotWithPersistentEvidence(snapshot, fallback, refreshError);
|
||||
if (!fallback) return {
|
||||
function traceSnapshotWithTerminalEvidence(snapshot, result, traceId, refreshError = null) {
|
||||
const evidence = agentRunTerminalTraceEvidence(result, traceId);
|
||||
if (snapshot?.status !== "missing") return traceSnapshotWithAttachedTerminalEvidence(snapshot, evidence, refreshError);
|
||||
if (!evidence) return {
|
||||
...snapshot,
|
||||
ok: false,
|
||||
traceStatus: "missing",
|
||||
retention: traceRetentionSummary("missing"),
|
||||
fallback: { available: false, source: null }
|
||||
retention: traceRetentionSummary("missing")
|
||||
};
|
||||
return {
|
||||
...snapshot,
|
||||
@@ -1899,51 +1910,51 @@ function traceSnapshotWithPersistentFallback(snapshot, persistedResult, traceId,
|
||||
traceStatus: "expired",
|
||||
persisted: true,
|
||||
retention: traceRetentionSummary("expired"),
|
||||
conversationId: fallback.conversationId,
|
||||
sessionId: fallback.sessionId,
|
||||
threadId: fallback.threadId,
|
||||
agentRun: fallback.agentRun,
|
||||
finalResponse: fallback.finalResponse,
|
||||
traceSummary: fallback.traceSummary,
|
||||
fallback: {
|
||||
conversationId: evidence.conversationId,
|
||||
sessionId: evidence.sessionId,
|
||||
threadId: evidence.threadId,
|
||||
agentRun: evidence.agentRun,
|
||||
finalResponse: evidence.finalResponse,
|
||||
traceSummary: evidence.traceSummary,
|
||||
terminalEvidence: {
|
||||
available: true,
|
||||
source: fallback.source,
|
||||
source: evidence.source,
|
||||
refresh: refreshError ? {
|
||||
attempted: true,
|
||||
ok: false,
|
||||
code: refreshError?.code ?? "agentrun_trace_refresh_failed",
|
||||
message: refreshError?.message ?? "AgentRun trace refresh failed; using persisted session summary.",
|
||||
message: refreshError?.message ?? "AgentRun trace events could not be refreshed; command result remains authoritative for final response.",
|
||||
valuesPrinted: false
|
||||
} : { attempted: false, ok: null },
|
||||
conversationId: fallback.conversationId,
|
||||
sessionId: fallback.sessionId,
|
||||
threadId: fallback.threadId,
|
||||
agentRun: fallback.agentRun,
|
||||
traceSummary: fallback.traceSummary,
|
||||
finalResponse: fallback.finalResponse,
|
||||
conversationId: evidence.conversationId,
|
||||
sessionId: evidence.sessionId,
|
||||
threadId: evidence.threadId,
|
||||
agentRun: evidence.agentRun,
|
||||
traceSummary: evidence.traceSummary,
|
||||
finalResponse: evidence.finalResponse,
|
||||
valuesPrinted: false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function traceSnapshotWithPersistentEvidence(snapshot, fallback, refreshError = null) {
|
||||
if (!fallback) return snapshot;
|
||||
function traceSnapshotWithAttachedTerminalEvidence(snapshot, evidence, refreshError = null) {
|
||||
if (!evidence) return snapshot;
|
||||
return {
|
||||
...snapshot,
|
||||
conversationId: snapshot.conversationId ?? fallback.conversationId,
|
||||
sessionId: snapshot.sessionId ?? fallback.sessionId,
|
||||
threadId: snapshot.threadId ?? fallback.threadId,
|
||||
agentRun: snapshot.agentRun ?? fallback.agentRun,
|
||||
finalResponse: snapshot.finalResponse ?? fallback.finalResponse,
|
||||
traceSummary: snapshot.traceSummary ?? fallback.traceSummary,
|
||||
fallback: snapshot.fallback ?? (refreshError ? {
|
||||
conversationId: snapshot.conversationId ?? evidence.conversationId,
|
||||
sessionId: snapshot.sessionId ?? evidence.sessionId,
|
||||
threadId: snapshot.threadId ?? evidence.threadId,
|
||||
agentRun: snapshot.agentRun ?? evidence.agentRun,
|
||||
finalResponse: snapshot.finalResponse ?? evidence.finalResponse,
|
||||
traceSummary: snapshot.traceSummary ?? evidence.traceSummary,
|
||||
terminalEvidence: snapshot.terminalEvidence ?? (refreshError ? {
|
||||
available: true,
|
||||
source: fallback.source,
|
||||
source: evidence.source,
|
||||
refresh: {
|
||||
attempted: true,
|
||||
ok: false,
|
||||
code: refreshError?.code ?? "agentrun_trace_refresh_failed",
|
||||
message: refreshError?.message ?? "AgentRun trace refresh failed; live events may be partial.",
|
||||
message: refreshError?.message ?? "AgentRun trace events could not be refreshed; live events may be partial.",
|
||||
valuesPrinted: false
|
||||
},
|
||||
valuesPrinted: false
|
||||
@@ -1951,10 +1962,10 @@ function traceSnapshotWithPersistentEvidence(snapshot, fallback, refreshError =
|
||||
};
|
||||
}
|
||||
|
||||
function persistentTraceFallback(result, traceId) {
|
||||
function agentRunTerminalTraceEvidence(result, traceId) {
|
||||
if (!result || typeof result !== "object") return null;
|
||||
const storedSummary = result.traceSummary && typeof result.traceSummary === "object" ? result.traceSummary : null;
|
||||
const finalResponse = persistentFinalResponse(result);
|
||||
const storedSummary = result.traceSummary && typeof result.traceSummary === "object" && result.traceSummary.source === "agentrun-command-result" ? result.traceSummary : null;
|
||||
const finalResponse = agentRunTerminalFinalResponse(result, traceId);
|
||||
const agentRun = result.agentRun && typeof result.agentRun === "object" ? {
|
||||
adapter: result.agentRun.adapter ?? null,
|
||||
runId: result.agentRun.runId ?? null,
|
||||
@@ -1967,10 +1978,10 @@ function persistentTraceFallback(result, traceId) {
|
||||
lastSeq: result.agentRun.lastSeq ?? null,
|
||||
valuesPrinted: false
|
||||
} : null;
|
||||
if (!storedSummary && !finalResponse && !agentRun) return null;
|
||||
if (!agentRun?.runId || !agentRun?.commandId || (!storedSummary && !finalResponse)) return null;
|
||||
const traceSummary = {
|
||||
traceId,
|
||||
source: storedSummary?.source ?? (finalResponse ? "agent-session-final-response" : "agent-session-agentrun"),
|
||||
source: "agentrun-command-result",
|
||||
sourceEventCount: numberOrNull(storedSummary?.sourceEventCount ?? storedSummary?.eventCount ?? result.runnerTrace?.eventCount),
|
||||
renderedRowSummary: storedSummary?.renderedRowSummary ?? null,
|
||||
noiseEventCount: numberOrNull(storedSummary?.noiseEventCount),
|
||||
@@ -1980,7 +1991,7 @@ function persistentTraceFallback(result, traceId) {
|
||||
valuesPrinted: false
|
||||
};
|
||||
return {
|
||||
source: storedSummary?.source ?? (finalResponse ? "agent-session-final-response" : "agent-session-agentrun"),
|
||||
source: "agentrun-command-result",
|
||||
conversationId: safeConversationId(result.conversationId) || null,
|
||||
sessionId: safeSessionId(result.sessionId) || null,
|
||||
threadId: safeOpaqueId(result.threadId) || null,
|
||||
@@ -1990,20 +2001,22 @@ function persistentTraceFallback(result, traceId) {
|
||||
};
|
||||
}
|
||||
|
||||
function persistentFinalResponse(result) {
|
||||
function agentRunTerminalFinalResponse(result, traceId) {
|
||||
const stored = result.finalResponse && typeof result.finalResponse === "object" ? result.finalResponse : null;
|
||||
const text = textValue(stored?.text ?? result.reply?.content ?? result.message?.content ?? result.assistantText);
|
||||
if (!text) return null;
|
||||
const storedTraceId = safeTraceId(stored?.traceId ?? result.traceId) || null;
|
||||
if (storedTraceId && storedTraceId !== traceId) return null;
|
||||
return {
|
||||
text,
|
||||
textChars: text.length,
|
||||
messageId: stored?.messageId ?? result.reply?.messageId ?? result.messageId ?? null,
|
||||
role: stored?.role ?? result.reply?.role ?? "assistant",
|
||||
status: stored?.status ?? result.status ?? null,
|
||||
traceId: safeTraceId(stored?.traceId ?? result.traceId) || null,
|
||||
traceId: traceId,
|
||||
createdAt: stored?.createdAt ?? result.reply?.createdAt ?? result.createdAt ?? null,
|
||||
updatedAt: stored?.updatedAt ?? result.updatedAt ?? null,
|
||||
source: stored?.source ?? "code-agent-result",
|
||||
source: "agentrun-command-result",
|
||||
valuesPrinted: false
|
||||
};
|
||||
}
|
||||
@@ -2012,8 +2025,8 @@ function traceRetentionSummary(status) {
|
||||
return {
|
||||
traceStatus: status,
|
||||
liveTraceStore: status === "missing" ? "missing" : "expired-or-evicted",
|
||||
policy: "live trace events are best-effort short-term storage; completed turns persist final response and summary in the agent session snapshot",
|
||||
replacementEvidence: status === "missing" ? "submit a fresh equivalent trace or inspect conversation/session if available" : "use fallback.finalResponse, fallback.traceSummary, conversationId, sessionId, threadId, and agentRun ids",
|
||||
policy: "live trace events are short-term storage; terminal final response and summary are resolved from AgentRun command result by traceId and commandId",
|
||||
replacementEvidence: status === "missing" ? "resolve the trace through AgentRun command registry/result or report the trace as missing" : "use terminalEvidence.finalResponse, terminalEvidence.traceSummary, conversationId, sessionId, threadId, and agentRun ids",
|
||||
valuesPrinted: false
|
||||
};
|
||||
}
|
||||
|
||||
@@ -397,46 +397,11 @@ test("hwlab-cli client session final-response verifies original conversation acr
|
||||
assert.equal(result.payload.validation.checks.listFound, true);
|
||||
assert.equal(result.payload.validation.checks.inspectTextMatchesResult, true);
|
||||
assert.equal(result.payload.validation.checks.listTextMatchesResult, true);
|
||||
assert.equal(result.payload.validation.checks.fallbackTextAbsent, true);
|
||||
assert.equal(result.payload.validation.repair.appearsRepaired, true);
|
||||
assert.equal(calls[1], "http://web.test/v1/agent/conversations?projectId=prj_v02_code_agent&limit=100");
|
||||
assert.deepEqual(result.payload.validation.failures, []);
|
||||
});
|
||||
|
||||
test("hwlab-cli client session final-response fails when old fallback is still visible", async () => {
|
||||
const fallbackText = "Code Agent 仍在处理,可以继续 steer 或等待 trace 完成。";
|
||||
const conversation = {
|
||||
conversationId: "cnv_issue834",
|
||||
projectId: "prj_v02_code_agent",
|
||||
sessionId: "ses_issue834",
|
||||
status: "idle",
|
||||
lastTraceId: "trc_issue834_final",
|
||||
messages: [{ role: "agent", traceId: "trc_issue834_final", status: "idle", text: fallbackText }]
|
||||
};
|
||||
const fetchImpl = async (url: string | URL) => {
|
||||
if (String(url).endsWith("/v1/agent/conversations/cnv_issue834")) {
|
||||
return new Response(JSON.stringify({ ok: true, conversation }), { status: 200 });
|
||||
}
|
||||
if (String(url).endsWith("/v1/agent/conversations?projectId=prj_v02_code_agent&limit=100")) {
|
||||
return new Response(JSON.stringify({ ok: true, conversations: [conversation] }), { status: 200 });
|
||||
}
|
||||
if (String(url).endsWith("/v1/agent/chat/result/trc_issue834_final")) {
|
||||
return new Response(JSON.stringify({ ok: true, status: "completed", traceId: "trc_issue834_final", reply: { content: "真实 final response" } }), { status: 200 });
|
||||
}
|
||||
return new Response(JSON.stringify({ ok: false, error: { code: "unexpected_route" } }), { status: 404 });
|
||||
};
|
||||
|
||||
const result = await runHwlabCli(["client", "session", "final-response", "cnv_issue834", "--base-url", "http://web.test", "--cookie", "hwlab_session=session-a"], { fetchImpl });
|
||||
|
||||
assert.equal(result.exitCode, 1);
|
||||
assert.equal(result.payload.action, "client.session.final-response");
|
||||
assert.equal(result.payload.validation.state, "failed");
|
||||
assert.equal(result.payload.validation.checks.fallbackTextAbsent, false);
|
||||
assert.ok(result.payload.validation.failures.includes("fallback-text-present"));
|
||||
assert.ok(result.payload.validation.failures.includes("inspect-text-result-mismatch"));
|
||||
assert.deepEqual(result.payload.validation.fallbackText.presentIn, ["inspect", "list"]);
|
||||
});
|
||||
|
||||
test("hwlab-cli client session final-response preserves inspect auth failure visibility", async () => {
|
||||
const fetchImpl = async (url: string | URL) => {
|
||||
if (String(url).endsWith("/v1/agent/conversations/cnv_issue834")) {
|
||||
@@ -455,49 +420,6 @@ test("hwlab-cli client session final-response preserves inspect auth failure vis
|
||||
assert.equal(result.payload.error?.code, undefined);
|
||||
});
|
||||
|
||||
test("hwlab-cli client agent inspect can resolve a session id through the conversation list", async () => {
|
||||
const calls: string[] = [];
|
||||
const conversation = {
|
||||
conversationId: "cnv_issue849",
|
||||
sessionId: "ses_issue849",
|
||||
threadId: "thread_issue849",
|
||||
status: "idle",
|
||||
lastTraceId: "trc_issue849",
|
||||
messages: [{ role: "agent", traceId: "trc_issue849", text: "final response" }]
|
||||
};
|
||||
const fetchImpl = async (url: string | URL) => {
|
||||
calls.push(String(url));
|
||||
if (String(url).endsWith("/v1/agent/chat/inspect?sessionId=ses_issue849")) {
|
||||
return new Response(JSON.stringify({ ok: false, status: "not_found", session: null }), { status: 404 });
|
||||
}
|
||||
if (String(url).endsWith("/v1/agent/conversations?projectId=prj_hwpod_workbench&limit=100")) {
|
||||
return new Response(JSON.stringify({ ok: true, conversations: [conversation] }), { status: 200 });
|
||||
}
|
||||
if (String(url).endsWith("/v1/agent/conversations/cnv_issue849")) {
|
||||
return new Response(JSON.stringify({ ok: true, status: "found", conversation }), { status: 200 });
|
||||
}
|
||||
return new Response(JSON.stringify({ ok: false, error: { code: "unexpected_route" } }), { status: 404 });
|
||||
};
|
||||
|
||||
const result = await runHwlabCli(["client", "agent", "inspect", "--session-id", "ses_issue849", "--base-url", "http://web.test", "--cookie", "hwlab_session=session-a"], { fetchImpl });
|
||||
|
||||
assert.equal(result.exitCode, 0);
|
||||
assert.equal(result.payload.action, "client.agent.inspect");
|
||||
assert.equal(result.payload.fallbackLookup.attempted, true);
|
||||
assert.equal(result.payload.fallbackLookup.source, "client.session.list");
|
||||
assert.equal(result.payload.fallbackLookup.conversationId, "cnv_issue849");
|
||||
assert.equal(result.payload.fallbackLookup.traceId, "trc_issue849");
|
||||
assert.equal(result.payload.route.path, "/v1/agent/conversations/cnv_issue849");
|
||||
assert.equal(result.payload.conversation.conversationId, "cnv_issue849");
|
||||
assert.equal(result.payload.conversation.sessionId, "ses_issue849");
|
||||
assert.equal(result.payload.body.status, "found");
|
||||
assert.deepEqual(calls, [
|
||||
"http://web.test/v1/agent/chat/inspect?sessionId=ses_issue849",
|
||||
"http://web.test/v1/agent/conversations?projectId=prj_hwpod_workbench&limit=100",
|
||||
"http://web.test/v1/agent/conversations/cnv_issue849"
|
||||
]);
|
||||
});
|
||||
|
||||
test("hwlab-cli rejects manual endpoint override in locked assembled runtime", async () => {
|
||||
const result = await runHwlabCli(["client", "auth", "status", "--base-url", "http://74.48.78.17:17666"], {
|
||||
env: {
|
||||
@@ -1678,7 +1600,7 @@ test("hwlab-cli client agent trace exposes missing Web render state without a bo
|
||||
assert.deepEqual(result.payload.data, result.payload.rendered);
|
||||
});
|
||||
|
||||
test("hwlab-cli client agent trace renders persisted summary for expired traces", async () => {
|
||||
test("hwlab-cli client agent trace renders AgentRun terminal evidence for expired traces", async () => {
|
||||
const finalText = "历史 trace 已过期,但 final response 仍可审计。";
|
||||
const result = await runHwlabCli(["client", "agent", "trace", "trc_expired", "--base-url", "http://web.test", "--cookie", "hwlab_session=session-a", "--render", "web"], {
|
||||
fetchImpl: async () => new Response(JSON.stringify({
|
||||
@@ -1693,17 +1615,17 @@ test("hwlab-cli client agent trace renders persisted summary for expired traces"
|
||||
threadId: "thread-expired",
|
||||
retention: { traceStatus: "expired", liveTraceStore: "expired-or-evicted" },
|
||||
agentRun: { runId: "run_expired", commandId: "cmd_expired", valuesPrinted: false },
|
||||
finalResponse: { text: finalText, textChars: finalText.length, role: "assistant", status: "completed", source: "code-agent-result", valuesPrinted: false },
|
||||
traceSummary: { source: "agent-session-snapshot", sourceEventCount: 26, terminalStatus: "completed", valuesPrinted: false },
|
||||
fallback: {
|
||||
finalResponse: { text: finalText, textChars: finalText.length, role: "assistant", status: "completed", source: "agentrun-command-result", valuesPrinted: false },
|
||||
traceSummary: { source: "agentrun-command-result", sourceEventCount: 26, terminalStatus: "completed", valuesPrinted: false },
|
||||
terminalEvidence: {
|
||||
available: true,
|
||||
source: "agent-session-snapshot",
|
||||
source: "agentrun-command-result",
|
||||
conversationId: "cnv_expired",
|
||||
sessionId: "ses_expired",
|
||||
threadId: "thread-expired",
|
||||
agentRun: { runId: "run_expired", commandId: "cmd_expired", valuesPrinted: false },
|
||||
finalResponse: { text: finalText, textChars: finalText.length, role: "assistant", status: "completed", valuesPrinted: false },
|
||||
traceSummary: { source: "agent-session-snapshot", sourceEventCount: 26, terminalStatus: "completed", valuesPrinted: false },
|
||||
traceSummary: { source: "agentrun-command-result", sourceEventCount: 26, terminalStatus: "completed", valuesPrinted: false },
|
||||
valuesPrinted: false
|
||||
}
|
||||
}), { status: 200 })
|
||||
@@ -1720,7 +1642,7 @@ test("hwlab-cli client agent trace renders persisted summary for expired traces"
|
||||
assert.match(result.payload.rendered.rows[0].header, /历史 trace 已过期/u);
|
||||
assert.match(result.payload.rendered.rows[0].body, /final response 仍可审计/u);
|
||||
assert.match(result.payload.rendered.rows[0].body, /run_expired/u);
|
||||
assert.equal(result.payload.rendered.fallback.available, true);
|
||||
assert.equal(result.payload.rendered.terminalEvidence.available, true);
|
||||
assert.equal(result.payload.rendered.traceSummary.sourceEventCount, 26);
|
||||
assert.deepEqual(result.payload.data, result.payload.rendered);
|
||||
});
|
||||
|
||||
+27
-85
@@ -863,53 +863,7 @@ async function agentCommand(context: any) {
|
||||
async function agentInspect(context: any) {
|
||||
const pathName = agentInspectPath(context);
|
||||
const response = await requestJson({ ...context, method: "GET", path: pathName });
|
||||
if (responseSucceeded(response) || !shouldFallbackInspectBySession(context, response)) {
|
||||
return responsePayload("client.agent.inspect", response, context, { route: route("GET", pathName), body: responseBodyForCli(response.body, context.parsed) });
|
||||
}
|
||||
|
||||
const fallback = await inspectConversationBySessionId(context, requiredSessionId(context.parsed.sessionId));
|
||||
if (!fallback) {
|
||||
return responsePayload("client.agent.inspect", response, context, {
|
||||
route: route("GET", pathName),
|
||||
fallbackLookup: { attempted: true, source: "client.session.list", found: false },
|
||||
body: responseBodyForCli(response.body, context.parsed)
|
||||
});
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function shouldFallbackInspectBySession(context: any, response: any): boolean {
|
||||
if (!text(context.parsed.sessionId)) return false;
|
||||
if (text(context.parsed.traceId) || text(context.parsed.conversationId) || text(context.parsed.threadId)) return false;
|
||||
return response.status === 404 || response.body?.status === "not_found" || response.body?.ok === false;
|
||||
}
|
||||
|
||||
async function inspectConversationBySessionId(context: any, sessionId: string) {
|
||||
const projectId = text(context.parsed.projectId) || DEFAULT_WORKBENCH_PROJECT_ID;
|
||||
const limit = numberOption(context.parsed.limit) ?? 100;
|
||||
const listPath = `/v1/agent/conversations?projectId=${encodeURIComponent(projectId)}&limit=${encodeURIComponent(String(limit))}`;
|
||||
const listResponse = await requestJson({ ...context, method: "GET", path: listPath });
|
||||
if (!responseSucceeded(listResponse)) return null;
|
||||
const match = Array.isArray(listResponse.body?.conversations)
|
||||
? listResponse.body.conversations.find((item: any) => text(item?.sessionId ?? item?.session?.sessionId) === sessionId) ?? null
|
||||
: null;
|
||||
const conversationId = text(match?.conversationId);
|
||||
if (!conversationId) return null;
|
||||
const inspectPath = `/v1/agent/conversations/${encodeURIComponent(conversationId)}`;
|
||||
const inspectResponse = await requestJson({ ...context, method: "GET", path: inspectPath, extraHeaders: text(match?.lastTraceId) ? { "x-trace-id": text(match.lastTraceId) } : undefined });
|
||||
return responsePayload("client.agent.inspect", inspectResponse, context, {
|
||||
route: route("GET", inspectPath),
|
||||
conversation: sessionSummary(inspectResponse.body?.conversation ?? match),
|
||||
fallbackLookup: {
|
||||
attempted: true,
|
||||
source: "client.session.list",
|
||||
listRoute: route("GET", listPath),
|
||||
matchedSessionId: sessionId,
|
||||
conversationId,
|
||||
traceId: text(match?.lastTraceId)
|
||||
},
|
||||
body: responseBodyForCli(inspectResponse.body, context.parsed)
|
||||
});
|
||||
return responsePayload("client.agent.inspect", response, context, { route: route("GET", pathName), body: responseBodyForCli(response.body, context.parsed) });
|
||||
}
|
||||
|
||||
function agentHelp() {
|
||||
@@ -959,7 +913,7 @@ function sessionHelp() {
|
||||
"switch CONVERSATION_ID|--conversation-id ID",
|
||||
"delete CONVERSATION_ID|--conversation-id ID --confirm",
|
||||
"inspect CONVERSATION_ID|--conversation-id ID [--trace-id TRACE]",
|
||||
"final-response CONVERSATION_ID|--conversation-id ID [--trace-id TRACE] [--project-id PROJECT] [--fallback-text TEXT]"
|
||||
"final-response CONVERSATION_ID|--conversation-id ID [--trace-id TRACE] [--project-id PROJECT]"
|
||||
]
|
||||
});
|
||||
}
|
||||
@@ -1087,8 +1041,7 @@ async function sessionFinalResponseCommand(context: any) {
|
||||
inspectConversation: conversation,
|
||||
listedConversation,
|
||||
traceId,
|
||||
resultBody: resultResponse.body,
|
||||
fallbackText: text(context.parsed.fallbackText) || "Code Agent 仍在处理,可以继续 steer 或等待 trace 完成。"
|
||||
resultBody: resultResponse.body
|
||||
});
|
||||
const success = responseSucceeded(inspectResponse) && responseSucceeded(listResponse) && responseSucceeded(resultResponse) && validation.ok;
|
||||
return {
|
||||
@@ -2793,8 +2746,7 @@ function finalResponseEarlyValidation({ conversationId, projectId, routeName, re
|
||||
checks: {
|
||||
inspectFound: false,
|
||||
listFound: false,
|
||||
resultHasFinalText: false,
|
||||
fallbackTextAbsent: false
|
||||
resultHasFinalText: false
|
||||
},
|
||||
httpStatus: response?.status,
|
||||
error: response?.body?.error && typeof response.body.error === "object"
|
||||
@@ -2808,19 +2760,12 @@ function finalResponseEarlyValidation({ conversationId, projectId, routeName, re
|
||||
};
|
||||
}
|
||||
|
||||
function finalResponseValidation({ conversationId, projectId, inspectConversation, listedConversation, traceId, resultBody, fallbackText }: any) {
|
||||
function finalResponseValidation({ conversationId, projectId, inspectConversation, listedConversation, traceId, resultBody }: any) {
|
||||
const inspectedLatest = latestAgentMessage(inspectConversation);
|
||||
const listedLatest = latestAgentMessage(listedConversation);
|
||||
const inspectText = text(inspectedLatest?.text);
|
||||
const listText = text(listedLatest?.text);
|
||||
const resultText = assistantText(resultBody) || "";
|
||||
const fallback = text(fallbackText);
|
||||
const textPairs = [
|
||||
{ source: "inspect", text: inspectText },
|
||||
{ source: "list", text: listText },
|
||||
{ source: "result", text: resultText }
|
||||
];
|
||||
const fallbackHits = fallback ? textPairs.filter((item) => item.text.includes(fallback)).map((item) => item.source) : [];
|
||||
const failures = [
|
||||
!inspectConversation ? "inspect-conversation-missing" : "",
|
||||
!listedConversation ? "list-conversation-missing" : "",
|
||||
@@ -2831,8 +2776,7 @@ function finalResponseValidation({ conversationId, projectId, inspectConversatio
|
||||
inspectedLatest && text(inspectedLatest.traceId) !== traceId ? "inspect-latest-agent-trace-mismatch" : "",
|
||||
listedLatest && text(listedLatest.traceId) !== traceId ? "list-latest-agent-trace-mismatch" : "",
|
||||
inspectText && resultText && inspectText !== resultText ? "inspect-text-result-mismatch" : "",
|
||||
listText && resultText && listText !== resultText ? "list-text-result-mismatch" : "",
|
||||
fallbackHits.length > 0 ? "fallback-text-present" : ""
|
||||
listText && resultText && listText !== resultText ? "list-text-result-mismatch" : ""
|
||||
].filter(Boolean);
|
||||
return {
|
||||
ok: failures.length === 0,
|
||||
@@ -2852,10 +2796,8 @@ function finalResponseValidation({ conversationId, projectId, inspectConversatio
|
||||
inspectLatestAgentTraceMatches: inspectedLatest ? text(inspectedLatest.traceId) === traceId : false,
|
||||
listLatestAgentTraceMatches: listedLatest ? text(listedLatest.traceId) === traceId : false,
|
||||
inspectTextMatchesResult: Boolean(inspectText && resultText && inspectText === resultText),
|
||||
listTextMatchesResult: Boolean(listText && resultText && listText === resultText),
|
||||
fallbackTextAbsent: fallbackHits.length === 0
|
||||
listTextMatchesResult: Boolean(listText && resultText && listText === resultText)
|
||||
},
|
||||
fallbackText: fallback ? { checked: true, presentIn: fallbackHits } : { checked: false, presentIn: [] },
|
||||
repair: clean({
|
||||
inspectSnapshotSource: text(inspectConversation?.snapshot?.source),
|
||||
listSnapshotSource: text(listedConversation?.snapshot?.source),
|
||||
@@ -3141,8 +3083,8 @@ function traceBodyForCli(body: any, parsed: ParsedArgs) {
|
||||
function webTraceRenderBody(traceObject: any, parsed: ParsedArgs) {
|
||||
const events = Array.isArray(traceObject?.events) ? traceObject.events : [];
|
||||
const rows = traceDisplayRows(traceObject ?? {}, events);
|
||||
const fallbackRows = rows.length > 0 ? [] : persistentTraceFallbackRows(traceObject);
|
||||
const allRows = rows.length > 0 ? rows : fallbackRows;
|
||||
const terminalRows = rows.length > 0 ? [] : terminalTraceEvidenceRows(traceObject);
|
||||
const allRows = rows.length > 0 ? rows : terminalRows;
|
||||
const noiseEventCount = traceNoiseEventCount(events);
|
||||
const rowLimit = Math.max(1, Math.min(numberOption(parsed.limit) ?? 80, 500));
|
||||
const rowTail = parsed.full === true ? allRows : allRows.slice(-rowLimit);
|
||||
@@ -3165,35 +3107,35 @@ function webTraceRenderBody(traceObject: any, parsed: ParsedArgs) {
|
||||
assistantText: assistantText(traceObject),
|
||||
rows: rowTail.map(compactTraceRenderRow),
|
||||
retention: traceObject?.retention,
|
||||
fallback: traceObject?.fallback,
|
||||
terminalEvidence: traceObject?.terminalEvidence,
|
||||
finalResponse: traceObject?.finalResponse,
|
||||
traceSummary: traceObject?.traceSummary,
|
||||
fullBodyAvailable: true
|
||||
});
|
||||
}
|
||||
|
||||
function persistentTraceFallbackRows(traceObject: any) {
|
||||
const fallback = traceObject?.fallback && typeof traceObject.fallback === "object" ? traceObject.fallback : null;
|
||||
const finalResponse = traceObject?.finalResponse ?? fallback?.finalResponse;
|
||||
const traceSummary = traceObject?.traceSummary ?? fallback?.traceSummary;
|
||||
function terminalTraceEvidenceRows(traceObject: any) {
|
||||
const evidence = traceObject?.terminalEvidence && typeof traceObject.terminalEvidence === "object" ? traceObject.terminalEvidence : null;
|
||||
const finalResponse = traceObject?.finalResponse ?? evidence?.finalResponse;
|
||||
const traceSummary = traceObject?.traceSummary ?? evidence?.traceSummary;
|
||||
const textValue = text(finalResponse?.text ?? finalResponse?.textPreview ?? traceSummary?.finalAssistantRow?.textPreview);
|
||||
if (!fallback?.available && !textValue && !traceSummary) return [];
|
||||
if (!evidence?.available && !textValue && !traceSummary) return [];
|
||||
const body = [
|
||||
textValue ? `final assistant: ${textValue}` : "final assistant: unavailable in persisted snapshot",
|
||||
`source: ${text(traceSummary?.source ?? fallback?.source ?? "agent-session-snapshot")}`,
|
||||
textValue ? `final assistant: ${textValue}` : "final assistant: unavailable in AgentRun terminal evidence",
|
||||
`source: ${text(traceSummary?.source ?? evidence?.source ?? "agentrun-command-result")}`,
|
||||
`terminalStatus: ${text(traceSummary?.terminalStatus ?? finalResponse?.status ?? traceObject?.status ?? "unknown")}`,
|
||||
`sourceEventCount: ${traceSummary?.sourceEventCount ?? traceObject?.eventCount ?? 0}`,
|
||||
fallback?.conversationId ? `conversationId: ${fallback.conversationId}` : null,
|
||||
fallback?.sessionId ? `sessionId: ${fallback.sessionId}` : null,
|
||||
fallback?.threadId ? `threadId: ${fallback.threadId}` : null,
|
||||
fallback?.agentRun?.runId ? `runId: ${fallback.agentRun.runId}` : null,
|
||||
fallback?.agentRun?.commandId ? `commandId: ${fallback.agentRun.commandId}` : null
|
||||
evidence?.conversationId ? `conversationId: ${evidence.conversationId}` : null,
|
||||
evidence?.sessionId ? `sessionId: ${evidence.sessionId}` : null,
|
||||
evidence?.threadId ? `threadId: ${evidence.threadId}` : null,
|
||||
evidence?.agentRun?.runId ? `runId: ${evidence.agentRun.runId}` : null,
|
||||
evidence?.agentRun?.commandId ? `commandId: ${evidence.agentRun.commandId}` : null
|
||||
].filter(Boolean).join("\n");
|
||||
return [{
|
||||
rowId: `${traceObject?.traceId ?? "trace"}:persisted-summary`,
|
||||
rowId: `${traceObject?.traceId ?? "trace"}:terminal-evidence`,
|
||||
seq: null,
|
||||
tone: "info",
|
||||
header: "历史 trace 已过期,显示持久化摘要",
|
||||
header: "历史 trace 已过期,显示 AgentRun 终态证据",
|
||||
bodyFormat: "text",
|
||||
terminal: true,
|
||||
body
|
||||
@@ -3217,7 +3159,7 @@ function traceResponseAliases(traceBody: any, parsed: ParsedArgs) {
|
||||
eventCountMismatch: traceBody.eventCountMismatch,
|
||||
assistantText: traceBody.assistantText,
|
||||
retention: traceBody.retention,
|
||||
fallback: traceBody.fallback,
|
||||
terminalEvidence: traceBody.terminalEvidence,
|
||||
finalResponse: traceBody.finalResponse,
|
||||
traceSummary: traceBody.traceSummary
|
||||
});
|
||||
@@ -3317,7 +3259,7 @@ function compactAgentRun(value: any) {
|
||||
}
|
||||
|
||||
function assistantText(body: any) {
|
||||
const direct = text(body?.reply?.content ?? body?.message?.content ?? body?.assistantText ?? body?.finalResponse?.text ?? body?.fallback?.finalResponse?.text);
|
||||
const direct = text(body?.reply?.content ?? body?.message?.content ?? body?.assistantText ?? body?.finalResponse?.text ?? body?.terminalEvidence?.finalResponse?.text);
|
||||
if (direct) return direct;
|
||||
if (Array.isArray(body?.assistantStreams)) {
|
||||
for (const item of [...body.assistantStreams].reverse()) {
|
||||
@@ -3464,7 +3406,7 @@ function compactBody(body: any) {
|
||||
blocker: body.blocker,
|
||||
summary: body.summary,
|
||||
retention: body.retention,
|
||||
fallback: body.fallback,
|
||||
terminalEvidence: body.terminalEvidence,
|
||||
finalResponse: body.finalResponse,
|
||||
traceSummary: body.traceSummary,
|
||||
targetId: body.targetId,
|
||||
|
||||
@@ -64,15 +64,15 @@ test("message trace panel collapses automatically when final response is availab
|
||||
assert.doesNotMatch(html, /<details[^>]* open=""/u);
|
||||
});
|
||||
|
||||
test("message trace panel renders persisted fallback for expired historical trace", () => {
|
||||
test("message trace panel renders AgentRun terminal evidence for expired historical trace", () => {
|
||||
const trace: RunnerTrace = {
|
||||
traceId: "trc_issue932_expired",
|
||||
status: "expired",
|
||||
traceStatus: "expired",
|
||||
events: [],
|
||||
eventCount: 31,
|
||||
fallback: { available: true, source: "agent-session-snapshot" },
|
||||
traceSummary: { sourceEventCount: 31, terminalStatus: "completed", source: "agent-session-snapshot" },
|
||||
terminalEvidence: { available: true, source: "agentrun-command-result" },
|
||||
traceSummary: { sourceEventCount: 31, terminalStatus: "completed", source: "agentrun-command-result" },
|
||||
finalResponse: { text: "历史最终回复" }
|
||||
};
|
||||
|
||||
@@ -80,6 +80,7 @@ test("message trace panel renders persisted fallback for expired historical trac
|
||||
|
||||
assert.match(html, /历史 trace 已过期/u);
|
||||
assert.match(html, /历史最终回复/u);
|
||||
assert.match(html, /AgentRun 终态证据/u);
|
||||
assert.match(html, /sourceEventCount=31/u);
|
||||
assert.doesNotMatch(html, /等待后端事件/u);
|
||||
});
|
||||
|
||||
@@ -51,7 +51,7 @@ export function MessageTracePanel({ trace, defaultOpen, storageKey, collapseWhen
|
||||
const last = summarizeLastEvent(trace);
|
||||
const rows = useMemo<TraceEventRow[]>(() => {
|
||||
const eventRows = traceDisplayRows(trace, events);
|
||||
return eventRows.length > 0 ? eventRows : persistentTraceFallbackRows(trace);
|
||||
return eventRows.length > 0 ? eventRows : terminalTraceEvidenceRows(trace);
|
||||
}, [trace, events]);
|
||||
const noiseCount = useMemo<number>(() => traceNoiseEventCount(events), [events]);
|
||||
const lastUpdatedAt = traceUpdatedAt(trace, events, last.ts);
|
||||
@@ -119,34 +119,34 @@ function traceCountText(trace: RunnerTrace, events: number, rows: number, noiseC
|
||||
return `${rows}/${events}/${rawTotal} 行${noise}`;
|
||||
}
|
||||
|
||||
function persistentTraceFallbackRows(trace: RunnerTrace): TraceEventRow[] {
|
||||
const fallback = objectOrNull(trace.fallback);
|
||||
function terminalTraceEvidenceRows(trace: RunnerTrace): TraceEventRow[] {
|
||||
const evidence = objectOrNull(trace.terminalEvidence);
|
||||
const summary = objectOrNull(trace.traceSummary);
|
||||
const response = objectOrNull(trace.finalResponse);
|
||||
const text = firstNonEmptyTraceText(response?.text, response?.content, response?.message, summary?.finalText, summary?.text);
|
||||
const sourceCount = positiveInteger(summary?.sourceEventCount ?? summary?.eventCount ?? trace.eventCount);
|
||||
if (!text && fallback?.available !== true && sourceCount === 0) return [];
|
||||
if (!text && evidence?.available !== true && sourceCount === 0) return [];
|
||||
const status = firstNonEmptyTraceText(trace.traceStatus, trace.status, summary?.terminalStatus, objectOrNull(trace.agentRun)?.terminalStatus) ?? "expired";
|
||||
const expired = isExpiredTraceStatus(trace.traceStatus, trace.status) || isExpiredTraceFallback(fallback);
|
||||
const expired = isExpiredTraceStatus(trace.traceStatus, trace.status) || isExpiredTerminalEvidence(evidence);
|
||||
const header = expired
|
||||
? `${trace.traceId ?? "trace"} 历史 trace 已过期,显示持久化摘要`
|
||||
: `${trace.traceId ?? "trace"} 完整 trace 回放中,先显示持久化摘要`;
|
||||
? `${trace.traceId ?? "trace"} 历史 trace 已过期,显示 AgentRun 终态证据`
|
||||
: `${trace.traceId ?? "trace"} 完整 trace 回放中,先显示 AgentRun 终态证据`;
|
||||
const meta = [
|
||||
`traceStatus=${status}`,
|
||||
sourceCount > 0 ? `sourceEventCount=${sourceCount}` : null,
|
||||
firstNonEmptyTraceText(fallback?.source, summary?.source) ? `source=${firstNonEmptyTraceText(fallback?.source, summary?.source)}` : null
|
||||
firstNonEmptyTraceText(evidence?.source, summary?.source) ? `source=${firstNonEmptyTraceText(evidence?.source, summary?.source)}` : null
|
||||
].filter(Boolean).join("\n");
|
||||
const body = [meta, text].filter(Boolean).join("\n\n") || (expired ? "后端没有保留原始 event,但已返回持久化 trace 摘要。" : "后端声明存在原始 event,正在通过 trace 回放入口补齐完整事件列表。");
|
||||
return [{ rowId: `fallback:${trace.traceId ?? "trace"}`, seq: null, tone: "source", header, body, terminal: true, bodyFormat: text ? "markdown" : "text" }];
|
||||
const body = [meta, text].filter(Boolean).join("\n\n") || (expired ? "后端没有保留原始 event,但 AgentRun command result 已返回终态证据。" : "后端声明存在原始 event,正在通过 trace 回放入口补齐完整事件列表。");
|
||||
return [{ rowId: `terminal:${trace.traceId ?? "trace"}`, seq: null, tone: "source", header, body, terminal: true, bodyFormat: text ? "markdown" : "text" }];
|
||||
}
|
||||
|
||||
function isExpiredTraceStatus(...values: unknown[]): boolean {
|
||||
return values.some((value) => ["expired", "missing"].includes(String(value ?? "").trim().toLowerCase()));
|
||||
}
|
||||
|
||||
function isExpiredTraceFallback(fallback: Record<string, unknown> | null): boolean {
|
||||
const store = String(fallback?.liveTraceStore ?? "").trim().toLowerCase();
|
||||
const source = String(fallback?.source ?? "").trim().toLowerCase();
|
||||
function isExpiredTerminalEvidence(evidence: Record<string, unknown> | null): boolean {
|
||||
const store = String(evidence?.liveTraceStore ?? "").trim().toLowerCase();
|
||||
const source = String(evidence?.source ?? "").trim().toLowerCase();
|
||||
return store.includes("expired") || source.includes("expired");
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { AgentChatReply, AgentRunProvenance, CodeAgentAvailability, LiveSur
|
||||
import { firstNonEmptyString, jsonPreview, nonEmptyString, shortToken } from "../utils";
|
||||
|
||||
export interface CodeAgentStatusSummary {
|
||||
kind: "long-lived-session" | "blocked" | "skill-cli-api-blocked" | "read-only-session-tools" | "stateless-one-shot" | "fallback-text-chat-only" | "degraded" | "unverified";
|
||||
kind: "long-lived-session" | "blocked" | "skill-cli-api-blocked" | "read-only-session-tools" | "stateless-one-shot" | "degraded" | "unverified";
|
||||
tone: "ok" | "warn" | "blocked" | "source" | "dev-live";
|
||||
label: string;
|
||||
icon: string;
|
||||
@@ -384,7 +384,7 @@ export function codeAgentSummaryRows(summary: CodeAgentStatusSummary): { key: st
|
||||
function sessionSummaryTone(summary: CodeAgentStatusSummary): CodeAgentStatusSummary["tone"] {
|
||||
if (summary.kind === "long-lived-session") return "ok";
|
||||
if (summary.kind === "blocked") return "blocked";
|
||||
if (["read-only-session-tools", "stateless-one-shot", "fallback-text-chat-only", "degraded"].includes(summary.kind)) return "warn";
|
||||
if (["read-only-session-tools", "stateless-one-shot", "degraded"].includes(summary.kind)) return "warn";
|
||||
return "source";
|
||||
}
|
||||
|
||||
@@ -397,4 +397,4 @@ export function summarizeLastEvent(trace: RunnerTrace | null | undefined): { lab
|
||||
const status = String(last?.status ?? "");
|
||||
const tone: CodeAgentStatusSummary["tone"] = status === "completed" ? "ok" : status === "running" || status === "pending" ? "warn" : status === "failed" ? "blocked" : "source";
|
||||
return { label: lastLabel, ts, tone };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,11 +128,11 @@ test("mergeTraceResults prefers persisted finalResponse text for expired or comp
|
||||
const finalMarkdown = "**结论**\n\n- 自然结束和刷新后都应该显示这一段。\n- Markdown 换行必须保留。";
|
||||
const terminal: AgentChatResultResponse = {
|
||||
status: "completed",
|
||||
traceId: "trc_issue959_fallback",
|
||||
traceId: "trc_issue959_terminal",
|
||||
assistantText: "自然运行时的压平文本: **结论** - 自然结束和刷新后都应该显示这一段。"
|
||||
};
|
||||
const trace: TraceSnapshot = {
|
||||
traceId: "trc_issue959_fallback",
|
||||
traceId: "trc_issue959_terminal",
|
||||
status: "completed",
|
||||
events: [],
|
||||
eventCount: 63,
|
||||
@@ -514,7 +514,7 @@ test("fetchTraceSnapshot and replayFullTrace use the active workbench project id
|
||||
}
|
||||
});
|
||||
|
||||
test("fetchTraceSnapshot accepts expired trace fallback without waiting for raw events", async () => {
|
||||
test("fetchTraceSnapshot accepts expired AgentRun terminal evidence without waiting for raw events", async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
const fetches: string[] = [];
|
||||
globalThis.fetch = (async (input: RequestInfo | URL) => {
|
||||
@@ -523,20 +523,20 @@ test("fetchTraceSnapshot accepts expired trace fallback without waiting for raw
|
||||
return jsonResponse({
|
||||
status: "completed",
|
||||
traceStatus: "expired",
|
||||
traceId: "trc_expired_fallback",
|
||||
traceId: "trc_expired_terminal",
|
||||
events: [],
|
||||
eventCount: 0,
|
||||
fallback: { available: true, source: "agent-session-snapshot" },
|
||||
traceSummary: { sourceEventCount: 31, terminalStatus: "completed" },
|
||||
terminalEvidence: { available: true, source: "agentrun-command-result" },
|
||||
traceSummary: { source: "agentrun-command-result", sourceEventCount: 31, terminalStatus: "completed" },
|
||||
finalResponse: { text: "过期 trace 的最终回复" }
|
||||
});
|
||||
}) as typeof fetch;
|
||||
|
||||
try {
|
||||
const snapshot = await fetchTraceSnapshot("trc_expired_fallback", "trc_expired_fallback", 1000);
|
||||
const snapshot = await fetchTraceSnapshot("trc_expired_terminal", "trc_expired_terminal", 1000);
|
||||
assert.equal(snapshot?.status, "expired");
|
||||
assert.equal(snapshot?.eventCount, 31);
|
||||
assert.equal((snapshot?.fallback as { source?: string })?.source, "agent-session-snapshot");
|
||||
assert.equal((snapshot?.terminalEvidence as { source?: string })?.source, "agentrun-command-result");
|
||||
assert.equal((snapshot?.finalResponse as { text?: string })?.text, "过期 trace 的最终回复");
|
||||
assert.equal(fetches.length, 1);
|
||||
} finally {
|
||||
|
||||
@@ -20,7 +20,7 @@ export interface TraceSnapshot {
|
||||
agentRun?: AgentRunProvenance;
|
||||
traceStatus?: string;
|
||||
retention?: unknown;
|
||||
fallback?: unknown;
|
||||
terminalEvidence?: unknown;
|
||||
finalResponse?: unknown;
|
||||
traceSummary?: unknown;
|
||||
lastEventLabel?: string;
|
||||
@@ -77,7 +77,7 @@ export function snapshotToRunnerTrace(snapshot: TraceSnapshot): NonNullable<Chat
|
||||
eventsCompacted: snapshot.eventsCompacted,
|
||||
traceStatus: snapshot.traceStatus,
|
||||
retention: snapshot.retention,
|
||||
fallback: snapshot.fallback,
|
||||
terminalEvidence: snapshot.terminalEvidence,
|
||||
finalResponse: snapshot.finalResponse,
|
||||
traceSummary: snapshot.traceSummary,
|
||||
agentRun: snapshot.agentRun,
|
||||
@@ -116,7 +116,7 @@ export function mergeTraceResults(terminal: AgentChatResultResponse, trace: Trac
|
||||
eventCount: trace.eventCount ?? events.length,
|
||||
traceStatus: trace.traceStatus ?? terminal.traceStatus,
|
||||
retention: trace.retention ?? terminal.retention,
|
||||
fallback: trace.fallback ?? terminal.fallback,
|
||||
terminalEvidence: trace.terminalEvidence ?? terminal.terminalEvidence,
|
||||
finalResponse: trace.finalResponse ?? terminal.finalResponse,
|
||||
traceSummary: trace.traceSummary ?? terminal.traceSummary,
|
||||
lastEventLabel: trace.lastEventLabel ?? terminal.lastEventLabel ?? events.at(-1)?.label
|
||||
@@ -357,7 +357,7 @@ export async function subscribeToTrace(config: TraceSubscriptionConfig): Promise
|
||||
agentRun: tracePolled.data.agentRun,
|
||||
traceStatus: typeof tracePolled.data.traceStatus === "string" ? tracePolled.data.traceStatus : undefined,
|
||||
retention: tracePolled.data.retention,
|
||||
fallback: tracePolled.data.fallback,
|
||||
terminalEvidence: tracePolled.data.terminalEvidence,
|
||||
finalResponse: tracePolled.data.finalResponse,
|
||||
traceSummary: tracePolled.data.traceSummary,
|
||||
lastEventLabel: String(lastEvent?.label ?? lastEvent?.type ?? tracePolled.data.lastEventLabel ?? ""),
|
||||
@@ -426,7 +426,7 @@ export async function fetchTraceSnapshot(traceId: string, traceUrlOrId: string,
|
||||
if (signal?.aborted) return null;
|
||||
if (tracePolled.ok && tracePolled.data) {
|
||||
const snapshot = snapshotFromTracePoll(traceId, tracePolled.data);
|
||||
if (hasTraceEvents(snapshot) || hasPersistedTraceFallback(snapshot)) return snapshot;
|
||||
if (hasTraceEvents(snapshot) || hasTerminalTraceEvidence(snapshot)) return snapshot;
|
||||
}
|
||||
if (attempts >= maxAttempts || Date.now() - startedAt >= totalTimeoutMs) break;
|
||||
await new Promise<void>((resolve) => window.setTimeout(resolve, Math.min(TRACE_SNAPSHOT_RETRY_INTERVAL_MS, totalTimeoutMs - (Date.now() - startedAt))));
|
||||
@@ -450,11 +450,11 @@ function hasTraceEvents(snapshot: TraceSnapshot): boolean {
|
||||
return Array.isArray(snapshot.events) && snapshot.events.length > 0;
|
||||
}
|
||||
|
||||
function hasPersistedTraceFallback(snapshot: TraceSnapshot): boolean {
|
||||
const fallback = objectOrNull(snapshot.fallback);
|
||||
function hasTerminalTraceEvidence(snapshot: TraceSnapshot): boolean {
|
||||
const evidence = objectOrNull(snapshot.terminalEvidence);
|
||||
const summary = objectOrNull(snapshot.traceSummary);
|
||||
return Boolean(
|
||||
fallback?.available === true ||
|
||||
evidence?.available === true ||
|
||||
finalResponseText(snapshot.finalResponse) ||
|
||||
positiveNumber(summary?.sourceEventCount ?? summary?.eventCount) > 0
|
||||
);
|
||||
@@ -472,7 +472,7 @@ function snapshotToTraceSnapshot(result: AgentChatResultResponse): TraceSnapshot
|
||||
eventCount: traceSnapshotEventCount(result, events),
|
||||
traceStatus: typeof result.traceStatus === "string" ? result.traceStatus : undefined,
|
||||
retention: result.retention,
|
||||
fallback: result.fallback,
|
||||
terminalEvidence: result.terminalEvidence,
|
||||
finalResponse: result.finalResponse,
|
||||
traceSummary: result.traceSummary,
|
||||
agentRun: result.agentRun,
|
||||
|
||||
Reference in New Issue
Block a user