Skip to content

fix(bin): require launch ownership and safer away-mode return checks - #2863

Open
pramendra wants to merge 7 commits into
kunchenguid:mainfrom
pramendra:fm/afk-daemon-short-life
Open

fix(bin): require launch ownership and safer away-mode return checks#2863
pramendra wants to merge 7 commits into
kunchenguid:mainfrom
pramendra:fm/afk-daemon-short-life

Conversation

@pramendra

Copy link
Copy Markdown

Intent

The away-mode daemon exits about 38 seconds after launch instead of staying up. Verify the leading hypothesis (launched as 'exec bin/fm-afk-start.sh' inside a harness-tracked background job, and a watcher already running that the daemon inherited but did not own) before fixing it - an 8-second liveness check passing tells nothing; verify the daemon is alive after at least 60 seconds under whichever launch shape is tested, with a log excerpt as evidence, and if it still exits cleanly with no signal from anyone, report that as a real daemon defect rather than retrying variations. Prevent the cascade where a dead daemon with state/.afk still present leaves supervision genuinely off (the Stop-owned auto-arm stands down while away mode claims ownership) and fm-afk-launch.sh then refuses to re-enter away mode until the return catch-up gate clears. Separately, bin/fm-afk-return.sh check is not read-only: running it cleared away mode and tore down the daemon terminal under a name that reads like a status probe - decide with evidence whether that is check's intended contract (and if so its name/help must say so) or a second defect, and fix accordingly. Acceptance: the daemon must survive past 60 seconds under the documented launch shape with log evidence; whatever the root cause, the fix must make the WRONG launch shape fail loudly or be impossible (a guard, a refusal, or a corrected entry point) rather than relying on an operator remembering - a documentation-only change is not acceptable; the fm-afk-return.sh check naming-versus-behavior question must be resolved one way or the other; add colocated tests where the behavior is testable; keep shellcheck clean; keep this repo's own gate green. Captain ruling on the investigation's guard-asymmetry findings (2026-08-23): widen return_guard to detect and report a leaked daemon terminal loudly, without refusing (do not make it a hard refusal); confirm and apply the wording correction on the launch-guard rationale comment/doc pointer while keeping the guard itself as-is.

What Changed

  • bin/fm-afk-start.sh now refuses (exit 2) unless the caller declares a launcher-owned shape via FM_AFK_STATE_PREPARED=1 or FM_AFK_LAUNCH_OWNED=1; a bare or exec-wrapped invocation previously started an away session with no terminal/lifecycle record for fm-afk-launch.sh stop or fm-afk-return.sh to reconcile. bin/fm-afk-launch.sh now sets FM_AFK_LAUNCH_OWNED=1 in the command it launches in the herdr/tmux terminal it creates.
  • bin/fm-afk-return.sh check now refuses to start the stop-and-drain body when away-mode state is still live and no catch-up gate is open, instead of silently clearing away mode and tearing down the daemon under a name that reads as a read-only probe. return_guard now also detects and loudly reports (without refusing) a leaked daemon-terminal record left behind after away mode has already ended, pointing at fm-afk-launch.sh stop to reconcile it.
  • Updated usage headers/help text for both scripts to state the new contract, corrected launch-guard rationale wording/doc pointers in fm-afk-launch.sh and docs/architecture.md/docs/scripts.md/.agents/skills/afk/SKILL.md, added a docs/verification/supervision.md record with log evidence reproducing the real defect (the check misfire, not a launch-shape issue) and confirming the daemon survives past 60s under both launch shapes, and added colocated regression tests in tests/fm-afk-launch.test.sh, tests/fm-afk-return.test.sh, and tests/fm-daemon.test.sh.

Risk Assessment

✅ Low: The branch root-causes the daemon-exit defect (fm-afk-return.sh check tearing down a live daemon under a status-probe-sounding name), makes the wrong launch shape fail loudly via a new declared-ownership guard in fm-afk-start.sh, resolves the check/guard naming-vs-behavior question by making guard the sole read-only command while documenting check's mutation, and implements the captain's 2026-08-23 ruling to widen return_guard into a non-blocking report for a leaked daemon terminal — all changes are well-bounded, internally consistent (I traced check_may_proceed and return_guard against the .afk/.afk-daemon-terminal state combinations and found no reachable wrong-result path), covered by colocated tests that execute the real scripts and assert observable output/state/exit codes (not source-text matching), backed by dated 60s+ survival log evidence in docs/verification/supervision.md, and shellcheck-clean.

Testing

