Skip to content

fix: safely reap orphaned lab and test processes - #76

Merged
MrGTV-love merged 13 commits into
mainfrom
fm/fm-lab-orphan-reaper
Oct 9, 2026
Merged

MrGTV-love merged 13 commits into
mainfrom
fm/fm-lab-orphan-reaper

Conversation

@MrGTV-love

Copy link
Copy Markdown
Owner

Intent

I would like all bugs to be fixed so tomorrow can be focused entirely to Vernant and not problems preventning Vernant from getting built.

A recommended architecture should come with proof.

Context (the backlog record for this bug, as filed):

Reap orphaned lab and test processes (repo: firstmate) (kind: ship) (since 2026-10-08)
Reap orphaned scratch-copy watchers, poll-publish-holder.sh, fm-wake-grant.sh release orphans and leftover go test binaries; extend fm-test-run.sh reaping to lab copies. Only processes provably owned by an ended lab or test run.

Source: data/fm-sandbox-offload-analysis/report.md (2026-10-08). 21-minute sample at load 102 on 18 cores: 0.1% idle, 56% kernel time, ~61% of CPU in sub-second processes, ~2,850 new processes per second, firstmate supervision scripts start 61% of short-lived process sightings (sleeps excluded); 106 of 155 agent sessions idle the whole window holding ~33 GB. Target for the R4 set over one fleet day: 1-min load <= 36 for >= 90% of the day, process creation < 500/s, kernel time < 25%, no new timeout-class failures. Pattern: measure, fix the root, test, measure again (as fm-wake-drain-fast did).

What Changed

  • Add provenance-based process reaping for ended lab and test runs, using command-line or working-directory attribution, lab runtime checks, and identity rechecks before signals.
  • Record lab owner identities and integrate reaping into fixture cleanup, startup recovery, and post-script runner sweeps, including scratch copies and nested parallel-worker fixtures.
  • Stop lock waits after their parent directory remains missing for five seconds, propagate acquisition failures before protected operations, and add regression coverage and safety documentation.

Risk Assessment

✅ Low: The ownership-gated cleanup and lock-failure propagation are consistent with the stated intent, and the complete source review found no substantiated material defects or unnecessary introduced components.

Testing

Focused regressions and live watcher, private-tmux, runner, identity-transition, and mutation checks passed after disposable-driver setup corrections; baseline execution reproduced the vanished-state hang, CLI/state evidence was retained, and fixtures were removed without running broad suites or fleet-day benchmarking.

  • Live validation: ✅ go - 12 of 13 scenarios driven live against the product
Scenario Result Live Evidence
Sweep an ended test's detached process without stopping live-owner, live-parent, unrelated, or prefix-sharing processes ✅ pass live End-to-end ownership, Go cwd, and startup-recovery regressions
Reap a real scratch-copy watcher inside a nested marked lab after its creator ends ✅ pass live Real watcher, private runtime, and grant-release transcript
Reap a detached compiled Go test binary by cwd without crossing ownership boundaries ✅ pass live End-to-end ownership, Go cwd, and startup-recovery regressions
Keep an orphaned lab process while its private tmux runtime remains active, then reap it after runtime shutdown ✅ pass live Real watcher, private runtime, and grant-release transcript
Allow owner-exit cleanup only for matching caller ownership and terminate its hidden process descendants ✅ pass live End-to-end ownership, Go cwd, and startup-recovery regressions
Clean up normal test exits and recover nested parallel-worker orphans after hard termination ✅ pass live End-to-end ownership, Go cwd, and startup-recovery regressions
Run a killed or timed-out script and reap its detached polling process before the next script ✅ pass live Runner reaps killed and timed-out scripts' detached processes
Skip direct and descendant processes whose identity changes after the ownership snapshot ✅ pass live Actual ownership snapshots and surviving changed identities
Stop abandoned grant release after sustained state loss while retaining blocking semantics for live holders and brief absences ✅ pass live Real watcher, private runtime, and grant-release transcript; Baseline release remains running after state disappears
Fail grant and queue operations without exposing or mutating protected state when state returns at the failure boundary ✅ pass live Observed lock-failure results for eleven operations
Preserve acknowledgement state when either queue-lock acquisition fails ✅ pass live Observed lock-failure results for eleven operations
Preserve request-link metadata when link, follow-up, or clear cannot acquire its lock ✅ pass live Observed lock-failure results for eleven operations
Meet the aggregate R4 fleet-day load, process-creation, and kernel-time targets ⏸️ untested no Disposable fixtures demonstrated the polling failure and its termination, but cannot establish real-fleet day-long distributions. This gate forbids accessing operator fleet data or controlling shared…
Evidence: Real watcher, private runtime, and grant-release transcript

Source: Real watcher, private runtime, and grant-release transcript

$ python3 .nm-live-validation/drive.py
$ tmux -f /dev/null -S ~/.no-mistakes/worktrees/32d18ed9638d/01M4FAAZCMK3BCMX01ZYFMDD9D/.v/tmux-501/x new-session -d -s fm-lab-orphan-reaper sleep 240
exit= 0
$ tmux -S ~/.no-mistakes/worktrees/32d18ed9638d/01M4FAAZCMK3BCMX01ZYFMDD9D/.v/tmux-501/x display-message -p #{socket_path},#{pid},0
~/.no-mistakes/worktrees/32d18ed9638d/01M4FAAZCMK3BCMX01ZYFMDD9D/.v/tmux-501/x,84856,0
exit= 0
real scratch watcher armed: pid=87542 same_identity=True
87542     1 Ss   bash ~/.no-mistakes/worktrees/32d18ed9638d/01M4FAAZCMK3BCMX01ZYFMDD9D/.nm-live-validation/manual-scan/fm-lab-nested/home/bin/fm-watch.sh
$ ~/.no-mistakes/worktrees/32d18ed9638d/01M4FAAZCMK3BCMX01ZYFMDD9D/bin/fm-test-reap-orphans.sh --tmpdir ~/.no-mistakes/worktrees/32d18ed9638d/01M4FAAZCMK3BCMX01ZYFMDD9D/.nm-live-validation/manual-scan
exit= 0
Live lab creator retained its detached real watcher.
$ ~/.no-mistakes/worktrees/32d18ed9638d/01M4FAAZCMK3BCMX01ZYFMDD9D/bin/fm-test-reap-orphans.sh --tmpdir ~/.no-mistakes/worktrees/32d18ed9638d/01M4FAAZCMK3BCMX01ZYFMDD9D/.nm-live-validation/manual-scan
reaped pid=87542 root=~/.no-mistakes/worktrees/32d18ed9638d/01M4FAAZCMK3BCMX01ZYFMDD9D/.nm-live-validation/manual-scan/fm-lab-nested/home cmd=bash ~/.no-mistakes/worktrees/32d18ed9638d/01M4FAAZCMK3BCMX01ZYFMDD9D/.nm-live-...
exit= 0
ended nested lab watcher after sweep: pid=87542 same_identity=False

