Skip to content

fix(claude): say so when a finished turn never commits a transcript - #1222

Merged
Juliusolsson05 merged 3 commits into
mainfrom
fix/claude-transcript-disconnect-marker
Sep 25, 2026
Merged

Juliusolsson05 merged 3 commits into
mainfrom
fix/claude-transcript-disconnect-marker

Conversation

@Juliusolsson05

@Juliusolsson05 Juliusolsson05 commented Sep 25, 2026 •

Copy link
Copy Markdown
Owner

Problem (#290, P1)

A Claude pane can stream live turns while no committed JSONL ever arrives, and resume later finds nothing. Recorded in debug bundle 2026-06-17T11-50-56-053-a6c70f19 (totalEntries 0, lastJsonlEntryAt null, transcriptStatus 'ready', 8 completed turns).

The transcript disconnected marker existed but could not fire. It was evaluated only when the provider id was first seen in proxy traffic, where zero entries is normal, so the predicate refused 'ready'. 'ready' is exactly the state the broken pane was in (see the 2026-09-20 audit note on #290).

Change

  • useIpcSubscriptions.ts: after a Claude turn_completed, a single delayed check (15 s) marks the pane disconnected if it still has zero committed entries and no durable transcript identity. A healthy pane has JSONL before turn_completed, so the grace only covers the tail watcher's latency. The existing record handler already resets the status to 'ready' on the first arriving record, so a false alarm clears itself. The check is one pending timer per session, Claude only, and is cleared on unmount.
  • The first-sight call site no longer marks anything, because it can't tell broken from not-yet-written.
  • providerSessionIdentity.ts: 'ready' is eligible; 'loading', 'error' and 'disconnected' are still excluded.

Tests (fail-first, recorded data)

The fixture is the recorded turn_started/turn_completed pair and provider id from the bundle (testing/fixtures/transcript-disconnect/), replayed through the real hook via the fake SessionFeed:

  • marked after the grace, and not at 14 s;
  • stays ready if a record lands within the grace;
  • never marks a durable identity;
  • never marks another provider.

Mutation-checked, each individually: 'ready' excluded, schedule removed, provider guard removed, grace 0. Each turns a test red.

Verification

npx tsc -b clean. src/renderer/src/workspace and session-runtime pass (182 files, 1,390 tests).
Not verified here: a live Claude pane. The grace length is a judgement (15 s); the log row transcript_never_committed records it.

Refs #290. This ships the marker slice only. The workspace duplicate providerSessionId cleanup, and detection for a pane still mid-turn (for example awaiting a tool), stay open on #290.

🤖 Generated with Claude Code

Review (Codex A, Codex B, Pi; two rounds, all MERGE-READY)

Finding Verdict Disposition
A (major): the marker was invisible in terminal view valid fixed in c2a2df0c: the banner shows disconnected, with a fail-first leaf test
A/B: one-pending-check and unmount cleanup untested valid tested in c2a2df0c
B (major): a window transfer or reload before the next turn loses the marker (runtime state does not travel) valid deferred to #290, recorded there (this PR says Refs, not Fixes)
Pi F1 (major): turn_completed vs turn_started trigger untested valid test in bdb68d79
Pi F2: provider kind not re-checked when the check fires valid fixed in bdb68d79, with a switch-inside-grace test
Pi F4 / F6: self-clearing untested; dead branch valid fixed in bdb68d79
Steering: 'Fixes #290' would close the whole issue valid changed to Refs #290 before review

A Claude pane can stream live turns while no committed JSONL ever arrives
(debug bundle 2026-06-17T11-50-56-053-a6c70f19: totalEntries 0,
transcriptStatus 'ready'). Resume then finds nothing. The disconnect
marker existed but could not fire: it was only evaluated when the provider
id was first seen, where zero entries is normal, so it was written to
refuse 'ready', which is the state the broken pane was in.

The check now runs after a completed Claude turn plus a 15 s grace: a
healthy pane has JSONL before turn_completed, so still-zero entries and no
durable identity at that point is the recorded failure. The first arriving
record already resets the status to 'ready', so a slow transcript clears
the banner instead of leaving it stuck. Claude only, one pending check per
session, cleared on unmount.

Tests replay the recorded turn from the bundle (fixture
testing/fixtures/transcript-disconnect/): marked after the grace and not
before, quiet if a record lands in time, never for a durable identity or
another provider. Each guard is mutation-checked (ready excluded, no
schedule, any provider, no grace).

Refs #290 (the marker slice only; duplicate provider ids and earlier live-stream detection stay open there)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Juliusolsson05
Juliusolsson05 force-pushed the fix/claude-transcript-disconnect-marker branch from 541ebb0 to 8367921 Compare September 25, 2026 05:58
Juliusolsson05 added a commit that referenced this pull request Sep 25, 2026
…ress

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…he check's timer invariants

Review round 1 on #1222 (2 Codex; the Pi report is pending):
- Major (A): terminal view never mounts TileLeaf's readiness text, and its
  banner showed only transcriptChannelError/liveChannelWarning, so the new
  marker was invisible where Claude panes are often watched. A
  'disconnected' transcript is the same lifetime class (it lasts until a
  committed record arrives), so it now takes the banner; transient
  transcriptError diagnostics still do not. Fail-first renderer test on the
  real leaf, including that the banner clears on 'ready'.
- Test gaps (A, B): one pending check per session and cleanup on unmount
  were each removable with tests green. Now pinned: a second turn inside the
  grace keeps the first deadline and schedules no second 15 s timer; an
  unmount before the deadline leaves the pane untouched.

Deferred (B, recorded on #290): the marker lives in renderer runtime state,
so a window transfer or full reload before the NEXT completed turn loses
it for a pane with no provider id. Carrying it across adoption is part of
#290's remaining identity work, not this slice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…he trigger and the self-clearing

Review round 1 (Pi) on #1222, all fixed:
- F1 (major, test gap): nothing distinguished the turn_completed trigger
  from turn_started. A long first turn can run past 15 s before the tail
  watcher delivers the user row, so a turn_started clock would alarm
  mid-turn. New test: a started-but-unfinished recorded turn never marks.
- F2: the provider kind was checked only when scheduling. A provider
  switch keeps the session id and resets the runtime, so the Claude check
  could land a Claude-worded warning on the replacing Codex pane. The kind
  is re-checked at fire time; tested with a switch inside the grace.
- F4: the 'clears itself on the first record' claim is now a test.
- F6: the first-sight site no longer carries a permanently-false branch;
  it just logs the observation.
(F3's dedupe/unmount gaps were already pinned in c2a2df0, after the Pi
reviewer's snapshot.)

Each new test is mutation-checked (turn_started trigger, no fire-time kind).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Juliusolsson05 added a commit that referenced this pull request Sep 25, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Juliusolsson05
Juliusolsson05 merged commit e9838e1 into main Sep 25, 2026
2 checks passed
@Juliusolsson05
Juliusolsson05 deleted the fix/claude-transcript-disconnect-marker branch September 25, 2026 06:44
Juliusolsson05 added a commit that referenced this pull request Sep 25, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Claude JSONL transcript can disconnect while proxy/semantic stream stays live, breaking resume identity

1 participant