fix(input): make sure a prompt sent through the API actually leaves the composer - #455
Merged
Merged
Conversation
…he composer Claude Code 2.1.277 takes typed text the moment its composer paints but ignores Enter for the first 30 to 50 seconds after it (measured 2026-09-19 through the input route: an Enter at 28 s stranded the prompt, one at 51 s submitted it). The text+Enter pair `sendInput` sends 50 ms apart therefore left every programmatic prompt sitting unsent, and every waiter burned its timeout on a turn that never started. Server: `SubmitVerifier` (session-submit-verifier.ts), armed from `writeViaMux` for every mux write that carried a carriage return, reads the pane on a 2 s to 60 s schedule and re-sends Enter only while the last composer line (the CLI's own prompt glyph) still holds the head of what was sent. An empty composer, other text, or no composer line at all ends it; a newer write replaces the schedule. Skill: `sendwait` gets the same loop (`_composer_text`, no-break space stripped by its bytes for BSD sed) for servers that predate this, and the preamble version moves to 1.30.1 so seeded agents pick up the fresh copy. SKILL.md's heredoc and the plugin mirror are regenerated. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The first shape of the Enter loop read the composer BETWEEN two short waits, and tested `wait.ended` (the session exiting) where it meant `timedOut`. A `stop` that fired while no wait was open was lost, since signals have no history, and a re-wait that had already resolved on `stop` fell through into another wait that could never see the edge again: measured twice, the answer was on screen and sendwait ran its whole 580 s slice anyway. The long re-wait (a tagged duplicate of the original frame) is now registered first and kept open in the background for the rest of the call; the loop reads the composer and re-sends Enter beside it, stops when the prompt has left or the wait's response has landed, then returns that response. Measured: the stranded prompt got one extra Enter and sendwait returned on `stop` at 36 s. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Ark0N
pushed a commit
that referenced
this pull request
Sep 19, 2026
…hole tree A full review of the release tree found seven things, and four of them were mine. **The gate was red, and I put it there.** Splitting `confirmed` into `confirmedContext` and `confirmedSwap` changed the wire field without moving three assertions that check it: `custom-model-one-shot-launch.test.ts` and two in `custom-model-run-menu-ui.test.ts` (the swap modal and the context modal, each of which already receives exactly the right per-question flag). Moved, with the titles. **Worse, my own tests for the split never ran.** The four cases in `session-custom-model.test.ts` that exist specifically to pin it call `mockRunning()`, which was declared inside a sibling `describe`, so they threw a ReferenceError during setup. The split would have shipped with no passing server-side coverage while the gate reported the failure as four broken tests rather than as four tests that were never written. `mockRunning` is hoisted to the outer describe. **The submit verifier pressed Enter into shell panes.** `#455`'s SubmitVerifier resolved its composer glyph as `promptGlyph ?? '❯'`, and only claude and codex declare one, so the other eight modes fell back to claude's `❯`. That is also starship's default shell prompt, and pure's, and spaceship's, and p10k lean's. On such a shell the line `❯ npm run build` sits on screen for as long as the command runs, the verifier reads it as an unsubmitted prompt, and re-presses Enter into the running program's stdin up to nine times on its 2s..60s schedule. Mostly a stray newline; not harmless against a y/N prompt, `read -p`, an installer or a pager, where it takes the default. The module's own fileoverview already stated the rule this broke. Now `?? ''`, which `promptStillInComposer()` already treats as inert, so the verifier runs only for a CLI that actually declares a composer. **My #451 dedent removal left a count behind**: "Two rules keep it honest" introducing three numbered rules. The rest is documentation the split outran. `confirmedContext`/`confirmedSwap` appeared in no doc at all, while `docs/api-reference.md` (the SemVer-covered contract) still told an integrator to retry with `confirmed: true` for both questions, which is precisely the thing the split exists to stop. Documented there, in `docs/custom-model-endpoints.md` and in CLAUDE.md. The custom-model changeset gained the split and the `CLAUDE_CONFIG_DIR` multi-user consequence, both user-visible and both previously absent, and #454's gained the one exception to its own claim: a Custom Endpoints launch ignores the Instance count stepper and always starts one session. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What broke
Claude Code 2.1.277 (auto-installed on 2026-09-18) changed how a fresh pane treats input: the composer accepts typed text the moment it paints, but ignores Enter for roughly the first 30 to 50 seconds. Before that version the same key sequence had worked for weeks.
Codeman delivers a programmatic prompt as
send-keys -l <text>followed 50 ms later bysend-keys Enter, and every caller that then waits for the turn (send-and-wait on the input route, the agent skill'ssendwait, cron, Ralph, the maintainer bot) assumes the Enter landed. Under 2.1.277 it does not: the prompt sits in the composer with0 tokens, the transcript stays empty, and the wait burns its whole timeout on a turn that never started.Measured on 2026-09-19 through the input route on a throwaway session: readiness at 8 s, text plus Enter at 8 s stranded, a bare Enter at 28 s still stranded, a bare Enter at 51 s submitted. The maintainer bot lost twelve reviews in a row overnight this way, each waiting out a 40-minute deadline "without a report".
What changes
Server, the general fix.
src/session-submit-verifier.tsis a small pure module:promptStillInComposer(screen, prompt, glyph)reads the LAST line that starts with the CLI's own composer glyph (capabilities.workDetect.promptGlyph, Claude's❯, Codex's›) and says whether it still holds the head of the prompt, whitespace removed. It is the last glyph line on purpose, because Claude Code echoes a submitted prompt with the same glyph higher up in the transcript.SubmitVerifierruns that check on a 2 s to 60 s schedule after a write and presses Enter again at every check that still finds the prompt. It stops on an empty composer (taken), on other text (the user's or the CLI's, never ours), and does nothing at all for a pane with no composer line (a shell, a direct-PTY session, a CLI whose glyph is not declared), since a blind Enter from a path every programmatic sender shares could confirm a dialog nobody asked about. A newer write replaces the schedule, so an old prompt can never submit a new one, and teardown cancels it.Session.writeViaMuxarms it for every mux write that carried a\r. A prompt that was taken costs exactly one pane capture at 2 s.Skill, for servers that predate this.
skills/codeman/preamble.shgets_composer_text, which reads the rendered pane the way_trust_keyalready does and returns the last❯line with all whitespace removed, including the no-break space Claude Code draws after the glyph (stripped by its bytes, since BSD sed has no\xHH).sendwaitnow uses it after its first 20-second wait. The long re-wait (a tagged duplicate of the original frame, so the server re-waits without retyping) is registered first and kept open in the background for the rest of the call, and beside it the loop reads the composer and sends another\rabout every ten seconds while the prompt's head is still there, up to twelve times. That shape matters: wait signals have no history, so astopthat fires while no wait is open (during a composer read between two short waits) is lost and the next wait runs its whole timeout on a turn that already ended. The first version of this loop did exactly that, twice, before the wait moved to the background. Prefix matching goes through"${c#"$head"}"so a*or?in a prompt cannot act as a glob.CODEMAN_PREAMBLEmoves from 1.22.0 to 1.30.1 (preamble header, SKILL.md's §0 loader and version checks,reference/recipes.md), so an agent whose cached preamble predates this falls back to the fresh copy. The §0 heredoc in SKILL.md and theplugins/codeman/mirror are regenerated (both byte-pinned by tests).Docs. The single-line-prompts gotcha in
CLAUDE.mdrecords the window, the verifier, and that theshift+tabfooter means "painted", never "accepting Enter". A patch changeset.Verified
tsc, lint, format,test/session-submit-verifier.test.ts(the schedule, the cap, every stop condition, the glyph, ANSI, the no-break space, cancellation, a throwing Enter),test/agent-skill.test.tsandtest/plugin-manifest.test.ts; fullnpm testgreen.CODEMAN_INSTANCE, own port and tmux socket), one throwaway Claude session, onePOST /api/sessions/:id/inputwith text plus\rand no client-side retry of any kind. The server log shows the verifier re-sending Enter and the prompt being taken.sendwait. The trace shows two composer reads (stranded, then empty), one extra Enter, andsendwaitreturningsignal: stopat 36 s with the answer in the transcript.Not in this PR
writeViaMuxstill sends text and Enter 50 ms apart; with the verifier behind it that is fine once the CLI is warm and self-correcting when it is not.❯), but it was not re-measured here.