Skip to content

fix: hand Pi supervision to the AFK daemon - #2298

Open
anthonyjackman wants to merge 8 commits into
kunchenguid:mainfrom
anthonyjackman:fm/pi-afk-watcher-handoff-h1
Open

fix: hand Pi supervision to the AFK daemon#2298
anthonyjackman wants to merge 8 commits into
kunchenguid:mainfrom
anthonyjackman:fm/pi-afk-watcher-handoff-h1

Conversation

@anthonyjackman

Copy link
Copy Markdown

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

  • Make the Pi watcher stand down during AFK mode, retire its exact token-bound wrapper/watcher tree across Unix and Windows/MSYS, and automatically restore one monitoring cycle on return.
  • Route queued wakes through crash-safe AFK daemon classification and logical deduplication so routine events are absorbed while actionable events remain deliverable without consuming the queue.
  • Reset handoff state at AFK lifecycle boundaries and add documentation and regression coverage for live handoff, sibling isolation, queue continuity, and clean return.

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.

Installed Pi lifecycle evidence
commit=f570e76ae235586325a421d365307051100c9829 + focused test-cleanup fix
pi_version=0.84.1
herdr_version=herdr 0.7.4
herdr_lab_session=fm-lab-pi-afk-watcher-h-1629781-31847
fixture=helper-provisioned named non-default Herdr lab + fm_test_tmproot throwaway Firstmate home
ok - real Pi extension acquired an ordinary home-scoped watcher cycle
fm-afk-launch: daemon launched in non-visible herdr workspace w2 (pane fm-lab-pi-afk-watcher-h-1629781-31847:w2:p1), supervising fm-lab-pi-afk-watcher-h-1629781-31847:w1:p1
ok - real Pi yielded its exact cycle and the away daemon acquired monitoring
ok - real away daemon absorbed routine progress without Pi model injection
ok - real actionable delivery stayed marked while the lossless queue drained to one logical signal
fm-afk-launch: terminal close command failed, but exact absence was confirmed
fm-afk-launch: away mode stopped; daemon terminal torn down and .afk cleared
ok - real return stopped away ownership and resumed extension monitoring without manual re-arm
fm-afk-launch: daemon launched in non-visible herdr workspace w3 (pane fm-lab-pi-afk-watcher-h-1629781-31847:w3:p1), supervising fm-lab-pi-afk-watcher-h-1629781-31847:w1:p1
fm-afk-launch: terminal close command failed, but exact absence was confirmed
fm-afk-launch: away mode stopped; daemon terminal torn down and .afk cleared
ok - real repeated Pi AFK entry and return converged to one extension-owned cycle
Evidence: Final lab teardown probe

Expected NotFound response confirms the named Herdr lab was removed after testing.

Error: Os { code: 2, kind: NotFound, message: "No such file or directory" }

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. terminateArmTree uses the exact process group only off Windows; on Windows it falls back to wrapper-only armChild.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 to handle_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, although fm-wake-drain.sh would 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 ignores taskkill failure 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 native ChildProcess.pid, but the helper uses it directly as /proc/<pid>; Cygwin documents separate PID/WINPID namespaces and /proc/<pid>/winpid as the mapping. The token check therefore exits before taskkill, leaving the Pi cycle owned. Have the exact wrapper publish its MSYS PID, verify its winpid equals 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: stopGeneration ignores a failed terminateArmTree, 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.lock after 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 terminal process.exit reaches this cleanup through the exit event. If synchronous retirement fails, requestArmTreeRetirement schedules a timeout that cannot run after exit, abandoning the wrapper/watcher; the new regression test keeps Node alive after session_shutdown and 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; retainedArmTrees blocks 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.sh
  • HERDR_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 --version and herdr --version
  • Post-run checks for removal of the named Herdr lab, throwaway Firstmate home, and cleanup registry
  • git status --short to confirm only the intentional test cleanup fix remains
✅ **Document** - passed

✅ No issues found.

🔧 **Lint** - 1 issue found → auto-fixed ✅
  • ⚠️ linter found issues (exit code 1)

🔧 Fix: Fix atomic persistence control flow, captain
✅ Re-checked - no issues remain.

✅ **Push** - passed

✅ No issues found.

@anthonyjackman
anthonyjackman force-pushed the fm/pi-afk-watcher-handoff-h1 branch from 3918790 to 1de47dc Compare August 13, 2026 05:06
@anthonyjackman
anthonyjackman force-pushed the fm/pi-afk-watcher-handoff-h1 branch from 1eec5d0 to a0ccc43 Compare August 13, 2026 18:47
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