Skip to content

fix(claude): read the composer live right before writing a prompt - #1390

Closed
Juliusolsson05 wants to merge 2 commits into
mainfrom
fix/claude-live-composer-gate
Closed

Juliusolsson05 wants to merge 2 commits into
mainfrom
fix/claude-live-composer-gate

Conversation

@Juliusolsson05

Copy link
Copy Markdown
Owner

Fixes #1294.

Plan: docs/plans/2026-09-27-claude-live-composer-gate.md (first commit).

Problem

Claude delivery decided whether it may type from a CACHED composer reading, ClaudeSession.derivePromptGateState → headless.getComposerState(). That reading goes wrong in two ways:

  • Staleness. claude-code-headless recomputes it only on its throttled screen event, which can stall behind pendingWrites (HeadlessTerminal.ts, "KNOWN ISSUE"). A stale empty then outlives a human who started typing.
  • Misclassification. It counts only plain cells as typed. A one-character draft (the character sits under the inverse cursor) and an [Image #1]-only draft (the whole chip is inverted) both read empty.

Either way the agent's prompt is typed into the human's draft and submitted with it.

Change

  • A live guard at the last moment before any prompt byte (text and image paths). deliverClaudePrompt reads the composer from the LIVE buffer: ClaudeSession.readComposer(), text and cell attributes at the same instant.
  • The same classifier as the rollback. It uses the fail-closed classifier fix(claude): delivery rollback reads the attribute-aware composer (#1291) #1309 introduced, renamed classifyClaudeComposerLive because it now has both callers. Only a dim-only placeholder reads empty.
  • Refusals are safe to retry.
    • drafted → before-write, retry-after-resolve, nothing written.
    • unpainted → before-write, retry-same-session, nothing written.
  • The cached gate still drives the UI readiness events. It no longer has the last word on writing.

Tests

promptDelivery.liveComposerGate.test.ts paints frames through claude-code-headless's real HeadlessTerminal with genuine SGR (dim 2, inverse 7), the method of the package's own composer tests. The cached gate says ready in every case.

  • Refused (red on main, where the agent prompt was written into the draft):
    • a plain draft typed after the last screen event;
    • a one-character draft under the inverse cursor;
    • an [Image #1]-only draft.
  • Still delivered: a dim prompt-suggestion placeholder, so the guard does not re-create the false drafted that attributes were added to fix.

promptDelivery.rollbackPlaceholder.test.ts (#1309) now states its pre-write composer as empty. Before, its "before" frame showed our typed prompt already sitting in the composer, which the new guard correctly refuses to type over. Its screen is unchanged, so absorption still sees no transition.

Verification

  • npx tsc -b clean.
  • src/providers/claude, src/main and src/shared: 323 of 324 files pass, 3,048 tests.
  • The one failure, src/main/workflows/control.system.test.ts, times out in 5 s in these local worktrees WITHOUT this change too: an origin/main-based worktree fails the same way. It is not touched here; CI is the gate.

Residual

Native voice interim text is painted dim, the same as a placeholder, so the live read also calls it empty. There is no recorded frame of it and no attribute separates it from a suggestion, so it is stated rather than guessed at.

🤖 Generated with Claude Code

Juliusolsson05 and others added 2 commits September 27, 2026 01:58
…1294)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude delivery trusted a cached composer reading. It is recomputed only
on the package's throttled screen event (which can stall behind
pendingWrites), and it counts only plain cells as typed. So a stale
'empty', a one-character draft under the inverse cursor, or an
[Image #1] chip let the agent's prompt be typed into a human's draft
and submitted with it.

Immediately before the first prompt byte (text and image paths),
deliverClaudePrompt now reads the composer from the live buffer
(readComposer: text and attributes at the same instant) with the
fail-closed classifier the rollback uses (#1309), renamed
classifyClaudeComposerLive. A draft refuses before-write, retry-safe,
with nothing written. A dim suggestion placeholder is still delivered.

Fail-first: frames painted through the real claude-code-headless
HeadlessTerminal with genuine SGR; on main all three drafts are
overwritten. The #1309 rollback tests now state an empty composer
before our first byte.

Fixes #1294

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:P3 Minor labels Sep 27, 2026
@Juliusolsson05

Copy link
Copy Markdown
Owner Author

Stale check (W1). Idle since 09:04 UTC because the reviewer queue was full. The PR still merges cleanly with main and CI is green at 164b4a03. Review a starts now; b and c follow as slots free.

@Juliusolsson05

Copy link
Copy Markdown
Owner Author

Status (W1): held for a manager decision on file overlap. Reviews a and b are both FIX-BEFORE-MERGE, with one shared P1:

  • The prompt-loss P1. SessionManager.deliverPromptWhenReady settles after ONE delivery attempt once the cached gate says ready. The new live guard can then refuse it (retry-after-resolve / retry-same-session, zero writes), and an orchestration prompt that was already promised as pending is dropped. The fix belongs in that waiter: keep waiting after a retry-safe, pre-write refusal, with pacing and cancellation. That file (and promptDelivery.ts) is also changed by fix(claude): reclaim a prompt Agent Code stranded in the native composer #1358, so I am asking the manager before editing.
  • Parser findings in claude-code-headless.
    • A status-like continuation row (plan mode) is ignored, so a human draft reads empty and the agent prompt is appended and submitted. This is P1.
    • A dim suggestion that wraps in a narrow terminal reads as a draft. This is P2.
    • Both are inherited from the package parser, and the fix is a package PR.
  • Tests, P2. The image branch and the exact refusal dispositions are unpinned. The fix(claude): delivery rollback reads the attribute-aware composer (#1291) #1309 rollback fixture uses a pre-write frame production cannot produce.

@Juliusolsson05

Copy link
Copy Markdown
Owner Author

Closed by B6 (manager, owner proxy) during the owner's PR freeze: its remaining fixes need a package PR and overlap W2's #1358 area, which can't be done under the PR freeze. Nothing is deleted. The branch stays on origin and the PR can be reopened. The linked issue stays open with this PR's findings as its starting point.

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:P3 Minor type:bug Something works wrong

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Claude composer gate reads cached/attribute state that can be stale or misclassify a draft

1 participant