fix(bin): variant-shape the zcode interrupt record and log TUI delivery retries - #9
Merged
Merged
Conversation
…ry retries Close the two deferred PR 6 re-review follow-ups (round-1 N2): - zcode_tui_deliver_brief logs one best-effort stderr line per delivery retry, naming the unconfirmed attempt, the window, and the bare-Enter probe it is about to run, so a field swallow regression is observable in the spawn's output and not only in tests. - fm_control_interrupt_ends_process takes the recorded launch variant (the task meta's zcode_tui value) from bin/fm-control.sh: a recorded TUI incarnation answers no, so the interrupt verification requires the agent alive and preserves the open busy record, while a headless zcode incarnation keeps today's both-shapes semantics. Tests extend both directions: the swallow ladder asserts its retry log (and the happy path asserts silence), and the interrupt truth table pins headless-alive, TUI-alive, and TUI-dead-refused beside the existing headless-dead case.
…he TUI postcondition
…, note TUI retry log
This was referenced Sep 30, 2026
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.
Intent
Close the two deferred follow-ups from the PR 6 re-review (round-1 N2, deferred by agreement), keeping the zcode TUI variant's delivery-retry behavior observable and its lifecycle semantics honest:
What Changed
fm_control_interrupt_ends_processinbin/fm-control-lib.shnow takes the task's recordedzcode_tuimeta value: a headless zcode incarnation keeps the process-ends answer, while a recorded TUI variant (zcode_tui=1) answers no, sobin/fm-control.shinterrupt requires the TUI agent alive and refuses a TUI that died under the key instead of reporting the dead state as a landed interrupt.bin/fm-control.shnow settles the post-interrupt agent state through theFM_CONTROL_SETTLE_WAITwindow viawait_agent_state(returning early on an observed death) rather than reading it once, so a lingering TUI shutdown is not published as an alive worker.zcode_tui_deliver_briefinbin/fm-spawn.shlogs one stderr line per unconfirmed brief-delivery attempt before the bare-Enter probe and retype, making a swallowed pointer observable in the spawn output; tests intests/fm-control.test.shandtests/fm-zcode-harness.test.shpin the variant-shaped interrupt truth table, the lingering-death settle, and the retry log line, and the zcode harness reference doc is updated to match.🤖 Generated with Claude Code
Risk Assessment
✅ Low: The change is a two-line semantic tweak plus one stderr log line, both narrowly scoped to the zcode harness: the new variant argument is read from the same task meta the spawn writes and the relaunch rewrites, the only caller passes it, every other harness path is unchanged, and the added tests exercise the real control and spawn binaries against fixtures rather than grepping source.
Testing
Ran the two targeted test files as baseline (fm-control 40 ok, fm-zcode-harness 22 ok), then rebuilt the isolated live lab (staged zcode HOME with a dummy key pointed at a local never-answering provider, isolated tmux servers, isolated firstmate homes) and drove eight scenarios against the real zcode TUI, the real headless worker, real fm-control, and real fm-spawn: busy-TUI interrupt stays alive with the record preserved; the round-1 failure (idle TUI dies under C-c) is now refused with the record left open; headless interrupt ends the process and retires the record; spawn happy path logs no retry; the fm-spawn-created task interrupts to alive; a delayed hook produces exactly one retry line and a landed spawn; an exhausted ladder logs both retries and fails closed with the window removed; and the retry line is on stderr alone. The real ~/.zcode config was verified byte-identical afterwards and every model call stayed on 127.0.0.1. All scenarios passed; the lab and its temp dirs were removed and the worktree is clean.
Evidence: Round-2 evidence index
Source: Round-2 evidence index
Evidence: S2 adversarial: idle TUI dies under C-c, fm-control now refuses (50 ms foreground samples)
Source: S2 adversarial: idle TUI dies under C-c, fm-control now refuses (50 ms foreground samples)
## $ fm-control.sh t1 interrupt (defaults) error: task t1's agent is 'dead' after its interrupt key; an interrupt must leave the agent running exit=1 elapsed=1.80s 0.51s node zcode-cli zcode-node-repl 0.61s node zcode-cli 1.58s node zcode-cli 1.68s bash ## busy files after (a refused interrupt must NOT retire the record) t1.busy-gen t1.busy-stateEvidence: S1: busy TUI interrupt stays alive through the settle window
Source: S1: busy TUI interrupt stays alive through the settle window
interrupt-delivered t1 harness=zcode backend=tmux verified=agent-alive cancel=unconfirmed exit=0 elapsed=6.54s › Describe this workspace in one sentence. Turn cancelled. ## foreground process group after (TUI must still be alive) 630133 630133 630133 node 630150 630133 630133 zcode-cliEvidence: S3: headless worker interrupt ends the process and retires the record
Source: S3: headless worker interrupt ends the process and retires the record
interrupt-delivered t2 harness=zcode backend=tmux verified=agent-ended-by-interrupt cancel=unconfirmed exit=0 elapsed=1.55s ^CError: Turn was cancelled. (traceId: ...) HEADLESS_EXIT=130 ## busy files after (headless dead shape must retire the record) (no t2.busy-* files)Evidence: S4/S5: real fm-spawn TUI happy path and interrupt of the spawned task
Source: S4/S5: real fm-spawn TUI happy path and interrupt of the spawned task
Evidence: S6/S7: retry line logged once with a delayed hook, both lines then fail-closed on exhaustion
Source: S6/S7: retry line logged once with a delayed hook, both lines then fail-closed on exhaustion
fm-spawn: task s2: zcode TUI brief delivery attempt 1 of 3 unconfirmed in window firstmate:fm-s2; retry 2 of 3 probes a bare Enter, then retypes the pointer spawned s2 harness=zcode kind=scout window=firstmate:fm-s2 ... exit=0 --- fm-spawn: task s3: zcode TUI brief delivery attempt 1 of 3 unconfirmed in window firstmate:fm-s3; retry 2 of 3 probes a bare Enter, then retypes the pointer fm-spawn: task s3: zcode TUI brief delivery attempt 2 of 3 unconfirmed in window firstmate:fm-s3; retry 3 of 3 probes a bare Enter, then retypes the pointer error: the zcode TUI brief pointer could not be confirmed delivered through the turn hook in window firstmate:fm-s3; inspect window firstmate:fm-s3 exit=1Evidence: S8: retry line is on stderr only
Source: S8: retry line is on stderr only
## stdout: spawned s4 harness=zcode kind=scout window=firstmate:fm-s4 ... ## stderr: fm-spawn: task s4: zcode TUI brief delivery attempt 1 of 3 unconfirmed in window firstmate:fm-s4; retry 2 of 3 probes a bare Enter, then retypes the pointerEvidence: Isolation: real ~/.zcode config unchanged; all model calls local
Source: Isolation: real ~/.zcode config unchanged; all model calls local
Evidence: Local provider request log
Source: Local provider request log
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
✅ **Review** - passed
✅ No issues found.
🔧 **Test** - 2 issues found → auto-fixed ✅
bin/fm-control.sh:437- Live on zcode 3.11.2-24: with zcode_tui=1 recorded,fm-control <id> interrupton an idle TUI sends C-c, which exits the TUI, yet fm-control printsverified=agent-aliveand exits 0, leaving the open busy record for a dead worker (evidence s2-tui-dies-under-key-refused.txt, s2b-tui-dies-under-key-timing.txt: 50 ms samples show node/zcode-cli still in the foreground for ~0.6 s after the key, then a bare shell). verify_interrupt_running reads agent_state once, immediately after delivery, so the TUI-variant refusal this change adds (the zcode-tui-dead cell of test_zcode_interrupt_postcondition_follows_the_recorded_variant) only fires for an instant death and never for the real TUI shutdown. The record logic is correct and the timing gap predates this change, so decide whether to add a bounded settle poll for the alive-required postcondition (mirroring do_exit's EXIT_WAIT loop, with a delayed-death fixture in tests/fm-control.test.sh) in this PR or ship as-is with a follow-up.verified=agent-aliveexit 0 while the pane dropped to a bare shell ~0.6 s later; busy record left openbin/fm-test-run.sh tests/fm-control.test.sh tests/fm-zcode-harness.test.sh(both scripts touched by the change; all pass)Live lab: staged ~/.zcode into a throwaway HOME with a dummy api key and baseURL at a local never-answering HTTP provider (python3), isolated tmux servers via TMUX_TMPDIR, FM_GATE_REFUSE_BYPASS=1 (the guard's documented test-harness escape hatch) for lab commands onlyFM_HOME=<lab> bin/fm-control.sh t1 interrupton a real busy zcode TUI with zcode_tui=1 and an armed busy record (s1)FM_HOME=<lab> bin/fm-control.sh t1 interrupton the same TUI while idle, twice, with 50 ms foreground-process sampling (s2, s2b)FM_HOME=<lab> bin/fm-control.sh t2 interrupton a real headlesszcode --mode yolo --promptworker (s3)bin/fm-spawn.sh s1 <proj> --scout --harness zcode --zcode-tuiagainst the real tmux backend, real treehouse, real zcode TUI (s4)FM_ZCODE_TUI_DELIVERY_POLLS=1 ... bin/fm-spawn.sh s2/s3 ... --zcode-tui(s5, s5b: hook flipped before the first check)bin/fm-spawn.sh s4 ... --zcode-tuiwith the staged lab hook wrapped insleep 2after install (s5c) andsleep 30for s5 (s7), hook restored byte-identical afterwardsFM_HOME=<spawnhome> bin/fm-control.sh s1 interrupton the fm-spawn-created TUI task (s6)Post-run: lab servers/provider killed, lab and /tmp/fm-s1..s5 removed, real ~/.zcode config and hook script mtimes unchanged,git status --porcelainclean🔧 Fix applied.
✅ Re-checked - no issues remain.
bash tests/fm-control.test.sh(40 ok, includes test_zcode_interrupt_postcondition_follows_the_recorded_variant with the FM_FAKE_INTERRUPT_STOPS_AGENT_LATER fixture)bash tests/fm-zcode-harness.test.sh(22 ok, includes the loud retry-ladder test and the variant-shaped ends-process table test)Live:FM_HOME=<lab> fm-control.sh t1 interrupton a real busy zcode 3.11.2-24 TUI with zcode_tui=1 (r2-s1)Live adversarial: same command on the idle real TUI that exits under C-c, with a 50 ms foreground-process sampler (r2-s2)Live:fm-control.sh t2 interrupton a real headlesszcode --promptworker with no zcode_tui line (r2-s3)Live:fm-spawn.sh s1 <proj> --scout --harness zcode --zcode-tuiagainst a real isolated tmux server (r2-s4)Live:fm-control.sh s1 interrupton the task fm-spawn created (r2-s5)Live:FM_ZCODE_TUI_DELIVERY_POLLS=1 ... fm-spawn.sh s2 ... --zcode-tuiwith the staged hook delayed ~2 s (r2-s6)Live adversarial:FM_ZCODE_TUI_DELIVERY_POLLS=2 FM_ZCODE_TUI_SWALLOW_PROBE_POLLS=2 ... fm-spawn.sh s3 ... --zcode-tuiwith the hook delayed past zcode's 10 s hook timeout (r2-s7)Live: spawn with stdout and stderr captured to separate files to prove the retry line's stream (r2-s8)sha256 comparison of the real ~/.zcode/cli/config.json and fm-turn-end.sh before and after the labLocal provider request log review (all calls from 127.0.0.1)✅ **Document** - passed
✅ No issues found.
✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.