{"record":{"id":"1579e0019773682a","repo":"can1357/oh-my-pi","slug":"stream-ended-before-message-stop","errorCode":null,"errorMessage":"stream ended before message_stop","messagePattern":"stream ended before message_stop","errorType":"exception","errorClass":"AnthropicStreamEnvelopeError","httpStatus":null,"severity":"error","filePath":"packages/ai/src/providers/anthropic.ts","lineNumber":2674,"sourceCode":"\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\");\n\t\t\t\t\t}\n\t\t\t\t\tif (openBlocks.size > 0) {\n\t\t\t\t\t\tfor (const [openIndex, openBlock] of openBlocks) {\n\t\t\t\t\t\t\treportAnthropicEnvelopeAnomaly(\n\t\t\t\t\t\t\t\t`stream ended with an unterminated ${openBlock.kind} block at index ${openIndex}`,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\tif (openBlock.kind === \"ignored\" || openBlock.contentIndex < 0) continue;\n\t\t\t\t\t\t\tconst danglingBlock = blocks[openBlock.contentIndex];\n\t\t\t\t\t\t\tif (danglingBlock) finalizeStreamBlock(danglingBlock, openBlock.contentIndex);\n\t\t\t\t\t\t}\n\t\t\t\t\t\topenBlocks.clear();\n\t\t\t\t\t}","sourceCodeStart":2656,"sourceCodeEnd":2692,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/ai/src/providers/anthropic.ts#L2656-L2692","documentation":"This AnthropicStreamEnvelopeError is thrown when the stream ends without a terminal envelope: neither a message_delta carrying stop_reason nor a message_stop frame arrived. The library deliberately fails the turn instead of finalizing the partial message, because treating a truncated stream as a clean stop would make the agent loop think the turn completed and silently halt mid-sentence.","triggerScenarios":"SSE iteration ends after message_start (and possibly partial content blocks) but before message_delta/message_stop — e.g. connection drop mid-generation, proxy cutting long-running streams, or gateway max-duration limits; notably distinct from the later message_stop-only degradation, which is tolerated as an anomaly.","commonSituations":"Load balancers or Cloudflare idle timeouts killing long generations; mobile/unstable networks; gateways imposing response-time caps shorter than generation time; server crashes mid-stream; max_tokens not reached but connection severed.","solutions":["Retry the turn — envelope errors are retried before replay-unsafe streams and later surface as an error turn whose completed tool calls are salvaged via recoverTransientErrorToolCall/retainCompletedToolCalls.","Reduce generation length (lower max_tokens, shorter prompts) or split work into smaller turns so streams finish within proxy timeouts.","Raise proxy/LB idle and response timeouts to exceed worst-case generation time.","Check network stability and whether the failure correlates with a specific gateway that caps stream duration."],"exampleFix":null,"handlingStrategy":"retry","validationCode":"// Ensure max_tokens and prompt size keep generation well under proxy response-time limits:\nif (estimatedGenerationMs > proxyTimeoutMs) {\n  throw new Error(\"Generation likely exceeds gateway timeout; reduce max_tokens or raise the proxy timeout.\");\n}","typeGuard":null,"tryCatchPattern":"try {\n  yield* streamAnthropicEvents(request);\n} catch (err) {\n  if (err instanceof AIError.AnthropicStreamEnvelopeError && /ended before message_stop/.test(err.message)) {\n    // salvage completed tool calls via recoverTransientErrorToolTurn / retainCompletedToolCalls\n    // then retry the remaining generation\n  } else {\n    throw err;\n  }\n}","preventionTips":["Configure gateway/LB response-time caps above worst-case generation duration.","Lower max_tokens or split long tasks into multiple turns to shorten streams.","Keep the agent-loop salvage path (recoverTransientErrorToolTurn) enabled so completed tool calls survive retries.","Alert on truncated-stream rates per gateway to catch timeout misconfigurations early."],"tags":["streaming","network","truncated-stream","anthropic"],"backgroundTag":"stream-truncated","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}