Skip to content

fix(claude): reclaim a prompt Agent Code stranded in the native composer - #1358

Merged
Juliusolsson05 merged 13 commits into
mainfrom
fix/claude-stranded-prompt-reclaim
Sep 27, 2026
Merged

Juliusolsson05 merged 13 commits into
mainfrom
fix/claude-stranded-prompt-reclaim

Conversation

@Juliusolsson05

@Juliusolsson05 Juliusolsson05 commented Sep 27, 2026 •

Copy link
Copy Markdown
Owner

Fixes #1350 for text prompts. Image deliveries are split out to #1359; see "Not in this PR" and the scope comment on #1350.

Plan: docs/plans/2026-09-27-claude-stranded-prompt-reclaim.md (first commit; it holds the full evidence).

Problem

A Claude delivery writes its prompt, then waits up to 5 s to see it in the composer. While Claude is busy mid-turn the text may not paint. Absorption times out, and the rollback watches about 0.4 s for our bytes before it may clear them. When it never sees them it gives up: "composer could not be recovered", do-not-retry.

The bytes then paint. Claude's gate reads any drafted composer as a human draft, deliberately with no timeout, so every later delivery is refused as "occupied by a human draft". The agent is unreachable for orchestration until someone clears the composer by hand.

Recorded: three incidents on 2026-09-27 in the lifecycle journal.

  • Each composer turned occupied 0.7–3.8 s after its delivery gave up.
  • One was a goal-loop continuation.
  • One session then refused four ac_agents_prompt calls over about 50 minutes, with an empty Agent Code draft.

The paste-debug journals hold 7 more rollback-unobserved deliveries, all reaching that verdict 5.6–5.7 s after the write.

