export interface AbortSourceTracker { requestAbortController: AbortController; requestSignal: AbortSignal; abortLocally(reason: Error): Error; getLocalAbortReason(): Error | undefined; wasCallerAbort(): boolean; } /** * Tracks whether a merged request signal was aborted by the caller or by provider-local logic. * * Caller aborts always take priority. When both the caller and a local watchdog fire near * each other, the merged `requestSignal.reason` reflects whichever AbortController called * `.abort()` first — but ordering is racy and not meaningful for upstream consumers. What * matters is intent: if the caller's signal aborted, the request was cancelled by the * caller, and any local watchdog reason is incidental and **MUST NOT** be surfaced as a * retryable transient error (which would cause auto-retry to re-enter streaming and leave * the UI showing a spinner the user already tried to cancel). */ export function createAbortSourceTracker(callerSignal?: AbortSignal): AbortSourceTracker { const requestAbortController = new AbortController(); const requestSignal = callerSignal ? AbortSignal.any([callerSignal, requestAbortController.signal]) : requestAbortController.signal; let localAbortReason: Error | undefined; return { requestAbortController, requestSignal, abortLocally(reason) { localAbortReason = reason; requestAbortController.abort(reason); return reason; }, getLocalAbortReason() { // Caller intent dominates. Surface a local reason only when the caller did not // abort, so timeout/idle-watchdog errors don't masquerade as the user's cancel. if (!localAbortReason || callerSignal?.aborted) return undefined; return requestSignal.reason === localAbortReason ? localAbortReason : undefined; }, wasCallerAbort() { // If the caller signal aborted, treat it as a caller abort regardless of which // AbortController won the race to set `requestSignal.reason`. The previous // `requestSignal.reason !== localAbortReason` heuristic flipped the result to // `false` when a local watchdog fired microseconds before the user's ESC, which // then routed user-initiated cancels through the auto-retry transient-error // path. return callerSignal?.aborted === true; }, }; }