fix: hand Pi supervision to the AFK daemon - #2298
Open
anthonyjackman wants to merge 8 commits into
Open
Conversation
anthonyjackman
force-pushed
the
fm/pi-afk-watcher-handoff-h1
branch
from
August 13, 2026 05:06
3918790 to
1de47dc
Compare
anthonyjackman
force-pushed
the
fm/pi-afk-watcher-handoff-h1
branch
from
August 13, 2026 18:47
1eec5d0 to
a0ccc43
Compare
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
Fix the reproduced Pi-primary AFK monitoring ownership defect so away mode is genuinely tokenless for routine notifications. Diagnose the exact ownership race by separating AFK entry while an already-armed Pi extension owns a live arm child from ordinary Pi supervision and AFK-before-arm, using counterfactual and disconfirming evidence. Implement a deterministic idempotent Pi-to-AFK handoff: when state/.afk exists, the Pi extension must neither arm nor retain primary-model routine monitoring; on live entry it must yield the exact home-scoped wrapper/watcher cycle without process scans, broad kills, sibling-home impact, duplicate watchers, queue loss, wrong-owner consumption, or duplicate delivery; the away daemon must absorb heartbeat, nonterminal progress, and other routine events without Pi model turns while marked away-supervisor decisions, failures, blockers, credentials, checks, and review-ready results remain deliverable. Return and session-open/restart must converge automatically: the daemon stops cleanly and the already-loaded Pi extension resumes exactly one ordinary cycle without a manual watcher command. Preserve AFK-absent Pi behavior and every non-Pi primary integration; quiet-attended-supervision-q1 is out of scope. Keep the contract in the narrow existing Pi/AFK implementation, watcher continuity documentation, Pi protocol, AFK operator skill, and dated maintainer evidence, without growing AGENTS.md or duplicating owners. Cover startup already away, live entry with extension arm, exact child/cycle yield, daemon acquisition, routine absorption, marked actionable delivery, lossless/deduped queue handling, clean automatic return, sibling isolation, and repeated convergence through public executable behavior. Run shellcheck, fm-lint, focused and applicable runner tests, documentation-audience validation, and a real installed-Pi lifecycle in the specifically named isolated Herdr lab and throwaway Firstmate home. The authorized follow-up diagnostic established that the recorded PID was the same live wrapper, not a zombie or reused PID: launch and failure PID, PPID, state/stat, start time, and command matched, and Node observed neither ChildProcess exit nor close. The bash wait deferred the wrapper trap after its watcher exited, so correct the exact signal/process-tree boundary and retain an identity-based retirement assertion that proves no running wrapper or watcher remains; do not weaken it to kill -0 alone, and remove temporary diagnostics.
What Changed
Risk Assessment
🚨 High: The Windows handoff can still kill the daemon’s watcher, abandon cleanup during terminal exit, or wedge indefinitely on permanent identity refusal, so the core ownership and convergence guarantees are not yet durable.
Testing
No baseline test output was supplied. Seven focused lifecycle/queue scripts and the real Pi 0.84.1–Herdr 0.7.4 lifecycle passed, demonstrating exact ownership transfer, routine-event absorption, single actionable delivery, lossless deduplication, sibling isolation, automatic return, and repeated convergence. The first live run exposed a test-fixture cleanup leak; after the test-only fix, the complete lifecycle passed again and left no lab or throwaway home. Lint, static-analysis, and documentation checks were not run because this assigned test phase explicitly forbids them; no visual artifact applies to this CLI/process-lifecycle change.
Evidence: Installed Pi AFK lifecycle transcript
Pi 0.84.1 / Herdr 0.7.4: exact Pi cycle yielded to away monitoring; routine progress caused no Pi turn; actionable delivery remained marked and deduplicated; return and repeated entry/return each restored one cycle automatically.Evidence: Final lab teardown probe
Expected NotFound response confirms the named Herdr lab was removed after testing.Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
⏭️ **Review** - skipped
.pi/extensions/fm-primary-pi-watch.ts:160- Captain, the requirement says to “correct the exact signal/process-tree boundary” without retaining duplicate watchers.terminateArmTreeuses the exact process group only off Windows; on Windows it falls back to wrapper-onlyarmChild.kill("SIGTERM"), while the spawn is also non-detached. The supported Pi Git Bash/MSYS path can therefore leave the watcher alive after its wrapper is terminated. Add identity-bounded Windows tree retirement or explicitly approve excluding that platform.bin/fm-supervise-daemon.sh:1294- The requirement mandates “lossless/deduped queue handling” and no duplicate delivery, but this loop sends every raw post-cursor row tohandle_wake. During handoff, the retiring watcher can append a check before updating its suppression marker and the daemon watcher can append the same logical check again; both rows escalate because checks have no seen-status dedupe, althoughfm-wake-drain.shwould compact them by kind/key. A crash after escalation but before the cursor write also replays the row. Apply durable logical deduplication at the shared non-consuming queue boundary before side effects.🔧 Fix: Fix identity-bounded AFK retirement and wake deduplication
3 errors still open:
bin/fm-pi-arm-tree-retire.sh:17- Captain, the criterion requires an “identity-based retirement assertion that proves no running wrapper or watcher remains,” but the helper ignorestaskkillfailure and returns success once only the wrapper token disappears. A partial tree kill can leave the watcher alive while the extension accepts the handoff. Verify both recorded wrapper and watcher identities before accepting retirement.bin/fm-afk-return.sh:128- The required crash-safe, duplicate-free repeated convergence remains reachable: TERM after the queued-wake cursor commit but before handling leaves.subsuper-pending-wake; return drains that queue record but this cleanup removes only the buffer and cursor. The next AFK session replays the stale pending check and delivers it again. Clear the pending journal and session dedupe ledger at the existing successful-return artifact boundary after the drain.bin/fm-supervise-daemon.sh:1328- The requested logical deduplication uses a different identity from the shared queue: queue compaction defines a wake by kind+key, while this check path treats the same key as new whenever its diagnostic payload changes. Two handoff observations arriving across snapshots therefore both escalate even though the sole drain collapses them. Apply the established kind+key identity while the prior record remains queued.🔧 Fix: Verify AFK retirement and reset durable dedupe state
1 error still open:
.pi/extensions/fm-primary-pi-watch.ts:167- Captain, the required “identity-bounded Windows/MSYS retirement” still fails when MSYS PID and Windows PID differ. Node passes nativeChildProcess.pid, but the helper uses it directly as/proc/<pid>; Cygwin documents separate PID/WINPID namespaces and/proc/<pid>/winpidas the mapping. The token check therefore exits beforetaskkill, leaving the Pi cycle owned. Have the exact wrapper publish its MSYS PID, verify itswinpidequals Node’s PID, then retire that native PID without scanning. See the official Cygwin process documentation.🔧 Fix: Bind Windows arm retirement across PID namespaces
1 error still open:
.pi/extensions/fm-primary-pi-watch.ts:271- Captain, the required “session-open/restart must converge automatically … exactly one ordinary cycle” and identity proof that no wrapper or watcher remains still fail here:stopGenerationignores a failedterminateArmTree, clears the PID publication, and forgets the child. An immediate Windows shutdown before PID publication can therefore leave the wrapper to start afterward; likewise, a partial tree kill can close the wrapper while its watcher survives, after which the close handler releases ownership. Retain and retry failed retirement ownership, clearing it only after both exact identities are confirmed gone.🔧 Fix: Retain Windows arm ownership through verified retirement
3 errors still open:
bin/fm-pi-arm-tree-retire.sh:140- Captain, this contradicts the required exact-cycle yield “without … wrong-owner consumption” and daemon acquisition: when no watcher was initially recorded, the helper samples the ambient same-home.watch.lockafter killing the wrapper. The concurrently starting AFK daemon can acquire that lock and have its watcher mistaken for the Pi child and killed; a slow surviving Pi child can instead appear after the 250 ms window and be missed. Bind the watcher identity to the wrapper’s token-owned child publication rather than infer ownership from the shared singleton lock..pi/extensions/fm-primary-pi-watch.ts:320- The requirement says to retain and retry the exact cycle until both identities are confirmed gone, but a terminalprocess.exitreaches this cleanup through theexitevent. If synchronous retirement fails,requestArmTreeRetirementschedules a timeout that cannot run after exit, abandoning the wrapper/watcher; the new regression test keeps Node alive aftersession_shutdownand does not cover this terminal path. Reconcile the persisted exact snapshot synchronously or on startup before permitting re-arm..pi/extensions/fm-primary-pi-watch.ts:227- Automatic return convergence and deliverable failures remain violated because every retirement refusal is treated as transient. An absent, ambiguous, mismatched, or reused identity causes unbounded 50 ms retries;retainedArmTreesblocks re-arm indefinitely, repeated synchronous helpers can stall Pi, and no actionable failure is surfaced. Distinguish transient publication delay from permanent identity refusal, retain the exact evidence, and surface a durable blocker instead of silently retrying forever.✅ **Test** - passed
✅ No issues found.
bin/fm-test-run.sh tests/fm-pi-watch-extension.test.sh tests/fm-daemon.test.sh tests/fm-afk-launch.test.sh tests/fm-afk-return.test.sh tests/fm-turnend-guard.test.sh tests/fm-wake-daemon-lifecycle-e2e.test.sh tests/fm-wake-queue.test.shHERDR_LAB_SESSION="$(bin/fm-herdr-lab.sh name pi-afk-watcher-handoff-h1)" FM_PI_AFK_HANDOFF_LIVE_E2E=1 tests/fm-pi-afk-handoff-live-e2e.test.sh(rerun after the cleanup fix)pi --versionandherdr --versionPost-run checks for removal of the named Herdr lab, throwaway Firstmate home, and cleanup registrygit status --shortto confirm only the intentional test cleanup fix remains✅ **Document** - passed
✅ No issues found.
🔧 **Lint** - 1 issue found → auto-fixed ✅
🔧 Fix: Fix atomic persistence control flow, captain
✅ Re-checked - no issues remain.
✅ **Push** - passed
✅ No issues found.