Skip to content

fix(client): abort legacy SSE reconnect chain when the originating request times out - #2616

Open
claude[bot] wants to merge 17 commits into
mainfrom
fix/2615-legacy-sse-reconnect-after-timeout
Open

fix(client): abort legacy SSE reconnect chain when the originating request times out#2616
claude[bot] wants to merge 17 commits into
mainfrom
fix/2615-legacy-sse-reconnect-after-timeout

fix(client,core): finish the onerror discipline sweep; wire cancel fo…

c1b55eb
Select commit
Loading
Failed to load commit list.
Claude / Claude Code Review completed Aug 6, 2026 in 13m 7s

Code review found 1 important issue

Found 5 candidates, confirmed 4. See review comments for details.

Details

Severity Count
🔴 Important 1
🟡 Nit 1
🟣 Pre-existing 2
Severity File:Line Issue
🔴 Important packages/core-internal/src/shared/protocol.ts:1183-1199 maxTotalTimeout settlement leaves the per-leg timer armed; it re-fires cancel() and POSTs a duplicate notifications/canc
🟡 Nit packages/client/src/client/streamableHttp.ts:1200-1213 terminateSession is the last surviving unguarded onerror site of the sweep this PR finishes
🟣 Pre-existing packages/client/src/client/streamableHttp.ts:964-993 Resumed requests: replayed progress notifications carry the original progressToken — onprogress/resetTimeoutOnProgress a

Annotations

Check failure on line 1199 in packages/core-internal/src/shared/protocol.ts

See this annotation in the file changed.

@claude claude / Claude Code Review

maxTotalTimeout settlement leaves the per-leg timer armed; it re-fires cancel() and POSTs a duplicate notifications/cancelled on legacy sessions

The c1b55eb reroute of the maxTotalTimeout settlement through cancel() leaves the per-leg timeout timer armed (_resetTimeout's over-budget branch deletes the _timeoutInfo entry and throws without clearTimeout, and cancel() never sets responseReceived), so when the orphaned timer later fires, cancel() re-runs in full and POSTs a second notifications/cancelled for the same requestId on any legacy-era (2025-11-25) transport. One-line fix: clearTimeout(info.timeoutId) in _resetTimeout's maxTotalTime

Check warning on line 1213 in packages/client/src/client/streamableHttp.ts

See this annotation in the file changed.

@claude claude / Claude Code Review

terminateSession is the last surviving unguarded onerror site of the sweep this PR finishes

Pre-existing line, but it is the last surviving unguarded `onerror` site of the discipline sweep this PR's c1b55eb claims to finish: `terminateSession`'s catch (`streamableHttp.ts:1264`) does `this.onerror?.(error as Error); throw error;` with no intentional-abort guard, while its DELETE fetch is issued with `signal: this._abortController?.signal` alone — the exact one-signal shape this PR just fixed at the POST-path catch at line 1210. `transport.close()` landing mid-DELETE (or `terminateSessio

Check notice on line 993 in packages/client/src/client/streamableHttp.ts

See this annotation in the file changed.

@claude claude / Claude Code Review

Resumed requests: replayed progress notifications carry the original progressToken — onprogress/resetTimeoutOnProgress are dead and each event fires a spurious unknown-token onerror

Pre-existing issue (inbound twin of the resumed-cancel id asymmetry already scoped in 473bb37): `_handleSseStream` remaps `replayMessageId` onto responses only, so replayed `notifications/progress` on a resumed stream carry the ORIGINAL request's progressToken — and since a `resumptionToken` send never POSTs the fresh id, the caller's `onprogress` (registered under the fresh messageId) never fires, `resetTimeoutOnProgress` never resets, and every progress event surfaces a spurious "unknown token