$ tmux -S ~/.no-mistakes/worktrees/32d18ed9638d/01M4FAAZCMK3BCMX01ZYFMDD9D/.v/tmux-501/x kill-server
exit= 0
$ tmux -f /dev/null -S ~/.no-mistakes/worktrees/32d18ed9638d/01M4FAAZCMK3BCMX01ZYFMDD9D/.v/tmux-501/x new-session -d -s fm-lab-orphan-reaper sleep 240
exit= 0
$ ~/.no-mistakes/worktrees/32d18ed9638d/01M4FAAZCMK3BCMX01ZYFMDD9D/bin/fm-test-reap-orphans.sh --tmpdir ~/.no-mistakes/worktrees/32d18ed9638d/01M4FAAZCMK3BCMX01ZYFMDD9D/.nm-live-validation/manual-scan
exit= 0
dead creator but real private runtime active: pid=57853 same_identity=True
57853     1 Ss   sleep 240
$ tmux -S ~/.no-mistakes/worktrees/32d18ed9638d/01M4FAAZCMK3BCMX01ZYFMDD9D/.v/tmux-501/x list-sessions
fm-lab-orphan-reaper: 1 windows (created Fri Oct  9 04:53:09 2026)
exit= 0
$ tmux -S ~/.no-mistakes/worktrees/32d18ed9638d/01M4FAAZCMK3BCMX01ZYFMDD9D/.v/tmux-501/x kill-server
exit= 0
$ ~/.no-mistakes/worktrees/32d18ed9638d/01M4FAAZCMK3BCMX01ZYFMDD9D/bin/fm-test-reap-orphans.sh --tmpdir ~/.no-mistakes/worktrees/32d18ed9638d/01M4FAAZCMK3BCMX01ZYFMDD9D/.nm-live-validation/manual-scan
reaped pid=57853 root=~/.no-mistakes/worktrees/32d18ed9638d/01M4FAAZCMK3BCMX01ZYFMDD9D/.nm-live-validation/manual-scan/fm-lab-runtime/home cmd=sleep 240
exit= 0
runtime ended and detached lab process reaped: pid=57853 same_identity=False

$ ~/.no-mistakes/worktrees/32d18ed9638d/01M4FAAZCMK3BCMX01ZYFMDD9D/bin/fm-wake-grant.sh activate 27921 live-proof
exit= 0
$ bash -c . "$1"; fm_wake_append signal key payload _ ~/.no-mistakes/worktrees/32d18ed9638d/01M4FAAZCMK3BCMX01ZYFMDD9D/bin/fm-wake-lib.sh
exit= 0
$ ~/.no-mistakes/worktrees/32d18ed9638d/01M4FAAZCMK3BCMX01ZYFMDD9D/bin/fm-wake-grant.sh publish live-proof 1
exit= 0
release still blocked after six seconds behind live holder: pid=39995 same_identity=True
39995 83692 S    bash ~/.no-mistakes/worktrees/32d18ed9638d/01M4FAAZCMK3BCMX01ZYFMDD9D/bin/fm-wake-grant.sh release live-proof
release after atomically removed state: exit=1 elapsed=6.49s stdout='' stderr=''
release after one-second state absence and holder unlock: exit=0 rows_exist=False owner_exists=True
LIVE PROOFS COMPLETE

exit=0 elapsed=86.53s
Evidence: Runner reaps killed and timed-out scripts' detached processes

Source: Runner reaps killed and timed-out scripts' detached processes

