fix: unify AgentRun trace result authority

This commit is contained in:
Codex Agent
2026-06-06 13:08:00 +08:00
parent a97fd6c3e2
commit 1830df467e
7 changed files with 613 additions and 244 deletions
+1
View File
@@ -88,6 +88,7 @@ HWLAB 是硬件实验室运行面和控制面项目。本文是 agent、指挥
- v0.2 `hwlab-cloud-web` 浏览器工作台规格:[docs/reference/spec-v02-hwlab-cloud-web.md](docs/reference/spec-v02-hwlab-cloud-web.md)。
- v0.2 Provider API Key 管理页、HWLAB 鉴权后委托 AgentRun 后端和 DeepSeek 官方链路验证规格:[docs/reference/spec-v02-provider-management.md](docs/reference/spec-v02-provider-management.md)。
- v0.2 Code Agent 由 `hwlab-cloud-api` 接入 AgentRun v0.1 共享执行基础设施,不再保留 HWLAB 自有 agent manager/worker 控制面:[docs/reference/agentrun-code-agent-dispatch.md](docs/reference/agentrun-code-agent-dispatch.md)。
- v0.2 Code Agent trace/result/final response 的权威数据源、派生缓存边界和 Web/CLI trace 展示路径:[docs/reference/spec-v02-code-agent-trace.md](docs/reference/spec-v02-code-agent-trace.md)。
- v0.2 `hwlab-agent-skills` 技能包服务规格:[docs/reference/spec-v02-hwlab-agent-skills.md](docs/reference/spec-v02-hwlab-agent-skills.md)。
- v0.2 `hwlab-cli` 固定 repo 短连接 client 规格:[docs/reference/spec-v02-hwlab-cli.md](docs/reference/spec-v02-hwlab-cli.md)。
- HWPOD Harness 快速迭代规格,定义 workspace-local `hwpod-spec``hwpod-cli``hwpod-ctl``hwpod-compiler-cli``hwpod-node-ops``hwpod-node`[docs/reference/spec-hwpod-harness.md](docs/reference/spec-hwpod-harness.md)。
+101
View File
@@ -0,0 +1,101 @@
# v0.2 Code Agent Trace 规格
本文定义 HWLAB v0.2 Code Agent trace、result 和 Cloud Web trace 展示的数据权威、读写路径和回归判定。Trace 是 Workbench 最复杂的用户可见状态面;实现必须先收敛 source of truth,再生成派生 UI/cache,不得通过叠加 fallback、多路径或兼容快照掩盖状态污染。
## 身份模型
| 身份 | 归属 | 语义 |
| --- | --- | --- |
| `traceId` | HWLAB Cloud API | 用户一次 Code Agent turn 的可见追踪 ID,也是 result/trace API 的主键。 |
| `conversationId` | HWLAB Workbench | 用户可见对话容器;只用于业务上下文和 UI 分组。 |
| `sessionId` | HWLAB Code Agent session | 用户显式创建或选择的业务 session,绑定 owner、provider profile 和权限。 |
| `threadId` | HWLAB/AgentRun/Codex | 会话连续性的唯一标准字段;不得新增历史 thread 别名或拼 prompt 替代。 |
| `runId` | AgentRun | 可复用执行壳;同一个 run 可承载同一 session 的多条 command。 |
| `commandId` | AgentRun | 单个 turn 的执行 identity;同一 `runId` 下每个 `traceId` 必须映射到唯一 command。 |
| `runnerId` / `jobName` | AgentRun / k8s | runner 尝试身份,只能作为执行诊断,不代表业务 session。 |
`traceId -> AgentRun commandId` 是 AgentRun 接入后的核心映射。`runId` 只能说明执行壳复用,不能单独说明某个用户 turn 的 final response。Cloud Web、CLI 和 issue closeout 必须同时展示业务身份和执行身份,避免把共享 run 的上一个 command 误认为当前 trace。
## 权威数据源
- **AgentRun command registry 是 completed AgentRun trace 的执行权威**Cloud API 必须通过 `GET /api/v1/runs/{runId}/commands?afterSeq=0&limit=100``idempotencyKey === traceId``payload.traceId === traceId` 找到当前 trace 的 command。
- **AgentRun command result 是 final response 权威**:找到 command 后,Cloud API 必须通过 `GET /api/v1/runs/{runId}/commands/{commandId}/result` 生成 `reply.content``finalResponse``providerTrace` 和 completed result payload。
- **AgentRun events 是 trace row 权威**Cloud API 从 `GET /api/v1/runs/{runId}/events` 读取事件,并按当前 `commandId` 过滤 assistant/tool/terminal rows。旧 command 的 assistant/tool/terminal 事件不得进入当前 trace 的可读事件列表。
- **`agent_sessions` 是业务 session 和 seed store,不是 final response authority**:它保存 owner、project、conversation/session/thread、lastTraceId、AgentRun run seed 和必要派生证据;读取 terminal result 时不得把 `session.finalResponse``session.traceSummary` 或旧 `traceResults` 当成 completed 文本权威。
- **内存 `codeAgentChatResults` 是短连接缓存,不是 terminal authority**running 状态可用它承接轮询状态;terminal AgentRun result 再次读取时必须重新走 command registry/result 或使用由该路径刚生成的新 payload。
- **`traceStore` 是 live event buffer,不是历史 final authority**:它服务增量轮询和短期 trace 展示;事件过期时可以报告 missing/expired,但不能用别的 trace 的 final response 填补。
## 写入流程
1. Cloud Web 或 `hwlab-cli client` 通过同源 `POST /v1/agent/chat` 提交 turn;请求必须携带显式选中的 `conversationId/sessionId/threadId` 和 provider profile 语义。
2. Cloud API 认证 actor,按 session owner 和 tool capability 装配 AgentRun 请求;HWPOD/runner 使用的用户 API key 只允许来自 owner 的 `HWLAB_API_KEY`,浏览器 session、cookie 或其他 env 别名不得成为 CLI/runner key 来源。
3. Cloud API 创建或复用 AgentRun `runId`,再创建新 commandcommand `idempotencyKey``payload.traceId` 必须等于本次 `traceId``payload.conversationId``payload.hwlabSessionId``payload.threadId``payload.providerProfile` 必须保留。
4. Cloud API 写入 session seed:业务 session、lastTraceId、AgentRun `runId`、可选当前 `commandId`、provider profile 和 owner 归属。该 seed 只用于后续找到 run,不承诺 final response 正确。
5. 初始 result 返回 `running/accepted`Web 和 CLI 进入短连接 result/trace 轮询。
写入路径不得把浏览器历史 messages、旧 trace final response、旧 command tail 或 session top-level snapshot 拼入新 command payload。历史只用于 UI 展示和排障,不是模型上下文,也不是新 result 的数据来源。
## Result 读取流程
`GET /v1/agent/chat/result/{traceId}` 是 terminal result 的唯一用户入口。AgentRun adapter 启用时,Cloud API 必须执行以下顺序:
1. 校验 `traceId` 和 session owner;普通用户只能读自己的 trace,admin 按授权读。
2. 从内存缓存或 `agent_sessions` 读取最小 AgentRun seed,至少需要 `runId`;如果没有 seed,返回 not found,不猜测其他 session。
3. 对 terminal 或可能 terminal 的 AgentRun result,必须用 AgentRun command registry 按 `traceId` 解析当前 `commandId`。不得相信缓存里的 `agentRun.commandId``providerTrace.commandId``traceSummary.agentRun.commandId` 或 top-level `lastTraceId` 作为最终映射。
4. 找到 command 后,读取 command result,并从 command result 重建 `reply``finalResponse``providerTrace``traceSummary``agentRun` 字段。
5. 写回内存缓存和 session 派生证据时,只能写刚由 command result 生成的 payload;旧 `finalResponse`、旧 `traceSummary` 和旧 `traceResults` 不得参与 merge 覆盖。
6. 如果 command registry 找不到该 `traceId` 对应 command,返回结构化错误,例如 `agentrun_trace_command_not_found`;不得回退到旧缓存、top-level snapshot、latest trace 或其他 command result。
Running 轮询可以返回 `202` 和当前 runnerTrace;这只是“尚未 terminal”的轮询语义,不是 final fallback。Completed / failed / canceled / blocked 等 terminal 状态必须经过同一 command registry/result 路径确认。
## Trace 读取与渲染流程
`GET /v1/agent/chat/trace/{traceId}` 服务完整 trace 回放,Cloud Web 与 `hwlab-cli client agent trace --render web` 必须共享同一 row 转换语义:
1. Cloud API 先用 result 同一路径解析当前 `traceId -> commandId`;对于 AgentRun traceevents 必须按该 `commandId` 过滤。
2. 可读 rows 至少覆盖 request/setup、tool call、assistant message、terminal/result 和 blocker。低价值 AgentRun backend echo、状态 echo、terminal boilerplate 和重复噪声可被 row renderer 去噪,但原始 trace JSON 仍应通过 `--full` 或下载保留。
3. Web 默认显示完整可读事件列表。Result polling 返回的 compacted head-tail runnerTrace 不能冒充完整 trace;发现 `eventsCompacted=true` 时必须继续请求 `/trace/{traceId}`
4. 如果完整 trace 已过期或缺失,UI 必须显式展示 missing/expired,并提供 result/inspect 可用证据;不得用最新 trace 或 session final response 填补历史 trace。
5. 同一 run 多 command 时,旧 command 的 assistant/tool/terminal rows 不能挤到新 command 的 final response 或 trace 尾部。取消/失败前的有价值上下文应作为脱敏 conversation facts 或当前 command 自身事件展示,不通过串线实现。
## 派生缓存边界
允许存在的派生状态只有两个用途:降低轮询开销和提升 UI 可见性。派生状态必须可被权威路径重建,且不得成为 terminal result 的替代 authority。
| 派生状态 | 允许用途 | 禁止用途 |
| --- | --- | --- |
| `codeAgentChatResults` | 保存刚提交的 running result、刚由 command result 生成的 terminal payload。 | 在 terminal 读取时跳过 command registry;返回旧 final response。 |
| `session.traceResults` | 保存按 traceId 写回的审计证据,加快 inspect/list 展示。 | 覆盖 command result;把旧 command 文本当成当前 trace final。 |
| `session.agentRun` | 保存最近或可复用 run seed。 | 证明当前 trace 的 commandId 或 final response。 |
| `session.finalResponse` / `session.traceSummary` | 只作为最近消息的派生展示,必须标明 traceId。 | 历史 result/trace 的 authority;修复其他 trace 时覆盖 latest。 |
| `runnerTrace` compact window | result 轮询里的轻量进度。 | Web 完整 trace 列表;历史 trace closeout 证据。 |
当派生状态和 AgentRun command registry 冲突时,以 command registry 和 command result 为准;冲突的派生状态应被下一次权威读取覆盖或忽略,不新增兼容分支。
## 禁止路径
- 不得为 trace/result 增加 parallel final response path、legacy mode、feature flag、旧 top-level fallback 或“查不到 command 就用 latest/cached result”的分支。
- 不得把 `runId` 当成 trace authority;共享 run 下必须使用 `commandId` 分割事件和 final response。
- 不得把 `provider=agentrun-v01` 当成真实模型/provider`agentrun-v01` 只属于 adapter/runner infrastructure 字段。
- 不得在 Web、CLI 或 closeout 中把 `AgentRun result is ready`、terminal status echo、boilerplate completion row 或 placeholder status 当成 assistant final response。
- 不得通过拼接浏览器历史消息、旧 trace rows 或旧 summary 来制造 continuitycontinuity 只由标准 `threadId` 和 AgentRun/Codex 原生 resume 证明。
- 不得新增 `HWLAB_API_KEY` 以外的 CLI/runner API key 来源,也不得让 Web session/cookie 成为 runner key fallback。
## 回归测试要求
源码层测试必须覆盖以下最小场景:
- 同一 `runId` 下两个 `traceId` / `commandId`,读取第一条历史 trace 时不显示第二条 command 的 assistant/tool/terminal rows。
- 第二条 trace 的 session top-level、`traceResults` 或内存 cache 被污染成第一条 command/final 时,`GET /result/{secondTraceId}` 必须通过 command registry/result 返回第二条 command 的 final response。
- 已 completed 的 AgentRun 内存缓存也必须走 command registry/result;不能只同步 running。
- command registry 找不到目标 `traceId` 时返回结构化错误,不返回旧 cache/latest/top-level。
- Web renderer 的纯逻辑测试覆盖 request/setup、tool call、assistant markdown、completion row、noise count 和 compacted trace 自动回放条件。
真实入口验收必须走目标 lane 的 public Cloud Web origin 或 Web 等价 CLI`POST /v1/agent/chat``GET /v1/agent/chat/result/{traceId}``GET /v1/agent/chat/trace/{traceId}``inspect/session list`。仅有单测、PR merge、PipelineRun 或源码证据不能关闭 trace/result 类 issue。
## 相关文档
- [spec-v02-hwlab-cloud-web.md](spec-v02-hwlab-cloud-web.md)Cloud Web 工作台、trace renderer 和 Web/CLI 同路径要求。
- [agentrun-code-agent-dispatch.md](agentrun-code-agent-dispatch.md)AgentRun run/command/resource bundle/credential 装配边界。
- [code-agent-chat-readiness.md](code-agent-chat-readiness.md)Code Agent 真实 completed 判定和 provider readiness。
- [spec-user-access.md](spec-user-access.md)Code Agent session owner、trace/result/cancel 权限边界。
+15
View File
@@ -452,6 +452,11 @@ test("workbench workspace status clears completed AgentRun active trace on read"
{ id: "evt_status_done", runId: "run_workspace_status", seq: 1, type: "terminal_status", payload: { commandId: "cmd_workspace_status", terminalStatus: "completed" }, createdAt: "2026-06-01T00:00:01.000Z" }
] });
}
if (request.method === "GET" && url.pathname === "/api/v1/runs/run_workspace_status/commands") {
return send({ items: [
{ id: "cmd_workspace_status", runId: "run_workspace_status", state: "completed", type: "turn", seq: 1, idempotencyKey: "trc_issue664_status_done", payload: { traceId: "trc_issue664_status_done", conversationId: "cnv_issue664_status", hwlabSessionId: "ses_issue664_status", threadId: "thread-issue-664", providerProfile: "deepseek" } }
] });
}
if (request.method === "GET" && url.pathname === "/api/v1/runs/run_workspace_status/commands/cmd_workspace_status/result") {
return send({
runId: "run_workspace_status",
@@ -582,6 +587,11 @@ test("workbench workspace terminal status sync preserves a newer selected conver
{ id: "evt_issue808_done", runId: "run_issue808_old", seq: 1, type: "terminal_status", payload: { commandId: "cmd_issue808_old", terminalStatus: "completed" }, createdAt: "2026-06-01T00:00:01.000Z" }
] });
}
if (request.method === "GET" && url.pathname === "/api/v1/runs/run_issue808_old/commands") {
return send({ items: [
{ id: "cmd_issue808_old", runId: "run_issue808_old", state: "completed", type: "turn", seq: 1, idempotencyKey: "trc_issue808_old_done", payload: { traceId: "trc_issue808_old_done", conversationId: "cnv_issue808_old", hwlabSessionId: "ses_issue808_old", threadId: "thread-issue-808-old", providerProfile: "deepseek" } }
] });
}
if (request.method === "GET" && url.pathname === "/api/v1/runs/run_issue808_old/commands/cmd_issue808_old/result") {
return send({
runId: "run_issue808_old",
@@ -872,6 +882,11 @@ test("workbench workspace status repairs terminal selected conversation after ac
if (request.method === "GET" && url.pathname === "/api/v1/runs/run_workspace_repair/events") {
return send({ items: [] });
}
if (request.method === "GET" && url.pathname === "/api/v1/runs/run_workspace_repair/commands") {
return send({ items: [
{ id: "cmd_workspace_repair", runId: "run_workspace_repair", state: "completed", type: "turn", seq: 1, idempotencyKey: "trc_issue664_repair_done", payload: { traceId: "trc_issue664_repair_done", conversationId: "cnv_issue664_repair", hwlabSessionId: "ses_issue664_repair", threadId: "thread-issue-664-repair", providerProfile: "deepseek" } }
] });
}
if (request.method === "GET" && url.pathname === "/api/v1/runs/run_workspace_repair/commands/cmd_workspace_repair/result") {
return send({
runId: "run_workspace_repair",
+1 -1
View File
@@ -1344,7 +1344,7 @@ class AccessController {
const persisted = cached ? null : await loadPersistedAgentRunResult(safeTraceId, options);
const current = cached ?? persisted ?? null;
if (!current || !canActorReadWorkspaceTrace(current, actor)) return null;
const synced = current?.agentRun?.runId && current.status === "running"
const synced = current?.agentRun?.runId
? await syncAgentRunChatResult({ traceId: safeTraceId, currentResult: current, options, traceStore: options.traceStore })
: { result: current };
const result = synced.result ?? current;
+168 -219
View File
@@ -428,8 +428,9 @@ function isAgentRunCommandAlreadyClaimed(error) {
return statusCode === 409 && /command\s+[^\s]+\s+is not pending:/u.test(message);
}
export async function syncAgentRunChatResult({ traceId, currentResult = null, options = {}, traceStore = defaultCodeAgentTraceStore }) {
const mapped = currentResult ?? await loadPersistedAgentRunResult(traceId, options);
export async function syncAgentRunChatResult({ traceId, currentResult = null, options = {}, traceStore = defaultCodeAgentTraceStore, appendResultEvent = true }) {
const initial = currentResult ?? await loadPersistedAgentRunResult(traceId, options);
const mapped = initial ? await resolveAgentRunTraceCommandMapping({ traceId, mapped: initial, options }) : initial;
if (!mapped?.agentRun?.runId || !mapped?.agentRun?.commandId) return { result: currentResult, runnerTrace: traceStore.snapshot(traceId), found: Boolean(currentResult) };
const env = options.env ?? process.env;
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
@@ -454,13 +455,70 @@ export async function syncAgentRunChatResult({ traceId, currentResult = null, op
valuesPrinted: false
};
const base = { ...mapped, agentRun: nextMapping, updatedAt: nowIso(options.now) };
const payload = agentRunResultToCodeAgentPayload({ base, result, traceStore, traceId });
const payload = agentRunResultToCodeAgentPayload({ base, result, traceStore, traceId, appendResultEvent });
options.codeAgentChatResults?.set?.(traceId, payload);
return { result: payload, runnerTrace: payload.runnerTrace ?? traceStore.snapshot(traceId), found: true };
}
async function resolveAgentRunTraceCommandMapping({ traceId, mapped, options = {} }) {
const safeId = safeTraceId(traceId);
const agentRun = mapped?.agentRun && typeof mapped.agentRun === "object" ? mapped.agentRun : null;
if (!safeId || !agentRun?.runId) return mapped;
const agentRunTraceId = safeTraceId(agentRun.traceId);
const providerTraceId = safeTraceId(agentRun.providerTrace?.traceId);
const traceSummaryCommandId = text(mapped?.traceSummary?.agentRun?.commandId);
const commandId = text(agentRun.commandId);
const traceFieldsMatch = agentRunTraceId === safeId && providerTraceId === safeId && (!traceSummaryCommandId || traceSummaryCommandId === commandId);
if (mapped.status === "running" && traceFieldsMatch) return mapped;
const env = options.env ?? process.env;
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
const managerUrl = resolveAgentRunManagerUrl(env, agentRun.managerUrl);
const timeoutMs = parsePositiveInteger(env.HWLAB_CODE_AGENT_AGENTRUN_HTTP_TIMEOUT_MS, 20_000);
const command = await findAgentRunCommandForTrace({ fetchImpl, managerUrl, timeoutMs, runId: agentRun.runId, traceId: safeId });
if (!command?.id) {
throw Object.assign(new Error(`AgentRun command registry has no command for ${safeId} in run ${agentRun.runId}`), {
code: "agentrun_trace_command_not_found",
statusCode: 404,
traceId: safeId,
runId: agentRun.runId
});
}
if (command.id === commandId) {
return {
...mapped,
finalResponse: null,
traceSummary: null,
agentRun: {
...agentRun,
traceId: safeId,
lastSeq: 0,
providerTrace: null,
commandState: command.state ?? agentRun.commandState ?? null,
updatedAt: nowIso(options.now),
valuesPrinted: false
}
};
}
return {
...mapped,
finalResponse: null,
traceSummary: null,
agentRun: {
...agentRun,
commandId: command.id,
traceId: safeId,
lastSeq: 0,
providerTrace: null,
commandState: command.state ?? agentRun.commandState ?? null,
updatedAt: nowIso(options.now),
valuesPrinted: false
}
};
}
export async function refreshAgentRunTrace({ traceId, result = null, options = {}, traceStore = defaultCodeAgentTraceStore }) {
const mapped = result ?? await loadPersistedAgentRunResult(traceId, options);
const initial = result ?? await loadPersistedAgentRunResult(traceId, options);
const mapped = initial ? await resolveAgentRunTraceCommandMapping({ traceId, mapped: initial, options }) : initial;
if (!mapped?.agentRun?.runId) return traceStore.snapshot(traceId);
const env = options.env ?? process.env;
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
@@ -609,141 +667,48 @@ export async function loadPersistedAgentRunResult(traceId, options = {}) {
const safeId = safeTraceId(traceId);
if (!safeId || typeof options.accessController?.getAgentSessionByTraceId !== "function") return null;
const session = await options.accessController.getAgentSessionByTraceId(safeId);
const traceEvidence = validAgentSessionTraceEvidence(session, safeId);
const latestTraceId = safeTraceId(session?.lastTraceId ?? session?.session?.lastTraceId ?? session?.session?.traceId);
const topLevelAgentRun = session?.session?.agentRun && typeof session.session.agentRun === "object" ? session.session.agentRun : null;
const agentRunTraceId = safeTraceId(topLevelAgentRun?.traceId);
const topLevelFinalTraceId = safeTraceId(session?.session?.finalResponse?.traceId ?? session?.session?.traceSummary?.traceId);
const topLevelConflictsTrace = (agentRunTraceId && agentRunTraceId !== safeId) || (topLevelFinalTraceId && topLevelFinalTraceId !== safeId);
const mayUseTopLevelAgentRun = !topLevelConflictsTrace && (latestTraceId === safeId || agentRunTraceId === safeId || topLevelFinalTraceId === safeId || (!latestTraceId && !agentRunTraceId && !topLevelFinalTraceId));
const topLevelTraceEvidence = mayUseTopLevelAgentRun ? agentSessionTopLevelTraceEvidence(session, safeId) : null;
if (!traceEvidence && topLevelTraceEvidence && ambiguousTopLevelAgentRunTrace(topLevelAgentRun)) {
const repaired = await repairPersistedAgentRunResultFromRunCommands({ traceId: safeId, session, options }).catch(() => null);
if (repaired) return repaired;
}
const agentRun = traceEvidence?.agentRun ?? (topLevelTraceEvidence ? topLevelAgentRun : null);
const restored = persistedAgentRunResultFromSession({ traceId: safeId, session, agentRun, traceEvidence, options });
if (restored) return restored;
return await repairPersistedAgentRunResultFromRunCommands({ traceId: safeId, session, options });
}
function persistedAgentRunResultFromSession({ traceId, session, agentRun, traceEvidence, options = {} }) {
if (!agentRun || typeof agentRun !== "object" || !agentRun.runId || !agentRun.commandId) return null;
if (!agentRunEvidenceMatchesTrace(agentRun, traceId, traceEvidence)) return null;
const status = firstNonEmpty(traceEvidence?.status, session?.status === "canceled" ? "canceled" : null, agentRun.terminalStatus, agentRun.commandState, "running");
const agentRun = agentRunSeedFromSession(session, safeId);
if (!agentRun?.runId) return null;
const backendProfile = firstNonEmpty(agentRun.backendProfile, "deepseek");
const status = firstNonEmpty(session?.status === "canceled" ? "canceled" : null, agentRun.terminalStatus, agentRun.commandState, agentRun.status, "running");
return {
accepted: true,
status,
shortConnection: true,
traceId,
conversationId: safeConversationId(traceEvidence?.conversationId ?? session?.conversationId) || agentRun.conversationId || null,
sessionId: safeSessionId(traceEvidence?.sessionId ?? session?.id) || agentRun.sessionId || null,
threadId: safeOpaqueId(traceEvidence?.threadId ?? session?.threadId) || agentRun.threadId || null,
messageId: traceEvidence?.messageId ?? session?.session?.messageId ?? `msg_${traceId.slice(4)}`,
createdAt: traceEvidence?.createdAt ?? session?.startedAt ?? session?.updatedAt ?? nowIso(options.now),
updatedAt: traceEvidence?.updatedAt ?? session?.updatedAt ?? nowIso(options.now),
traceId: safeId,
conversationId: safeConversationId(session?.conversationId) || agentRun.conversationId || null,
sessionId: safeSessionId(session?.id) || agentRun.sessionId || null,
threadId: safeOpaqueId(session?.threadId) || agentRun.threadId || null,
messageId: `msg_${safeId.slice(4)}`,
createdAt: session?.startedAt ?? session?.updatedAt ?? nowIso(options.now),
updatedAt: session?.updatedAt ?? nowIso(options.now),
ownerUserId: session?.ownerUserId,
provider: providerForBackendProfile(agentRun.backendProfile ?? "deepseek"),
model: modelForBackendProfile(agentRun.backendProfile, options.env ?? process.env),
backend: backendForBackendProfile(agentRun.backendProfile ?? "deepseek"),
infrastructureBackend: `agentrun-v01/${agentRun.backendProfile ?? "deepseek"}`,
ownerRole: session?.ownerRole,
provider: providerForBackendProfile(backendProfile),
model: modelForBackendProfile(backendProfile, options.env ?? process.env),
backend: backendForBackendProfile(backendProfile),
infrastructureBackend: `agentrun-v01/${backendProfile}`,
capabilityLevel: AGENTRUN_CAPABILITY_LEVEL,
sessionMode: AGENTRUN_SESSION_MODE,
implementationType: AGENTRUN_IMPLEMENTATION_TYPE,
finalResponse: traceEvidence?.finalResponse ?? session?.session?.finalResponse ?? null,
traceSummary: traceEvidence?.traceSummary ?? session?.session?.traceSummary ?? null,
agentRun: { ...agentRun, adapter: ADAPTER_ID, valuesPrinted: false },
agentRun: { ...agentRun, adapter: ADAPTER_ID, traceId: safeId, commandId: text(agentRun.commandId) || null, valuesPrinted: false },
valuesPrinted: false
};
}
async function repairPersistedAgentRunResultFromRunCommands({ traceId, session, options = {} }) {
const seedAgentRun = session?.session?.agentRun && typeof session.session.agentRun === "object" ? session.session.agentRun : null;
if (!seedAgentRun?.runId) return null;
const env = options.env ?? process.env;
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
const managerUrl = resolveAgentRunManagerUrl(env, seedAgentRun.managerUrl);
const timeoutMs = parsePositiveInteger(env.HWLAB_CODE_AGENT_AGENTRUN_HTTP_TIMEOUT_MS, 20_000);
const commands = await agentRunJson(fetchImpl, managerUrl, `/api/v1/runs/${encodeURIComponent(seedAgentRun.runId)}/commands?afterSeq=0&limit=100`, { method: "GET", timeoutMs });
const command = (Array.isArray(commands?.items) ? commands.items : []).find((item) => agentRunCommandMatchesTrace(item, traceId));
if (!command?.id) return null;
const [result, eventsResponse] = await Promise.all([
agentRunJson(fetchImpl, managerUrl, `/api/v1/runs/${encodeURIComponent(seedAgentRun.runId)}/commands/${encodeURIComponent(command.id)}/result`, { method: "GET", timeoutMs }),
agentRunJson(fetchImpl, managerUrl, `/api/v1/runs/${encodeURIComponent(seedAgentRun.runId)}/events?afterSeq=0&limit=500`, { method: "GET", timeoutMs }).catch(() => ({ items: [] }))
]);
const rawEvents = Array.isArray(eventsResponse?.items) ? eventsResponse.items : [];
const commandSeqs = rawEvents
.filter((event) => agentRunEventCommandId(event) === command.id)
.map((event) => Number(event?.seq ?? 0))
.filter(Number.isFinite);
const firstSeq = commandSeqs.length ? Math.min(...commandSeqs) : 0;
const afterSeq = firstSeq > 0 ? firstSeq - 1 : 0;
const endSeq = Math.max(0, ...commandSeqs);
const traceEvents = rawEvents.filter((event) => agentRunEventBelongsToTrace(event, { currentCommandId: command.id, afterSeq, endSeq }));
const now = nowIso(options.now);
const backendProfile = firstNonEmpty(seedAgentRun.backendProfile, result?.backendProfile, command?.payload?.providerProfile, "deepseek");
const agentRun = {
...seedAgentRun,
adapter: ADAPTER_ID,
managerUrl,
backendProfile,
runId: result?.runId ?? seedAgentRun.runId,
commandId: command.id,
attemptId: result?.attemptId ?? seedAgentRun.attemptId ?? null,
runnerId: result?.runnerId ?? seedAgentRun.runnerId ?? null,
jobName: result?.jobName ?? seedAgentRun.jobName ?? null,
namespace: result?.namespace ?? seedAgentRun.namespace ?? DEFAULT_RUNNER_NAMESPACE,
status: result?.status ?? command.state ?? seedAgentRun.status ?? null,
runStatus: result?.runStatus ?? seedAgentRun.runStatus ?? null,
commandState: result?.commandState ?? command.state ?? null,
terminalStatus: result?.terminalStatus ?? null,
eventStartSeq: afterSeq > 0 ? afterSeq + 1 : null,
lastSeq: endSeq || 0,
traceId,
sessionId: result?.sessionRef?.sessionId ?? command?.payload?.sessionId ?? seedAgentRun.sessionId ?? null,
conversationId: result?.sessionRef?.conversationId ?? command?.payload?.conversationId ?? seedAgentRun.conversationId ?? session?.conversationId ?? null,
threadId: result?.sessionRef?.threadId ?? command?.payload?.threadId ?? seedAgentRun.threadId ?? session?.threadId ?? null,
valuesPrinted: false
};
const finalText = firstNonEmpty(result?.reply);
const finalResponse = finalText ? {
text: finalText,
textChars: finalText.length,
messageId: `msg_${traceId.slice(4)}`,
role: "assistant",
status: result?.status ?? result?.terminalStatus ?? "completed",
traceId,
createdAt: command.updatedAt ?? result?.updatedAt ?? now,
updatedAt: command.updatedAt ?? result?.updatedAt ?? now,
source: "agentrun-command-result-repair",
valuesPrinted: false
} : null;
const traceSummary = {
traceId,
source: "agent-session-agentrun-command-repair",
sourceEventCount: traceEvents.length,
terminalStatus: result?.terminalStatus ?? command.state ?? null,
agentRun: { runId: agentRun.runId, commandId: agentRun.commandId, lastSeq: agentRun.lastSeq, eventStartSeq: agentRun.eventStartSeq, valuesPrinted: false },
updatedAt: now,
valuesPrinted: false
};
const traceEvidence = {
traceId,
status: result?.status ?? result?.terminalStatus ?? command.state ?? "completed",
conversationId: agentRun.conversationId,
sessionId: safeSessionId(command?.payload?.hwlabSessionId) || safeSessionId(session?.id) || agentRun.sessionId,
threadId: agentRun.threadId,
messageId: `msg_${traceId.slice(4)}`,
createdAt: command.createdAt ?? session?.startedAt ?? now,
updatedAt: command.updatedAt ?? now,
finalResponse,
traceSummary,
agentRun,
valuesRedacted: true,
secretMaterialStored: false
};
await persistTraceRepairEvidence({ session, traceId, traceEvidence, options });
return persistedAgentRunResultFromSession({ traceId, session, agentRun, traceEvidence, options });
function agentRunSeedFromSession(session, traceId) {
const snapshot = session?.session && typeof session.session === "object" ? session.session : null;
const traceResults = snapshot?.traceResults && typeof snapshot.traceResults === "object" ? snapshot.traceResults : null;
const traceAgentRun = traceResults?.[traceId]?.agentRun && typeof traceResults[traceId].agentRun === "object" ? traceResults[traceId].agentRun : null;
if (traceAgentRun?.runId) return traceAgentRun;
const topLevelAgentRun = snapshot?.agentRun && typeof snapshot.agentRun === "object" ? snapshot.agentRun : null;
return topLevelAgentRun?.runId ? topLevelAgentRun : null;
}
async function findAgentRunCommandForTrace({ fetchImpl, managerUrl, timeoutMs, runId, traceId }) {
if (!runId || !safeTraceId(traceId)) return null;
const commands = await agentRunJson(fetchImpl, managerUrl, `/api/v1/runs/${encodeURIComponent(runId)}/commands?afterSeq=0&limit=100`, { method: "GET", timeoutMs });
return (Array.isArray(commands?.items) ? commands.items : []).find((item) => agentRunCommandMatchesTrace(item, traceId)) ?? null;
}
function agentRunCommandMatchesTrace(command, traceId) {
@@ -751,71 +716,6 @@ function agentRunCommandMatchesTrace(command, traceId) {
return command?.idempotencyKey === traceId || payload.traceId === traceId;
}
async function persistTraceRepairEvidence({ session, traceId, traceEvidence, options = {} }) {
if (!session?.id || !session.ownerUserId || typeof options.accessController?.recordAgentSessionOwner !== "function") return;
await options.accessController.recordAgentSessionOwner({
ownerUserId: session.ownerUserId,
sessionId: session.id,
projectId: session.projectId,
agentId: session.agentId,
status: session.status,
conversationId: session.conversationId,
threadId: session.threadId,
lastTraceId: session.lastTraceId,
session: { traceResults: { [traceId]: traceEvidence }, source: "agent-session-trace-repair", valuesRedacted: true, secretMaterialStored: false }
});
}
function agentSessionTraceEvidence(session, traceId) {
const id = safeTraceId(traceId);
const traceResults = session?.session?.traceResults && typeof session.session.traceResults === "object" ? session.session.traceResults : null;
const evidence = id && traceResults?.[id] && typeof traceResults[id] === "object" ? traceResults[id] : null;
return evidence ? { ...evidence, traceId: id } : null;
}
function validAgentSessionTraceEvidence(session, traceId) {
const evidence = agentSessionTraceEvidence(session, traceId);
if (!evidence) return null;
return agentRunEvidenceMatchesTrace(evidence.agentRun, traceId, evidence, { requireAgentRunTraceId: true }) ? evidence : null;
}
function agentSessionTopLevelTraceEvidence(session, traceId) {
const snapshot = session?.session && typeof session.session === "object" ? session.session : null;
if (!snapshot) return null;
const evidence = {
finalResponse: snapshot.finalResponse,
traceSummary: snapshot.traceSummary
};
return agentRunEvidenceMatchesTrace(snapshot.agentRun, traceId, evidence) ? evidence : null;
}
function ambiguousTopLevelAgentRunTrace(agentRun) {
const run = agentRun && typeof agentRun === "object" ? agentRun : null;
return Boolean(run?.runId && run?.commandId && !safeTraceId(run.traceId));
}
function agentRunEvidenceMatchesTrace(agentRun, traceId, traceEvidence = null, options = {}) {
const id = safeTraceId(traceId);
if (!id) return false;
const run = agentRun && typeof agentRun === "object" ? agentRun : null;
const runTraceId = safeTraceId(run?.traceId);
if (run?.commandId && runTraceId && runTraceId !== id) return false;
if (options.requireAgentRunTraceId === true && run?.commandId && runTraceId !== id) return false;
const providerTrace = run?.providerTrace && typeof run.providerTrace === "object" ? run.providerTrace : null;
const summary = traceEvidence?.traceSummary && typeof traceEvidence.traceSummary === "object" ? traceEvidence.traceSummary : null;
const summaryRun = summary?.agentRun && typeof summary.agentRun === "object" ? summary.agentRun : null;
const finalResponse = traceEvidence?.finalResponse && typeof traceEvidence.finalResponse === "object" ? traceEvidence.finalResponse : null;
const traceIds = [runTraceId, providerTrace?.traceId, summary?.traceId, finalResponse?.traceId]
.map((value) => safeTraceId(value))
.filter(Boolean);
if (traceIds.some((value) => value !== id)) return false;
const commandIds = [run?.commandId, providerTrace?.commandId, summaryRun?.commandId]
.map((value) => text(value))
.filter(Boolean);
if (commandIds.length > 1 && new Set(commandIds).size > 1) return false;
return true;
}
export function agentRunSessionEvidence(payload = {}) {
if (!payload?.agentRun) return {};
return {
@@ -1314,7 +1214,7 @@ function agentRunToolCalls(result = {}, status = "completed") {
}];
}
function agentRunResultToCodeAgentPayload({ base, result, traceStore, traceId }) {
function agentRunResultToCodeAgentPayload({ base, result, traceStore, traceId, appendResultEvent = true }) {
const terminalStatus = String(result?.terminalStatus ?? "");
const terminal = terminalStatus === "completed" || terminalStatus === "failed" || terminalStatus === "blocked" || terminalStatus === "cancelled";
if (!terminal) {
@@ -1325,17 +1225,21 @@ function agentRunResultToCodeAgentPayload({ base, result, traceStore, traceId })
const terminalEventCreatedAt = agentRunResultTraceCreatedAt(runnerTrace, now);
const providerTrace = agentRunProviderTrace({ base, result, terminalStatus });
if (terminalStatus === "completed" && String(result?.reply ?? "").trim()) {
traceStore.append(traceId, {
type: "result",
status: "completed",
label: "agentrun:result:completed",
createdAt: terminalEventCreatedAt,
message: "AgentRun result is ready for HWLAB short-connection polling.",
runId: base.agentRun.runId,
commandId: base.agentRun.commandId,
terminal: true,
valuesPrinted: false
});
const finalResponse = agentRunCompletedFinalResponse({ base, result, traceId, now });
const traceSummary = agentRunCompletedTraceSummary({ base, runnerTrace, finalResponse, traceId });
if (appendResultEvent) {
traceStore.append(traceId, {
type: "result",
status: "completed",
label: "agentrun:result:completed",
createdAt: terminalEventCreatedAt,
message: "AgentRun result is ready for HWLAB short-connection polling.",
runId: base.agentRun.runId,
commandId: base.agentRun.commandId,
terminal: true,
valuesPrinted: false
});
}
return {
...base,
status: "completed",
@@ -1350,11 +1254,13 @@ function agentRunResultToCodeAgentPayload({ base, result, traceStore, traceId })
skills: { status: "delegated", provider: ADAPTER_ID, count: 0, items: [], valuesPrinted: false },
longLivedSessionGate: agentRunLongLivedSessionGate(base),
providerTrace,
finalResponse,
traceSummary,
reply: {
messageId: base.messageId ?? `msg_${traceId.slice(4)}`,
messageId: finalResponse.messageId,
role: "assistant",
content: String(result.reply),
createdAt: now
content: finalResponse.text,
createdAt: finalResponse.createdAt
},
usage: null,
agentRun: { ...base.agentRun, terminalStatus, completed: true, reuseEligible: true, providerTrace, valuesPrinted: false },
@@ -1365,18 +1271,20 @@ function agentRunResultToCodeAgentPayload({ base, result, traceStore, traceId })
const code = canceled ? "agentrun_canceled" : result?.failureKind ?? (terminalStatus === "blocked" ? "agentrun_blocked" : "agentrun_failed");
const message = result?.failureMessage ?? result?.blocker?.message ?? (canceled ? "AgentRun command was canceled" : "AgentRun command failed");
const attribution = agentRunFailureAttribution({ code, message, canceled });
traceStore.append(traceId, {
type: "result",
status: canceled ? "canceled" : "failed",
label: `agentrun:result:${canceled ? "canceled" : terminalStatus || "failed"}`,
createdAt: terminalEventCreatedAt,
errorCode: code,
message: attribution.summary,
runId: base.agentRun.runId,
commandId: base.agentRun.commandId,
terminal: true,
valuesPrinted: false
});
if (appendResultEvent) {
traceStore.append(traceId, {
type: "result",
status: canceled ? "canceled" : "failed",
label: `agentrun:result:${canceled ? "canceled" : terminalStatus || "failed"}`,
createdAt: terminalEventCreatedAt,
errorCode: code,
message: attribution.summary,
runId: base.agentRun.runId,
commandId: base.agentRun.commandId,
terminal: true,
valuesPrinted: false
});
}
const partialContext = partialAgentRunContext(runnerTrace);
return {
...base,
@@ -1418,6 +1326,47 @@ function agentRunResultToCodeAgentPayload({ base, result, traceStore, traceId })
};
}
function agentRunCompletedFinalResponse({ base, result, traceId, now }) {
const textValue = String(result?.reply ?? "").trim();
return {
text: textValue,
textChars: textValue.length,
role: "assistant",
status: "completed",
traceId,
messageId: base.messageId ?? base.reply?.messageId ?? `msg_${traceId.slice(4)}`,
createdAt: base.reply?.createdAt ?? base.createdAt ?? now,
updatedAt: now,
valuesPrinted: false
};
}
function agentRunCompletedTraceSummary({ base, runnerTrace, finalResponse, traceId }) {
const events = Array.isArray(runnerTrace?.events) ? runnerTrace.events : [];
const lastSeq = Math.max(0, ...events.map((event) => Number(event?.seq ?? 0)).filter(Number.isFinite));
return {
traceId,
source: "agentrun-command-result",
sourceEventCount: Number(runnerTrace?.eventCount ?? events.length ?? 0),
terminalStatus: "completed",
finalAssistantRow: {
role: finalResponse.role,
status: finalResponse.status,
textChars: finalResponse.textChars,
textPreview: finalResponse.text.slice(0, 240),
messageId: finalResponse.messageId,
valuesPrinted: false
},
agentRun: {
runId: base.agentRun?.runId ?? null,
commandId: base.agentRun?.commandId ?? null,
lastSeq,
valuesPrinted: false
},
valuesPrinted: false
};
}
function partialAgentRunContext(runnerTrace = {}) {
const events = Array.isArray(runnerTrace.events) ? runnerTrace.events : [];
const assistantMessages = events
+299 -15
View File
@@ -355,6 +355,13 @@ test("cloud api /v1/agent/chat delegates v0.2 turns to AgentRun v0.1 over adapte
}
return send({ id: secondTurn ? "cmd_hwlab_adapter_second" : "cmd_hwlab_adapter", runId: "run_hwlab_adapter", state: "pending", type: "turn", seq: secondTurn ? 2 : 1 });
}
if (request.method === "GET" && url.pathname === "/api/v1/runs/run_hwlab_adapter/commands") {
return send({ items: [
{ id: "cmd_hwlab_adapter", runId: "run_hwlab_adapter", state: "completed", type: "turn", seq: 1, idempotencyKey: "trc_server-test-agentrun-adapter", payload: { traceId: "trc_server-test-agentrun-adapter", conversationId: "cnv_server-test-agentrun", hwlabSessionId: "ses_server-test-agentrun", threadId: "019e8078-db67-7750-a5d9-1a99f3abd445", providerProfile: "deepseek" } },
{ id: "cmd_hwlab_adapter_steer", runId: "run_hwlab_adapter", state: "completed", type: "steer", seq: 7, idempotencyKey: "trc_steer_server_test", payload: { traceId: "trc_steer_server_test", targetTraceId: "trc_server-test-agentrun-adapter", targetCommandId: "cmd_hwlab_adapter", conversationId: "cnv_server-test-agentrun", hwlabSessionId: "ses_server-test-agentrun", threadId: "019e8078-db67-7750-a5d9-1a99f3abd445", providerProfile: "deepseek" } },
{ id: "cmd_hwlab_adapter_second", runId: "run_hwlab_adapter", state: "completed", type: "turn", seq: 2, idempotencyKey: "trc_server-test-agentrun-adapter-second", payload: { traceId: "trc_server-test-agentrun-adapter-second", conversationId: "cnv_server-test-agentrun", hwlabSessionId: "ses_server-test-agentrun", threadId: "019e8078-db67-7750-a5d9-1a99f3abd445", providerProfile: "deepseek" } }
] });
}
if (request.method === "POST" && url.pathname === "/api/v1/runs/run_hwlab_adapter/runner-jobs") {
const secondRunnerJob = body.commandId === "cmd_hwlab_adapter_second";
assert.ok(body.commandId === "cmd_hwlab_adapter" || secondRunnerJob);
@@ -752,6 +759,11 @@ test("cloud api AgentRun adapter reports persistent thread resume when a complet
assert.match(body.payload.prompt, /ISSUE812_RESUME/u);
return send({ id: "cmd_issue812_resume", runId: "run_issue812_resume", state: "pending", type: "turn", seq: 1 });
}
if (request.method === "GET" && url.pathname === "/api/v1/runs/run_issue812_resume/commands") {
return send({ items: [
{ id: "cmd_issue812_resume", runId: "run_issue812_resume", state: "completed", type: "turn", seq: 1, idempotencyKey: "trc_server-test-issue812-resume", payload: { traceId: "trc_server-test-issue812-resume", conversationId, hwlabSessionId, threadId, providerProfile: "deepseek" } }
] });
}
if (request.method === "POST" && url.pathname === "/api/v1/runs/run_issue812_resume/runner-jobs") {
assert.equal(body.commandId, "cmd_issue812_resume");
return send({
@@ -890,6 +902,11 @@ test("cloud api AgentRun adapter exposes invalid tool-call attribution in result
if (request.method === "POST" && url.pathname === "/api/v1/runs/run_invalid_tool/commands") {
return send({ id: "cmd_invalid_tool", runId: "run_invalid_tool", state: "pending", type: "turn", seq: 1 });
}
if (request.method === "GET" && url.pathname === "/api/v1/runs/run_invalid_tool/commands") {
return send({ items: [
{ id: "cmd_invalid_tool", runId: "run_invalid_tool", state: "failed", type: "turn", seq: 1, idempotencyKey: "trc_server-test-agentrun-invalid-tool", payload: { traceId: "trc_server-test-agentrun-invalid-tool", conversationId: "cnv_invalid_tool", hwlabSessionId: "ses_server-test-invalid-tool", threadId: "thread_invalid_tool", providerProfile: "deepseek" } }
] });
}
if (request.method === "POST" && url.pathname === "/api/v1/runs/run_invalid_tool/runner-jobs") {
return send({
action: "create-kubernetes-job",
@@ -1030,6 +1047,11 @@ test("cloud api AgentRun adapter maps minimax-m3 provider profile to AgentRun ba
assert.match(body.payload.prompt, /MiniMax-M3/u);
return send({ id: "cmd_hwlab_minimax_m3", runId: "run_hwlab_minimax_m3", state: "pending", type: "turn", seq: 1 });
}
if (request.method === "GET" && url.pathname === "/api/v1/runs/run_hwlab_minimax_m3/commands") {
return send({ items: [
{ id: "cmd_hwlab_minimax_m3", runId: "run_hwlab_minimax_m3", state: "completed", type: "turn", seq: 1, idempotencyKey: "trc_server-test-agentrun-minimax-m3", payload: { traceId: "trc_server-test-agentrun-minimax-m3", conversationId: "cnv_server-test-minimax-m3", hwlabSessionId: "ses_server-test-minimax-m3", threadId: null, providerProfile: "minimax-m3" } }
] });
}
if (request.method === "POST" && url.pathname === "/api/v1/runs/run_hwlab_minimax_m3/runner-jobs") {
assert.equal(body.commandId, "cmd_hwlab_minimax_m3");
return send({
@@ -1201,6 +1223,11 @@ test("cloud api AgentRun adapter scopes AgentRun sessions by backend profile", a
assert.equal(body.payload.hwlabSessionId, hwlabSessionId);
return send({ id: "cmd_hwlab_profile_switch_m3", runId: "run_hwlab_profile_switch_m3", state: "pending", type: "turn", seq: 1 });
}
if (request.method === "GET" && url.pathname === "/api/v1/runs/run_hwlab_profile_switch_m3/commands") {
return send({ items: [
{ id: "cmd_hwlab_profile_switch_m3", runId: "run_hwlab_profile_switch_m3", state: "completed", type: "turn", seq: 1, idempotencyKey: "trc_server-test-agentrun-profile-switch", payload: { traceId: "trc_server-test-agentrun-profile-switch", conversationId: "cnv_server-test-profile-switch", hwlabSessionId, threadId: "019e8078-db67-7750-a5d9-1a99f3abd445", providerProfile: "minimax-m3" } }
] });
}
if (request.method === "POST" && url.pathname === "/api/v1/runs/run_hwlab_profile_switch_m3/runner-jobs") {
assert.equal(body.commandId, "cmd_hwlab_profile_switch_m3");
return send({
@@ -1574,8 +1601,10 @@ test("cloud api /v1/agent/chat/inspect returns not_found for empty conversation
}
});
test("cloud api trace returns persisted summary when live trace store has expired", async () => {
test("cloud api trace uses AgentRun command result evidence when live trace store has expired", async () => {
const ownerSessions = new Map();
const agentRunCalls = [];
const commandFinalText = "历史 trace 已过期,但 final response 来自 AgentRun command result。";
ownerSessions.set("ses_issue842_expired", testAgentSessionRecord({
sessionId: "ses_issue842_expired",
projectId: "prj_v02_code_agent",
@@ -1585,8 +1614,8 @@ test("cloud api trace returns persisted summary when live trace store has expire
status: "active",
session: {
finalResponse: {
text: "历史 trace 已过期,但 final response 仍可审计。",
textChars: 31,
text: "旧 session snapshot final 不应作为历史 trace 权威。",
textChars: 30,
role: "assistant",
status: "completed",
traceId: "trc_issue842_expired",
@@ -1595,7 +1624,7 @@ test("cloud api trace returns persisted summary when live trace store has expire
},
traceSummary: {
traceId: "trc_issue842_expired",
source: "agent-session-snapshot",
source: "stale-agent-session-snapshot",
sourceEventCount: 26,
terminalStatus: "completed",
noiseEventCount: 4,
@@ -1615,12 +1644,52 @@ test("cloud api trace returns persisted summary when live trace store has expire
valuesRedacted: true
}
}));
const agentRunServer = createHttpServer(async (request, response) => {
const url = new URL(request.url || "/", "http://127.0.0.1");
agentRunCalls.push({ method: request.method, path: url.pathname, search: url.search });
const send = (data) => {
response.writeHead(200, { "content-type": "application/json" });
response.end(`${JSON.stringify({ ok: true, data, traceId: "trc_fake_issue842_expired" })}\n`);
};
if (request.method === "GET" && url.pathname === "/api/v1/runs/run_issue842_expired/commands") {
return send({ items: [
{ id: "cmd_issue842_expired", runId: "run_issue842_expired", state: "completed", type: "turn", seq: 1, idempotencyKey: "trc_issue842_expired", payload: { traceId: "trc_issue842_expired", conversationId: "cnv_issue842_expired", hwlabSessionId: "ses_issue842_expired", threadId: "thread-issue-842-expired", providerProfile: "deepseek" } }
] });
}
if (request.method === "GET" && url.pathname === "/api/v1/runs/run_issue842_expired/events") {
return send({ items: [] });
}
if (request.method === "GET" && url.pathname === "/api/v1/runs/run_issue842_expired/commands/cmd_issue842_expired/result") {
return send({
runId: "run_issue842_expired",
commandId: "cmd_issue842_expired",
attemptId: "attempt_issue842_expired",
runnerId: "runner_issue842_expired",
jobName: "agentrun-v01-runner-issue842-expired",
namespace: "agentrun-v01",
status: "completed",
runStatus: "completed",
commandState: "completed",
terminalStatus: "completed",
completed: true,
reply: commandFinalText,
lastSeq: 26,
eventCount: 26,
sessionRef: { sessionId: "ses_agentrun_issue842_expired", conversationId: "cnv_issue842_expired", threadId: "thread-issue-842-expired" }
});
}
response.writeHead(404, { "content-type": "application/json" });
response.end(`${JSON.stringify({ ok: false, message: `unexpected ${request.method} ${url.pathname}` })}\n`);
});
await new Promise((resolve) => agentRunServer.listen(0, "127.0.0.1", resolve));
const agentRunPort = agentRunServer.address().port;
const traceStore = createCodeAgentTraceStore();
const server = createCloudApiServer({
traceStore,
env: {
HWLAB_CODE_AGENT_ADAPTER: "agentrun-v01",
AGENTRUN_MGR_URL: "http://127.0.0.1:9",
AGENTRUN_MGR_URL: `http://127.0.0.1:${agentRunPort}`,
HWLAB_CODE_AGENT_AGENTRUN_ALLOW_NON_K3S_URL: "1",
HWLAB_ENVIRONMENT: "v02",
HWLAB_GITOPS_PROFILE: "v02"
@@ -1649,16 +1718,24 @@ test("cloud api trace returns persisted summary when live trace store has expire
assert.equal(body.traceStatus, "expired");
assert.equal(body.eventCount, 0);
assert.equal(body.fallback.available, true);
assert.equal(body.finalResponse.text, "历史 trace 已过期,但 final response 仍可审计。");
assert.equal(body.traceSummary.sourceEventCount, 26);
assert.equal(body.fallback.source, "agentrun-command-result");
assert.equal(body.finalResponse.text, commandFinalText);
assert.equal(body.traceSummary.source, "agentrun-command-result");
assert.equal(body.traceSummary.sourceEventCount, 0);
assert.equal(body.agentRun.runId, "run_issue842_expired");
assert.equal(body.agentRun.commandId, "cmd_issue842_expired");
assert.equal(body.retention.liveTraceStore, "expired-or-evicted");
assert.equal(JSON.stringify(body).includes("旧 session snapshot final"), false);
assert.ok(agentRunCalls.some((call) => call.path === "/api/v1/runs/run_issue842_expired/commands"));
assert.ok(agentRunCalls.some((call) => call.path === "/api/v1/runs/run_issue842_expired/commands/cmd_issue842_expired/result"));
assert.equal(JSON.stringify(body).includes("password"), false);
} finally {
await new Promise((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
await new Promise((resolve, reject) => {
agentRunServer.close((error) => (error ? reject(error) : resolve()));
});
}
});
@@ -1684,10 +1761,21 @@ test("cloud api trace replays an earlier AgentRun command after same-run lastSeq
const agentRunServer = createHttpServer(async (request, response) => {
const url = new URL(request.url || "/", "http://127.0.0.1");
calls.push({ method: request.method, path: url.pathname, afterSeq: url.searchParams.get("afterSeq") });
if (request.method === "GET" && url.pathname === `/api/v1/runs/${runId}/events`) {
const send = (data) => {
response.writeHead(200, { "content-type": "application/json" });
response.end(`${JSON.stringify({ ok: true, data: { items: [...firstEvents, ...secondEvents] }, traceId: "trc_fake_issue955_replay" })}\n`);
return;
response.end(`${JSON.stringify({ ok: true, data, traceId: "trc_fake_issue955_replay" })}\n`);
};
if (request.method === "GET" && url.pathname === `/api/v1/runs/${runId}/commands`) {
return send({ items: [
{ id: firstCommandId, runId, state: "completed", idempotencyKey: firstTraceId, createdAt: "2026-06-06T01:43:30.040Z", updatedAt: "2026-06-06T01:43:56.000Z", payload: { traceId: firstTraceId, conversationId: "cnv_issue955_replay", hwlabSessionId: "ses_issue955_replay", threadId: "thread-issue955-replay", providerProfile: "deepseek" } },
{ id: secondCommandId, runId, state: "completed", idempotencyKey: "trc_issue955_second_trace", createdAt: "2026-06-06T01:46:11.000Z", updatedAt: "2026-06-06T01:46:28.000Z", payload: { traceId: "trc_issue955_second_trace", conversationId: "cnv_issue955_replay", hwlabSessionId: "ses_issue955_replay", threadId: "thread-issue955-replay", providerProfile: "deepseek" } }
] });
}
if (request.method === "GET" && url.pathname === `/api/v1/runs/${runId}/commands/${firstCommandId}/result`) {
return send({ runId, commandId: firstCommandId, status: "completed", runStatus: "completed", commandState: "completed", terminalStatus: "completed", completed: true, reply: firstFinalText, lastSeq: 35, eventCount: 35, sessionRef: { sessionId: "ses_agentrun_deepseek_issue955_replay", conversationId: "cnv_issue955_replay", threadId: "thread-issue955-replay" } });
}
if (request.method === "GET" && url.pathname === `/api/v1/runs/${runId}/events`) {
return send({ items: [...firstEvents, ...secondEvents] });
}
response.writeHead(404, { "content-type": "application/json" });
response.end(`${JSON.stringify({ ok: false, message: `unexpected ${request.method} ${url.pathname}` })}\n`);
@@ -1760,7 +1848,6 @@ test("cloud api trace replays an earlier AgentRun command after same-run lastSeq
test("cloud api repairs historical same-session AgentRun trace after lastTraceId advances (#955)", async () => {
const calls = [];
const persistedOwnerInputs = [];
const ownerSessions = new Map();
const runId = "run_issue955_historical";
const firstTraceId = "trc_issue955_historical_first";
@@ -1857,7 +1944,6 @@ test("cloud api repairs historical same-session AgentRun trace after lastTraceId
return [...ownerSessions.values()].find((session) => session.lastTraceId === traceId || session.session?.messages?.some((message) => message.traceId === traceId)) ?? null;
},
async recordAgentSessionOwner(input) {
persistedOwnerInputs.push(input);
const existing = ownerSessions.get(input.sessionId) ?? testAgentSessionRecord(input);
const nextSession = { ...(existing.session ?? {}), ...(input.session ?? {}) };
nextSession.traceResults = { ...(existing.session?.traceResults ?? {}), ...(input.session?.traceResults ?? {}) };
@@ -1889,9 +1975,6 @@ test("cloud api repairs historical same-session AgentRun trace after lastTraceId
assert.ok(calls.some((call) => call.path === `/api/v1/runs/${runId}/commands`));
assert.ok(calls.some((call) => call.path === `/api/v1/runs/${runId}/commands/${firstCommandId}/result`));
assert.equal(calls.some((call) => call.path === `/api/v1/runs/${runId}/commands/${secondCommandId}/result`), false);
const repair = persistedOwnerInputs.find((input) => input.session?.traceResults?.[firstTraceId]);
assert.ok(repair);
assert.equal(repair.session.traceResults[firstTraceId].agentRun.commandId, firstCommandId);
assert.equal(ownerSessions.get("ses_issue955_historical").session.traceResults[firstTraceId].finalResponse.text, firstFinalText);
const repairedSession = ownerSessions.get("ses_issue955_historical");
assert.equal(repairedSession.lastTraceId, secondTraceId);
@@ -1923,6 +2006,207 @@ test("cloud api repairs historical same-session AgentRun trace after lastTraceId
}
});
test("cloud api result polling repairs polluted completed AgentRun memory cache (#955)", async () => {
const calls = [];
const runId = "run_issue955_live_cache";
const firstTraceId = "trc_issue955_live_cache_first";
const secondTraceId = "trc_issue955_live_cache_second";
const firstCommandId = "cmd_issue955_live_cache_first";
const secondCommandId = "cmd_issue955_live_cache_second";
const firstFinalText = "目前只有一个 HWPOD 可用:d601-f103-v2。";
const secondFinalText = "编译成功!hwpod build completedKeil MDK USART atk_f103.hex queued。";
const events = [
{ id: "evt_issue955_live_first_message", runId, seq: 10, type: "assistant_message", payload: { commandId: firstCommandId, itemId: "msg_issue955_live_first", text: firstFinalText }, createdAt: "2026-06-06T01:46:19.000Z" },
{ id: "evt_issue955_live_first_done", runId, seq: 11, type: "terminal_status", payload: { commandId: firstCommandId, terminalStatus: "completed" }, createdAt: "2026-06-06T01:46:19.000Z" },
{ id: "evt_issue955_live_second_message", runId, seq: 20, type: "assistant_message", payload: { commandId: secondCommandId, itemId: "msg_issue955_live_second", text: secondFinalText }, createdAt: "2026-06-06T01:46:28.000Z" },
{ id: "evt_issue955_live_second_done", runId, seq: 21, type: "terminal_status", payload: { commandId: secondCommandId, terminalStatus: "completed" }, createdAt: "2026-06-06T01:46:28.000Z" }
];
const agentRunServer = createHttpServer(async (request, response) => {
const url = new URL(request.url || "/", "http://127.0.0.1");
calls.push({ method: request.method, path: url.pathname, search: url.search });
const send = (data) => {
response.writeHead(200, { "content-type": "application/json" });
response.end(`${JSON.stringify({ ok: true, data, traceId: "trc_fake_issue955_live_cache" })}\n`);
};
if (request.method === "GET" && url.pathname === `/api/v1/runs/${runId}/commands`) {
return send({ items: [
{ id: firstCommandId, runId, state: "completed", idempotencyKey: firstTraceId, payload: { traceId: firstTraceId, conversationId: "cnv_issue955_live_cache", hwlabSessionId: "ses_issue955_live_cache", threadId: "thread-issue955-live-cache" } },
{ id: secondCommandId, runId, state: "completed", idempotencyKey: secondTraceId, payload: { traceId: secondTraceId, conversationId: "cnv_issue955_live_cache", hwlabSessionId: "ses_issue955_live_cache", threadId: "thread-issue955-live-cache" } }
] });
}
if (request.method === "GET" && url.pathname === `/api/v1/runs/${runId}/commands/${firstCommandId}/result`) {
return send({ runId, commandId: firstCommandId, status: "completed", runStatus: "completed", commandState: "completed", terminalStatus: "completed", completed: true, reply: firstFinalText, lastSeq: 11, eventCount: 11 });
}
if (request.method === "GET" && url.pathname === `/api/v1/runs/${runId}/commands/${secondCommandId}/result`) {
return send({ runId, commandId: secondCommandId, status: "completed", runStatus: "completed", commandState: "completed", terminalStatus: "completed", completed: true, reply: secondFinalText, lastSeq: 21, eventCount: 21 });
}
if (request.method === "GET" && url.pathname === `/api/v1/runs/${runId}/events`) {
return send({ items: events });
}
response.writeHead(404, { "content-type": "application/json" });
response.end(`${JSON.stringify({ ok: false, message: `unexpected ${request.method} ${url.pathname}` })}\n`);
});
await new Promise((resolve) => agentRunServer.listen(0, "127.0.0.1", resolve));
const agentRunPort = agentRunServer.address().port;
const codeAgentChatResults = new Map();
codeAgentChatResults.set(secondTraceId, {
accepted: true,
status: "completed",
shortConnection: true,
traceId: secondTraceId,
conversationId: "cnv_issue955_live_cache",
sessionId: "ses_issue955_live_cache",
threadId: "thread-issue955-live-cache",
ownerUserId: TEST_AGENT_ACTOR.id,
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 },
agentRun: {
adapter: "agentrun-v01",
managerUrl: `http://127.0.0.1:${agentRunPort}`,
runId,
commandId: firstCommandId,
traceId: secondTraceId,
lastSeq: 11,
terminalStatus: "completed",
providerTrace: { traceId: secondTraceId, runId, commandId: firstCommandId, terminalStatus: "completed", valuesPrinted: false },
valuesPrinted: false
},
providerTrace: { traceId: secondTraceId, runId, commandId: firstCommandId, terminalStatus: "completed", valuesPrinted: false },
valuesPrinted: false
});
const server = createCloudApiServer({
traceStore: createCodeAgentTraceStore(),
codeAgentChatResults,
env: {
HWLAB_CODE_AGENT_ADAPTER: "agentrun-v01",
AGENTRUN_MGR_URL: `http://127.0.0.1:${agentRunPort}`,
HWLAB_CODE_AGENT_AGENTRUN_ALLOW_NON_K3S_URL: "1",
HWLAB_ENVIRONMENT: "v02",
HWLAB_GITOPS_PROFILE: "v02"
},
accessController: {
required: false,
async authenticate() {
return { ok: true, actor: TEST_AGENT_ACTOR, session: TEST_AUTH_SESSION };
}
}
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const response = await fetch(`http://127.0.0.1:${port}/v1/agent/chat/result/${secondTraceId}`, {
headers: { cookie: "hwlab_session=test-stub-session" }
});
assert.equal(response.status, 200);
const body = await response.json();
const text = JSON.stringify(body);
assert.equal(body.traceId, secondTraceId);
assert.equal(body.status, "completed");
assert.equal(body.agentRun.commandId, secondCommandId);
assert.equal(body.agentRun.traceId, secondTraceId);
assert.equal(body.reply.content, secondFinalText);
assert.match(text, //u);
assert.match(text, /Keil MDK/u);
assert.doesNotMatch(text, / HWPOD /u);
assert.ok(calls.some((call) => call.path === `/api/v1/runs/${runId}/commands`));
assert.ok(calls.some((call) => call.path === `/api/v1/runs/${runId}/commands/${secondCommandId}/result`));
assert.equal(calls.some((call) => call.path === `/api/v1/runs/${runId}/commands/${firstCommandId}/result`), false);
assert.equal(codeAgentChatResults.get(secondTraceId).agentRun.commandId, secondCommandId);
} finally {
await new Promise((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
await new Promise((resolve, reject) => {
agentRunServer.close((error) => (error ? reject(error) : resolve()));
});
}
});
test("cloud api result polling fails closed when AgentRun command registry misses trace (#955)", async () => {
const calls = [];
const runId = "run_issue955_registry_missing";
const traceId = "trc_issue955_registry_missing";
const staleCommandId = "cmd_issue955_registry_stale";
const staleFinalText = "目前只有一个 HWPOD 可用:d601-f103-v2。";
const agentRunServer = createHttpServer(async (request, response) => {
const url = new URL(request.url || "/", "http://127.0.0.1");
calls.push({ method: request.method, path: url.pathname });
if (request.method === "GET" && url.pathname === `/api/v1/runs/${runId}/commands`) {
response.writeHead(200, { "content-type": "application/json" });
response.end(`${JSON.stringify({ ok: true, data: { items: [] }, traceId: "trc_fake_issue955_missing" })}\n`);
return;
}
response.writeHead(404, { "content-type": "application/json" });
response.end(`${JSON.stringify({ ok: false, message: `unexpected ${request.method} ${url.pathname}` })}\n`);
});
await new Promise((resolve) => agentRunServer.listen(0, "127.0.0.1", resolve));
const agentRunPort = agentRunServer.address().port;
const codeAgentChatResults = new Map();
codeAgentChatResults.set(traceId, {
accepted: true,
status: "completed",
shortConnection: true,
traceId,
conversationId: "cnv_issue955_registry_missing",
sessionId: "ses_issue955_registry_missing",
threadId: "thread-issue955-registry-missing",
ownerUserId: TEST_AGENT_ACTOR.id,
ownerRole: TEST_AGENT_ACTOR.role,
reply: { role: "assistant", content: staleFinalText },
finalResponse: { text: staleFinalText, textChars: staleFinalText.length, role: "assistant", status: "completed", traceId, valuesPrinted: false },
agentRun: { adapter: "agentrun-v01", managerUrl: `http://127.0.0.1:${agentRunPort}`, runId, commandId: staleCommandId, traceId, terminalStatus: "completed", valuesPrinted: false },
valuesPrinted: false
});
const server = createCloudApiServer({
traceStore: createCodeAgentTraceStore(),
codeAgentChatResults,
env: {
HWLAB_CODE_AGENT_ADAPTER: "agentrun-v01",
AGENTRUN_MGR_URL: `http://127.0.0.1:${agentRunPort}`,
HWLAB_CODE_AGENT_AGENTRUN_ALLOW_NON_K3S_URL: "1",
HWLAB_ENVIRONMENT: "v02",
HWLAB_GITOPS_PROFILE: "v02"
},
accessController: {
required: false,
async authenticate() {
return { ok: true, actor: TEST_AGENT_ACTOR, session: TEST_AUTH_SESSION };
}
}
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const response = await fetch(`http://127.0.0.1:${port}/v1/agent/chat/result/${traceId}`, {
headers: { cookie: "hwlab_session=test-stub-session" }
});
assert.equal(response.status, 404);
const body = await response.json();
const text = JSON.stringify(body);
assert.equal(body.error.code, "agentrun_trace_command_not_found");
assert.equal(body.error.traceId, traceId);
assert.equal(body.error.runId, runId);
assert.doesNotMatch(text, / HWPOD /u);
assert.ok(calls.some((call) => call.path === `/api/v1/runs/${runId}/commands`));
assert.equal(calls.some((call) => call.path.includes("/result")), false);
} finally {
await new Promise((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
await new Promise((resolve, reject) => {
agentRunServer.close((error) => (error ? reject(error) : resolve()));
});
}
});
test("cloud api result polling compacts large runnerTrace while preserving providerTrace", async () => {
const workspace = await mkdtemp(path.join(os.tmpdir(), "hwlab-agent-result-compact-"));
const codexHome = await mkdtemp(path.join(os.tmpdir(), "hwlab-agent-result-compact-codex-home-"));
+28 -9
View File
@@ -828,7 +828,8 @@ export async function handleCodeAgentChatResultHttp(request, response, url, opti
});
return;
}
if ((result?.agentRun?.runId && result.status === "running") || (!result && codeAgentAgentRunAdapterEnabled(options.env ?? process.env))) {
const adapterEnabled = codeAgentAgentRunAdapterEnabled(options.env ?? process.env);
if ((result?.agentRun?.runId && adapterEnabled) || (!result && adapterEnabled)) {
try {
const synced = await syncAgentRunChatResult({ traceId, currentResult: result, options, traceStore: options.traceStore ?? defaultCodeAgentTraceStore });
if (synced.result && !canAccessOwnedResult(synced.result, options.actor)) {
@@ -868,7 +869,7 @@ export async function handleCodeAgentChatResultHttp(request, response, url, opti
message: error?.message ?? "AgentRun result polling failed",
waitingFor: "agentrun-result"
});
if (result?.agentRun) {
if (result?.agentRun && result.status === "running") {
sendJson(response, 202, {
accepted: true,
status: "running",
@@ -882,6 +883,18 @@ export async function handleCodeAgentChatResultHttp(request, response, url, opti
});
return;
}
if (result?.agentRun) {
sendJson(response, error?.statusCode === 404 ? 404 : 502, {
error: {
code: error?.code ?? "agentrun_result_poll_failed",
message: error?.message ?? "AgentRun result polling failed",
traceId,
runId: result.agentRun.runId ?? null,
commandId: result.agentRun.commandId ?? null
}
});
return;
}
}
}
if (result && result.status !== "running") {
@@ -1360,7 +1373,7 @@ export async function handleCodeAgentSteerHttp(request, response, options) {
}
}
async function recordCodeAgentSessionOwner({ payload = {}, params = {}, options = {}, status = "active" } = {}) {
async function recordCodeAgentSessionOwner({ payload = {}, params = {}, options = {}, status = "active", preserveLastTraceId = false } = {}) {
const ownerUserId = options.actor?.id ?? params.ownerUserId;
if (!ownerUserId || !options.accessController?.recordAgentSessionOwner) return null;
const traceId = safeTraceId(payload.traceId ?? params.traceId);
@@ -1377,7 +1390,7 @@ async function recordCodeAgentSessionOwner({ payload = {}, params = {}, options
projectId: params.projectId ?? payload.projectId ?? null,
conversationId,
threadId,
traceId,
traceId: preserveLastTraceId ? undefined : traceId,
status,
session: codeAgentSessionOwnerEvidence(payload, params)
});
@@ -1491,12 +1504,12 @@ function isWorkbenchOrphanActiveTrace(snapshot) {
async function resolveWorkbenchActiveTraceResult(traceId, options = {}) {
if (!safeTraceId(traceId)) return null;
const cached = options.codeAgentChatResults?.get?.(traceId) ?? null;
if (cached?.agentRun?.runId && cached.status === "running") {
if (cached?.agentRun?.runId) {
try {
const synced = await syncAgentRunChatResult({ traceId, currentResult: cached, options, traceStore: options.traceStore ?? defaultCodeAgentTraceStore });
return synced.result ?? cached;
return synced.result ?? null;
} catch {
return cached;
return null;
}
}
if (cached) return cached;
@@ -1505,7 +1518,7 @@ async function resolveWorkbenchActiveTraceResult(traceId, options = {}) {
const persisted = await loadPersistedAgentRunResult(traceId, options);
if (!persisted?.agentRun?.runId) return persisted;
const synced = await syncAgentRunChatResult({ traceId, currentResult: persisted, options, traceStore: options.traceStore ?? defaultCodeAgentTraceStore });
return synced.result ?? persisted;
return synced.result ?? null;
} catch {
return null;
}
@@ -1835,7 +1848,7 @@ export async function handleCodeAgentTraceHttp(request, response, url, options)
return;
}
const agentRunResult = result?.agentRun ? result : await loadPersistedAgentRunResult(traceId, options);
let agentRunResult = result?.agentRun ? result : await loadPersistedAgentRunResult(traceId, options);
let refreshError = null;
if (agentRunResult && !canAccessOwnedResult(agentRunResult, options.actor)) {
sendJson(response, 403, {
@@ -1848,6 +1861,12 @@ export async function handleCodeAgentTraceHttp(request, response, url, options)
}
if (agentRunResult?.agentRun) {
try {
const synced = await syncAgentRunChatResult({ traceId, currentResult: agentRunResult, options, traceStore, appendResultEvent: false });
agentRunResult = synced.result ?? agentRunResult;
if (agentRunResult?.status && agentRunResult.status !== "running") {
recordCodeAgentConversationFact(agentRunResult, options);
await recordCodeAgentSessionOwner({ payload: agentRunResult, params: agentRunResult, options, status: codeAgentOwnerStatusForResult(agentRunResult), preserveLastTraceId: true });
}
await refreshAgentRunTrace({ traceId, result: agentRunResult, options, traceStore });
} catch (error) {
refreshError = error;