From 9b2c637b64cf29e6338d69f6f54c4c0d0b64fcde Mon Sep 17 00:00:00 2001 From: Codex Date: Tue, 2 Jun 2026 03:04:31 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=AE=9E=E6=97=B6=E4=B8=8A=E6=8A=A5=20r?= =?UTF-8?q?unner=20backend=20=E8=BF=9B=E5=B1=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/runner/run-once.ts | 29 +++++++++++++++++++ .../cases/50-hwlab-manual-dispatch.ts | 5 ++++ 2 files changed, 34 insertions(+) diff --git a/src/runner/run-once.ts b/src/runner/run-once.ts index ecab3d6..e2fcc8d 100644 --- a/src/runner/run-once.ts +++ b/src/runner/run-once.ts @@ -135,8 +135,10 @@ async function executeCommand(api: RunnerManagerApi, options: RunnerOnceOptions, const acked = await api.getCommand(options.runId, command.id); if (acked.state === "cancelled") return await reportCancelled(api, options.runId, command.id, runner, attemptId, "command cancelled before backend start"); await assertNotCancelled(api, options.runId, command.id); + await api.appendEvent(options.runId, { type: "backend_status", payload: { phase: "backend-turn-started", commandId: command.id, attemptId, runnerId: runner.id, backendProfile: options.backendProfile ?? null, workspaceReady: Boolean(workspacePath) } }); const abortController = new AbortController(); const stopCancelWatch = watchCancellation(api, options.runId, command.id, abortController); + const stopBackendProgress = startBackendProgress(api, options.runId, command.id, attemptId, runner.id, options.backendProfile ?? null); try { const latestRun = await api.getRun(options.runId); const backendOptions = { ...options, ...(workspacePath ? { workspacePath } : {}), abortSignal: abortController.signal }; @@ -151,6 +153,8 @@ async function executeCommand(api: RunnerManagerApi, options: RunnerOnceOptions, const failure = { failureKind, terminalStatus: terminalStatusForFailure(failureKind), message: errorMessage(error) }; return await reportCommandFailure(api, options.runId, command.id, runner, attemptId, failure, "runner:execute"); } finally { + stopBackendProgress(); + await appendBestEffort(api, options.runId, { type: "backend_status", payload: { phase: "backend-turn-finished", commandId: command.id, attemptId, runnerId: runner.id } }); stopCancelWatch(); } } @@ -200,6 +204,31 @@ function startHeartbeat(api: RunnerManagerApi, runId: string, runnerId: string, }; } +function startBackendProgress(api: RunnerManagerApi, runId: string, commandId: string, attemptId: string, runnerId: string, backendProfile: string | null): () => void { + let stopped = false; + let ticks = 0; + const startedAt = Date.now(); + const emit = async (): Promise => { + if (stopped) return; + ticks += 1; + await appendBestEffort(api, runId, { type: "backend_status", payload: { phase: "backend-turn-running", commandId, attemptId, runnerId, backendProfile, elapsedMs: Date.now() - startedAt, ticks } }); + }; + const timer = setInterval(() => { void emit(); }, 10_000); + void emit(); + return () => { + stopped = true; + clearInterval(timer); + }; +} + +async function appendBestEffort(api: RunnerManagerApi, runId: string, event: BackendEvent): Promise { + try { + await api.appendEvent(runId, event); + } catch { + // Visibility events are best effort; terminal command reporting remains authoritative. + } +} + function annotateCommandEvent(event: BackendEvent, commandId: string, attemptId: string, runnerId: string): BackendEvent { return { ...event, payload: { ...event.payload, commandId, attemptId, runnerId } }; } diff --git a/src/selftest/cases/50-hwlab-manual-dispatch.ts b/src/selftest/cases/50-hwlab-manual-dispatch.ts index 191ae1c..f188bf4 100644 --- a/src/selftest/cases/50-hwlab-manual-dispatch.ts +++ b/src/selftest/cases/50-hwlab-manual-dispatch.ts @@ -108,6 +108,11 @@ console.log(JSON.stringify({ apiVersion: manifest.apiVersion, kind: manifest.kin assert.equal(multiEvents.filter((event) => event.type === "backend_status" && event.payload?.phase === "codex-app-server-starting").length, 1); assert.equal(multiEvents.filter((event) => event.type === "backend_status" && event.payload?.phase === "codex-app-server:reused").length, 1); assert.equal(multiEvents.filter((event) => event.type === "backend_status" && event.payload?.phase === "resource-bundle-materialized").length, 1); + for (const commandId of [multiTurn.commandId, secondCommand.id]) { + assert.ok(multiEvents.some((event) => event.type === "backend_status" && event.payload?.phase === "backend-turn-started" && event.payload?.commandId === commandId), `command ${commandId} must emit backend-turn-started before waiting on Codex`); + assert.ok(multiEvents.some((event) => event.type === "backend_status" && event.payload?.phase === "backend-turn-running" && event.payload?.commandId === commandId), `command ${commandId} must emit backend-turn-running while Codex is active`); + assert.ok(multiEvents.some((event) => event.type === "backend_status" && event.payload?.phase === "backend-turn-finished" && event.payload?.commandId === commandId), `command ${commandId} must emit backend-turn-finished after Codex returns`); + } assert.equal(multiEvents.filter((event) => event.type === "backend_status" && event.payload?.phase === "command-terminal").length, 2); const secondEnvelope = await client.get(`/api/v1/runs/${multiTurn.runId}/commands/${secondCommand.id}/result`) as JsonRecord; assert.equal(secondEnvelope.terminalStatus, "completed");