From a044366325a2038a2455ebf938f8f1f8c759fbe3 Mon Sep 17 00:00:00 2001 From: Jose Angel Morena Date: Tue, 9 Jun 2026 01:31:20 +0200 Subject: [PATCH] fix: drain pending events before breaking on session idle in JSON format mode The loop() function in opencode run --format json breaks immediately when it receives a session.status = idle event. In containerized environments, this event races ahead of text and step-finish part events in the SSE pipeline, causing incomplete JSONL output (only step_start emitted). Track active step lifecycle with a counter. When idle arrives while steps are still open, defer the break and drain remaining events with a 3s safety timeout. --- packages/opencode/src/cli/cmd/run.ts | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts index 18d033dadb3c..126c3e041c56 100644 --- a/packages/opencode/src/cli/cmd/run.ts +++ b/packages/opencode/src/cli/cmd/run.ts @@ -632,8 +632,13 @@ export const RunCommand = effectCmd({ async function loop(client: OpencodeClient, events: Awaited>) { const toggles = new Map() let error: string | undefined + let activeSteps = 0 + let pendingIdle = false + let idleTimeoutHandle: ReturnType | undefined + let idleTimedOut = false for await (const event of events.stream) { + if (idleTimedOut) break if ( event.type === "message.updated" && event.properties.sessionID === sessionID && @@ -673,11 +678,17 @@ export const RunCommand = effectCmd({ } if (part.type === "step-start") { + activeSteps++ if (emit("step_start", { part })) continue } if (part.type === "step-finish") { - if (emit("step_finish", { part })) continue + activeSteps = Math.max(0, activeSteps - 1) + if (emit("step_finish", { part })) { + if (pendingIdle && activeSteps <= 0) { clearTimeout(idleTimeoutHandle); break } + continue + } + if (pendingIdle && activeSteps <= 0) { clearTimeout(idleTimeoutHandle); break } } if (part.type === "text" && part.time?.end) { @@ -716,8 +727,12 @@ export const RunCommand = effectCmd({ err = String(props.error.data.message) } error = error ? error + EOL + err : err - if (emit("error", { error: props.error })) continue + if (emit("error", { error: props.error })) { + if (pendingIdle) { clearTimeout(idleTimeoutHandle); break } + continue + } UI.error(err) + if (pendingIdle) { clearTimeout(idleTimeoutHandle); break } } if ( @@ -725,6 +740,11 @@ export const RunCommand = effectCmd({ event.properties.sessionID === sessionID && event.properties.status.type === "idle" ) { + if (activeSteps > 0) { + pendingIdle = true + idleTimeoutHandle = setTimeout(() => { idleTimedOut = true }, 3_000) + continue + } break } @@ -750,6 +770,7 @@ export const RunCommand = effectCmd({ } } } + clearTimeout(idleTimeoutHandle) return error } const cwd = args.attach ? (directory ?? sess.directory ?? (await current(sdk))) : (directory ?? root)