Skip to content

fix(claude): delivery rollback reads the attribute-aware composer (#1291) - #1309

Merged
Juliusolsson05 merged 9 commits into
mainfrom
fix/claude-rollback-attribute-composer
Sep 25, 2026
Merged

Juliusolsson05 merged 9 commits into
mainfrom
fix/claude-rollback-attribute-composer

Conversation

@Juliusolsson05

@Juliusolsson05 Juliusolsson05 commented Sep 25, 2026 •

Copy link
Copy Markdown
Owner

Fixes #1291.

Found by the Stage 3 C1 hunt. Plan: docs/plans/2026-09-25-claude-rollback-attribute-composer.md (first commit). This PR needs claude-code-headless#63 (merged, 64fd0eaf), and the submodule is bumped to it.

Evidence

The paste-debug corpus (1,082 journals) holds 4 rollback-exhausted {presses: 64}, 1 rollback-cleared and 7 rollback-unobserved. After a kill empties Claude's composer, Claude repaints placeholder text (a prompt suggestion, or a hint). The text-only read calls it a draft, so every kill looks like a failure and the prompt is yanked back.

The exhausted journals carry no screen, so the repainted placeholder is the explanation that fits them (one prompt was a typical suggestion, "yes fix all 9"), not a recorded observation.

What merges

  • A live read. ClaudeSession.readComposer() returns the screen and the composer cell attributes (getComposerAttributes(), from style(dispatch): pad content from status index #63) from the LIVE terminal buffer in one call. It never uses the throttled per-frame state, which lags a 25 ms kill loop (review round 1).
  • The classifier (classifyRollbackComposer):
    • Text-only base, fail closed. Its empty is trusted for a bare marker.
    • Overruled to empty only when the live attributes show a painted placeholder: dim cells, no plain cells, and an attribute-aware read of empty (which also checks continuation rows).
    • Overruled to drafted when text-only says empty for an allowlisted hint ("Press up to edit") but the live cells are plain (review round 2). A kill clears only the current visual row, so typed text reading like a hint can remain, and a retry would append to it.
    • A leftover character under the inverse cursor stays drafted.
  • The observe → kill → yank structure is unchanged.

Tests

  • Rollback cases: a dim suggestion ends rollback-cleared after one kill (red on main), an inverse leftover and a typed known-hint row are not cleared, and a bare marker with no attributes clears.
  • A real ClaudeCodeHeadless-painted frame test: dim suggestion, typed text, a bare marker, a dim hint, and a typed hint.
  • A dim marker row with a typed continuation row stays drafted.
  • ClaudeSession.readComposer: null without a headless, and screen and attributes read together.

The Claude runtime suite passes, and npx tsc -b is clean.

Residual

An all-dim draft (for example native voice interim text) could look like a placeholder. No recorded rollback frame shows it; tracked with #1294.

🤖 Generated with Claude Code

Juliusolsson05 and others added 3 commits September 25, 2026 07:38
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…#1291)

After absorption timed out, the rollback classified the composer
text-only, and Claude repaints placeholder text (a prompt suggestion, a
hint) into an emptied composer, which text-only reads as a draft: 4 of 5
recorded rollbacks exhausted 64 kills and yanked the prompt back,
stranding it. It now prefers the session's attribute-aware reading (the
one the prompt gate uses), keeping the fail-closed text read as fallback.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Juliusolsson05 added a commit that referenced this pull request Sep 25, 2026
…ate rule, #1309

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Juliusolsson05 and others added 3 commits September 25, 2026 08:15
…d needs a painted placeholder to call it empty

- A1: the cached composer state moves only on a throttled (and stallable)
  screen event, slower than the 25 ms kill loop, so it could keep saying
  'drafted' over an emptied composer. The rollback now reads text and cell
  attributes from the live buffer at the same instant (claude-code-headless
  getComposerAttributes, bumped to #63's head).
- A2: attributes alone called a leftover character under the inverse
  cursor 'empty'. The text read stays the base; it is overruled to 'empty'
  only when dim placeholder cells are painted and nothing is typed.
- Tests: dim suggestion (cleared), inverse residual (not cleared), bare
  marker without attributes (cleared).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…al headless frame; evidence stated precisely

- The classifier is exported and tested with a real ClaudeCodeHeadless:
  painted frames go through the live screen and attribute pipeline (dim
  suggestion empty, typed text drafted, bare marker empty).
- The comment now cites the whole corpus (4 exhausted, 1 cleared, 7
  unobserved) and says the placeholder repaint fits the exhausted records
  rather than being observed in them (no screens were recorded).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… is a draft

The text-only read allowlists "Press up to edit" as empty. A kill clears
only the current visual row, so a prompt whose first row reads like that
hint could be left behind, plain, and reported cleared with retrySafe. Live
plain cells now veto that empty. Tests pin the real painted frame, the
rollback outcome, the continuation-row guard and ClaudeSession.readComposer.

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

Copy link
Copy Markdown
Owner Author

Round 2 disposition (final round), head ab9ab02

  • A + B (major): typed text matching a known hint (❯ Press up to edit, plain cells) was classified as cleared. Fixed. When live attributes show plain (typed) cells on the marker row, a text-only empty becomes drafted. The bare marker and the painted dim hint stay empty. Fail-first: the real-ClaudeCodeHeadless painted-frame assertion and a rollback test (rollback-cleared / retrySafe) were red, now green.
  • B (minor): the real adapter was untested. ClaudeSession.readComposer() now has a test: null without a headless, and screen and attributes from the live headless in one call.
  • A/B surviving mutation (continuation guard): pinned. Attributes sample only the marker row, so a dim marker row with text on a continuation row must stay drafted.
  • A suspicion (an all-dim draft, e.g. native voice interim text): not elevated. No recorded rollback frame shows it; it stays with Claude composer gate reads cached/attribute state that can be stale or misclassify a draft #1294.

205 Claude runtime tests pass; npx tsc -b is clean. No round 3 per §7.3. Merge order is unchanged: claude-code-headless #63 first, then the bump here.

🤖 Generated with Claude Code

@Juliusolsson05

Copy link
Copy Markdown
Owner Author

Gate check before merge: head 4e453b59 contains main 7f273e06, and quality-gate and minimum-node-fixture-gate passed (the earlier failure was the #1261 palette flake, fixed by #1300). Three reviewers; disposition above. The body is updated to the merged behaviour. Merging.

🤖 Generated with Claude Code

@Juliusolsson05
Juliusolsson05 merged commit 1167367 into main Sep 25, 2026
2 checks passed
@Juliusolsson05
Juliusolsson05 deleted the fix/claude-rollback-attribute-composer branch September 25, 2026 17:01
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 delivery rollback strands the prompt when a dim suggestion repaints (rollback-exhausted)

1 participant