{"record":{"id":"6ba743a142d61dc9","repo":"can1357/oh-my-pi","slug":"aborterror-6ba743","errorCode":null,"errorMessage":"AbortError","messagePattern":"AbortError","errorType":"exception","errorClass":"AbortError","httpStatus":null,"severity":"warning","filePath":"packages/ai/src/providers/anthropic.ts","lineNumber":2659,"sourceCode":"\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\tcalculateCost(model, output.usage);\n\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;","sourceCodeStart":2641,"sourceCodeEnd":2677,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/ai/src/providers/anthropic.ts#L2641-L2677","documentation":"This AIError.AbortError is thrown when the Anthropic stream ends without any events while the caller had aborted the request. After a stream-termination condition the library first checks local timeout aborts and then whether the caller aborted, and surfaces a clean AbortError instead of a generic stream error.","triggerScenarios":"The caller's AbortSignal fired (activeAbortTracker.wasCallerAbort() is true) and the SSE stream then terminated before any event was processed — e.g. user cancellation, request timeout, or application shutdown during a streaming Anthropic call with no first event received.","commonSituations":"User cancels a generation in the UI; a deadline/AbortSignal.timeout() elapses before the model responds; process shutdown aborts in-flight streams; slow first token causing the caller to abort.","solutions":["Expected behavior on cancellation — handle AbortError distinctly from real failures and treat it as a cancelled turn.","If unintended, increase or remove the AbortSignal timeout so the model has time to emit the first event.","Check code paths that abort the signal (UI cancel buttons, orchestration timeouts) if cancellation was not expected.","Retry the request if the abort was caused by an aggressive deadline; consider streaming-first-token timeouts instead."],"exampleFix":"// before\nconst res = await streamAnthropic({ prompt }); // no signal, random teardown aborts\n\n// after\nconst controller = new AbortController();\ntry {\n  const res = await streamAnthropic({ prompt, signal: controller.signal });\n} catch (err) {\n  if (err instanceof AIError.AbortError) return; // cancelled, not a failure\n  throw err;\n}","handlingStrategy":"try-catch","validationCode":"if (signal?.aborted) {\n  return; // don't start a stream that will immediately be aborted\n}","typeGuard":"function isAbortError(err: unknown): err is AIError.AbortError {\n  return err instanceof AIError.AbortError || (err instanceof Error && err.name === \"AbortError\");\n}","tryCatchPattern":"try {\n  for await (const event of stream) {\n    handle(event);\n  }\n} catch (err) {\n  if (err instanceof AIError.AbortError) {\n    return; // cancellation is expected, not a failure\n  } else {\n    throw err;\n  }\n}","preventionTips":["Treat AbortError as a control-flow signal, not an error, in agent/UI layers.","Set first-token timeouts generously (models can take many seconds before the first event).","Use AbortController per request and abort only from intentional cancel paths.","Use AbortSignal.timeout() with a margin above p99 time-to-first-token."],"tags":["abort","cancellation","streaming","anthropic"],"backgroundTag":"request-aborted","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}