fix(afk): refuse bare-shell supervisor targets and bound watcher shutdown#862
Open
ManveerBhullar wants to merge 1 commit into
Open
fix(afk): refuse bare-shell supervisor targets and bound watcher shutdown#862ManveerBhullar wants to merge 1 commit into
ManveerBhullar wants to merge 1 commit into
Conversation
…down Two away-mode reliability defects (scout afk-daemon-reliability-scout): 1. Startup validation checked only that the supervisor pane existed (fm_backend_target_exists), so an explicit FM_SUPERVISOR_TARGET pointing at a bare shell (a wrong tmux window, or an agent that exited to its login shell) started cleanly, then deferred every escalation until the max-defer wedge. Now also probe fm_backend_agent_alive and fail fast on a confident `dead` (mirroring the secondmate-liveness policy that gates on `dead` only); `unknown` (pi's node, unreadable panes) warns and continues so a real primary is never refused. A test-seam opt-out FM_SUPERVISOR_ALLOW_DEAD_TARGET covers isolated E2E tests that simulate an agent pane with a shell-named process; the composer guard still prevents any injection into a shell regardless. 2. The daemon's cleanup wait()ed its watcher child without a bound. The watcher blocks in an external `sleep $FM_POLL` and bash defers a trapped SIGTERM until that sleep finishes, so on fm-afk-launch stop the daemon could block ~FM_POLL (15s) and exceed stop's 10s budget, surfacing as "away-mode daemon did not exit after SIGTERM". Cleanup now SIGKILLs the watcher after a short grace (FM_WATCHER_SHUTDOWN_GRACE_SECS, default 5), well inside the budget; the watcher's durable state is the on-disk wake queue and suppression markers, so a forced kill never loses work.
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.
What
Two away-mode supervision reliability repairs, both reproduced and root-caused in scout
afk-daemon-reliability-scout(report in that task), implemented on the daemon only — no broad runtime-backend redesign, no manual target guessing, no broad tmux sweeps.1. Fail fast at startup for a confidently dead/bare-shell supervisor target
fm_backend_target_existsonly proved the pane was present, so an explicitFM_SUPERVISOR_TARGETpointing at a bare shell (e.g. the wrong tmux window such asfirstmate:0.0, or an agent that had exited to its login shell) passed startup validation. The composer guard then read that shell aspending/unknownand deferred every escalation until the max-defer wedge (~300 s).The daemon's startup now also probes
fm_backend_agent_alive(bin/fm-supervise-daemon.sh):dead→ refuse fast (logstartup failed: ... bare shell, exit 1, release lock + pidfile), mirroring the secondmate-liveness policy that gates ondeadonly;unknown(pi's genericnode, or an unreadable pane) → warn and continue, so a real primary is never falsely refused.The composer guard is unchanged — it is what correctly prevented typing into a shell; this just surfaces the problem immediately instead of at the max-defer wedge.
Test seam:
FM_SUPERVISOR_ALLOW_DEAD_TARGET=1downgrades the refusal to a warn. No external signal can distinguish an isolated E2E test's simulated agent pane (a shell-named process drawing a composer) from a real bare shell, so the opt-out is explicit. It is never set in production, and the composer guard still prevents any injection into a shell regardless.2. Bound watcher shutdown so AFK stop completes inside its budget
The daemon's cleanup
wait()ed its watcher child without a bound.fm-watch.shblocks each cycle in an externalsleep $FM_POLL(default 15) and bash defers a trapped SIGTERM until that sleep finishes, so onfm-afk-launch.sh stopthe daemon could block ~FM_POLLand exceed stop's 10 s SIGTERM budget — surfacing asaway-mode daemon did not exit after SIGTERM; preserving lifecycle state(the daemon did exit shortly after; a laterstopfound it absent).Cleanup now SIGKILLs the watcher after a short grace (
FM_WATCHER_SHUTDOWN_GRACE_SECS, default 5), well inside the 10 s budget. The watcher's durable state is the on-disk wake queue and suppression markers, so a forced kill never loses work.Why these are the right scope
$TMUX_PANEauto-discovery; the fix makes a wrong-but-existing target fail fast.stopis a benign consequence (the detached session auto-collapses on pane-process exit), not a separate defect.Verification
target_exists, reads composerpending; defect 2:stop= 10.4 s with the timeout message; minimaltrap … TERM; sleep 15fires its trap only once the external sleep completes).stopcompletes in ~6.3–6.5 s with the normal "away mode stopped" line;bin/fm-lint.sh(pinned shellcheck 0.11.0, CI's gate): clean.tests/fm-daemon.test.sh:test_daemon_refuses_bare_shell_supervisor_target,test_daemon_does_not_refuse_unknown_supervisor_target,test_daemon_allow_dead_opt_out_permits_simulated_pane(real tmux, real daemon executed directly);tests/fm-afk-launch.test.sh:e2e_tmux_daemon_stop_bounded_inside_budget(real daemon, not the sleeper placeholder, so a watcher child actually exists).tests/fm-afk-inject-e2e.test.sh,tests/fm-afk-inject-herdr-e2e.test.sh,tests/fm-afk-pi-herdr-return-e2e.test.sh).docs/tmux-backend.md("Agent liveness probe") anddocs/herdr-backend.md("Daemon shutdown bounds the watcher wait").Notes
bin/fm-supervise-daemon.sh(both fixes) + tests + docs. No change to the shared watcher poll loop (a broader interruptible-sleep improvement for the always-on path is noted as out of scope).main(rebased; feat: shard portable tests and add bounded local parallelism #841 landed in the meantime).