Ran the three targeted behavioral test suites covering every changed script (fm-afk-return.test.sh, fm-afk-launch.test.sh, fm-daemon.test.sh) — all passed, including the new tests that directly reproduce and verify the fixes: check-refuses-to-start-the-return, guard-reports-leaked-terminal-without-refusing, launch-ownership-declaration for both tmux and herdr backends, and afk-start-refuses-an-unowned-direct-launch. The pinned-ShellCheck project lint gate ran clean on every modified file. The pre-existing docs/verification/supervision.md evidence record (part of this change set) independently satisfies the ≥60s liveness requirement with real log/transcript excerpts, and I verified the corrected doc-pointer heading actually exists in docs/herdr-backend.md. No test failures, no environment issues, and no leftover artifacts in the working tree.

Evidence: Away-mode daemon lifetime verification (60s+ liveness, log excerpts)

Source: Away-mode daemon lifetime verification (60s+ liveness, log excerpts)

Both launch shapes hosted as Claude Code tracked background jobs: exec-wrapped `exec bin/fm-afk-start.sh` alive at 4m47s; documented native `FM_AFK_STATE_PREPARED=1 bin/fm-afk-start.sh` alive at 1m54s. Root cause isolated to `bin/fm-afk-return.sh check` sharing begin's stop-and-drain body: BEFORE: daemon pid=35356 alive=yes; .afk present=yes; gate present=no -> fm-afk-launch: away mode stopped; daemon terminal torn down and .afk cleared -> AFTER: daemon alive=no; .afk present=no. Fixed check now refuses that case: T+30s daemon alive=yes; fm-afk-return: refusing to start the away-mode return from check ...; rc=2; T+75s daemon alive=yes.

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

✅ **Review** - passed

✅ No issues found.

✅ **Test** - passed

✅ No issues found.

  • bin/fm-test-run.sh tests/fm-afk-return.test.sh tests/fm-afk-launch.test.sh
  • bin/fm-test-run.sh tests/fm-daemon.test.sh
  • bin/fm-lint.sh bin/fm-afk-start.sh bin/fm-afk-launch.sh bin/fm-afk-return.sh tests/fm-afk-launch.test.sh tests/fm-afk-return.test.sh tests/fm-daemon.test.sh
  • grep -n '^#' docs/herdr-backend.md (confirmed 'Away-mode supervisor support' heading matches the corrected doc pointer)
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

Your Name added 7 commits August 23, 2026 22:13
An away-mode session read as "the daemon exits about 38 seconds after
launch". It did not exit on its own: `bin/fm-afk-return.sh check` shares
`begin`'s stop-and-drain body, so running it with away mode active and no
catch-up gate open performed the entire return - SIGTERM to the daemon,
`state/.afk` cleared - under a subcommand name that reads like a status
probe. The daemon logged its ordinary signal-trapped shutdown and its host
job reported exit 0, which is why the teardown looked spontaneous. Because
the Stop-owned auto-arm stands down while away mode claims ownership, the
result was supervision genuinely off.

`check` must stay able to complete an interrupted `begin` - the gate is
published before any lifecycle mutation - so it keeps the shared body and
instead refuses the one case that cannot be a continuation: no gate open
while away-mode lifecycle state is still live. The usage block now marks
both `begin` and `check` as mutating and names `guard` as the only
read-only command.

The same session launched the daemon as `exec bin/fm-afk-start.sh`, which
is neither documented shape. Measured against a private tmux socket, both
`exec`-wrapped and documented launches stayed up well past 60 seconds, so
the launch shape was not the cause - but a bare or `exec`-wrapped entry
opens an away session with no launcher lifecycle record for stop or return
to reconcile. The entry now refuses unless the caller declares which shape
it is: `FM_AFK_STATE_PREPARED=1` for the harness-native background job, or
`FM_AFK_LAUNCH_OWNED=1`, which `bin/fm-afk-launch.sh` sets in the command
it runs in its own non-visible terminal.

Both refusals have colocated regressions, and the measurements are
recorded in docs/verification/supervision.md.
…refuse

The captain ruled option D on the guard-asymmetry finding: a leaked
away-mode daemon terminal record with no active away mode should surface
loudly to the captain, not block ordinary work behind a refusal.

return_guard already refused on live away mode or a pending catch-up gate,
but said nothing when neither was true and state/.afk-daemon-terminal
still existed - the exact leaked-terminal shape. It now reports that case
on stderr and still returns 0.
@greptile-apps

greptile-apps Bot commented Aug 23, 2026

Copy link
Copy Markdown

Confidence Score: 5/5

The PR appears safe to merge, with the supported launch and return lifecycle paths remaining internally consistent and covered by behavioral tests.

The ownership markers reach both supported terminal launch paths, native startup remains gated by launcher-prepared state, and check now refuses before any lifecycle mutation when no return gate exists.

Reviews (1): Last reviewed commit: "fix(bin): widen return_guard to report a..." | Re-trigger Greptile

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.

1 participant