Repository navigation
fix(server): a refused Claude turn no longer throws away its session - #16287
SunkenInTime wants to merge 1 commit into
Conversation
When Claude refused a turn because background work was still running, the context handoff for that turn stayed marked as pending. The next turn read that as an uncertain delivery and replaced the native session with a fresh one fed a text summary, losing tool output, reasoning and live background work. The refusal happens before Claude reads the prompt, so the handoff is now put back the way it was. The next turn resumes the same session and delivers the missed messages inline. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
ApprovabilityVerdict: Approved at Macroscope's review found this PR approvable — This narrowly restores context-handoff state when Claude rejects a turn before reading it, preventing an unnecessary native-session replacement. The production change is localized and covered by an integration test, with unrelated provider failures left unchanged. You can add or adjust custom eligibility rules. Learn more. |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configuration
📒 Files selected for processing (3)
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 7 remain after this review. 📝 WalkthroughWalkthroughThe turn-start flow now restores unsent context handoffs when Claude refuses a query replacement before reading the prompt. An integration test checks that refused requests are included in a later successful turn on the same native thread. ChangesContext handoff restoration
Priority: ➖ Normal Estimated code review effort: 2 (Simple) | ~10 minutes Change: Bug fix Sequence Diagram(s)sequenceDiagram
participant ProviderAdapter
participant ProviderTurnStartService
participant ContextHandoffDelivery
ProviderAdapter->>ProviderTurnStartService: Turn start fails with pre-prompt refusal
ProviderTurnStartService->>ContextHandoffDelivery: Run delivery.unsent
ContextHandoffDelivery->>ContextHandoffDelivery: Persist pending handoffs
Suggested reviewers: Merge Risk: ⚪ Minimal · up to The change appears ready to merge after normal checks; no actionable merge-blocking risk remains. Security Architecture ReviewSecurity architecture risk: 🔵 Low · up to The change preserves conversation continuity after a request is refused before delivery, while retaining conservative recovery for other failures. No introduced security vulnerability was established, but cancellation and delayed-write edge cases remain incompletely resolved. Retained concerns Security review detailsSecurity Blast Radius
Trust Boundaries and Controls
Resilience and Maintainability Implications
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
Claude ran a command earlier in both threads and was told to keep its output to itself. On main it no longer knows the number after two refused messages. With this PR it does.
Problem
When Claude refuses a message because background work is still running ("this model or setting change would end them"), a later message moves the thread to a brand-new Claude session. That session only gets T3's text recap of the thread, which leaves out tool calls, command output and reasoning, so Claude forgets what it did. Before #15770 the thread got stuck for good at this point. Now it keeps working, but without its memory.
To reproduce: in a Claude thread, have Claude start a background command, change the effort, send two messages (both get refused), wait for the command to finish, then send again.
Change
Claude has no history injection, so T3 marks a turn's catch-up context as
pendingbefore it starts the turn, and clears the marker once Claude accepts the prompt. The refusal happens before Claude reads the prompt, so the marker stays behind. The next turn sees apendingdelivery on the live session, treats the history as possibly half-delivered, and takes theuncertain_history_deliveryfallback that replaces the session.deliverContextHandoffsnow also returnsunsent, which writes the handoffs back as they were before the marker.ProviderTurnStartServiceruns it when the start fails withClaudeBackgroundWorkBlocksQueryReplacementError, matched by tag likeProviderFailure.tsalready does. The next turn resumes the same session and sends the missed messages inline.Only this refusal is covered. Other failures before the prompt reaches Claude, such as the CLI failing to spawn or an attachment that can't be read, still leave the marker. Covering those needs a provider-neutral "never sent" signal from every adapter, and nobody has reported hitting them.
Scope and approval
This is the second half of the triaged #15103: "A turn start that never reaches the CLI shouldn't leave a pending delivery that later forces this fallback" (triage). #15770 fixed the first half.
Verification
5e29148eto7c7734c4, and Claude answered "I do not know" and said it had no record of the node command. With the fix the thread stayed on6971aa5dand Claude answered866528915, which matches the command's stdout in that session's transcript. After the two refusals, the handoff's delivery waspendingon main and unset with the fix.ProviderSwitch.integration.test.ts, "keeps the native session after turns refused before reaching the provider". It fails on main because a second native session gets created, and passes here.ProviderSwitch.integration,ContextHandoffBudget,ProviderTurnStartServiceandClaudeAutomaticDelivery.integrationtests: 105 of 106 passed. The failure was a polling timeout in "switches providers while consuming a pending cross-provider merge-back" during a 16-minute run. That test passes on its own with and without this change.apps/servertypecheck, lint and format are clean on the touched files.Claude Opus 5.5 in T3 Code (Claude Code harness).
🤖 Generated with Claude Code