From 7c6df8d9b4a0fb46e2906f31a57a03688cfb0233 Mon Sep 17 00:00:00 2001 From: Codex Agent Date: Sat, 6 Jun 2026 11:20:48 +0800 Subject: [PATCH] fix: index historical AgentRun traces --- internal/cloud/access-control.ts | 99 ++++++++++- internal/cloud/code-agent-agentrun-adapter.ts | 155 ++++++++++++++++-- internal/cloud/server-agent-chat.test.ts | 132 +++++++++++++++ 3 files changed, 369 insertions(+), 17 deletions(-) diff --git a/internal/cloud/access-control.ts b/internal/cloud/access-control.ts index 56bd5f9a..2158edca 100644 --- a/internal/cloud/access-control.ts +++ b/internal/cloud/access-control.ts @@ -1555,7 +1555,7 @@ class MemoryAccessStore { async getAgentSession(id) { return this.agentSessions.get(id) ?? null; } async getAgentSessionByTraceId(traceId) { return [...this.agentSessions.values()] - .filter((session) => session.lastTraceId === traceId) + .filter((session) => session.lastTraceId === traceId || Boolean(agentSessionTraceEvidence(session, traceId)) || agentSessionContainsTraceId(session, traceId)) .sort((a, b) => String(b.updatedAt ?? "").localeCompare(String(a.updatedAt ?? "")))[0] ?? null; } async listAgentSessionsForUser(input = {}) { @@ -1694,7 +1694,7 @@ class PostgresAccessStore extends MemoryAccessStore { return pgAgentSession(result.rows?.[0]); } async getAgentSession(id) { await this.ensureSchema(); const result = await this.query("SELECT id, project_id, agent_id, status, started_at, ended_at, owner_user_id, conversation_id, thread_id, last_trace_id, session_json, updated_at FROM agent_sessions WHERE id = $1 LIMIT 1", [id]); return pgAgentSession(result.rows?.[0]); } - async getAgentSessionByTraceId(traceId) { await this.ensureSchema(); const result = await this.query("SELECT id, project_id, agent_id, status, started_at, ended_at, owner_user_id, conversation_id, thread_id, last_trace_id, session_json, updated_at FROM agent_sessions WHERE last_trace_id = $1 ORDER BY updated_at DESC NULLS LAST LIMIT 1", [traceId]); return pgAgentSession(result.rows?.[0]); } + async getAgentSessionByTraceId(traceId) { await this.ensureSchema(); const result = await this.query("SELECT id, project_id, agent_id, status, started_at, ended_at, owner_user_id, conversation_id, thread_id, last_trace_id, session_json, updated_at FROM agent_sessions WHERE last_trace_id = $1 OR session_json LIKE $2 ORDER BY updated_at DESC NULLS LAST LIMIT 1", [traceId, `%${traceId}%`]); return pgAgentSession(result.rows?.[0]); } async listAgentSessionsForUser(input = {}) { await this.ensureSchema(); const role = textOr(input.actorRole, "user"); @@ -1852,6 +1852,7 @@ function mergeAgentSessionOwnerEvidence(nextValue, existingValue) { const existing = normalizeObject(existingValue); const next = normalizeObject(nextValue); const merged = { ...existing, ...next }; + merged.traceResults = mergeAgentSessionTraceResults(existing.traceResults, next.traceResults ?? next.traceResult ?? next); const existingMessages = Array.isArray(existing.messages) ? existing.messages : null; const nextMessages = Array.isArray(next.messages) ? next.messages : null; if (nextMessages?.length) { @@ -1871,6 +1872,11 @@ function mergeAgentSessionOwnerEvidence(nextValue, existingValue) { const nextChatMessages = Array.isArray(next.chatMessages) ? next.chatMessages : null; if (existingChatMessages?.length && (!nextChatMessages || nextChatMessages.length === 0)) merged.chatMessages = existingChatMessages; if (!next.agentRun && existing.agentRun) merged.agentRun = existing.agentRun; + const activeTraceEvidence = latestAgentSessionTraceEvidence(merged); + if (activeTraceEvidence) { + if (!merged.finalResponse && activeTraceEvidence.finalResponse) merged.finalResponse = activeTraceEvidence.finalResponse; + if (!merged.traceSummary && activeTraceEvidence.traceSummary) merged.traceSummary = activeTraceEvidence.traceSummary; + } const nextCount = numberOrNull(next.messageCount); const existingCount = numberOrNull(existing.messageCount); if (Array.isArray(merged.messages)) { @@ -1885,6 +1891,71 @@ function mergeAgentSessionOwnerEvidence(nextValue, existingValue) { } return merged; } +function mergeAgentSessionTraceResults(existingValue, nextValue) { + const records = new Map(); + for (const source of [existingValue, nextValue]) { + for (const entry of traceResultEntries(source)) { + const normalized = normalizeAgentSessionTraceResult(entry); + if (!normalized) continue; + records.set(normalized.traceId, { ...(records.get(normalized.traceId) ?? {}), ...normalized }); + } + } + const items = [...records.values()] + .sort((a, b) => String(b.updatedAt ?? b.createdAt ?? "").localeCompare(String(a.updatedAt ?? a.createdAt ?? ""))) + .slice(0, 50); + return Object.fromEntries(items.reverse().map((item) => [item.traceId, item])); +} +function traceResultEntries(value) { + if (!value) return []; + if (Array.isArray(value)) return value; + if (typeof value === "object") { + if (safeTraceIdLocal(value.traceId)) return [value]; + return Object.entries(value) + .filter(([key]) => safeTraceIdLocal(key)) + .map(([traceId, entry]) => ({ ...normalizeObject(entry), traceId })); + } + return []; +} +function normalizeAgentSessionTraceResult(value) { + const record = normalizeObject(value); + const traceId = safeTraceIdLocal(record.traceId); + if (!traceId) return null; + const agentRun = normalizeObject(record.agentRun); + const result = pruneEmpty({ + traceId, + status: textOr(record.status, ""), + conversationId: textOr(record.conversationId, ""), + sessionId: textOr(record.sessionId, ""), + threadId: textOr(record.threadId, ""), + messageId: textOr(record.messageId, ""), + createdAt: textOr(record.createdAt, ""), + updatedAt: textOr(record.updatedAt, ""), + finalResponse: redactTraceFinalResponse(record.finalResponse), + traceSummary: redactTraceSummary(record.traceSummary), + agentRun: redactAgentRunSummary(agentRun), + valuesRedacted: true, + secretMaterialStored: false + }); + return result.agentRun?.runId || result.finalResponse || result.traceSummary ? result : null; +} +function agentSessionTraceEvidence(session, traceId) { + const id = safeTraceIdLocal(traceId); + if (!id) return null; + const snapshot = normalizeObject(session?.session); + return normalizeAgentSessionTraceResult(snapshot.traceResults?.[id] ?? snapshot.traceResult?.[id] ?? null); +} +function latestAgentSessionTraceEvidence(snapshot) { + const id = safeTraceIdLocal(snapshot?.lastTraceId ?? snapshot?.traceId); + if (!id) return null; + return normalizeAgentSessionTraceResult(snapshot.traceResults?.[id] ?? null); +} +function agentSessionContainsTraceId(session, traceId) { + const id = safeTraceIdLocal(traceId); + if (!id) return false; + const snapshot = normalizeObject(session?.session); + if (safeTraceIdLocal(snapshot.lastTraceId ?? snapshot.traceId) === id) return true; + return [snapshot.messages, snapshot.chatMessages].some((messages) => Array.isArray(messages) && messages.some((message) => safeTraceIdLocal(message?.traceId) === id)); +} function normalizeFinalResponseConversationMessages(messages, finalResponse) { const finalText = textOr(finalResponse?.text, ""); const traceId = safeTraceIdLocal(finalResponse?.traceId); @@ -2357,11 +2428,16 @@ function redactTraceSummary(value) { const summary = normalizeObject(value); if (Object.keys(summary).length === 0) return undefined; return pruneEmpty({ + traceId: textOr(summary.traceId, ""), source: textOr(summary.source, ""), terminalStatus: textOr(summary.terminalStatus, ""), sourceEventCount: numberOrNull(summary.sourceEventCount ?? summary.eventCount), + noiseEventCount: numberOrNull(summary.noiseEventCount), + omittedNoiseCount: numberOrNull(summary.omittedNoiseCount), lastEventLabel: textOr(summary.lastEventLabel, ""), - finalAssistantRow: redactTraceFinalResponse(summary.finalAssistantRow) + updatedAt: textOr(summary.updatedAt, ""), + finalAssistantRow: redactTraceFinalResponse(summary.finalAssistantRow), + agentRun: redactAgentRunSummary(summary.agentRun) }); } function redactAgentRunSummary(value) { @@ -2369,12 +2445,27 @@ function redactAgentRunSummary(value) { if (Object.keys(agentRun).length === 0) return undefined; return pruneEmpty({ adapter: textOr(agentRun.adapter, ""), + managerUrl: textOr(agentRun.managerUrl, ""), backendProfile: textOr(agentRun.backendProfile, ""), + providerId: textOr(agentRun.providerId, ""), runId: textOr(agentRun.runId, ""), commandId: textOr(agentRun.commandId, ""), + attemptId: textOr(agentRun.attemptId, ""), + runnerId: textOr(agentRun.runnerId, ""), + runnerJobId: textOr(agentRun.runnerJobId, ""), jobName: textOr(agentRun.jobName, ""), namespace: textOr(agentRun.namespace, ""), - terminalStatus: textOr(agentRun.terminalStatus, "") + status: textOr(agentRun.status, ""), + runStatus: textOr(agentRun.runStatus, ""), + commandState: textOr(agentRun.commandState, ""), + terminalStatus: textOr(agentRun.terminalStatus, ""), + lastSeq: numberOrNull(agentRun.lastSeq), + eventStartSeq: numberOrNull(agentRun.eventStartSeq ?? agentRun.commandStartSeq ?? agentRun.startSeq), + sessionId: textOr(agentRun.sessionId, ""), + conversationId: textOr(agentRun.conversationId, ""), + threadId: textOr(agentRun.threadId, ""), + traceId: textOr(agentRun.traceId, ""), + reuseEligible: typeof agentRun.reuseEligible === "boolean" ? agentRun.reuseEligible : undefined }); } function conversationsFromAgentSessions(sessions = []) { diff --git a/internal/cloud/code-agent-agentrun-adapter.ts b/internal/cloud/code-agent-agentrun-adapter.ts index 5d6979d8..70afbb7c 100644 --- a/internal/cloud/code-agent-agentrun-adapter.ts +++ b/internal/cloud/code-agent-agentrun-adapter.ts @@ -606,22 +606,35 @@ export async function steerAgentRunChatTurn({ traceId, currentResult = null, par } export async function loadPersistedAgentRunResult(traceId, options = {}) { - if (!safeTraceId(traceId) || typeof options.accessController?.getAgentSessionByTraceId !== "function") return null; - const session = await options.accessController.getAgentSessionByTraceId(traceId); - const agentRun = session?.session?.agentRun; + const safeId = safeTraceId(traceId); + if (!safeId || typeof options.accessController?.getAgentSessionByTraceId !== "function") return null; + const session = await options.accessController.getAgentSessionByTraceId(safeId); + const traceEvidence = agentSessionTraceEvidence(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 mayUseTopLevelAgentRun = latestTraceId === safeId || agentRunTraceId === safeId || (!latestTraceId && !agentRunTraceId); + const agentRun = traceEvidence?.agentRun ?? (mayUseTopLevelAgentRun ? 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; + const status = firstNonEmpty(traceEvidence?.status, session?.status === "canceled" ? "canceled" : null, agentRun.terminalStatus, agentRun.commandState, "running"); return { accepted: true, - status: session.status === "canceled" ? "canceled" : "running", + status, shortConnection: true, traceId, - conversationId: safeConversationId(session.conversationId) || agentRun.conversationId || null, - sessionId: safeSessionId(session.id) || agentRun.sessionId || null, - threadId: safeOpaqueId(session.threadId) || agentRun.threadId || null, - messageId: session.session?.messageId ?? `msg_${traceId.slice(4)}`, - createdAt: session.startedAt ?? session.updatedAt ?? nowIso(options.now), - updatedAt: session.updatedAt ?? nowIso(options.now), - ownerUserId: session.ownerUserId, + 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), + ownerUserId: session?.ownerUserId, provider: providerForBackendProfile(agentRun.backendProfile ?? "deepseek"), model: modelForBackendProfile(agentRun.backendProfile, options.env ?? process.env), backend: backendForBackendProfile(agentRun.backendProfile ?? "deepseek"), @@ -629,13 +642,129 @@ export async function loadPersistedAgentRunResult(traceId, options = {}) { capabilityLevel: AGENTRUN_CAPABILITY_LEVEL, sessionMode: AGENTRUN_SESSION_MODE, implementationType: AGENTRUN_IMPLEMENTATION_TYPE, - finalResponse: session.session?.finalResponse ?? null, - traceSummary: session.session?.traceSummary ?? null, + finalResponse: traceEvidence?.finalResponse ?? session?.session?.finalResponse ?? null, + traceSummary: traceEvidence?.traceSummary ?? session?.session?.traceSummary ?? null, agentRun: { ...agentRun, adapter: ADAPTER_ID, 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 agentRunCommandMatchesTrace(command, traceId) { + const payload = command?.payload && typeof command.payload === "object" ? command.payload : {}; + 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; +} + export function agentRunSessionEvidence(payload = {}) { if (!payload?.agentRun) return {}; return { diff --git a/internal/cloud/server-agent-chat.test.ts b/internal/cloud/server-agent-chat.test.ts index 1b754adb..60d6dd09 100644 --- a/internal/cloud/server-agent-chat.test.ts +++ b/internal/cloud/server-agent-chat.test.ts @@ -1758,6 +1758,138 @@ 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"; + const secondTraceId = "trc_issue955_historical_second"; + const firstCommandId = "cmd_issue955_historical_first"; + const secondCommandId = "cmd_issue955_historical_second"; + const firstFinalText = "目前只有一个 HWPOD 可用:\n\n- d601-f103-v2 (board: D601-F103-V2 / STM32F103)\n\nAPI 返回 count=1, availableCount=1。"; + const secondFinalText = "编译成功!hwpod build completed。"; + const events = [ + { id: "evt_issue955_hist_first_created", runId, seq: 1, type: "command_created", payload: { commandId: firstCommandId }, createdAt: "2026-06-06T01:43:23.000Z" }, + { id: "evt_issue955_hist_first_tool", runId, seq: 25, type: "tool_call", payload: { method: "item/completed", type: "commandExecution", toolName: "commandExecution", itemId: "call_issue955_hwpod_list", command: "/bin/sh -lc 'hwpod list --available'", status: "completed", exitCode: 0, outputSummary: '{\"count\":1,\"availableCount\":1}', commandId: firstCommandId }, createdAt: "2026-06-06T01:43:46.000Z" }, + { id: "evt_issue955_hist_first_message", runId, seq: 34, type: "assistant_message", payload: { commandId: firstCommandId, itemId: "msg_issue955_first", text: firstFinalText }, createdAt: "2026-06-06T01:46:19.000Z" }, + { id: "evt_issue955_hist_first_done", runId, seq: 35, type: "terminal_status", payload: { commandId: firstCommandId, terminalStatus: "completed" }, createdAt: "2026-06-06T01:46:19.000Z" }, + { id: "evt_issue955_hist_second_created", runId, seq: 36, type: "command_created", payload: { commandId: secondCommandId }, createdAt: "2026-06-06T01:46:11.000Z" }, + { id: "evt_issue955_hist_second_message", runId, seq: 58, type: "assistant_message", payload: { commandId: secondCommandId, itemId: "msg_issue955_second", text: secondFinalText }, createdAt: "2026-06-06T01:46:28.000Z" }, + { id: "evt_issue955_hist_second_done", runId, seq: 67, 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_historical" })}\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:46.360Z", payload: { traceId: firstTraceId, conversationId: "cnv_issue955_historical", hwlabSessionId: "ses_issue955_historical", threadId: "thread-issue955-historical", providerProfile: "deepseek" } }, + { id: secondCommandId, runId, state: "completed", idempotencyKey: secondTraceId, createdAt: "2026-06-06T01:46:11.000Z", updatedAt: "2026-06-06T01:46:28.000Z", payload: { traceId: secondTraceId, conversationId: "cnv_issue955_historical", hwlabSessionId: "ses_issue955_historical", threadId: "thread-issue955-historical", 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", conversationId: "cnv_issue955_historical", threadId: "thread-issue955-historical" } }); + } + 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: 67, eventCount: 67, sessionRef: { sessionId: "ses_agentrun_deepseek_issue955", conversationId: "cnv_issue955_historical", threadId: "thread-issue955-historical" } }); + } + 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; + ownerSessions.set("ses_issue955_historical", testAgentSessionRecord({ + sessionId: "ses_issue955_historical", + projectId: "prj_v02_code_agent", + conversationId: "cnv_issue955_historical", + threadId: "thread-issue955-historical", + lastTraceId: secondTraceId, + status: "active", + session: { + messages: [ + { id: "msg_issue955_user_first", role: "user", text: "看看有几个hwpod可用", traceId: firstTraceId, status: "submitted" }, + { id: "msg_issue955_user_second", role: "user", text: "试一下编译 d601-f103-v2", traceId: secondTraceId, status: "submitted" }, + { id: "msg_issue955_second", role: "agent", text: secondFinalText, traceId: secondTraceId, status: "idle" } + ], + agentRun: { adapter: "agentrun-v01", managerUrl: `http://127.0.0.1:${agentRunPort}`, runId, commandId: secondCommandId, traceId: secondTraceId, backendProfile: "deepseek", terminalStatus: "completed", lastSeq: 67, valuesPrinted: false }, + finalResponse: { text: secondFinalText, textChars: secondFinalText.length, role: "assistant", status: "completed", traceId: secondTraceId, valuesPrinted: false }, + traceSummary: { traceId: secondTraceId, source: "agent-session-snapshot", sourceEventCount: 31, terminalStatus: "completed", agentRun: { runId, commandId: secondCommandId, lastSeq: 67, valuesPrinted: false }, valuesPrinted: false }, + valuesRedacted: true + } + })); + + const server = createCloudApiServer({ + traceStore: createCodeAgentTraceStore(), + 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 }; + }, + async getAgentSessionByTraceId(traceId) { + 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 ?? {}) }; + const record = testAgentSessionRecord({ ...existing, ...input, session: nextSession }); + ownerSessions.set(record.id, record); + return record; + } + } + }); + 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/trace/${firstTraceId}`, { + 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, firstTraceId); + assert.equal(body.status, "completed"); + assert.equal(body.finalResponse.text, firstFinalText); + assert.equal(body.agentRun.commandId, firstCommandId); + assert.equal(body.traceSummary.sourceEventCount, 4); + assert.ok(body.events.some((event) => event.commandId === firstCommandId)); + assert.equal(body.events.some((event) => event.commandId === secondCommandId), false); + assert.match(text, /d601-f103-v2/u); + assert.doesNotMatch(text, /编译成功/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/${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); + } 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-"));