fix(claude-sdk-oauth): fork stream-start-timeout retries at the pre-turn boundary (fixes #723) - #1034
Merged
Merged
Conversation
…urn boundary (fixes #723) A stream-start-timeout abort pushed the turn user payload into the resident SDK lineage but recordSyncedStream never ran, so the in-memory binding kept pointing at the pre-turn boundary. The same-turn retry then re-attached and re-appended the identical user message, growing the transcript ~8K per attempt and re-billing the whole conversation every retry. Remember an in-memory retry checkpoint (the attempted turn full sent-stream digest) on aborted/failed/discarded attempts. When the retry re-sends that exact turn, decideFromBinding forks at the pre-turn assistant boundary instead of reattaching, so the retry byte-layout matches the failed attempt and rides the prefix cache. A stalled first turn with no boundary re-seeds byte-identically. Observed as continuity reason timeout_retry.
…#723) The plain retry path already removes the terminal error assistant before re-sending, so failed and retried requests carry identical context.messages. Pin that invariant as a regression guard with a mutation proof (disabling the removal flips the assertion).
…watchdog The comment claimed the setting clamps the stream-start guard; since the 2026-08-18 reconciliation it caps only the retry-continuation watchdog at max(cap, streamStartTimeoutMs). Comment-only.
…th (issue #723) The fullstack harness gains stallNextResponse() (headers flushed, first SSE event withheld) so a turn can stall at stream start against the loopback server. The new probe stalls a continuation turn, lets auto-retry fire, and asserts every post-stall continuity decision forks with deltaMessages == 1 — no flatten, no re-bill. Also repairs the harness for the post-#969 ambient opt-in gate: seeds the sandbox CLAUDE_CONFIG_DIR credential store and sets the enabled opt-in.
…-retry-continuity # Conflicts: # packages/coding-agent/CHANGELOG.md # packages/coding-agent/src/core/changes.md
…-retry-continuity # Conflicts: # packages/coding-agent/src/core/changes.md
…-retry-continuity # Conflicts: # packages/coding-agent/CHANGELOG.md
Owner
Author
|
Maintainer note — GitHub Actions never delivered events for this PR. Zero workflow runs exist for every head SHA of this branch ( Local equivalents of the required gates, run at the final HEAD
Merging with |
…-retry-continuity # Conflicts: # packages/coding-agent/CHANGELOG.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes the retry-storm re-billing from #723: a stream-start timeout abort pushed the turn's user payload into the resident SDK lineage but never advanced the continuity binding, so the same-turn retry re-attached and re-appended the identical user message — the transcript grew ~8K per attempt and the whole conversation was re-billed at full price on every retry (measured $25/6min; $1,084 over 3 days on worker dispatch, per the issue comment).
session-turn-attempt.ts: an attempt that ends aborted/failed/discarded now remembers an in-memory retry checkpoint — the binding anchored at the pre-turn boundary plus the attempted turn's full sent-stream digest (unansweredTurnDigest).session-continuity.ts: when the SAME turn retries (full-turn digest matches),decideFromBindingforks at the pre-turn assistant boundary instead of re-attaching, so the retry's request byte-layout matches the failed attempt and rides the prefix cache; observed as continuity reasontimeout_retry. A stalled first turn with no boundary re-seeds byte-identically (cache read after the first write).723-provider-timeout-retry-request-identityproves the plain retry path sendscontext.messagesdeep-equal to the failed attempt (with mutation proof), and the stalestreamRetryTimeoutMsdocstring now matches the reconciled watchdog semantics.claude-sdk-oauth-fullstack-harness.mjspredates the feat(auth): make ambient Claude and Cursor CLI lanes explicit opt-in #969 ambient opt-in gate — it now seeds the sandboxCLAUDE_CONFIG_DIR/.credentials.jsonand sets theenabledopt-in, and gainsstallNextResponse()(headers flushed, first SSE event withheld). New probeclaude-sdk-oauth-stream-stall-retry-probe.mjs.Root cause
QA & Evidence
723-claude-sdk-oauth-timeout-abort-retry-continuity.test.ts— RED captured (red-c1.txt:maxSendsPerLineage 2 !== 1, double cold-seed), GREEN after fix (green-c1.txt, 2/2).723-provider-timeout-retry-request-identity.test.ts(GREEN; mutation proofred-c2-mutation.txtfails the assertion when the removal is disabled).claude-sdk-oauth-continuity-retry-checkpoint.test.ts(checkpoint branch fires only for the exact unanswered turn; fail-closed ordering preserved); decision-table + binding-store round-trip cases extended.claude-sdk-oauth-*.test.ts+ retry suites 54 files / 430 tests pass;test/suite/regressions/150 files / 465 tests pass;tsc --noEmitclean.Command:
The probe uses an isolated agent dir, a 127.0.0.1-only loopback SSE server, a seeded dummy credential store, and cleans up its subprocesses, server, and sandbox.
Risks & Residuals
retry.maxRetries(default 3) and the registry eviction caps.Related Issues
4f b3d5e4f)