$ python3 .nm-live-validation/runner-proof.py
$ FM_PROOF_MODE=killed ~/.no-mistakes/worktrees/32d18ed9638d/01M4FAAZCMK3BCMX01ZYFMDD9D/.nm-live-validation/runner-proof/repo/bin/fm-test-run.sh --jobs 1 --per-script-timeout-secs 60 tests/fm-live-leak.test.sh tests/fm-live-observe.test.sh
FM_TEST_BEGIN 2026-10-09T09:58:43Z tests/fm-live-leak.test.sh family=unclassified expected_gate_skip=none
Fixture launched detached poll-publish-holder pid=20907 mode=killed
FM_TEST_END 2026-10-09T09:58:56Z tests/fm-live-leak.test.sh exit=137 duration_ms=12412 gate_skip=false
FM_TEST_BEGIN 2026-10-09T09:58:56Z tests/fm-live-observe.test.sh family=unclassified expected_gate_skip=none
Next script observes orphan pid=20907 same_identity=false
FM_TEST_END 2026-10-09T09:59:20Z tests/fm-live-observe.test.sh exit=0 duration_ms=24488 gate_skip=false
FM_TEST_SUMMARY total=2 failed=1 skipped_gate=0 duration_ms=37480
FM_TEST_SUMMARY_FAMILY family=unclassified count=2 duration_ms=36900 failed=1
FM_TEST_SLOWEST rank=1 script=tests/fm-live-observe.test.sh duration_ms=24488
FM_TEST_SLOWEST rank=2 script=tests/fm-live-leak.test.sh duration_ms=12412
fm-test-run: reaped after tests/fm-live-leak.test.sh: reaped pid=20907 root=~/.no-mistakes/worktrees/32d18ed9638d/01M4FAAZCMK3BCMX01ZYFMDD9D/.nm-live-validation/runner-proof/fm-live-leak.dOtCDP cmd=bash ~/.no-mistakes/worktrees/32d18ed9638d/01M4FAAZCMK3BCMX01ZYFMDD9D/.nm-live-...
runner exit= 1
Runner preserved killed failure and next script saw no surviving owned orphan.
$ FM_PROOF_MODE=timeout ~/.no-mistakes/worktrees/32d18ed9638d/01M4FAAZCMK3BCMX01ZYFMDD9D/.nm-live-validation/runner-proof/repo/bin/fm-test-run.sh --jobs 1 --per-script-timeout-secs 60 tests/fm-live-leak.test.sh tests/fm-live-observe.test.sh
FM_TEST_BEGIN 2026-10-09T09:59:21Z tests/fm-live-leak.test.sh family=unclassified expected_gate_skip=none
Fixture launched detached poll-publish-holder pid=4709 mode=timeout
not ok - tests/fm-live-leak.test.sh exceeded the per-script bound of 60s and was terminated
FM_TEST_END 2026-10-09T10:00:38Z tests/fm-live-leak.test.sh exit=124 duration_ms=77181 gate_skip=false
FM_TEST_BEGIN 2026-10-09T10:00:38Z tests/fm-live-observe.test.sh family=unclassified expected_gate_skip=none
Next script observes orphan pid=4709 same_identity=false
FM_TEST_END 2026-10-09T10:01:12Z tests/fm-live-observe.test.sh exit=0 duration_ms=34165 gate_skip=false
FM_TEST_SUMMARY total=2 failed=1 skipped_gate=0 duration_ms=111641
FM_TEST_SUMMARY_FAMILY family=unclassified count=2 duration_ms=111346 failed=1
FM_TEST_SLOWEST rank=1 script=tests/fm-live-leak.test.sh duration_ms=77181
FM_TEST_SLOWEST rank=2 script=tests/fm-live-observe.test.sh duration_ms=34165
fm-test-run: reaped after tests/fm-live-leak.test.sh: reaped pid=4709 root=~/.no-mistakes/worktrees/32d18ed9638d/01M4FAAZCMK3BCMX01ZYFMDD9D/.nm-live-validation/runner-proof/fm-live-leak.pcTQ9x cmd=bash ~/.no-mistakes/worktrees/32d18ed9638d/01M4FAAZCMK3BCMX01ZYFMDD9D/.nm-live-...
runner exit= 1
Runner preserved timeout failure and next script saw no surviving owned orphan.

exit=0 elapsed=150.65s
Evidence: Actual ownership snapshots and surviving changed identities

Source: Actual ownership snapshots and surviving changed identities

$ python3 .nm-live-validation/identity-proof.py
direct: actual ownership snapshot: 83558     1 Fri Oct  9 05:02:25 2026     bash ~/.no-mistakes/worktrees/32d18ed9638d/01M4FAAZCMK3BCMX01ZYFMDD9D/.nm-live-validation/identity-scan/fm-direct/replacement.sh ~/.no-mistakes/worktrees/32d18ed9638d/01M4FAAZCMK3BCMX01ZYFMDD9D/.nm-live-validation/identity-scan/fm-direct/transition ~/.no-mistakes/worktrees/32d18ed9638d/01M4FAAZCMK3BCMX01ZYFMDD9D/.nm-live-validation/identity-scan/fm-direct/ready
direct: same PID after exec: Fri Oct  9 05:02:25 2026     sleep 240
$ bin/fm-test-reap-orphans.sh --tmpdir ~/.no-mistakes/worktrees/32d18ed9638d/01M4FAAZCMK3BCMX01ZYFMDD9D/.nm-live-validation/identity-scan
direct: reaper_exit=0 replacement_still_alive=True
descendant: actual ownership snapshot: 98070 98049 Fri Oct  9 05:02:35 2026     bash ~/.no-mistakes/worktrees/32d18ed9638d/01M4FAAZCMK3BCMX01ZYFMDD9D/.nm-live-validation/identity-scan/fm-descendant/replacement.sh ~/.no-mistakes/worktrees/32d18ed9638d/01M4FAAZCMK3BCMX01ZYFMDD9D/.nm-live-validation/identity-scan/fm-descendant/transition ~/.no-mistakes/worktrees/32d18ed9638d/01M4FAAZCMK3BCMX01ZYFMDD9D/.nm-live-validation/identity-scan/fm-descendant/ready
descendant: same PID after exec: Fri Oct  9 05:02:35 2026     sleep 240
$ bin/fm-test-reap-orphans.sh --tmpdir ~/.no-mistakes/worktrees/32d18ed9638d/01M4FAAZCMK3BCMX01ZYFMDD9D/.nm-live-validation/identity-scan
reaped pid=98049 root=~/.no-mistakes/worktrees/32d18ed9638d/01M4FAAZCMK3BCMX01ZYFMDD9D/.nm-live-validation/identity-scan/fm-descendant cmd=bash ~/.no-mistakes/worktrees/32d18ed9638d/01M4FAAZCMK3BCMX01ZYFMDD9D/.nm-live-...
reaped pid=98073 root=~/.no-mistakes/worktrees/32d18ed9638d/01M4FAAZCMK3BCMX01ZYFMDD9D/.nm-live-validation/identity-scan/fm-descendant cmd=sleep 240
descendant: reaper_exit=0 replacement_still_alive=True
REAL IDENTITY TRANSITIONS PRESERVED

exit=0 elapsed=19.99s
Evidence: Observed lock-failure results for eleven operations

Source: Observed lock-failure results for eleven operations

