Repository navigation
fix(bin): report a worker parked on the exit picker as blocked-on-prompt - #6682
tiago-peixoto wants to merge 8 commits into
Conversation
A pane parked on that picker still read as healthy quiet after Enter was refused. The existing stale and wedge reports now carry the dialog name, including through away-mode escalation. Refs kunchenguid#4754
|
|
Speaking as Kun's firstmate: I read tip
Attestation MATCH for Contract-class: new-default. #4754 was stamped restore for the remaining scope, but this tip adds wake sources the unconfigured product does not run today (second-mate picker wakes, and wedge escalation for a finished worker). Those are new default-on surfaces, not a fix to an existing specified path. Annotating existing worker stale wakes on its own would be closer to restore. VISION (per rule):
This is with the maintainer for a decision, not waiting on you. Because it changes default supervision behavior, it won't auto-merge. Nothing more is needed from you unless the maintainer asks for changes. Thank you for building on #6666 instead of competing with it. |
A second mate stays exempt from stale capture, and a finished worker in away mode no longer ages into a wedge. The name remains on the stale and wedge wakes a worker already raises.
Comments Outside DiffThese findings could not be posted inline.
|
|
The two parts that ran by default, and that main does not run, are removed. The change now only names the dialog in the stale and wedge wakes that main already raises. |
|
Escalating a finished worker parked on the picker was removed because it added behaviour that does not run on main today. The description lists that case as not covered. It is left for a follow-up. |
Keep the exit-picker name on stale wakes main already raises, and keep main's interruptible pane capture.
|
Speaking as Kun's firstmate: thanks, @tiago-peixoto. Narrowing this was the right call. I re-reviewed head What changed since then: the two parts that ran by default and that main doesn't run are gone.
What the tip does now:
CI: all green on Contract-class: new-default (narrower than before, but the same class). Every unconfigured home's watcher now reads Claude's rendered picker on each worker poll and changes the text of stale and wedge wakes. The away daemon also keeps a new state record. Main doesn't promise naming a dialog in supervision wakes. The stale wake itself works as specified, so this adds information to a default path rather than fixing a broken one. It's a small presentation addition and I think a good one, but it's default-on, so it won't auto-merge. What Kun will weigh: whether to take a default-on VISION.md, rule by rule
Next step: this is otherwise ready, and it's with Kun for a merge-or-leave decision, not waiting on you. Nothing more is needed from you unless he asks for changes. |
Intent
Refs #4754.
Contribute the supervision part of #4754, which the maintainer reopened and labelled ready-for-pr (contract class: restore) after #6666 fixed the control side. What remains on main, in the maintainer's words: a worker parked on a dialog still reads as healthy quiet; the watcher and away-mode liveness do not report blocked-on-prompt. The issue's expected behaviour: a classified modal or exit dialog should be visible to supervision as blocked-on-prompt, not as healthy idle.
This contribution covers the dialog that pull request 6666 already recognises, the Claude background-task exit picker. Recognising other dialogs (MCP approval, model-downgrade confirmation, other exit confirmations) is not part of it.
What Changed
bin/fm-watch.shnow runs the existingfm_composer_blocking_dialogmatcher on the stale-path pane capture it already takes for a worker. When the pane shows the Claude background-task exit picker, the plainstale:wake reason and the final wedge escalation reason end withblocked-on-prompt: <dialog name>inside the one parenthetical. Triage and schedule are unchanged, and the deferral, dead-record and declared-wait recheck reasons keep their wording.bin/fm-supervise-daemon.sh(the away-mode daemon) reads that name off the stale wake. It appends the name to a status-log escalation, and it saves the name instate/.subsuper-dialog-<key>for a self-handled stale so the laterstale persisted ... (possible wedge, blocked-on-prompt: <name>)line still carries it. The file is removed with the stale marker, with pause tracking, and by the watcher on the first poll where the pane no longer shows the picker.tests/fm-watch-triage.test.shandtests/fm-daemon.test.shgain cases for this reporting, using fakes and not a live pane.docs/architecture.md,docs/verification/runtime-backends.md, thefm-watch.shheader and the afk skill describe the new reason suffix.Risk Assessment
✅ Low: The two fix rounds net out to comment and doc wording only (git diff d25bed3..HEAD shows no code or test change), the wedge deferral functions are untouched against the base, and the remaining code adds a name suffix on existing stale wakes whose daemon parsing, key derivation and file cleanup I traced without finding a wrong result.
Testing
I built a disposable lab home with bin/fm-lab-home.sh, started real Claude in a private tmux pane, started a background sleep, and typed /exit to open the real exit picker. I then ran the real watcher inside that tmux server and recorded each wake line, ran the real away daemon with a 20 second wedge threshold and recorded its escalation buffer and log, and drove three adversarial cases (picker closed with Esc, picker words quoted by Claude above a normal composer, pane registered as a second mate). Six scenarios passed live. The seventh scenario, the away daemon's dialog handling for status-driven paths, is untested live: I did not drive it in the lab, and only tests/fm-daemon.test.sh covers it. The wedge escalation scenario used a two-line stub for the crew-state verdict, because the real verdict for a lab pane is unknown and the wedge timer needs a working verdict; the watcher, tmux, pane and picker were real. I ran tests/fm-daemon.test.sh once, and it passed, including the four dialog tests. I did not run tests/fm-watch-triage.test.sh, which takes about 20 minutes; remote CI owns it. No screenshot applies: the surface is wake text, captured as text files.
stale: lab:fm-picker (blocked-on-prompt: Claude background-task exit picker); pane recorded in 01-real-claude-exit-picker-pane.txt(idle 503s, possible wedge, escalation 1, blocked-on-prompt: ...)and(... escalation 3, demand-deep-inspection: ..., blocked-on-prompt: ...). Crew-st…stale persisted 21s (possible wedge, blocked-on-prompt: Claude background-task exit picker): lab:fm-pickerin state/.subsuper-escalationsstale: lab:fm-pickerandstale persisted 21s (possible wedge): lab:fm-pickerstale: lab:fm-pickerbash tests/fm-daemon.test.shpassed, includinga dialog name joins the escalation of an unseen statusand…Evidence: Real Claude 2.1.292 exit picker in the lab pane
Source: Real Claude 2.1.292 exit picker in the lab pane
Evidence: Watcher plain stale wake names the picker
Source: Watcher plain stale wake names the picker
stale: lab:fm-picker (blocked-on-prompt: Claude background-task exit picker)Evidence: Wedge escalations 1 and 3 name the picker
Source: Wedge escalations 1 and 3 name the picker
stale: lab:fm-picker (idle 503s, possible wedge, escalation 1, blocked-on-prompt: Claude background-task exit picker) stale: lab:fm-picker (idle 501s, possible wedge, escalation 3, demand-deep-inspection: same pane has wedge-escalated 3 times in a row - do not re-absorb on the run-step/pane state alone, blocked-on-prompt: Claude background-task exit picker)Evidence: Away daemon persistence line names the picker
Source: Away daemon persistence line names the picker
stale persisted 21s (possible wedge, blocked-on-prompt: Claude background-task exit picker): lab:fm-pickerEvidence: Away mode: closed picker drops the name
Source: Away mode: closed picker drops the name
stale persisted 21s (possible wedge): lab:fm-pickerEvidence: Quoted picker text stays a plain stale
Source: Quoted picker text stays a plain stale
stale: lab:fm-pickerEvidence: Second mate on the picker stays quiet
Source: Second mate on the picker stays quiet
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
bin/fm-watch.sh- merge conflict rebasing onto origin/main🔧 **Review** - 2 issues found → auto-fixed (2) ✅
bin/fm-watch.sh:1558- wedge_timer_check adds the picker name only on the final escalation line. The three deferrals that return before it send their own stale reasons without the name: wedge_defer_wait (reason built at bin/fm-watch.sh:1426), wedge_defer_writing (bin/fm-watch.sh:1204) and wedge_dead_record (bin/fm-watch.sh:1496). Concrete trace outside away mode: a worker's crew state readsworking(run-step), the pane shows the exit picker, and the background shell that caused the picker writes files in the task worktree. The first stale is absorbed into the wedge timer with no wake. At FM_STALE_ESCALATE_SECS, crew_worktree_written_since is true, so the watcher sendsstale: <win> (idle Ns, writing its worktree for Ns, rechecked on a long cadence not a wedge; confirm the writes are real progress). That line never says blocked-on-prompt, and it repeats on the long cadence while the worker stays parked. The intent says a classified dialog "should be visible to supervision as blocked-on-prompt, not as healthy idle". docs/architecture.md:22 also states that a stale wake for such a pane "also ends its reason with blocked-on-prompt", which is not true for these reasons. Passing the name to those reasons widens which wake lines change, and the last commit deliberately narrowed that set, so the author must choose: (a) pass$dialogthrough stale_reason_naming_dialog on the wedge_defer_wait and wedge_defer_writing reasons, or (b) keep them as they are and narrow the sentence in docs/architecture.md:22 to the plain stale and wedge-escalation reasons.bin/fm-watch.sh:42- The new header sentence about the picker name sits between "...another routine supervision resume." and the existing fragment "Unless afk is active." On main that fragment qualified the wedge escalation count and demand-deep-inspection sentence. It now reads as qualifying the picker sentence, which is wrong: the away branch at bin/fm-watch.sh:3144 does add the name. Move the picker sentence after "Unless afk is active." so the fragment stays attached to the sentence it qualified.🔧 Fix applied.
3 issues (2 warnings, 1 info) still open:
bin/fm-watch.sh:1558- wedge_timer_check adds the picker name only on the final escalation line. The three deferrals that return before it send their own stale reasons without the name: wedge_defer_wait (reason built at bin/fm-watch.sh:1426), wedge_defer_writing (bin/fm-watch.sh:1204) and wedge_dead_record (bin/fm-watch.sh:1496). Concrete trace outside away mode: a worker's crew state readsworking(run-step), the pane shows the exit picker, and the background shell that caused the picker writes files in the task worktree. The first stale is absorbed into the wedge timer with no wake. At FM_STALE_ESCALATE_SECS, crew_worktree_written_since is true, so the watcher sendsstale: <win> (idle Ns, writing its worktree for Ns, rechecked on a long cadence not a wedge; confirm the writes are real progress). That line never says blocked-on-prompt, and it repeats on the long cadence while the worker stays parked. The intent says a classified dialog "should be visible to supervision as blocked-on-prompt, not as healthy idle". docs/architecture.md:22 also states that a stale wake for such a pane "also ends its reason with blocked-on-prompt", which is not true for these reasons. Passing the name to those reasons widens which wake lines change, and the last commit deliberately narrowed that set, so the author must choose: (a) pass$dialogthrough stale_reason_naming_dialog on the wedge_defer_wait and wedge_defer_writing reasons, or (b) keep them as they are and narrow the sentence in docs/architecture.md:22 to the plain stale and wedge-escalation reasons.bin/fm-watch.sh:42- The new header sentence about the picker name sits between "...another routine supervision resume." and the existing fragment "Unless afk is active." On main that fragment qualified the wedge escalation count and demand-deep-inspection sentence. It now reads as qualifying the picker sentence, which is wrong: the away branch at bin/fm-watch.sh:3144 does add the name. Move the picker sentence after "Unless afk is active." so the fragment stays attached to the sentence it qualified.bin/fm-watch.sh:1647- Sibling left behind by the round 1 fix. Round 1 made the three wedge_timer_check deferral reasons carry the picker name, but handle_paused_stale still builds its recheck reason without it and takes no dialog argument. Trace outside away mode: a worker's last status line ispaused: waiting on the upstream release cut, the pane is not busy and shows the Claude background-task exit picker. The poll loop computesdialogat bin/fm-watch.sh:3112, then the declared-wait branch calls handle_paused_stale (callers at bin/fm-watch.sh:3236, 3246, 3251, and bin/fm-watch.sh:1708 from busy_turn_bound_check). resurface_absorbed sendsstale: <win> (paused Ns, awaiting external - declared pause, rechecked on a long cadence not a wedge; confirm the wait still holds)with noblocked-on-prompt:. That line repeats once per FM_PAUSE_RESURFACE_SECS while the worker stays parked, so the supervisor reads a healthy declared wait. The same worker gets the name when its crew state readsworkingand it reaches wedge_defer_wait instead (that is what the new test test_exit_picker_wait_deferral_names_the_dialog sets up), so the result depends on which absorber takes the pane. All four reasons in handle_paused_stale are affected: captain-held (bin/fm-watch.sh:1622), declared time beyond cadence (1630), declared time passed (1634), declared pause (1640). The away-mode counterpart has the same gap: the daemon'spauseaction calls stale_marker_remove, which deletes the saved name (bin/fm-supervise-daemon.sh:1688), and its pause re-surface line never names the dialog. docs/architecture.md:22 says a stale wake for such a pane "also ends its reason with blocked-on-prompt", and the comment on stale_reason_naming_dialog (bin/fm-watch.sh:1749) already allows for it by noting the pause matcher keys on "declared pause,". Naming the dialog here widens which wake lines change, and the round 1 instruction limited the fix to three named reasons, so the author must choose: (a) pass$dialoginto handle_paused_stale and runstale: $win ($reason)through stale_reason_naming_dialog, with one test, or (b) keep the declared-wait rechecks as they are and state that exception in docs/architecture.md:22 and the bin/fm-watch.sh header.🔧 Fix applied.
✅ Re-checked - no issues remain.
✅ **Test** - passed
✅ No issues found.
stale: lab:fm-picker (blocked-on-prompt: Claude background-task exit picker); pane recorded in 01-real-claude-exit-picker-pane.txt(idle 503s, possible wedge, escalation 1, blocked-on-prompt: ...)and(... escalation 3, demand-deep-inspection: ..., blocked-on-prompt: ...). Crew-st…stale persisted 21s (possible wedge, blocked-on-prompt: Claude background-task exit picker): lab:fm-pickerin state/.subsuper-escalationsstale: lab:fm-pickerandstale persisted 21s (possible wedge): lab:fm-pickerstale: lab:fm-pickerbash tests/fm-daemon.test.shpassed, includinga dialog name joins the escalation of an unseen statusand…bin/fm-lab-home.sh create $LABthentmux -L fm-lab new-session ... claudeon TMUX_TMPDIR=$LAB/tmux; backgroundsleep 1800, then/exitto open the real pickerFM_HOME=$LAB FM_POLL=2 bin/fm-watch.shrun in a lab tmux window against metawindow=lab:fm-picker kind=ship backend=tmux- plain stale wakeSame watcher withFM_CREW_STATE_BINstubbed to a working run-step verdict and.stale-sincebackdated 500s - wedge escalation 1 and escalation 3touch $LAB/state/.afkthenFM_STALE_ESCALATE_SECS=20 FM_SUPERVISOR_TARGET=lab:super bin/fm-supervise-daemon.sh- away persistence lineSame daemon run: re-report the picker, press Esc in the pane, readstate/.subsuper-dialog-pickerandstate/.subsuper-escalationsAsked lab Claude to print the picker's three lines above its composer, then ran the watcherReopened the picker, setkind=secondmate, ran the watcher for 25sbash tests/fm-daemon.test.sh(passed; unit test only, not a live drive)Teardown: answered the picker,tmux -L fm-lab kill-server,rm -rf $LAB,git status --shortempty✅ **Document** - passed
✅ No issues found.
✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.