Skip to content

fix(server): a refused Claude turn no longer throws away its session - #16287

Open
SunkenInTime wants to merge 1 commit into
pingdotgg:mainfrom
SunkenInTime:t3code/claude-refused-turn-keeps-session
Open

SunkenInTime wants to merge 1 commit into
pingdotgg:mainfrom
SunkenInTime:t3code/claude-refused-turn-keeps-session

Conversation

@SunkenInTime

Copy link
Copy Markdown
Contributor
Before (main) After (this PR)
before after

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 pending before 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 a pending delivery on the live session, treats the history as possibly half-delivered, and takes the uncertain_history_delivery fallback that replaces the session.

deliverContextHandoffs now also returns unsent, which writes the handoffs back as they were before the marker. ProviderTurnStartService runs it when the start fails with ClaudeBackgroundWorkBlocksQueryReplacementError, matched by tag like ProviderFailure.ts already 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

  • Real app: dev server from this branch, Claude Sonnet 5, driven in headless Edge with the steps above (the screenshots). On main the thread moved from session 5e29148e to 7c7734c4, and Claude answered "I do not know" and said it had no record of the node command. With the fix the thread stayed on 6971aa5d and Claude answered 866528915, which matches the command's stdout in that session's transcript. After the two refusals, the handoff's delivery was pending on main and unset with the fix.
  • New integration test in 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, ProviderTurnStartService and ClaudeAutomaticDelivery.integration tests: 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/server typecheck, lint and format are clean on the touched files.

Claude Opus 5.5 in T3 Code (Claude Code harness).

🤖 Generated with Claude Code

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>
@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:M 30-99 changed lines (additions + deletions). labels Oct 6, 2026
@macroscopeapp

macroscopeapp Bot commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Approved at 0d5ec22

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.

@coderabbitai

coderabbitai Bot commented Oct 6, 2026

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository: pingdotgg/t3code/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: a9f585a8-837a-4541-8be6-c14f8d711830
📥 Commits

Reviewing files that changed from the base of the PR and between 758dc29 and 0d5ec22.

📒 Files selected for processing (3)
  • apps/server/src/orchestration-v2/ContextHandoffDelivery.ts
  • apps/server/src/orchestration-v2/ProviderTurnStartService.ts
  • apps/server/src/orchestration-v2/testkit/ProviderSwitch.integration.test.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 7 remain after this review.


📝 Walkthrough

Walkthrough

The 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.

Changes

Context handoff restoration

Layer / File(s) Summary
Expose the unsent handoff effect
apps/server/src/orchestration-v2/ContextHandoffDelivery.ts
The delivery result adds an unsent effect. For inline transcript delivery, running the effect persists pending handoffs. Other return paths provide Effect.void.
Restore handoffs after a refused start
apps/server/src/orchestration-v2/ProviderTurnStartService.ts, apps/server/src/orchestration-v2/testkit/ProviderSwitch.integration.test.ts
The service recognizes Claude’s pre-prompt query-replacement refusal, including nested causes, and runs delivery.unsent. The integration test verifies that two refused requests appear in the next successful turn on the same native thread.

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
Loading

Suggested reviewers: juliusmarminge

Merge Risk: ⚪ Minimal · up to 0d5ec

The change appears ready to merge after normal checks; no actionable merge-blocking risk remains.

Security Architecture Review

Security architecture risk: 🔵 Low · up to 0d5ec

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
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The examined change affects context delivery and session continuity for an existing application conversation. Restoration preserves handoff identity and uses the existing thread-scoped persistence path; no expansion to another conversation or service was established.

Trust Boundaries and Controls

  • observed — The production refusal is generated by the adapter before replacing its live process and before offering the user prompt. Reuse checks native-thread identity, selection, and effective query-policy identity; restoring handoffs does not bypass those checks.

Resilience and Maintainability Implications

  • observed — The existing outbox excludes later same-thread lifecycle work while an earlier effect is running or pending. Startup also checks the current run-attempt identity before submission. These controls counter the ordinary concurrent-start overwrite hypothesis, but do not conclusively resolve cancellation overlap.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 1 functions across 3 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: a refused Claude turn no longer causes the session to be discarded.
Description check ✅ Passed The description covers the problem, change, scope and approval, and focused verification. It also identifies the known test timeout and remaining out-of-scope failure cases.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M 30-99 changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant