Transport resilience: spurious fetch failed on normal-length turns + no result recovery
Priority: high. Surfaced while smoke-testing the OpenCode subagent flow on PR #1; root-cause diagnosis by Claude Code, plus related self-review findings #16/#17.
Symptom
A ~6-minute adversarial review completed successfully server-side (ses_0c072d6d…, log shows exiting loop, no server error), but the companion returned TypeError: fetch failed and the job was recorded as failed with no output. The findings (454 KB) were fully recoverable via GET /session/{id}/message on the still-running server.
Root cause
captureTurn (lib/opencode.mjs) drives each turn over two long-lived HTTP connections to the shared server: an SSE event subscription and the client.sendMessage POST (opencode-server.mjs), which is held open for the entire turn and awaited in Promise.all([state.completion, responsePromise]). Node/undici defaults to a ~300s headers/body timeout. A normal deep review (single streaming step stalled ~3m37s; ~5m50s total) exceeds that budget → fetch failed, even though the server turn is healthy.
Compounding gap: no recovery path
handleResult (opencode-companion.mjs) reads the local job file only — it never re-fetches from the server, so a completed-but-uncaptured turn is unrecoverable via result.
runServerTurn discards the already-completed server turn on any client-side throw.
Related self-review findings
Suggested direction
- On a client-side
fetch failed/timeout, re-fetch the session's final message over HTTP (GET /session/{id}/message) before declaring failure.
- Give
result a server-side fallback so result <job> transparently recovers completed turns.
- Stop relying on a single long-held
sendMessage fetch: prefer prompt_async + SSE/poll as the source of truth, and/or tune undici timeouts and add an explicit outer turn timeout.
Ref: PR #1.
Transport resilience: spurious
fetch failedon normal-length turns + no result recoveryPriority: high. Surfaced while smoke-testing the OpenCode subagent flow on PR #1; root-cause diagnosis by Claude Code, plus related self-review findings #16/#17.
Symptom
A ~6-minute adversarial review completed successfully server-side (
ses_0c072d6d…, log showsexiting loop, no server error), but the companion returnedTypeError: fetch failedand the job was recorded asfailedwith no output. The findings (454 KB) were fully recoverable viaGET /session/{id}/messageon the still-running server.Root cause
captureTurn(lib/opencode.mjs) drives each turn over two long-lived HTTP connections to the shared server: an SSE event subscription and theclient.sendMessagePOST (opencode-server.mjs), which is held open for the entire turn and awaited inPromise.all([state.completion, responsePromise]). Node/undici defaults to a ~300s headers/body timeout. A normal deep review (single streaming step stalled ~3m37s; ~5m50s total) exceeds that budget →fetch failed, even though the server turn is healthy.Compounding gap: no recovery path
handleResult(opencode-companion.mjs) reads the local job file only — it never re-fetches from the server, so a completed-but-uncaptured turn is unrecoverable viaresult.runServerTurndiscards the already-completed server turn on any client-side throw.Related self-review findings
scheduleResponseFallbackCompletiontimer can complete a turn with a stale/truncatedfinalMessageifmessage.updatedarrives after thesendMessageresponse.session.idle+ the 250ms backup (eventOpenTimeoutMsonly guards stream-open).Suggested direction
fetch failed/timeout, re-fetch the session's final message over HTTP (GET /session/{id}/message) before declaring failure.resulta server-side fallback soresult <job>transparently recovers completed turns.sendMessagefetch: preferprompt_async+ SSE/poll as the source of truth, and/or tune undici timeouts and add an explicit outer turn timeout.Ref: PR #1.