What merges

  • Ownership is proven structurally, not by comparing text. The rollback's own comment explains why text can't prove it: the screen is clipped and wrap-lossy, and pastes collapse to [Pasted text #N].
  • SessionManager keeps a stranded-delivery mark per session.
    • It is set when a Claude text delivery ends with promptWritten && !enterWritten and not ok.
    • It is bound to the process (steering q75). The mark stores the registry entry the delivery wrote to. A late failure from a process that has since exited or been replaced under the same id sets nothing, and a mark is handed to a delivery only for that same process. So an old delivery can never authorize clearing a replacement's human draft.
    • A failed delivery that wrote retires any standing mark before deciding whether to set its own. Its bytes superseded whatever the old mark described. So an image delivery that reclaimed a text strand and then stranded itself cannot pass the stale mark to the next delivery.
    • Only Claude sessions are marked, because only Claude's delivery can reclaim. Image deliveries are never marked.
    • Every PTY writer passes through recordInputWrite, so any write whose origin is not delivery clears the mark: raw terminal typing, remote input, a condition answer.
    • It also clears on process cleanup and on a successful delivery.
    • While it stands, nothing but deliveries has written, and each of those refused before writing, so the composer holds only our bytes.
  • The next Claude delivery gets strandedComposer: { strandedAt }. If the composer reads ready while the mark stands, the recorded late paint (0.7–3.8 s) may still be coming, so the delivery holds the reservation and polls up to 8 s (STRANDED_PAINT_WINDOW_MS) for it. Writing at once could put the new prompt beside our late bytes, and one Enter would submit both. If the gate then says occupied, it holds the reservation and clears the composer with the existing verified kill loop (the helper shared with the rollback, now killComposerToEmpty). It then re-awaits readiness and delivers.
  • If the composer can't be verified empty, the text is yanked back and the delivery is refused with a message naming the earlier Agent Code prompt.
  • sessions.inputInspect (ac_agents_input_inspect) reports nativeDraft.strandedDelivery: true, with a matching reason, while the mark stands.

Not in this PR

Tests

  • promptDelivery.strandedReclaim.test.ts:
    • with the mark, an occupied composer is cleared (one kill, verified empty) and the prompt is delivered;
    • without the mark, it is refused as a human draft, as before, with no writes;
    • a composer that won't clear gets 64 kills, one yank, a refusal naming the earlier prompt, and no prompt or Enter.
  • sessionManager.strandedDelivery.test.ts, through the manager and the real Claude delivery code:
    • the recorded sequence (write while busy, give up after about 5.4 s, paint late, next delivery) reclaims and delivers;
    • a raw write() in between voids the mark and the delivery is refused;
    • inputInspect reports strandedDelivery: true;
    • process A exits, B takes the same id with a human draft, and A's delivery then fails: no mark, and no writes to B (red on the round-1 head);
    • a replacement registered before the old process's (generation-owned) cleanup does not inherit A's mark: no inspection report and no Ctrl+U to B;
    • text strands, then an image delivery reclaims it and strands: the mark is retired, and the third delivery refuses rather than Ctrl+U the image leftovers;
    • the text paints 2 s into the next delivery: it waits, clears, then delivers;
    • the next delivery starts 6 s after the failure and the text paints at +7 s: it still waits (pins the window against the recorded lag);
    • an image delivery that strands is not marked;
    • a thrown write marks, and process exit clears the mark;
    • a refusal before writing does not mark;
    • a non-Claude session is not marked. This is driven through Pi's "unknown" outcome; Codex writes paste+Enter atomically and never reaches the guard.
  • Red checks: the new cases are red on main. Each guard was mutated out on its own, and each mutation fails a test: the foreign-write clear, the write-side process check, the handoff and inspection entry checks, the exit clear, the refusal, Claude-only, the image guard, and mark retirement.
  • Gates: npx tsc -b is clean. The focused suites (Claude runtime, session-manager delivery, screen gate, control SDK, sessions) pass on 770e6922 (main merged): 48 files, 343 tests.

🤖 Generated with Claude Code

Juliusolsson05 and others added 2 commits September 26, 2026 20:37
… composer (#1350)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ser instead of refusing it as a human draft

A delivery whose bytes painted after its rollback stopped watching left them
in Claude's composer, and the gate then refused every later delivery as a
human draft. SessionManager now marks a session stranded while no other
writer has touched the PTY since; the next delivery clears the composer
under its reservation and continues. inputInspect reports the mark.

Fixes #1350

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Juliusolsson05 Juliusolsson05 added provider:claude Affects Claude Code integration type:bug Something works wrong class:C1-prompt-delivery Prompt delivery inferred from the terminal screen sev:P1 Core flow broken or badly degraded labels Sep 27, 2026
…he late paint, never reclaim image strands

Round-1 review of #1358 (steering q75): a late failure of a replaced process
could mark the same-id successor and clear its human draft; a delivery inside
the recorded 0.7-3.8 s paint lag skipped the reclaim; image pills are not
proven safe to kill.

Refs #1350

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Juliusolsson05 and others added 7 commits September 26, 2026 22:49
…als, import the control stack statically

Round-1 review b of #1358.

Refs #1350

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…unknown outcome

Codex writes paste+Enter atomically, so the Codex test never reached the
kind check (it passed with the guard deleted). Pi's bridge 'unknown'
outcome reports promptWritten && !enterWritten, which does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Mutation battery on 6eeac3b: the write-side entry check, the
strandedComposer handoff check and the exit clear each survived alone,
because the other layer covered for them. A replacement registered before
the old process's generation-owned cleanup now pins the read side; the
map itself pins the write side and the exit clear (their only effect
once the row is gone is not retaining the dead RegistryEntry).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#1358 verification a (blocker): an image delivery that reclaimed a text
mark and then stranded left the old mark standing, so the next delivery
Ctrl+U'd a composer holding image pills. Any delivery that wrote now
clears the mark before deciding whether to set its own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#1358 verification b: the only window test reinstalled fake timers, which
reset the clock and put strandedAt in the future, so a 500 ms window
survived. One clock throughout; the next delivery starts 6 s after the
failure and the text paints at +7 s. Red with 500 ms and 6.5 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Juliusolsson05

Copy link
Copy Markdown
Owner Author

Review disposition. Round 1 at c41614b4: a, b and c all FIX-BEFORE-MERGE. Steering q75 put a hold on the PR for reviewer a's blocker. Verification: a round 2 FIX-BEFORE-MERGE (a new blocker, fixed), a round 3 MERGE-READY at 0466ebb6; c MERGE-READY at 0466ebb6; b MERGE-READY in round 2 at 0466ebb6, with one minor, confirmed fixed in round 3 at 770e6922.

Fixed

  • a, blocker (q75): a late failure from process A could mark a same-id replacement B and Ctrl+U B's human draft.

    • The mark stores the RegistryEntry it was written for. noteStrandedDelivery, hasStrandedDelivery and the strandedComposer handoff each compare it with the current entry.
    • Tests cover A exiting before B registers and B registering before A's (generation-owned) cleanup.
    • Each check was mutated out alone, and each is red (verified independently by a in round 3).
  • a, verification blocker: an image delivery that reclaimed a text mark and then stranded kept the old mark, so the next delivery would Ctrl+U image leftovers. Any failed delivery that wrote now retires the standing mark. The test is the reviewer's exact sequence, red before the fix. a also probed the after-Enter case: the mark is gone, and the next delivery refuses.

  • c: a delivery arriving before the late paint could write beside our unpainted bytes. While the mark stands and the composer reads ready, the delivery holds the reservation and polls up to 8 s. The recorded lag is 0.7–3.8 s. Tested.

  • c: Fixes #1350 overstated image recovery. Image deliveries are never marked. The body and bug(claude): a prompt whose absorption times out is left in the native composer, and every later delivery refuses it as a human draft #1350 are scoped to text, and bug(claude): a timed-out image delivery still strands pills and text in the native composer #1359 tracks the image path.

  • c: the mark could outlive its process. It is cleared in cleanupSessionState, and pinned on the map.

  • b:

    • the flaky inputInspect test: static import of the control stack;
    • no test for the exit clear: added;
    • no test that a refusal doesn't mark: added;
    • "the next delivery clears it" was promised for every provider: only Claude sessions are marked. That test drives Pi's "unknown" outcome, because Codex writes paste+Enter atomically and could never reach the guard.
  • b, verification round (minor): the paint window's lower bound was unpinned. A 500 ms window survived, because the only window test reinstalled fake timers and so reset the clock. A one-clock test (next delivery 6 s after the failure, paint at +7 s) is red at 500 ms and at 6.5 s. b confirmed this.

Declined, as suspicions without evidence (a, round 1): provider-internal PTY writers that bypass recordInputWrite. No app consumer exists today. A thrown first write marking with no bytes delivered is harmless while the next gate reads ready.

Unproven, out of scope: whether Ctrl+U/Ctrl+Y is safe on a real Claude image pill (#1359).

🤖 Generated with Claude Code

@Juliusolsson05
Juliusolsson05 merged commit 817015b into main Sep 27, 2026
2 checks passed
Juliusolsson05 added a commit that referenced this pull request Sep 27, 2026
@Juliusolsson05
Juliusolsson05 deleted the fix/claude-stranded-prompt-reclaim branch September 27, 2026 19:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

class:C1-prompt-delivery Prompt delivery inferred from the terminal screen provider:claude Affects Claude Code integration sev:P1 Core flow broken or badly degraded type:bug Something works wrong

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug(claude): a prompt whose absorption times out is left in the native composer, and every later delivery refuses it as a human draft

1 participant