{
  "product": "unmodified target CLI and library functions",
  "timing": "actual sleeps; DEBUG trace only synchronizes OS directory moves and restoration at the acquisition-failure return",
  "observations": [
    {
      "action": "activate",
      "observed_result": "activate: real_wait=4.75s exit=1 restored_before_failure_return=True public_state_unchanged=True foreign_lock_retained=True stdout='' stderr=''",
      "transcript": "~/.no-mistakes/evidence/01M4FAAZCMK3BCMX01ZYFMDD9D/mutation-grants-live.log"
    },
    {
      "action": "publish",
      "observed_result": "publish: real_wait=9.11s exit=1 restored_before_failure_return=True public_state_unchanged=True foreign_lock_retained=True stdout='' stderr=''",
      "transcript": "~/.no-mistakes/evidence/01M4FAAZCMK3BCMX01ZYFMDD9D/mutation-grants-live.log"
    },
    {
      "action": "release",
      "observed_result": "release: real_wait=6.31s exit=1 restored_before_failure_return=True public_state_unchanged=True foreign_lock_retained=True stdout='' stderr=''",
      "transcript": "~/.no-mistakes/evidence/01M4FAAZCMK3BCMX01ZYFMDD9D/mutation-grants-live.log"
    },
    {
      "action": "deactivate",
      "observed_result": "deactivate: real_wait=4.57s exit=1 restored_before_failure_return=True public_state_unchanged=True foreign_lock_retained=True stdout='' stderr=''",
      "transcript": "~/.no-mistakes/evidence/01M4FAAZCMK3BCMX01ZYFMDD9D/mutation-grants-live.log"
    },
    {
      "action": "append",
      "observed_result": "append: real_wait=4.72s exit=1 restored_before_failure_return=True public_state_unchanged=True foreign_lock_retained=True stdout='' stderr=''",
      "transcript": "~/.no-mistakes/evidence/01M4FAAZCMK3BCMX01ZYFMDD9D/mutation-live.log"
    },
    {
      "action": "keys",
      "observed_result": "keys: real_wait=4.72s exit=1 restored_before_failure_return=True public_state_unchanged=True foreign_lock_retained=True stdout='' stderr=''",
      "transcript": "~/.no-mistakes/evidence/01M4FAAZCMK3BCMX01ZYFMDD9D/mutation-live.log"
    },
    {
      "action": "drain-first",
      "observed_result": "drain-first: real_wait=4.41s exit=1 restored_before_failure_return=True public_state_unchanged=True foreign_lock_retained=True stdout='' stderr=''",
      "transcript": "~/.no-mistakes/evidence/01M4FAAZCMK3BCMX01ZYFMDD9D/mutation-live.log"
    },
    {
      "action": "drain-second",
      "observed_result": "drain-second: real_wait=5.13s exit=1 restored_before_failure_return=True public_state_unchanged=True foreign_lock_retained=True stdout='' stderr=''",
      "transcript": "~/.no-mistakes/evidence/01M4FAAZCMK3BCMX01ZYFMDD9D/mutation-live.log"
    },
    {
      "action": "link",
      "observed_result": "link: real_wait=5.18s exit=1 restored_before_failure_return=True public_state_unchanged=True foreign_lock_retained=True stdout='' stderr=''",
      "transcript": "~/.no-mistakes/evidence/01M4FAAZCMK3BCMX01ZYFMDD9D/mutation-live.log"
    },
    {
      "action": "followups",
      "observed_result": "followups: real_wait=5.45s exit=1 restored_before_failure_return=True public_state_unchanged=True foreign_lock_retained=True stdout='' stderr=''",
      "transcript": "~/.no-mistakes/evidence/01M4FAAZCMK3BCMX01ZYFMDD9D/mutation-live.log"
    },
    {
      "action": "clear",
      "observed_result": "clear: real_wait=5.24s exit=1 restored_before_failure_return=True public_state_unchanged=True foreign_lock_retained=True stdout='' stderr=''",
      "transcript": "~/.no-mistakes/evidence/01M4FAAZCMK3BCMX01ZYFMDD9D/mutation-live.log"
    }
  ]
}
Evidence: End-to-end ownership, Go cwd, and startup-recovery regressions

Source: End-to-end ownership, Go cwd, and startup-recovery regressions

$ TMPDIR=<worktree>/.nm-live-validation GOCACHE=<fixture>/go-cache bash tests/fm-test-reap-orphans.test.sh
ok - disappearance checks distinguish a replaced identity from the still-live original
ok - a stub is left alone while the run that made it is alive
ok - the reaper stops a stub whose owning run has ended
ok - the reaper leaves live-parent, unrelated and prefix-sharing processes alone
ok - the reaper is idempotent
ok - lab creation records its caller and ended scratch-copy watchers are reaped
ok - go-build-only argv is attributed by cwd without crossing ownership boundaries
ok - live labs persist after up exits until their recorded runtime ends
ok - failed lab creation without a runtime is ended when its creator exits
ok - active and indeterminate private lab servers prevent reaping until stopped
ok - an owner claim must name the caller or an ancestor
ok - the owner-exit sweep only reaps roots whose marker names the caller
ok - owner-exit authority requires the original owner identity as well as its PID
ok - the owner-exit sweep stops a hidden stub and everything below it
PASS
ok - a test that exits normally leaves no stub of its own behind
ok - sourcing the test library reaps the stub a killed test left behind
ok - startup recovery reaps killed parallel tests' nested stubs without crossing ownership boundaries
ok - nested marked lab watchers are reaped while unmarked nested homes are left alone
tests/fm-test-reap-orphans.test.sh: line 463: 58747 Killed: 9               env TMPDIR="$NESTED_WORKER_TMP" FM_TEST_LIB="$ROOT/tests/lib.sh" FM_TEST_MODE=killed FM_TEST_PIDFILE="$TMP_ROOT/nested-killed.pid" FM_TEST_SKIP_ORPHAN_REAP=1 bash "$TMP_ROOT/child.sh" > /dev/null 2>&1
tests/lib.sh: line 218: 85554 Killed: 9               bash "$ROOT_DEAD/live-parent.sh" "$ROOT_DEAD/release"

exit=0 elapsed=222.37s
Evidence: Supplementary deterministic ownership and mutation regressions

Source: Supplementary deterministic ownership and mutation regressions

