{"record":{"id":"388a863f137edf52","repo":"can1357/oh-my-pi","slug":"stream-ended-before-message-start","errorCode":null,"errorMessage":"stream ended before message_start","messagePattern":"stream ended before message_start","errorType":"exception","errorClass":"AnthropicStreamEnvelopeError","httpStatus":null,"severity":"error","filePath":"packages/ai/src/providers/anthropic.ts","lineNumber":2662,"sourceCode":"\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else if (event.type === \"message_stop\") {\n\t\t\t\t\t\t\tsawTerminalEnvelope = true;\n\t\t\t\t\t\t\tsawMessageStop = true;\n\t\t\t\t\t\t\t// The protocol is complete even if a broken keep-alive leaves the HTTP body open.\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tconst firstEventTimeoutError = activeAbortTracker.getLocalAbortReason();\n\t\t\t\t\tif (firstEventTimeoutError) {\n\t\t\t\t\t\tthrow firstEventTimeoutError;\n\t\t\t\t\t}\n\t\t\t\t\tif (activeAbortTracker.wasCallerAbort()) {\n\t\t\t\t\t\tthrow new AIError.AbortError();\n\t\t\t\t\t}\n\t\t\t\t\tif (!sawEvent || !sawMessageStart) {\n\t\t\t\t\t\tthrow new AIError.AnthropicStreamEnvelopeError(\"stream ended before message_start\");\n\t\t\t\t\t}\n\t\t\t\t\tif (!sawTerminalEnvelope) {\n\t\t\t\t\t\t// Neither a message_delta stop_reason nor message_stop arrived: the\n\t\t\t\t\t\t// connection died mid-generation. Finalizing the partial message as\n\t\t\t\t\t\t// a clean \"stop\" would make the agent loop treat the truncated turn\n\t\t\t\t\t\t// as complete (silent mid-sentence halt), so fail the turn. The\n\t\t\t\t\t\t// envelope error is transparently retried before replay-unsafe\n\t\t\t\t\t\t// content streams; afterwards it surfaces as an error turn whose\n\t\t\t\t\t\t// complete tool calls the agent loop salvages\n\t\t\t\t\t\t// (`recoverTransientErrorToolTurn` recognizes the envelope-error\n\t\t\t\t\t\t// text and `retainCompletedToolCalls` drops half-streamed calls).\n\t\t\t\t\t\tthrow new AIError.AnthropicStreamEnvelopeError(\"stream ended before message_stop\");\n\t\t\t\t\t}\n\t\t\t\t\tif (!sawMessageStop) {\n\t\t\t\t\t\t// A stop_reason arrived via message_delta, so generation finished;\n\t\t\t\t\t\t// only the trailing message_stop frame is missing (non-conforming\n\t\t\t\t\t\t// gateway). Degrade to best-effort instead of discarding the turn.\n\t\t\t\t\t\treportAnthropicEnvelopeAnomaly(\"stream ended before message_stop\");","sourceCodeStart":2644,"sourceCodeEnd":2680,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/ai/src/providers/anthropic.ts#L2644-L2680","documentation":"This AnthropicStreamEnvelopeError is thrown when the SSE stream terminates without delivering the mandatory `message_start` event (and often no events at all). The library cannot attribute or finalize a message it never saw begin, so it fails the turn; this error is transparently retried before replay-unsafe content streams.","triggerScenarios":"The response body ends (connection close, gateway drop, immediate 200-with-empty-stream) before any message_start frame, with no local first-event timeout error and no caller abort — checked right after the SSE loop in iterateAnthropicEvents.","commonSituations":"Flaky networks or load balancers closing idle connections; proxies buffering SSE and cutting the stream; Anthropic 529/overload responses funneled into a dead stream; server-side timeout before first token; misconfigured gateways that accept the request but emit nothing.","solutions":["Retry the request — the library retries envelope errors transparently when replay is safe; enable/configure retries.","Check network path (VPN, proxy, LB idle timeouts) for premature connection termination of SSE streams.","Verify the gateway supports streaming (no buffering, correct content-type text/event-stream) and forwards Anthropic error frames.","Increase first-event timeouts and check Anthropic status for overload incidents."],"exampleFix":null,"handlingStrategy":"retry","validationCode":"// Precheck that the endpoint streams and stays alive:\nconst res = await fetch(endpoint, { method: \"POST\", headers, signal, body });\nif (!res.ok || !res.headers.get(\"content-type\")?.includes(\"text/event-stream\")) {\n  throw new Error(`Expected SSE 200, got ${res.status} ${res.headers.get(\"content-type\")}`);\n}","typeGuard":null,"tryCatchPattern":"try {\n  yield* streamAnthropicEvents(request);\n} catch (err) {\n  if (err instanceof AIError.AnthropicStreamEnvelopeError && /ended before message_start/.test(err.message)) {\n    // transient: retry with backoff (library retries automatically when replay-safe)\n  } else {\n    throw err;\n  }\n}","preventionTips":["Enable/configure retries for envelope errors in agent loops.","Raise LB/proxy idle timeouts so connections survive time-to-first-token.","Monitor Anthropic status for overload (529) incidents correlated with these failures.","Ensure gateways flush SSE immediately (disable buffering/response smoothing)."],"tags":["streaming","network","sse","truncated-stream"],"backgroundTag":"stream-truncated","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}