fix(chat): a structured agent's question can always be answered from the chat
Summary
A structured Claude session (driver: "claude", the chat's header says "stdio") that asked an AskUserQuestion while helpers it had left in the background kept working showed a bare "Claude asks you" card with no options. The reply box said "Pick an answer above first", Send was disabled, and the terminal can't be opened while the agent waits. There was nothing the person could do.
Root cause: its agent-host log keeps the helpers' lines beside the conversation. A chat read afresh (a box restart, a chat nobody had open for a while) reads the log's last 4 MB, and there the helpers had written megabytes after the question. The window held no conversation and no question, so the pane fell back to the permission-style card, which locked the reply box.
conv.read (a long file, opened afresh)
- read its last 4 MB
+ read its last 4 MB
+ fewer than 20 items? read again from 16 MB back, then 64 MB (as Before pages)
addMessage (another agent's message or ping)
closeArtifacts()
- closeQuestions() # a helper back counted as the person's answer
atMenu (lib/questions.ts): the reply box waits for a pick above
- any ask the box reports → locked
+ structured: only something to pick here locks it (a form the box fills in,
+ options read, options still being read); with nothing, the reply is the
+ answer (claudeDriver.Send: no, and the words)
+ in its terminal: as before (its own screen opens as the way out)
<Permission> card with no options
- "Its options aren't on its screen: answer it in the terminal."
+ structured: "Reply below to answer it."
The pane also skips the stand-in card for a form it just showed and the record has answered, while the box catches up (keyed to that form's call in that wait). conv gets one constructor (newConv/restart) in place of five hand-written copies, two of which had already drifted.
verify-berthd: a new chat-structured-ask-live scenario (a real structured Claude, four background helpers, a box restart). dev-box.sh gains VERIFY_DRIVER=claude and restart [berthd], and features/chat-ask-card.md gains structured-form and structured-reply.
Evidence
A real structured Claude Code on a dev box of this checkout (chat-structured-ask-live). Four background helpers read 100 files while the question waited, and they reported back with the question 6.2 MB before the log's end. Then the box restarted, so the chat read the log afresh.
- Before (main's berthd and app): only a bare card; "answer it in the terminal"; Send disabled.

- After, nothing to pick (this app, main's berthd, so the window still has no form): "Reply below to answer it"; Send works. Claude's log gets the words as the answer (
control_response) and it goes on.


- After (this berthd and app): the whole conversation, the form with its options, and Crew "All back". Picks 2.5 and Customers are submitted, Claude's log has both answers and Claude replies "Release 2.5, for customers.", and no stand-in card appears meanwhile.


Tests:
TestAQuestionStaysOpenWhileItsHelpersWorkOn host log: question, 5 MB of a helper's lines, its notification
main: read afresh → no items FAIL
here: read afresh → prompt first, open question last PASS
TestAHelpersWordDoesntAnswerTheAgentsQuestion question, helper ping, hand-back → still open; a prompt → settled
main: FAIL here: PASS
questions.test.ts lastQuestion / atMenu, structured vs terminal PASS
e2e/ask-card.spec.ts (+4): structured form after helper pings; no-options card takes a reply;
a next question after an answered one keeps its card; a terminal permission still holds the box 11/11 PASS
A fresh read of a real 21 MB log of this shape takes 140 ms, against 0 items before. Checks: go vet, go test -race ./internal/transcript, the app's checks, pnpm test (581), pnpm build, tsc -p e2e, the docs build, and e2e/ask-card + e2e/chat (24).
- In the full
go test -race ./...,TestTheCatalogIsCappedfails on this machine on main too: it counts this machine's real command list. TestGitHubLooks…andTestServicesAreCountedfailed only under the full run's load; both pass alone.cargo checkcan't buildglib-syshere (no GTK dev libraries). No Rust changed.
Merge Danger
Door: two-way
App and transcript-reading only. No stored data or API changes. A revert restores the old behaviour.
Blast Radius: chat
- A fresh read of a long log whose last 4 MB hold fewer than 20 items now parses up to 64 MB once (about 0.1–0.3 s, under the reader's lock). It runs only on the first read, never on a poll.
- Another agent's message no longer closes an open question; its tool result or the person's next prompt still does.
- In structured sessions the reply box no longer locks at a card with nothing to pick. Typed text answers with a no plus the words, as
claudeDriver.Sendalready did when sent withforce.
This touches the same pane as #184 (the hourglass for a turn that ended while its work runs on), in nearby lines: #184 changes state/mode=, this one openQ/atMenu/blocked. Whichever merges second rebases with a small conflict at most.
🤖 Generated with Claude Code