$ TMPDIR=<worktree>/.nm-live-validation bash tests/fm-orphan-safety.test.sh
ok - snapshot-direct preserves the process ownership boundary
ok - snapshot-descendant preserves the process ownership boundary
ok - after-cont preserves the process ownership boundary
ok - before-kill preserves the process ownership boundary
ok - survivor-kill preserves the process ownership boundary
ok - owner-unknown preserves the process ownership boundary
ok - activate propagates acquisition failure without touching protected state
ok - publish propagates acquisition failure without touching protected state
ok - release propagates acquisition failure without touching protected state
ok - deactivate propagates acquisition failure without touching protected state
ok - append propagates acquisition failure without touching protected state
ok - keys propagates acquisition failure without touching protected state
ok - drain-first propagates acquisition failure without touching protected state
ok - drain-second propagates acquisition failure without touching protected state
ok - link leaves metadata unchanged after acquisition failure
ok - followups leaves metadata unchanged after acquisition failure
ok - clear leaves metadata unchanged after acquisition failure

exit=0 elapsed=141.51s
Evidence: Baseline release remains running after state disappears

Source: Baseline release remains running after state disappears

$ ~/.no-mistakes/worktrees/32d18ed9638d/01M4FAAZCMK3BCMX01ZYFMDD9D/.nm-live-validation/baseline/bin/fm-wake-grant.sh activate 31551 baseline-proof
exit=0
After lock state was atomically removed for 10.22s: release_exit=None
32987 77644 S    bash ~/.no-mistakes/worktrees/32d18ed9638d/01M4FAAZCMK3BCMX01ZYFMDD9D/.nm-live-validation/baseline/bin/fm-wake-grant.sh release baseline-proof

Baseline reproduces indefinitely waiting release; target exited with code 1 in live-product.log. Baseline waiter stopped by fixture cleanup, not by the product.

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 6 issues found → auto-fixed (10) ✅
  • 🚨 bin/fm-test-reap-orphans.sh:264 - The ownership snapshot contains PID, parent and command, but no birth identity. If a selected short-lived descendant exits and its PID is reused before line 264, the replacement's identity is recorded and still_same subsequently authorizes signalling an unrelated process. Capture identity together with ownership evidence and revalidate that evidence before accepting the target. Related sites: bin/fm-test-reap-orphans.sh:163 captures the unbound snapshot; :244 propagates descendant ownership; :274 checks only the later identity; :294-295 signals CONT and TERM after one check; :308-309 authorizes KILL. The new tests also retain already-reaped PIDs and unconditionally signal them at tests/fm-test-reap-orphans.test.sh:30; record and check their identities before cleanup.
  • 🚨 bin/fm-test-reap-orphans.sh:127 - A failed identity lookup is treated as proof that the owner ended, even after kill -0 established that its PID exists. For example, a live test has a detached polling stub, another test starts a sweep, and the owner's ps lookup fails under resource pressure: marker_owner_dead returns success and the live test's stub becomes eligible for termination. Unknown identity must leave the root untouched, not authorize reaping. Related sites: bin/fm-test-reap-orphans.sh:125 establishes liveness; :156 admits the root; :233 selects its detached processes; tests/lib.sh:432 and bin/fm-test-run.sh:2622 invoke this decision automatically.
  • 🚨 bin/fm-wake-lib.sh:1281 - The newly possible acquisition failure is not propagated by existing callers. fm-wake-grant.sh uses set -u, not set -e, and continues after failure in activate, publish, release and deactivate. If a deleted state directory returns just after the grace expires and another process acquires its queue lock, the old waiter can inspect or change grant records without owning that lock. Update callers to stop before entering protected operations when acquisition fails. Related sites: bin/fm-wake-lib.sh:2121-2122 performs append after an unchecked acquire; :2181-2183 performs a supposedly locked read and can report success after failure; bin/fm-wake-grant.sh:44, :60, :89 and :98 all mark the lock held unconditionally.
  • 🚨 bin/fm-test-reap-orphans.sh:154 - Required orphan classes remain outside the implementation. The criterion explicitly requires 'Reap orphaned scratch-copy watchers ... and leftover go test binaries; extend fm-test-run.sh reaping to lab copies.' The new scan accepts only fm-/.fm-test-fixture, whereas fm-lab-home.sh creates .fm-lab-home and fm-live-lab.sh creates fmlab. roots with .fm-live-lab records. An ended lab's detached watcher under a still-existing scratch home is therefore skipped. An already-reparented Go test binary whose argv names go-build/... rather than a marked fixture root is also skipped, even when its cwd belongs to the ended run. Related sites: bin/fm-test-reap-orphans.sh:148 requires the test marker in owner mode; :232 requires an argv root match; :244 can recover rootless descendants only while ancestry remains in the snapshot; bin/fm-test-run.sh:2618-2622 merely runs the same restricted reaper from the local copy. Complete attribution at the lab/process creation boundary without weakening ownership proof; any additional durable provenance machinery needed for that remedy requires authorization.
  • ⚠️ bin/fm-test-reap-orphans.sh:65 - Simplification: the new public --dry-run mode is not required by the orphan-reaping intent. Executable termination and non-termination assertions can supply the requested proof without introducing a separate preview interface. Recommend removing this optional mode and its parallel output/control paths at bin/fm-test-reap-orphans.sh:282-289, together with its dedicated cases at tests/fm-test-reap-orphans.test.sh:127-130 and :175-179.
  • ⚠️ bin/fm-wake-lib.sh:1273 - Simplification: FM_LOCK_PARENT_GONE_GRACE_SECONDS introduces a configurable behavior beyond the stated requirement to end waits on vanished state. No intent requirement needs operators to select this grace; a fixed five-second grace is narrower. Recommend removing the override and its validation branch at bin/fm-wake-lib.sh:1273-1274, and exercising the fixed grace in tests/fm-wake-queue.test.sh:2779 and :2807 rather than adding a configuration surface for those cases.

