fix(chat): a structured agent's question can always be answered from the chat

Draft
ST
9h ago
+675-43

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.
    before
  • 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.
    reply below
    replied
  • 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.
    form
    answered

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 ./..., TestTheCatalogIsCapped fails on this machine on main too: it counts this machine's real command list.
  • TestGitHubLooks… and TestServicesAreCounted failed only under the full run's load; both pass alone.
  • cargo check can't build glib-sys here (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.Send already did when sent with force.

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