Repository navigation
Description
Summary
RequestExecutor.httpError discards the native cause of an HTTP response-body read failure and omits safe stream-progress metadata. A mid-stream provider failure is therefore persisted and logged only as a generic Decode error (200 POST ...), making the underlying transport failure irretrievable.
Mentioning @kitlangton, who observed and investigated the incident.
Environment
- opencode version:
0.0.0-next-17430 - OS: macOS 25.5.0, Darwin arm64
- Terminal: Ghostty 1.3.1 (
TERM_PROGRAM=ghostty) - Shell:
/bin/zsh - Install/channel:
next - Active plugins:
session-control.ts,opencode-voice, and the V2 debug workspace plugin
Reproduction
The production incident was intermittent, but the loss is deterministic with a test executor:
- Return an HTTP 200 response whose body stream emits one or more valid chunks.
- Reject the
ReadableStreamafter those chunks with a nested native error or a cause that does not decode as{ message: string }. - Consume the response through
RequestExecutor.stream. - Inspect the resulting
AIError, service log, and persisted session error.
Expected Behavior
The failure should retain safe, structured diagnostics without retaining response payloads:
- HTTP operation and status
- Normalized native error tag/class and code
- Bounded cause-chain tags/codes
- Chunks and bytes successfully read
- Decoded event count and last event type
- Whether a terminal protocol event was observed
- Content type/encoding and elapsed time
The native payload, generated text, reasoning content, tool arguments, credentials, arbitrary headers, and raw SSE frames should not be logged or persisted.
Actual Behavior
The assistant message was persisted with only:
{
"type": "provider.transport",
"message": "Decode error (200 POST https://chatgpt.com/backend-api/codex/responses)"
}The service log contained the same generic message as both the AIError and nested cause. Nine reasoning-summary events had been persisted before the body read failed, but no answer text or terminal response event arrived. A later retry succeeded and the service remained healthy.
Additional Context
The information is lost along this path on current v2:
- Effect captures the native
ReadableStreamrejection asDecodeError.causeineffect/unstable/http/HttpClientResponse.ts. packages/ai/src/route/executor.tsunwrapsreason.cause, butnativeTransportFailureonly decodes a shallow{ message, code, cause }shape.- If decoding the source fails,
httpErrorfalls back toinput.error.message, the outer generic decode message, rather than inspecting a bounded cause chain. httpErrorconstructs a replacementAIErrorwithout the original causal chain and includes only request context for read failures, despite theHttpClientErrorcarrying the response.packages/core/src/session/to-session-error.tsdeliberately persists only the projected session error type, message, and optional status.
Suggested minimal changes:
- Recursively inspect a bounded cause chain and retain only normalized tags/classes and codes.
- Include allowlisted response metadata and HTTP status for read failures.
- Count chunks/bytes at
RequestExecutor.streamand decoded event metadata at the protocol boundary without retaining payloads. - Emit one structured sanitized diagnostic when a stream fails, including failures that will be retried.
- Add a regression test for a successful HTTP 200 stream that emits valid chunks and then fails.
- Replace arbitrary redacted diagnostic headers/query values with explicit allowlists.
Raw response retention is intentionally not proposed: truncation limits size, not sensitivity, and even a short SSE frame may contain generated text, prompts, tool arguments, credentials, account identifiers, or encrypted reasoning.
Related but distinct: #41320 reports a Cloudflare 1010 disconnection. This issue concerns preservation of sanitized diagnostics after any mid-stream response-body failure.
Activity
JavaGT commentedon Aug 21, 2026
Confirming this at production scale, and it's worse for multi-agent workflows than the log line suggests.
Environment
Production evidence (single day, one machine)
From the server log (
Failed to drain Sessionentries), all againsthttps://opencode.ai/zen/v1/chat/completions:AI.Error: RequestExecutor.read: Decode error (200 POST https://opencode.ai/zen/v1/chat/completions)AI.Error: RequestExecutor.read: ECONNRESET: The socket connection was closed unexpectedlyAll of them fatal to the turn. Zero retried. The generic message is the only signal — there is no way to tell from the persisted error whether the provider truncated the body, sent malformed SSE, or the local socket died, which is exactly the diagnostic loss this issue describes.
Why sub-agents make this acute
This machine runs a long-running multi-agent program (~90 sub-agent sessions/day through the Zen free models). For orchestrators, each one of these failures is a worker that dies silently:
The failure rate scales with fleet size, so heavy sub-agent users absorb this superlinearly. Preserving the native cause + stream-progress metadata (as the PR does) would let tooling distinguish provider-side truncation from local transport faults and route retries accordingly — that distinction is exactly what's missing today.
Dante-dan commentedon Oct 27, 2026
#52634 already fixes the generic decode message and native-cause unwrapping. I implemented the stream-progress part of this report in a focused reference patch.
It counts chunks/bytes, decoded frames, terminal observation and monotonic elapsed time; records the last normalized LLM event tag and allowlisted content type/encoding; and projects these optional fields into persisted session errors. One new diagnostic logs only status, operation and this progress metadata.
The regression emits two valid SSE chunks before rejecting the stream, checks persistence projection, and verifies generated text, native messages and arbitrary headers stay out of the new progress object and diagnostic. Validation: 1,938 AI tests passed (28 skipped), 13 session-error tests passed, and all 35 typecheck tasks passed. Lint passed with warnings.
This addresses progress metadata; existing raw error/cause/header handling and bounded native cause-chain normalization remain outside this patch.
opencodejr commentedon Oct 28, 2026
Thanks for the detailed write-up. I reproduced this with a test against
RequestExecutor.stream: a mocked HTTP 200 response sends two valid SSE chunks, then theReadableStreamfails. The script below sets this up.Before #52634 (317dfbc): a failure whose cause has no string
messageproduced exactlyDecode error (200 POST https://chatgpt.com/backend-api/codex/responses)with codeDecodeError, as you describe.On latest
dev(651032b): that generic message is gone. Reads now fail withConnection lost while reading the response[: code: detail], and the HTTP 200 status and the native cause object are kept. Some diagnostics are still lost:TypeError("terminated")with a cause carryingUND_ERR_SOCKET: works; the message and code includeUND_ERR_SOCKET.ECONNRESET) on the innermost error: message isConnection lost while reading the response: terminatedandcodeis undefined.ERR_STREAM_PREMATURE_CLOSE) but no string message: message is justConnection lost while reading the responseandcodeis undefined.The root cause is that
nativeTransportFailureonly decodes a{ message: string }shape and looks at most one cause deep.Reproduction script (repro.sh)
Run it:
bash repro.sh