🔧 Fix applied.
6 issues (4 errors, 2 warnings) still open:

  • 🚨 bin/fm-wake-lib.sh:1281 - The newly possible acquisition failure is not propagated by existing callers. fm-wake-grant.sh uses set -u, not set -e, and continues after failure in activate, publish, release and deactivate. If a deleted state directory returns just after the grace expires and another process acquires its queue lock, the old waiter can inspect or change grant records without owning that lock. Update callers to stop before entering protected operations when acquisition fails. Related sites: bin/fm-wake-lib.sh:2121-2122 performs append after an unchecked acquire; :2181-2183 performs a supposedly locked read and can report success after failure; bin/fm-wake-grant.sh:44, :60, :89 and :98 all mark the lock held unconditionally.
  • 🚨 bin/fm-test-reap-orphans.sh:154 - Required orphan classes remain outside the implementation. The criterion explicitly requires 'Reap orphaned scratch-copy watchers ... and leftover go test binaries; extend fm-test-run.sh reaping to lab copies.' The new scan accepts only fm-/.fm-test-fixture, whereas fm-lab-home.sh creates .fm-lab-home and fm-live-lab.sh creates fmlab. roots with .fm-live-lab records. An ended lab's detached watcher under a still-existing scratch home is therefore skipped. An already-reparented Go test binary whose argv names go-build/... rather than a marked fixture root is also skipped, even when its cwd belongs to the ended run. Related sites: bin/fm-test-reap-orphans.sh:148 requires the test marker in owner mode; :232 requires an argv root match; :244 can recover rootless descendants only while ancestry remains in the snapshot; bin/fm-test-run.sh:2618-2622 merely runs the same restricted reaper from the local copy. Complete attribution at the lab/process creation boundary without weakening ownership proof; any additional durable provenance machinery needed for that remedy requires authorization.
  • ⚠️ bin/fm-test-reap-orphans.sh:65 - Simplification: the new public --dry-run mode is not required by the orphan-reaping intent. Executable termination and non-termination assertions can supply the requested proof without introducing a separate preview interface. Recommend removing this optional mode and its parallel output/control paths at bin/fm-test-reap-orphans.sh:282-289, together with its dedicated cases at tests/fm-test-reap-orphans.test.sh:127-130 and :175-179.
  • ⚠️ bin/fm-wake-lib.sh:1273 - Simplification: FM_LOCK_PARENT_GONE_GRACE_SECONDS introduces a configurable behavior beyond the stated requirement to end waits on vanished state. No intent requirement needs operators to select this grace; a fixed five-second grace is narrower. Recommend removing the override and its validation branch at bin/fm-wake-lib.sh:1273-1274, and exercising the fixed grace in tests/fm-wake-queue.test.sh:2779 and :2807 rather than adding a configuration surface for those cases.
  • 🚨 bin/fm-wake-lib.sh:1281 - Round 1 fixed six acquisition callers but left reachable sibling critical sections unchecked. For example, an acknowledgement waits while STATE disappears, acquisition returns 1, and STATE returns with another queue writer holding the lock. fm-wake-drain.sh continues without ownership; an append between its queue snapshot and replacement at :962 is silently lost. Remaining sites: bin/fm-wake-drain.sh:836,916 (acknowledgement acquisitions); bin/fm-watch.sh:2099 (queued process-event presentation); bin/fm-mail.sh:378 (append and delivery journal; wake_for is called through || at :601); bin/fm-afk-return.sh:913 (catch-up publication, which itself recreates STATE at :210); bin/fm-lock.sh:164,209 (session-lock and identity publication); bin/fm-tasks-axi.sh:307,309 (task mutations); bin/fm-backlog-handoff.sh:852,859 (conditional pending-delivery recovery); bin/fm-control.sh:677 (busy-generation metadata cleanup); bin/fm-spawn.sh:1688,5860 (conditional presentation recovery and trace metadata publication); bin/fm-x-lib.sh:920,951,1009 (request-link publication, follow-up count, and link removal). These paths either lack errexit or execute inside condition-tested functions where it is disabled. Explicitly propagate acquisition failure at each critical-section entry before reading protected records, marking the lock held, or mutating state. The acknowledgement-atomicity claim at docs/watcher-continuity.md:319 must hold for these paths too.
  • 🚨 tests/fm-test-reap-orphans.test.sh:242 - Round 1's F1 cleanup fix still accepts a replacement process's identity as ownership evidence. child_env normal completes only after its exit sweep has reaped the stub, but :242 then registers that already-reaped PID; if it has been reused, track at :40 records the unrelated replacement's identity and cleanup at :30-32 authorizes killing it. The killed-child registration at :249 has the same exposure when another automatic sweep reaps the dead owner's stub before registration. A remaining sibling is tests/fm-test-run.test.sh:2080-2087: after the runner has reaped the stub, the test polls the old PID and unconditionally kills any replacement found there. Remove registration of the already-cleaned normal stub, and have fixtures record identity while establishing process ownership for the other cases. Use that original identity for disappearance checks at tests/fm-test-reap-orphans.test.sh:45-50 and tests/fm-test-run.test.sh:2082-2087, and revalidate it before cleanup signals.

🔧 Fix applied.
2 warnings still open:

  • ⚠️ tests/fm-wake-queue.test.sh:2789 - The vanished-parent regression kills and waits for the holder before deleting state at lines 2789–2791. Between those operations, the waiter can legitimately reclaim the dead holder's lock and write waiter.rc=0, causing a false test failure. fm_lock_try_acquire immediately permits reclaiming a dead numeric holder; no freshness grace prevents this race. Round 2 retained this sequencing while changing the test to exercise the fixed grace. Remove or rename the parent while the holder is still alive, then terminate and reap the holder. The same test is reached through the ordinary suite call at tests/fm-wake-queue.test.sh:3781 and the targeted call at :3773.
  • ⚠️ tests/fm-test-run.test.sh:2422 - Simplification: Round 2 introduced permanent --orphan-review subset modes here and at tests/fm-wake-queue.test.sh:3772. The authorized fixes require executable regression tests, not additional suite-selection interfaces; both regressions already run through their ordinary suites. Revert these two dispatch branches to the minimal fix, retaining the regression tests and their normal suite calls.

