From e0c3b80c48aeeb31f4def816a2ca0ad2febaa77a Mon Sep 17 00:00:00 2001 From: lyon Date: Thu, 18 Jun 2026 18:08:25 +0800 Subject: [PATCH] fix: avoid blocking terminal trace reads --- internal/cloud/server-agent-chat.test.ts | 137 +++++++++++++++++++++++ internal/cloud/server-code-agent-http.ts | 4 +- 2 files changed, 138 insertions(+), 3 deletions(-) diff --git a/internal/cloud/server-agent-chat.test.ts b/internal/cloud/server-agent-chat.test.ts index 9e8b5752..2960ab55 100644 --- a/internal/cloud/server-agent-chat.test.ts +++ b/internal/cloud/server-agent-chat.test.ts @@ -2510,6 +2510,143 @@ test("cloud api trace skips AgentRun refresh when terminal snapshot is already c } }); +test("cloud api trace schedules terminal side effects without blocking reads (#1422)", async () => { + const ownerCalls = []; + const factCalls = []; + let releaseOwnerWrite; + let ownerWriteReleased = false; + const ownerWrite = new Promise((resolve) => { + releaseOwnerWrite = () => { + if (ownerWriteReleased) return; + ownerWriteReleased = true; + resolve(); + }; + }); + const traceId = "trc_issue1422_trace_effects_async"; + const runId = "run_issue1422_trace_effects_async"; + const commandId = "cmd_issue1422_trace_effects_async"; + const finalText = "trace reads should not wait for terminal side effects."; + const traceStore = createCodeAgentTraceStore(); + traceStore.append(traceId, { + type: "backend", + status: "running", + label: "agentrun:backend:run-created", + source: "agentrun", + sourceSeq: 1, + runId, + commandId, + valuesPrinted: false + }); + traceStore.append(traceId, { + type: "result", + status: "completed", + label: "agentrun:terminal:completed", + source: "agentrun", + sourceSeq: 3, + runId, + commandId, + terminal: true, + valuesPrinted: false + }); + const codeAgentChatResults = new Map([[traceId, { + accepted: true, + status: "completed", + shortConnection: true, + traceId, + conversationId: "cnv_issue1422_trace_effects_async", + sessionId: "ses_issue1422_trace_effects_async", + threadId: "thread-issue1422-trace-effects-async", + 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:1", + 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, + codeAgentChatResults, + env: { + HWLAB_CODE_AGENT_ADAPTER: "agentrun-v01", + HWLAB_CODE_AGENT_AGENTRUN_PROVIDER_ID: "D601", + HWLAB_ENVIRONMENT: "v02", + HWLAB_GITOPS_PROFILE: "v02" + }, + sessionRegistry: { + recordFact(conversationId, fact) { + factCalls.push({ conversationId, fact }); + return { ok: true }; + } + }, + accessController: { + required: false, + async authenticate() { + return { ok: true, actor: TEST_AGENT_ACTOR, session: TEST_AUTH_SESSION }; + }, + async recordAgentSessionOwner(input) { + ownerCalls.push(input); + await ownerWrite; + return { ok: true, sessionId: input.sessionId }; + } + } + }); + await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); + + try { + const { port } = server.address(); + const fetchTrace = async () => { + const response = await Promise.race([ + fetch(`http://127.0.0.1:${port}/v1/agent/traces/${traceId}?limit=1`, { + headers: { cookie: "hwlab_session=test-stub-session" } + }), + delay(250).then(() => null) + ]); + assert.ok(response, "trace read should not wait for terminal side effects to finish"); + assert.equal(response.status, 200); + const body = await response.json(); + assert.equal(body.status, "completed"); + assert.equal(body.eventCount, 2); + assert.equal(body.finalResponse.text, finalText); + }; + for (let i = 0; i < 2; i += 1) { + await fetchTrace(); + } + for (let i = 0; i < 20 && ownerCalls.length < 1; i += 1) await delay(10); + assert.equal(ownerCalls.length, 1); + assert.equal(factCalls.length, 1); + assert.equal(codeAgentChatResults.get(traceId).turnStatusTerminalEffects.pending, true); + releaseOwnerWrite(); + for (let i = 0; i < 20 && codeAgentChatResults.get(traceId).turnStatusTerminalEffects?.recorded !== true; i += 1) await delay(10); + assert.equal(codeAgentChatResults.get(traceId).turnStatusTerminalEffects.recorded, true); + } finally { + releaseOwnerWrite?.(); + await new Promise((resolve, reject) => { + server.close((error) => (error ? reject(error) : resolve())); + }); + } +}); + test("cloud api turn status skips AgentRun refresh for complete terminal evidence (#1422)", async () => { const calls = []; const traceId = "trc_issue1422_terminal_fast_path"; diff --git a/internal/cloud/server-code-agent-http.ts b/internal/cloud/server-code-agent-http.ts index 79bc80aa..0c080dca 100644 --- a/internal/cloud/server-code-agent-http.ts +++ b/internal/cloud/server-code-agent-http.ts @@ -2564,9 +2564,7 @@ export async function handleCodeAgentTraceHttp(request, response, url, options) agentRunResult = synced.result ?? agentRunResult; } if (isTraceCommandTerminalStatus(agentRunResult?.status)) { - await finalizeCodeAgentBillingUsage({ payload: agentRunResult, params: agentRunResult, options: requestOptions }); - recordCodeAgentConversationFact(agentRunResult, requestOptions); - await recordCodeAgentSessionOwner({ payload: agentRunResult, params: agentRunResult, options: requestOptions, status: codeAgentOwnerStatusForResult(agentRunResult), preserveLastTraceId: true }); + scheduleCodeAgentTerminalTurnStatusEffects({ payload: agentRunResult, params: agentRunResult, options: requestOptions, preserveLastTraceId: true }); } } catch (error) { refreshError = error;