Skip to content

feat(guide): Esc interrupts the turn, and the floor is visible [spec 03-03] - #140

Merged
wine-fall merged 4 commits into
mainfrom
zachg-0819--guide-boundary
Aug 20, 2026
Merged

feat(guide): Esc interrupts the turn, and the floor is visible [spec 03-03]#140
wine-fall merged 4 commits into
mainfrom
zachg-0819--guide-boundary

Conversation

@wine-fall

Copy link
Copy Markdown
Owner

Implements specs/spec10/10-tui.md (§2.3, §3.4) and specs/spec03/03-03-guide-harness.md (§7) — the conversation-partner boundary, boot-time slice (PR 1 of 2; the mid-broadcast /setup recall is the follow-up and closes #97's reopen gap).

Nine user-pinned decisions from this session's grilling, built:

  1. Two identities. The input line always has exactly one partner: the DJ, or the setup guide (a real foreground agent session). The floor rides hello.mode on attach and a new mode message live; the extension rule (at most one foreground agent, identity line names it) is written in spec 10 §3.4 — no framework until a second agent exists.
  2. Three-point face change while the guide holds the floor: status strip (in the workshop · the setup guide has the floor), identity line (setup guide · claude), input line (warm ink + talking to the setup guide · esc interrupts · /done hands back). The floor returns the moment the conversation ends — before the closing re-probe, so no line ever queues for the wrong partner.
  3. Esc interrupts, never dismisses. Mid-turn: query.interrupt() via the new GuideRequest.onSession handle — the turn dies, the session lives, the next typed line still commands the guide ("continue" finally has an owner). Idle: Esc ends the conversation exactly like /done (the Esc-Esc exit). Before the y: "not now". Only /quit kills the session. No idle timeout.
  4. Mechanics: the one-shot stop latch becomes an esc pulse (answers the pending read, re-arms); write_voice_config's abort is armed per call (escEpoch), so a cut noticed mid-validate still cancels that call even after the next turn resets the flow flag.
  5. /quit acks instantly: every way a quit begins routes through Director.beginQuit, which prints going off the air... the moment the quit is heard — the ~3s teardown was honest work read as a hang.

Verification

  • pnpm test 849 green (new: turn-interrupt loop vs a fake SDK with interrupt(), esc-pulse semantics, the three-state Esc router incl. the mid-validate race and Esc-Esc end-to-end, mode broadcast + late-attach hello, quit acks on all three paths); tsc + oxlint clean.
  • Real-SDK smoke (scratch/smoke-turn-interrupt.ts): a guide turn running a 45s tool was cut by interruptTurn() at 5.7s, the SDK answered with the turn's result, the reply loop pulled the next input, and the session answered a second turn. The Esc contract holds at the real boundary.
  • Real-TUI pty smoke (pyte, 180×48, stub brain): boot + log flood + Esc regression PASS (pinned ident stands, interrupt reaches the engine as noise safely).
  • Peer review (codex gpt-5.6-sol, xhigh): 3 findings, 3 applied (per-call abort watch; floor returned before the closing re-probe; merged-/quit ack), each locked with a regression test.

AI coding brief

  • Original request: After the previous fix shipped, the user Esc'd a running install, typed "continue" 17s later, and the DJ answered it as chat — the boundary between talking to murmur and talking to a real code agent was invisible and Esc conflated interrupt with dismissal. A structured grilling produced nine pinned decisions; this PR builds the boot-time slice.
  • Manual interventions: The user chose all nine forks (two identities; interrupt-not-end; boot serial; no idle timeout; build mid-broadcast recall next; music keeps playing there; three-point face; rules-not-framework; ack-only quit fix).
  • Retro: Grilling before building was the right call — the Esc semantics reversal (kill → interrupt) would have been an expensive re-ship if guessed wrong. The real-SDK smoke of query.interrupt() was the one fact unit fakes could not supply and was worth its tokens.

🤖 Generated with Claude Code

…03-03]

The conversation-partner boundary (grilling session, nine user-pinned
decisions; spec 10 §3.4 + spec 03-03 §7) — boot-time slice:

- Two identities: the input line always has exactly one partner — the DJ,
  or the setup guide (a real foreground agent session). The floor rides
  `hello.mode` / the `mode` message; the TUI paints a three-point face
  (strip, identity line, warm input ink + placeholder) while the guide
  holds it, and the floor returns the moment the conversation ends.
- Esc interrupts, never dismisses: mid-turn it cuts only the turn in
  flight (query.interrupt() via the new GuideRequest.onSession handle —
  proven against the real SDK: a 45s tool was cut at 5.7s and the session
  answered a second turn); at the idle reply prompt it ends the
  conversation exactly like /done (the Esc-Esc exit); before the y it is
  "not now". Only /quit kills the session. No idle timeout — a waiting
  guide waits, with the exit written on the prompt.
- The one-shot stop latch becomes an esc PULSE (answers the pending read,
  re-arms), and write_voice_config's abort is armed per call (escEpoch),
  so a cut noticed mid-validate still cancels that call after the next
  turn resets the flow flag.
- Every way a quit begins prints "going off the air..." the moment it is
  heard (Director.beginQuit) — the teardown was honest work read as a hang.

Peer review (codex gpt-5.6-sol, xhigh): 3 findings, 3 applied — the
per-call abort watch, the floor returned before the closing re-probe, and
the merged-/quit ack path — each locked with a regression test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
wine-fall and others added 3 commits August 19, 2026 14:19
…c 03-03]

Found in the user's live verification of the Esc-Esc exit: ending the
streaming input right after an interrupted turn makes the SDK iterator
throw its error result ([ede_diagnostic] ... stop_reason=tool_use), and
the throw rode runSetup straight through runApp — the engine died where
the broadcast should have started. Reproduced against the real SDK.

runSetup now absorbs whatever the session dies of: one honest info line,
the floor handed back, the closing re-probe still running — the radio
always launches (spec 03-03). Locked with a regression test on the exact
crash shape.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
User ask: the Esc-stop acknowledgment drowned in the tool flood. Rare
flow-transition lines (the stop, the unexpected session end, the
going-off ack) now ride `info` with `tone: 'flow'` — the TUI renders
them warm with a block marker, the plain host prints them unchanged.
The stop line also says where things stand: "stopped — the setup guide
is waiting for you".

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…pec 10]

The identity line and input turned warm in guide mode but the status strip
kept the room accent (user report) — the third point of the three-point
face now matches in both compositions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@wine-fall
wine-fall merged commit 9ea6d07 into main Aug 20, 2026
3 checks passed
@wine-fall
wine-fall deleted the zachg-0819--guide-boundary branch August 20, 2026 06:51
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.

A failing voice endpoint never reopens the setup path — segments skip silently forever

1 participant