🔧 Fix applied.
3 warnings still open:

  • ⚠️ tests/fm-wake-queue.test.sh:2789 - The vanished-parent regression kills and waits for the holder before deleting state at lines 2789–2791. Between those operations, the waiter can legitimately reclaim the dead holder's lock and write waiter.rc=0, causing a false test failure. fm_lock_try_acquire immediately permits reclaiming a dead numeric holder; no freshness grace prevents this race. Round 2 retained this sequencing while changing the test to exercise the fixed grace. Remove or rename the parent while the holder is still alive, then terminate and reap the holder. The same test is reached through the ordinary suite call at tests/fm-wake-queue.test.sh:3781 and the targeted call at :3773.
  • ⚠️ tests/fm-test-run.test.sh:2422 - Simplification: Round 2 introduced permanent --orphan-review subset modes here and at tests/fm-wake-queue.test.sh:3772. The authorized fixes require executable regression tests, not additional suite-selection interfaces; both regressions already run through their ordinary suites. Revert these two dispatch branches to the minimal fix, retaining the regression tests and their normal suite calls.
  • ⚠️ tests/fm-wake-queue.test.sh:2788 - Round 3 closes the dead-holder reclaim race but leaves two ways this regression can fail with correct production code. Recursive rm removes the lock before removing its parent; a waiter scheduled between those operations can legitimately acquire and record rc=0. Separately, the sleep/liveness check at :2785–2787 does not establish that the waiter finished sourcing fm-wake-lib.sh at :2780; a delayed waiter can run that library's mkdir -p initialization after deletion, recreate STATE, and acquire successfully. Synchronize on waiter readiness after library initialization, then atomically rename STATE away while the holder is alive before terminating and reaping it. The invariant applies to both entrypoints at tests/fm-wake-queue.test.sh:3773 (targeted dispatch) and :3779 (ordinary suite).

🔧 Fix applied.
2 issues (1 error, 1 warning) still open:

  • 🚨 bin/fm-test-reap-orphans.sh:220 - Round 2 added lab provenance but left discovery limited to immediate children of the scan directory. A supported scratch-copy layout such as /tmp/fm-lab.XXXXXX/home carries its marker at home/.fm-lab-home, which this glob never reads. If its creator dies with a detached home/bin/fm-watch.sh running, the runner's sweep finds no proven root and exits successfully at :229 without stopping the watcher. Nested lab homes are already used at tests/fm-backend-herdr-recovery-lock-e2e.test.sh:56 and :634-635. The affected boundary is marker discovery here; bin/fm-lab-home.sh:69 writes provenance at the actual home, while bin/fm-test-run.sh:2622 and tests/lib.sh:432 both depend on this default discovery. Discover nested marked homes within the bounded temporary lab/test containers, retaining the existing owner, runtime, parent, and target-identity checks.
  • ⚠️ tests/fm-orphan-safety.test.sh:240 - Simplification: the new per-case suite-selection interface is another optional review-only dispatch surface, alongside the two removed in Round 4. No orphan-reaping requirement needs these selectors: the default all branch already executes every regression. Remove the identity selectors at :240, queue/grant selectors at :241, and request-link selectors at :242, retaining all regression functions and their ordinary suite execution. This reduces the earlier fix round to the required behavioral proof without changing product behavior.

🔧 Fix applied.
2 warnings still open:

  • ⚠️ tests/fm-orphan-safety.test.sh:240 - Simplification: the new per-case suite-selection interface is another optional review-only dispatch surface, alongside the two removed in Round 4. No orphan-reaping requirement needs these selectors: the default all branch already executes every regression. Remove the identity selectors at :240, queue/grant selectors at :241, and request-link selectors at :242, retaining all regression functions and their ordinary suite execution. This reduces the earlier fix round to the required behavioral proof without changing product behavior.
  • ⚠️ tests/fm-test-reap-orphans.test.sh:49 - Round 2's F8 fix introduced an identity lookup that selects the oldest registration of a PID. This suite repeatedly terminates fixtures and registers new processes without removing old registrations. If a later fixture receives a recycled PID, track appends its new identity, but alive selects the earlier, dead fixture's identity and reports the currently owned process as gone. Consequently, a correct reaper can fail a preservation assertion, or wait_gone can pass while the new fixture remains running. Correct the shared registration/lookup boundary to select the most recently registered ownership identity, retaining identity checks before cleanup signals. Related sites are tests/fm-test-reap-orphans.test.sh:41 and :42 (append-only registrations), :60 (disappearance lookup), and the implicit-identity consumers at :155, :160, :164, :167, :168, :169, :197, :204, :236, :237, :238, :262, :266, :278, :303, :306, :309, :314, :349, :353, :359, :364, :365, :366, :418, :421, :437, :438, :443, :444, and :446.

🔧 Fix applied.
1 warning still open:

  • ⚠️ tests/fm-orphan-safety.test.sh:240 - Simplification: the new per-case suite-selection interface is another optional review-only dispatch surface, alongside the two removed in Round 4. No orphan-reaping requirement needs these selectors: the default all branch already executes every regression. Remove the identity selectors at :240, queue/grant selectors at :241, and request-link selectors at :242, retaining all regression functions and their ordinary suite execution. This reduces the earlier fix round to the required behavioral proof without changing product behavior.

🔧 Fix applied.
2 warnings still open:

  • ⚠️ tests/fm-orphan-safety.test.sh:240 - Simplification: the new per-case suite-selection interface is another optional review-only dispatch surface, alongside the two removed in Round 4. No orphan-reaping requirement needs these selectors: the default all branch already executes every regression. Remove the identity selectors at :240, queue/grant selectors at :241, and request-link selectors at :242, retaining all regression functions and their ordinary suite execution. This reduces the earlier fix round to the required behavioral proof without changing product behavior.
  • ⚠️ tests/fm-orphan-safety.test.sh:6 - Simplification: prior fix-round commit 6804c9f introduced the optional FM_ORPHAN_SAFETY_BIN alternate-code-tree interface. The required ownership and lock-failure regressions do not need this permanent override; setting it makes their results describe another checkout rather than the change under review. It selects the reaper at tests/fm-orphan-safety.test.sh:74, queue/grant/acknowledgement implementations at :99–103, :143–150 and :161–176, and request-link implementations at :191–192, :212 and :226. Reduce that fix round to the required behavioral proof by replacing the override with BIN="$ROOT/bin", retaining every regression.

