fix(server): stop Claude resume handshakes from completing turns that never ran - #5710
Conversation
A Claude result arriving with no local turnState emitted an untargeted turn.completed (no turnId). Real turns get turnState in sendTurn and assistant messages outside a turn auto-start a synthetic one, so the only things that land in that branch are the resume handshake (system/init + result(num_turns: 0)), a late result for a turn already completed locally, or a stream failure with no turn in flight — none of which is a turn. Ingestion's strict lifecycle guard rejected the phantom while an active turn was tracked, but fell through to accept it when activeTurnId was null — exactly the state while a turn start is pending — flipping the session back to "ready" for a turn that never existed (observed 2026-08-07 at 19:03:51 and 19:20). Two-sided fix: - ClaudeAdapter: a result with no turnState keeps its token-usage emission but no longer emits turn.completed; a structured log (claude.turn.result-without-active-turn) stays as the field tripwire. - ProviderRuntimeIngestion: with no active turn tracked, only completions that name their turn are applied. Targeted completions with no tracked turn still land (turn.started loss stays recoverable), and the other adapters were audited: Grok/Cursor/OpenCode always attach turnId. Both suppression tests fail against the unfixed code (verified by reverting the sources and re-running); the targeted-completion control passes on both, proving no legitimate completion is suppressed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016BStzvrcLQ5gYmWscVk26C
SDKResultSuccess and SDKResultError both declare num_turns as required, so the widening cast was noise. Review finding (INFO). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016BStzvrcLQ5gYmWscVk26C
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
ApprovabilityVerdict: Approved e670eb0 This is a well-scoped bug fix that prevents Claude resume handshakes from incorrectly completing turns that never ran. The changes are defensive (rejecting invalid events), clearly documented, and include comprehensive test coverage for both the rejection and acceptance paths. You can customize Macroscope's approvability policy. Learn more. |
…tchet The two tests added here each seeded their session with an inline `Effect.runPromise(harness.engine.dispatch(...))`, pushing this file from 31 to 33 manual Effect runtime calls — one past its `no-manual-effect-runtime-in-tests` baseline — so `vp check` failed with two errors. The harness already wraps its other runtime entry points (`readModel`, `drain`), so dispatch is wrapped the same way: one `dispatch` helper next to the engine, used for the three seed commands inside `createHarness` and exposed on the harness for the new tests. That takes the file to 29 occurrences, under the baseline, rather than raising the baseline. Test-only; no behavior change. ProviderRuntimeIngestion 47/47, ClaudeAdapter 67/67, `vp check` clean, server typecheck clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Dismissing prior approval to re-evaluate e670eb0
|
Pushed e670eb0 to fix the The two tests I added each seeded their session with an inline The harness already wraps its other runtime entry points ( Test-only, no production code touched. Locally: |
## What's Changed * feat(web): show how many subagents are running at a glance by @t3dotgg in pingdotgg/t3code#5745 * fix(web): add missing cursor-pointer styling to dropdowns and interactive buttons by @naMqe-h in pingdotgg/t3code#5716 * fix(server): stop Claude resume handshakes from completing turns that never ran by @gfsaaser24 in pingdotgg/t3code#5710 **Full Changelog**: pingdotgg/t3code@v0.0.33-nightly.20260808.1038...v0.0.33-nightly.20260809.1039 Upstream release: https://github.com/pingdotgg/t3code/releases/tag/v0.0.33-nightly.20260809.1039
The desktop update tooltip flattens the release body into plain bullet lines, so the structured nightly highlights (`## Nightly highlights`, `### Added`, …) rendered their headings as list items and wasted 3 of the tooltip's 8 item slots. This PR makes the generated changelog and the tooltip work together, and leans harder into the LLM pipeline while keeping attribution. ## Changes - **Nightly highlights are a flat list** of plain items with compact PR references that survive the tooltip's markup stripping (`#61` for fork PRs, `t3code#123` for upstream, short SHAs for direct commits). Removals are phrased as "Remove …" via prompt guidance instead of a section label. - **Attribution is structural:** summary items are now `{ text, evidenceIds }` objects. The model only returns evidence IDs; URLs are rendered deterministically and IDs are validated against the collected evidence, so references can't be fabricated. The rolling fork-features issue gets the same per-item references. - **Extraction sees diffs:** each commit contributes a truncated patch (4 KB cap, lockfiles excluded) so descriptions are grounded in what actually changed rather than paraphrasing PR titles. Over the 160k-char prompt budget, the largest diffs are dropped first — titles and PR bodies never are. - **`## What's Changed` stays** as the exhaustive record and now credits contributors once per PR (`by @username`, fetched via `gh api` with graceful fallback). - **The tooltip parser stops at `## What's Changed`** when curated highlights precede it, so raw commit lines no longer mix with highlight lines. Bodies that open with the commit list (upstream-style, or a nightly with no highlights) still show it. ## Pipeline example Evidence in (abridged; one upstream commit from the delta below): ```jsonc { "id": "pingdotgg#5756", "title": "fix(web): usage page loses the cost quality panel, gains a back button", "description": "<sanitized PR body>", "files": ["apps/web/src/components/usage/UsagePage.tsx", "…"], "diff": "--- a/apps/web/src/components/usage/UsagePage.tsx\n+import { useCanGoBack, useNavigate, useRouter } from \"@tanstack/react-router\";\n…" } ``` Summary items out (actual output — note one PR correctly yielding both an improvement and a removal): ```json { "text": "Return from Usage through history or to home", "evidenceIds": ["pingdotgg#5756"] } { "text": "Remove Cost quality panel from the Usage page", "evidenceIds": ["pingdotgg#5756"] } ``` Rendered in the release body: ```markdown - Return from Usage through history or to home ([t3code#5756](pingdotgg#5756)) - Remove Cost quality panel from the Usage page ([t3code#5756](pingdotgg#5756)) ``` ## Regenerated release notes for `v0.0.33-nightly.20260809.121` Rerunning the new generator against the existing release's refs produced 13 items (the published release had 5, plus 3 heading lines). What the tooltip shows, via the actual parser: ``` • Identify sidebar threads with unsent drafts (#23) • Show subagent activity counts in right-panel controls (t3code#5745) • Return from Usage through history or to home (t3code#5756) • Give agents access to pasted image files (t3code#5757) • Manage projects from the dedicated Settings page (t3code#5768) • Preserve selected diff view across panel closures and reloads (t3code#5731) • Show pointer cursors for clickable controls and actions (t3code#5716) • Resume Claude sessions without falsely completing skipped turns (t3code#5710) ``` <details> <summary>Full regenerated nightly highlights</summary> - Identify sidebar threads with unsent drafts ([#23](#23)) - Show subagent activity counts in right-panel controls ([t3code#5745](pingdotgg#5745)) - Return from Usage through history or to home ([t3code#5756](pingdotgg#5756)) - Give agents access to pasted image files ([t3code#5757](pingdotgg#5757)) - Manage projects from the dedicated Settings page ([t3code#5768](pingdotgg#5768)) - Preserve selected diff view across panel closures and reloads ([t3code#5731](pingdotgg#5731)) - Show pointer cursors for clickable controls and actions ([t3code#5716](pingdotgg#5716)) - Resume Claude sessions without falsely completing skipped turns ([t3code#5710](pingdotgg#5710)) - Stop the active Codex turn before queued follow-ups ([t3code#5762](pingdotgg#5762)) - Keep pinned thread ordering stable while saving changes ([t3code#5767](pingdotgg#5767)) - Show multi-device usage progress while totals load ([t3code#5772](pingdotgg#5772)) - Remove Cost quality panel from the Usage page ([t3code#5756](pingdotgg#5756)) - Remove the limited project settings modal ([t3code#5768](pingdotgg#5768)) </details> <details> <summary>Regenerated rolling fork-features summary (excerpt, for issue #43)</summary> ## Added - Identify yngatech builds through names, icons, palettes, and About details ([#3](#3), [#27](#27), [#29](#29), [#30](#30), [#56](#56)) - Choose and preview sounds for completed agent turns ([#5](#5), [#7](#7), [#19](#19)) - Detect GitHub outages and show status in the sidebar ([#14](#14)) - Show unsent thread drafts with sidebar pencil indicators ([#23](#23)) - Start new threads with GitHub issues as context ([#31](#31)) - Open thread pull or merge requests from command palette ([#46](#46)) - Synchronize existing-thread text and composer settings across connected devices ([#39](#39), [#44](#44)) ## Improved - Inspect full commands, labeled streams, numeric exit codes, and failures ([#11](#11), [#4](#4), [#36](#36)) - Close terminals directly from the grouped terminal list ([#22](#22)) - Show clearer snooze countdowns near hour and day boundaries ([#45](#45)) - Show setup script outcomes in the thread timeline ([#40](#40)) - Install yngatech alongside upstream T3 Code on Windows ([#49](#49)) </details> ## Rollout The generator and workflow changes take effect on the next nightly. The tooltip parser change ships inside the app, so currently installed builds still show the mixed list until users update onto a build containing it. --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
The desktop update tooltip flattens the release body into plain bullet lines, so the structured nightly highlights (`## Nightly highlights`, `### Added`, …) rendered their headings as list items and wasted 3 of the tooltip's 8 item slots. This PR makes the generated changelog and the tooltip work together, and leans harder into the LLM pipeline while keeping attribution. ## Changes - **Nightly highlights are a flat list** of plain items with compact PR references that survive the tooltip's markup stripping (`#61` for fork PRs, `t3code#123` for upstream, short SHAs for direct commits). Removals are phrased as "Remove …" via prompt guidance instead of a section label. - **Attribution is structural:** summary items are now `{ text, evidenceIds }` objects. The model only returns evidence IDs; URLs are rendered deterministically and IDs are validated against the collected evidence, so references can't be fabricated. The rolling fork-features issue gets the same per-item references. - **Extraction sees diffs:** each commit contributes a truncated patch (4 KB cap, lockfiles excluded) so descriptions are grounded in what actually changed rather than paraphrasing PR titles. Over the 160k-char prompt budget, the largest diffs are dropped first — titles and PR bodies never are. - **`## What's Changed` stays** as the exhaustive record and now credits contributors once per PR (`by @username`, fetched via `gh api` with graceful fallback). - **The tooltip parser stops at `## What's Changed`** when curated highlights precede it, so raw commit lines no longer mix with highlight lines. Bodies that open with the commit list (upstream-style, or a nightly with no highlights) still show it. ## Pipeline example Evidence in (abridged; one upstream commit from the delta below): ```jsonc { "id": "pingdotgg#5756", "title": "fix(web): usage page loses the cost quality panel, gains a back button", "description": "<sanitized PR body>", "files": ["apps/web/src/components/usage/UsagePage.tsx", "…"], "diff": "--- a/apps/web/src/components/usage/UsagePage.tsx\n+import { useCanGoBack, useNavigate, useRouter } from \"@tanstack/react-router\";\n…" } ``` Summary items out (actual output — note one PR correctly yielding both an improvement and a removal): ```json { "text": "Return from Usage through history or to home", "evidenceIds": ["pingdotgg#5756"] } { "text": "Remove Cost quality panel from the Usage page", "evidenceIds": ["pingdotgg#5756"] } ``` Rendered in the release body: ```markdown - Return from Usage through history or to home ([t3code#5756](pingdotgg#5756)) - Remove Cost quality panel from the Usage page ([t3code#5756](pingdotgg#5756)) ``` ## Regenerated release notes for `v0.0.33-nightly.20260809.121` Rerunning the new generator against the existing release's refs produced 13 items (the published release had 5, plus 3 heading lines). What the tooltip shows, via the actual parser: ``` • Identify sidebar threads with unsent drafts (#23) • Show subagent activity counts in right-panel controls (t3code#5745) • Return from Usage through history or to home (t3code#5756) • Give agents access to pasted image files (t3code#5757) • Manage projects from the dedicated Settings page (t3code#5768) • Preserve selected diff view across panel closures and reloads (t3code#5731) • Show pointer cursors for clickable controls and actions (t3code#5716) • Resume Claude sessions without falsely completing skipped turns (t3code#5710) ``` <details> <summary>Full regenerated nightly highlights</summary> - Identify sidebar threads with unsent drafts ([#23](#23)) - Show subagent activity counts in right-panel controls ([t3code#5745](pingdotgg#5745)) - Return from Usage through history or to home ([t3code#5756](pingdotgg#5756)) - Give agents access to pasted image files ([t3code#5757](pingdotgg#5757)) - Manage projects from the dedicated Settings page ([t3code#5768](pingdotgg#5768)) - Preserve selected diff view across panel closures and reloads ([t3code#5731](pingdotgg#5731)) - Show pointer cursors for clickable controls and actions ([t3code#5716](pingdotgg#5716)) - Resume Claude sessions without falsely completing skipped turns ([t3code#5710](pingdotgg#5710)) - Stop the active Codex turn before queued follow-ups ([t3code#5762](pingdotgg#5762)) - Keep pinned thread ordering stable while saving changes ([t3code#5767](pingdotgg#5767)) - Show multi-device usage progress while totals load ([t3code#5772](pingdotgg#5772)) - Remove Cost quality panel from the Usage page ([t3code#5756](pingdotgg#5756)) - Remove the limited project settings modal ([t3code#5768](pingdotgg#5768)) </details> <details> <summary>Regenerated rolling fork-features summary (excerpt, for issue #43)</summary> ## Added - Identify yngatech builds through names, icons, palettes, and About details ([#3](#3), [#27](#27), [#29](#29), [#30](#30), [#56](#56)) - Choose and preview sounds for completed agent turns ([#5](#5), [#7](#7), [#19](#19)) - Detect GitHub outages and show status in the sidebar ([#14](#14)) - Show unsent thread drafts with sidebar pencil indicators ([#23](#23)) - Start new threads with GitHub issues as context ([#31](#31)) - Open thread pull or merge requests from command palette ([#46](#46)) - Synchronize existing-thread text and composer settings across connected devices ([#39](#39), [#44](#44)) ## Improved - Inspect full commands, labeled streams, numeric exit codes, and failures ([#11](#11), [#4](#4), [#36](#36)) - Close terminals directly from the grouped terminal list ([#22](#22)) - Show clearer snooze countdowns near hour and day boundaries ([#45](#45)) - Show setup script outcomes in the thread timeline ([#40](#40)) - Install yngatech alongside upstream T3 Code on Windows ([#49](#49)) </details> ## Rollout The generator and workflow changes take effect on the next nightly. The tooltip parser change ships inside the app, so currently installed builds still show the mixed list until users update onto a build containing it. --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
The desktop update tooltip flattens the release body into plain bullet lines, so the structured nightly highlights (`## Nightly highlights`, `### Added`, …) rendered their headings as list items and wasted 3 of the tooltip's 8 item slots. This PR makes the generated changelog and the tooltip work together, and leans harder into the LLM pipeline while keeping attribution. ## Changes - **Nightly highlights are a flat list** of plain items with compact PR references that survive the tooltip's markup stripping (`#61` for fork PRs, `t3code#123` for upstream, short SHAs for direct commits). Removals are phrased as "Remove …" via prompt guidance instead of a section label. - **Attribution is structural:** summary items are now `{ text, evidenceIds }` objects. The model only returns evidence IDs; URLs are rendered deterministically and IDs are validated against the collected evidence, so references can't be fabricated. The rolling fork-features issue gets the same per-item references. - **Extraction sees diffs:** each commit contributes a truncated patch (4 KB cap, lockfiles excluded) so descriptions are grounded in what actually changed rather than paraphrasing PR titles. Over the 160k-char prompt budget, the largest diffs are dropped first — titles and PR bodies never are. - **`## What's Changed` stays** as the exhaustive record and now credits contributors once per PR (`by @username`, fetched via `gh api` with graceful fallback). - **The tooltip parser stops at `## What's Changed`** when curated highlights precede it, so raw commit lines no longer mix with highlight lines. Bodies that open with the commit list (upstream-style, or a nightly with no highlights) still show it. ## Pipeline example Evidence in (abridged; one upstream commit from the delta below): ```jsonc { "id": "pingdotgg#5756", "title": "fix(web): usage page loses the cost quality panel, gains a back button", "description": "<sanitized PR body>", "files": ["apps/web/src/components/usage/UsagePage.tsx", "…"], "diff": "--- a/apps/web/src/components/usage/UsagePage.tsx\n+import { useCanGoBack, useNavigate, useRouter } from \"@tanstack/react-router\";\n…" } ``` Summary items out (actual output — note one PR correctly yielding both an improvement and a removal): ```json { "text": "Return from Usage through history or to home", "evidenceIds": ["pingdotgg#5756"] } { "text": "Remove Cost quality panel from the Usage page", "evidenceIds": ["pingdotgg#5756"] } ``` Rendered in the release body: ```markdown - Return from Usage through history or to home ([t3code#5756](pingdotgg#5756)) - Remove Cost quality panel from the Usage page ([t3code#5756](pingdotgg#5756)) ``` ## Regenerated release notes for `v0.0.33-nightly.20260809.121` Rerunning the new generator against the existing release's refs produced 13 items (the published release had 5, plus 3 heading lines). What the tooltip shows, via the actual parser: ``` • Identify sidebar threads with unsent drafts (#23) • Show subagent activity counts in right-panel controls (t3code#5745) • Return from Usage through history or to home (t3code#5756) • Give agents access to pasted image files (t3code#5757) • Manage projects from the dedicated Settings page (t3code#5768) • Preserve selected diff view across panel closures and reloads (t3code#5731) • Show pointer cursors for clickable controls and actions (t3code#5716) • Resume Claude sessions without falsely completing skipped turns (t3code#5710) ``` <details> <summary>Full regenerated nightly highlights</summary> - Identify sidebar threads with unsent drafts ([#23](#23)) - Show subagent activity counts in right-panel controls ([t3code#5745](pingdotgg#5745)) - Return from Usage through history or to home ([t3code#5756](pingdotgg#5756)) - Give agents access to pasted image files ([t3code#5757](pingdotgg#5757)) - Manage projects from the dedicated Settings page ([t3code#5768](pingdotgg#5768)) - Preserve selected diff view across panel closures and reloads ([t3code#5731](pingdotgg#5731)) - Show pointer cursors for clickable controls and actions ([t3code#5716](pingdotgg#5716)) - Resume Claude sessions without falsely completing skipped turns ([t3code#5710](pingdotgg#5710)) - Stop the active Codex turn before queued follow-ups ([t3code#5762](pingdotgg#5762)) - Keep pinned thread ordering stable while saving changes ([t3code#5767](pingdotgg#5767)) - Show multi-device usage progress while totals load ([t3code#5772](pingdotgg#5772)) - Remove Cost quality panel from the Usage page ([t3code#5756](pingdotgg#5756)) - Remove the limited project settings modal ([t3code#5768](pingdotgg#5768)) </details> <details> <summary>Regenerated rolling fork-features summary (excerpt, for issue #43)</summary> ## Added - Identify yngatech builds through names, icons, palettes, and About details ([#3](#3), [#27](#27), [#29](#29), [#30](#30), [#56](#56)) - Choose and preview sounds for completed agent turns ([#5](#5), [#7](#7), [#19](#19)) - Detect GitHub outages and show status in the sidebar ([#14](#14)) - Show unsent thread drafts with sidebar pencil indicators ([#23](#23)) - Start new threads with GitHub issues as context ([#31](#31)) - Open thread pull or merge requests from command palette ([#46](#46)) - Synchronize existing-thread text and composer settings across connected devices ([#39](#39), [#44](#44)) ## Improved - Inspect full commands, labeled streams, numeric exit codes, and failures ([#11](#11), [#4](#4), [#36](#36)) - Close terminals directly from the grouped terminal list ([#22](#22)) - Show clearer snooze countdowns near hour and day boundaries ([#45](#45)) - Show setup script outcomes in the thread timeline ([#40](#40)) - Install yngatech alongside upstream T3 Code on Windows ([#49](#49)) </details> ## Rollout The generator and workflow changes take effect on the next nightly. The tooltip parser change ships inside the app, so currently installed builds still show the mixed list until users update onto a build containing it. --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
What Changed
A Claude SDK
resultarriving when the adapter has no local turn state no longer emits aturn.completedwith noturnId. The token-usage emission stays; a structuredclaude.turn.result-without-active-turnlog replaces the event so the trigger stays measurable in the field. As defense in depth, ingestion's lifecycle guard now only applies completions that name their turn when no active turn is tracked.Why
Resuming a Claude session makes the CLI answer with
system/initplus aresult(num_turns: 0, zero usage)handshake. The adapter's result handler treated that like any other result: with noturnStateit emitted an untargetedturn.completed, and ingestion's strict guard — which rejects untargeted completions while a turn is active — fell through to accept them whenactiveTurnIdwas null. That is exactly the state while a turn start is pending, so the phantom flipped the session from "starting" back to "ready" for a turn that never ran.A result with no local turn is never a real turn's completion: real turns get
turnStateinsendTurn, and assistant messages arriving outside a turn auto-start a synthetic one. What lands in that branch is the resume handshake, a late result for a turn already completed locally, or a stream failure with nothing in flight. Every adapter was audited before tightening ingestion — Codex populatesturnIdfromnotification.params.turn.idon the wire; Grok, Cursor, and OpenCode attach it unconditionally — so no legitimate completion becomes rejectable. Consumers were audited too: nothing reads the suppressed event's payload, checkpoint capture already requires aturnId, and no client consumes rawturn.completed.Three new tests. The two suppression tests fail against the unfixed code (verified by reverting the sources and re-running); a targeted-completion control proves late-but-real completions still land.
ClaudeAdapter+ProviderRuntimeIngestion: 114/114 on this base, typecheck clean.Checklist
I included before/after screenshots for any UI changes(no UI changes)I included a video for animation/interaction changes(n/a)Change authored by Claude Fable 5 via Claude Code.
Note
Medium Risk
Changes provider runtime event emission and orchestration turn lifecycle guards; behavior is narrow and well-tested but affects session state transitions during Claude resume and pending turn starts.
Overview
Fixes a bug where resuming a Claude session could flip thread session state from
startingtoreadyeven though no user turn had finished.Claude adapter: When a SDK
resultarrives with no localturnState(resume handshakenum_turns: 0, late results, etc.), the adapter no longer emits an untargetedturn.completed. It still records token usage and logsclaude.turn.result-without-active-turnfor observability.Ingestion (defense in depth): If no active turn is tracked,
turn.completedis applied only when the event includes aturnId. Untargeted completions are ignored so they cannot clear a pending turn start.Tests cover adapter suppression, ingestion reject/accept behavior, and a small harness
dispatchhelper.Reviewed by Cursor Bugbot for commit e670eb0. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Fix Claude resume handshakes from emitting untargeted
turn.completedevents with no active turnClaudeAdapterreceives a result with no activeturnState(e.g. during a resume handshake), it no longer emits an untargetedturn.completedevent; it logs aclaude.turn.result-without-active-turninfo entry and only emits token usage.ProviderRuntimeIngestionnow rejects untargetedturn.completedevents (those lacking aturnId) when no active turn is tracked, preventing the thread lifecycle from advancing incorrectly.turn.completedevents that include aturnIdare still accepted regardless of whether an active turn is tracked.startingdue to a spurious untargeted completion will now remain instartinguntil a real turn completes.Macroscope summarized e670eb0.