From e765f23af9395371e4daf56f74ddffb9431bc664 Mon Sep 17 00:00:00 2001 From: lyon Date: Thu, 18 Jun 2026 13:03:21 +0800 Subject: [PATCH] fix: skip completed AgentRun turn refresh --- internal/cloud/code-agent-agentrun-adapter.ts | 33 ++++++- internal/cloud/server-agent-chat.test.ts | 93 +++++++++++++++++++ internal/cloud/server-code-agent-http.ts | 3 +- 3 files changed, 127 insertions(+), 2 deletions(-) diff --git a/internal/cloud/code-agent-agentrun-adapter.ts b/internal/cloud/code-agent-agentrun-adapter.ts index 3cb48abc..7480043f 100644 --- a/internal/cloud/code-agent-agentrun-adapter.ts +++ b/internal/cloud/code-agent-agentrun-adapter.ts @@ -440,6 +440,10 @@ function isAgentRunCommandAlreadyClaimed(error) { export async function syncAgentRunChatResult({ traceId, currentResult = null, options = {}, traceStore = defaultCodeAgentTraceStore, appendResultEvent = true, refreshEvents = true, forceResultSync = false }) { const initial = currentResult ?? await loadPersistedAgentRunResult(traceId, options); const mapped = initial ? await resolveAgentRunTraceCommandMapping({ traceId, mapped: initial, options }) : initial; + if (!forceResultSync && options.skipAgentRunTerminalRefreshIfComplete === true && agentRunTerminalResultRefreshSatisfied(mapped, traceId)) { + const runnerTrace = mapped.runnerTrace ?? traceStore.snapshot(traceId); + return { result: mapped, runnerTrace, found: true, eventsRefreshed: false, resultSynced: false, terminalRefreshSkipped: true }; + } if (!mapped?.agentRun?.runId || !mapped?.agentRun?.commandId) return { result: currentResult, runnerTrace: traceStore.snapshot(traceId), found: Boolean(currentResult), eventsRefreshed: false, resultSynced: false }; const env = options.env ?? process.env; const fetchImpl = options.fetchImpl ?? globalThis.fetch; @@ -491,6 +495,33 @@ function agentRunCommandResultSyncRequired(mapped = {}, eventsResponse = null) { return Boolean(agentRunTerminalStatusFromEvents(eventsResponse?.events)); } +function agentRunTerminalResultRefreshSatisfied(mapped = {}, traceId = null) { + const safeId = safeTraceId(traceId ?? mapped?.traceId); + const agentRun = mapped?.agentRun && typeof mapped.agentRun === "object" ? mapped.agentRun : null; + const traceSummary = mapped?.traceSummary && typeof mapped.traceSummary === "object" ? mapped.traceSummary : null; + const finalResponse = mapped?.finalResponse && typeof mapped.finalResponse === "object" ? mapped.finalResponse : null; + const providerTrace = agentRun?.providerTrace && typeof agentRun.providerTrace === "object" + ? agentRun.providerTrace + : mapped?.providerTrace && typeof mapped.providerTrace === "object" + ? mapped.providerTrace + : null; + const commandId = text(agentRun?.commandId); + const runId = text(agentRun?.runId); + if (!safeId || !runId || !commandId) return false; + if (!isAgentRunTerminalStatus(mapped?.status ?? agentRun?.terminalStatus ?? agentRun?.commandState ?? agentRun?.status)) return false; + if (safeTraceId(mapped?.traceId ?? agentRun?.traceId) !== safeId) return false; + if (safeTraceId(providerTrace?.traceId) !== safeId) return false; + const finalTraceId = safeTraceId(finalResponse?.traceId ?? safeId); + if (finalTraceId && finalTraceId !== safeId) return false; + if (!String(finalResponse?.text ?? "").trim()) return false; + if (traceSummary?.source !== "agentrun-command-result") return false; + if (safeTraceId(traceSummary.traceId ?? safeId) !== safeId) return false; + const summaryAgentRun = traceSummary.agentRun && typeof traceSummary.agentRun === "object" ? traceSummary.agentRun : null; + if (text(summaryAgentRun?.runId) !== runId) return false; + if (text(summaryAgentRun?.commandId) !== commandId) return false; + return true; +} + function agentRunTerminalStatusFromEvents(events = []) { if (!Array.isArray(events)) return null; for (const event of [...events].reverse()) { @@ -529,7 +560,7 @@ async function resolveAgentRunTraceCommandMapping({ traceId, mapped, options = { 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; + if (traceFieldsMatch && (mapped.status === "running" || (options.skipAgentRunTerminalRefreshIfComplete === true && agentRunTerminalResultRefreshSatisfied(mapped, safeId)))) return mapped; const env = options.env ?? process.env; const fetchImpl = options.fetchImpl ?? globalThis.fetch; const managerUrl = resolveAgentRunManagerUrl(env, agentRun.managerUrl); diff --git a/internal/cloud/server-agent-chat.test.ts b/internal/cloud/server-agent-chat.test.ts index 210fb627..4331bf53 100644 --- a/internal/cloud/server-agent-chat.test.ts +++ b/internal/cloud/server-agent-chat.test.ts @@ -2303,6 +2303,99 @@ test("cloud api turn status reuses AgentRun result-sync trace refresh (#1422)", } }); +test("cloud api turn status skips AgentRun refresh for complete terminal evidence (#1422)", async () => { + const calls = []; + const traceId = "trc_issue1422_terminal_fast_path"; + const runId = "run_issue1422_terminal_fast_path"; + const commandId = "cmd_issue1422_terminal_fast_path"; + const finalText = "终态结果已经由 AgentRun command result 投影完成。"; + 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 }); + response.writeHead(500, { "content-type": "application/json" }); + response.end(`${JSON.stringify({ ok: false, message: `unexpected refresh ${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([[traceId, { + accepted: true, + status: "completed", + shortConnection: true, + traceId, + conversationId: "cnv_issue1422_terminal_fast_path", + sessionId: "ses_issue1422_terminal_fast_path", + threadId: "thread-issue1422-terminal-fast-path", + ownerUserId: TEST_AGENT_ACTOR.id, + ownerRole: TEST_AGENT_ACTOR.role, + finalResponse: { text: finalText, textChars: finalText.length, role: "assistant", status: "completed", traceId, valuesPrinted: false }, + traceSummary: { + traceId, + source: "agentrun-command-result", + sourceEventCount: 3, + terminalStatus: "completed", + finalAssistantRow: { role: "assistant", status: "completed", textChars: finalText.length, textPreview: finalText, messageId: `msg_${traceId.slice(4)}`, valuesPrinted: false }, + agentRun: { runId, commandId, lastSeq: 3, valuesPrinted: false }, + valuesPrinted: false + }, + agentRun: { + adapter: "agentrun-v01", + managerUrl: `http://127.0.0.1:${agentRunPort}`, + runId, + commandId, + traceId, + lastSeq: 3, + status: "completed", + commandState: "completed", + terminalStatus: "completed", + providerTrace: { traceId, runId, commandId, terminalStatus: "completed", valuesPrinted: false }, + valuesPrinted: false + }, + providerTrace: { traceId, runId, commandId, 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_CODE_AGENT_AGENTRUN_PROVIDER_ID: "D601", + 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/turns/${traceId}`, { + headers: { cookie: "hwlab_session=test-stub-session" } + }); + assert.equal(response.status, 200); + const body = await response.json(); + assert.equal(body.status, "completed"); + assert.equal(body.terminal, true); + assert.equal(body.finalResponse.text, finalText); + assert.equal(body.traceSummary.agentRun.commandId, commandId); + assert.deepEqual(calls, []); + } 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 repairs historical same-session AgentRun trace after lastTraceId advances (#955)", async () => { const calls = []; const ownerSessions = new Map(); diff --git a/internal/cloud/server-code-agent-http.ts b/internal/cloud/server-code-agent-http.ts index 21f43bd6..5e2178e5 100644 --- a/internal/cloud/server-code-agent-http.ts +++ b/internal/cloud/server-code-agent-http.ts @@ -1219,7 +1219,7 @@ async function resolveCodeAgentTurnStatusSnapshot(traceId, options) { try { const synced = await syncAgentRunChatResult({ traceId, currentResult: result, options: refreshOptions, traceStore }); result = synced.result ?? result; - turnRefreshSatisfiedByResultSync = synced.eventsRefreshed === true || synced.resultSynced === true; + turnRefreshSatisfiedByResultSync = synced.eventsRefreshed === true || synced.resultSynced === true || synced.terminalRefreshSkipped === true; if (result && !canAccessOwnedResult(result, options.actor)) return forbiddenTurnSnapshot(traceId); if (result && isTraceCommandTerminalStatus(result.status)) { await finalizeCodeAgentBillingUsage({ payload: result, params: result, options }); @@ -1286,6 +1286,7 @@ function codeAgentTurnStatusRefreshOptions(options = {}) { const timeoutMs = Math.max(1, Math.min(configuredBudgetMs, upstreamTimeoutMs)); return { ...options, + skipAgentRunTerminalRefreshIfComplete: true, env: { ...env, HWLAB_CODE_AGENT_AGENTRUN_HTTP_TIMEOUT_MS: String(timeoutMs),