🔧 Fix applied.
1 warning still open:

  • ⚠️ tests/fm-test-reap-orphans.test.sh:106 - Round 2 introduced an initialization race when capturing fixture identities; Round 6's newest-registration fix leaves it reachable. The launcher backgrounds a command and immediately reads its identity, which includes argv (bin/fm-wake-lib.sh:89 and :104). If the child has forked but not yet exec'd, the recorded identity describes the inherited shell. After exec, alive reports that still-running fixture as gone: preservation assertions can fail with correct production code, and wait_gone can falsely prove termination. Publishing the PID file afterward does not establish child readiness. Capture the initialized executable's identity before publishing readiness, or use an exec-stable birth identity with the expected command, retaining identity checks before cleanup signals. The same invariant applies in tests/fm-test-reap-orphans.test.sh:40 (shared track, called at :117, :143, :190, :233, :258, :340, :345 and :428), :93/:245/:272/:287 (owner-marker captures), :123 (identity fixture), and :391 (child-script identity publication); affected shared consumers are :30 (cleanup), :53 (alive) and :60 (disappearance).

🔧 Fix applied.
1 warning still open:

  • ⚠️ bin/fm-test-reap-orphans.sh:227 - The F12 nested-home fix round (3fd8c97) left the parallel runner's test-fixture sibling undiscoverable. With --jobs, fixture markers live at /tmp/fm-test-run.XXXXXX/wN/tmp/fm-*.XXXXXX/.fm-test-fixture (bin/fm-test-run.sh:2798–2809; tests/lib.sh:392). If hard termination interrupts the owning run before its worker sweep, an orphaned poll-publish-holder.sh can remain indefinitely: its actual loop at tests/fm-pr-check-security.test.sh:3333 has no bound. A subsequent ordinary startup sweep at tests/lib.sh:432 scans /tmp, but :227 only discovers immediate test markers and :230 recursively discovers only lab-home markers. It therefore returns successfully without recognizing this ended test's root. Extend the existing bounded-container discovery to nested .fm-test-fixture markers as well, preserving all ownership and target-identity checks. Related changed-code boundaries are bin/fm-test-run.sh:2622 (runner sweep), tests/lib.sh:392 (marker publication), and tests/lib.sh:432 (later startup recovery).

🔧 Fix applied.
✅ Re-checked - no issues remain.

✅ **Test** - passed

✅ No issues found.

  • Live validation: ✅ go - 12 of 13 scenarios driven live against the product
Scenario Result Live Evidence
Sweep an ended test's detached process without stopping live-owner, live-parent, unrelated, or prefix-sharing processes ✅ pass live End-to-end ownership, Go cwd, and startup-recovery regressions
Reap a real scratch-copy watcher inside a nested marked lab after its creator ends ✅ pass live Real watcher, private runtime, and grant-release transcript
Reap a detached compiled Go test binary by cwd without crossing ownership boundaries ✅ pass live End-to-end ownership, Go cwd, and startup-recovery regressions
Keep an orphaned lab process while its private tmux runtime remains active, then reap it after runtime shutdown ✅ pass live Real watcher, private runtime, and grant-release transcript
Allow owner-exit cleanup only for matching caller ownership and terminate its hidden process descendants ✅ pass live End-to-end ownership, Go cwd, and startup-recovery regressions
Clean up normal test exits and recover nested parallel-worker orphans after hard termination ✅ pass live End-to-end ownership, Go cwd, and startup-recovery regressions
Run a killed or timed-out script and reap its detached polling process before the next script ✅ pass live Runner reaps killed and timed-out scripts' detached processes
Skip direct and descendant processes whose identity changes after the ownership snapshot ✅ pass live Actual ownership snapshots and surviving changed identities
Stop abandoned grant release after sustained state loss while retaining blocking semantics for live holders and brief absences ✅ pass live Real watcher, private runtime, and grant-release transcript; Baseline release remains running after state disappears
Fail grant and queue operations without exposing or mutating protected state when state returns at the failure boundary ✅ pass live Observed lock-failure results for eleven operations
Preserve acknowledgement state when either queue-lock acquisition fails ✅ pass live Observed lock-failure results for eleven operations
Preserve request-link metadata when link, follow-up, or clear cannot acquire its lock ✅ pass live Observed lock-failure results for eleven operations
Meet the aggregate R4 fleet-day load, process-creation, and kernel-time targets ⏸️ untested no Disposable fixtures demonstrated the polling failure and its termination, but cannot establish real-fleet day-long distributions. This gate forbids accessing operator fleet data or controlling shared…
  • TMPDIR=<worktree-local fixture> GOCACHE=<fixture>/go-cache bash tests/fm-test-reap-orphans.test.sh
  • TMPDIR=&lt;worktree-local fixture&gt; bash tests/fm-orphan-safety.test.sh — completed after increasing the initial 120-second execution bound.
  • python3 .nm-live-validation/drive.py — real scratch-copy watcher, private tmux runtime, vanished-state grant release, and brief state restoration.
  • python3 .nm-live-validation/runner-proof.py — real scratch-copy runner with SIGKILL and timeout fixtures; scan-only isolation wrapper forwarded to the unchanged reaper.
  • python3 .nm-live-validation/identity-proof.py — real direct and descendant processes changed executable identity after the actual ownership snapshot.
  • python3 .nm-live-validation/mutation-proof.py — actual-time acquisition failures and successful retries for grant, queue, acknowledgement, and request-link operations.
  • git archive --format=tar fd9b8b020201a029880b3980b545f352ae357a39 bin followed by the baseline fm-wake-grant.sh release — remained running after state was absent for ten seconds.
  • Scoped final orphan sweeps and process inspection; removed disposable drivers, scratch copies, generated Go binaries/cache, state fixtures, and private socket directory.
✅ **Document** - passed

✅ No issues found.

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

🔧 Fix applied.
✅ Re-checked - no issues remain.

✅ **Push** - passed

✅ No issues found.

A lock wait on a deleted directory spun forever (fm-wake-grant.sh release,
lab watchers), and test stubs outlived dead tests and their fixtures.

- fm_lock_acquire_wait returns 1 once the lock directory has been gone for a grace
- bin/fm-test-reap-orphans.sh stops processes a fixture marker proves belong to an ended run
- tests/lib.sh runs it at cleanup (owner) and at start (dead owners)
- fm-test-run.sh runs it after each script
@MrGTV-love
MrGTV-love merged commit dc62c5e into main Oct 9, 2026
22 checks passed
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