Skip to content

fix(bin): canonicalize watcher lock owner paths - #1894

Open
moneyteamameen wants to merge 3 commits into
kunchenguid:mainfrom
moneyteamameen:fm/fix-guard-path-casing
Open

fix(bin): canonicalize watcher lock owner paths#1894
moneyteamameen wants to merge 3 commits into
kunchenguid:mainfrom
moneyteamameen:fm/fix-guard-path-casing

Conversation

@moneyteamameen

Copy link
Copy Markdown

Intent

Fix Firstmate queue item fix-guard-path-casing (FM-GUARD-001): watcher health and lock ownership must compare canonical Firstmate home and watcher paths so macOS case-insensitive GitHub/github spellings identify the same healthy watcher. Preserve fail-closed PID and identity validation, exact per-home isolation, cross-home safety boundaries, absent final watcher-leaf and symlink/realpath owner behavior, and all existing watcher lifecycle semantics. Add focused regressions for case-differing paths, cross-home mismatch, wrong live PID, dead PID, symlink paths, and the existing absent-leaf contract. Limit changes to bin/fm-wake-lib.sh and its directly relevant tests. Preserve evidence that tests/fm-pi-watch-extension.test.sh has an unrelated timing flake in its TypeScript-only hung-successor fixture; do not weaken or skip relevant watcher, wake, lock, guard, shell, or CI validation. Push a feature branch, create a PR, drive CI green, and never merge.

What Changed

  • Canonicalize watcher home and executable paths across filesystem-case aliases, symlinks, and absent watcher leaves while preserving fail-closed and cross-home checks.
  • Reuse canonical owner matching during health checks and stale-lock cleanup so restarts can replace aliased reused-PID locks without signaling unrelated processes.
  • Add focused regressions for path aliases, missing leaves, cross-home isolation, wrong or dead PIDs, and restart recovery, and document the ownership contract.

Risk Assessment

✅ Low: The correction is well bounded: restart cleanup now uses the shared canonical owner boundary, strict PID/identity validation and cross-home isolation remain intact, and redundant polling canonicalization was removed.

Testing

Baseline inspection confirmed the supplied target and changed-file set; focused watcher-lock, watch-arm, turn-end guard, and Claude auto-arm tests passed, and an evidence-producing case-insensitive CLI restart confirmed canonical ownership, safe stale-lock replacement, and persisted state end to end. The supplied unrelated TypeScript-only Pi hung-successor timing flake was not rerun in this targeted phase, and its test remains unchanged; broad CI remains owned by the outer pipeline.

Evidence: Case-alias CLI end-to-end transcript

watcher: started pid=70871 (beacon fresh); the stale lock was replaced while the unrelated live PID remained alive, and both lowercase owner paths resolved to their canonical counterparts.

filesystem_case_alias: GitHub/github and FmHome/fmhome resolve to the same directories
operator_command: FM_HOME=/var/folders/s8/0_mzgfjj34v2wr0gvxxh64n80000gn/T/no-mistakes-evidence/01KZE7T1DEATFDSH3D504NSNHE/case-alias-cli-fixture/github/fmhome /var/folders/s8/0_mzgfjj34v2wr0gvxxh64n80000gn/T/no-mistakes-evidence/01KZE7T1DEATFDSH3D504NSNHE/case-alias-cli-fixture/github/firstmate/bin/fm-watch-arm.sh --restart
cli_output: watcher: started pid=70871 (beacon fresh)
unrelated_live_pid_preserved: yes (pid=70814)
stale_lock_replaced_with_live_watcher: yes (pid=70871)
persisted_lock_fm_home: /var/folders/s8/0_mzgfjj34v2wr0gvxxh64n80000gn/T/no-mistakes-evidence/01KZE7T1DEATFDSH3D504NSNHE/case-alias-cli-fixture/github/fmhome
persisted_lock_watcher_path: /var/folders/s8/0_mzgfjj34v2wr0gvxxh64n80000gn/T/no-mistakes-evidence/01KZE7T1DEATFDSH3D504NSNHE/case-alias-cli-fixture/github/firstmate/bin/fm-watch.sh
canonical_home_owner_match: yes (/private/var/folders/s8/0_mzgfjj34v2wr0gvxxh64n80000gn/T/no-mistakes-evidence/01KZE7T1DEATFDSH3D504NSNHE/case-alias-cli-fixture/GitHub/FmHome)
canonical_watcher_owner_match: yes (/Users/mohammedameen/.no-mistakes/worktrees/ac543050f1c8/01KZE7T1DEATFDSH3D504NSNHE/bin/fm-watch.sh)
Evidence: Focused watcher-lock regressions
ok - simultaneous watcher starts leave exactly one live process
ok - fm_pid_identity real ps fallback is locale-invariant
ok - fm_pid_identity is locale-invariant across LC_ALL/LC_TIME
ok - /proc process identity ignores simulated btime changes
ok - /proc process identity detects pid reuse
ok - MSYS /proc process identity regression skipped on non-Windows host
ok - killed watcher stale lock is reclaimed
ok - live watcher lock with stale heartbeat is actionable
ok - guard banner leads when down with pending wakes (repair-after-drain) and stays silent when live and fresh
ok - watcher health canonicalizes owner paths while PID and cross-home guards stay fail-closed
ok - concurrent fm_lock_try_acquire yields exactly one winner
ok - dead-pid stale lock is reclaimed by a single acquirer
ok - concurrent stale-lock steal yields exactly one winner
ok - live steal mutex is not reclaimed
ok - live-held lock is not stolen
ok - empty mid-acquire lock keeps a minimum grace
ok - late original claimant cannot claim a recreated lock
ok - paused mid-acquire claimant backs off to active stealer
ok - watch restart refuses to signal a reused pid
ok - watch restart reclaims an aliased reused-pid lock without signaling its pid
ok - watch restart attaches to a verified healthy peer and later surfaces a successor gap
ok - watcher self-evicts when the lock pid no longer names it
ok - arm turns clean self-eviction without a successor into a typed failure
ok - arm attaches to a live fresh watcher and fails loudly when that cycle has no successor
ok - attached arm signals record a classified lifecycle entry
ok - arm starts+confirms a fresh watcher on a clean lock and self-heals a dead-pid lock (never healthy off a dead pid)
ok - arm cleans child watcher and temp output on HUP
ok - arm propagates an immediate watcher wake before confirmation
ok - arm attaches to a peer watcher after child stands down and surfaces a missing successor
watcher: lock held by live pid 37218 but heartbeat is stale for 839466581s (>300s); inspect or stop that watcher before re-arming.
ok - arm reports FAILED and exits non-zero when no fresh watcher can be confirmed
ok - cycle-exit ledger links a verified successor and remains size-capped
ok - SIGSTOP distinguishes live PID from stale beacon and termination records the exit class
Evidence: Watch-arm lifecycle regressions
ok - watch-arm: an attached arm reports the wake its cycle delivered instead of a false failure
ok - watch-arm: a delivered wake consumed by the handling turn still closes the attached arm cleanly
ok - watch-arm: a cycle that delivered no wake of its own still fails loudly
Evidence: Turn-end guard regressions
ok - fm_supervision_unhealthy: false with no state/*.meta at all
ok - fm_supervision_unhealthy: true with in-flight task and no beacon ever
ok - fm_supervision_unhealthy: true with in-flight task and a beacon far outside the grace window
ok - fm_supervision_unhealthy: false with in-flight task and a fresh beacon
ok - fm_supervision_status: FM_SUP_QUEUE_PENDING tracks state/.wake-queue
ok - fm_supervision_needed: X-mode relay poll needs supervision
ok - fm_supervision_unhealthy: source-only home needs supervision
ok - fm-turnend-guard: silent no-op with nothing in flight
ok - fm-turnend-guard: blocks when a fresh beacon has no live watcher lock
ok - fm-turnend-guard: non-Claude path blocks a source-only home
ok - fm-turnend-guard: blocks on a dead watcher lock even when the beacon is fresh
ok - fm-turnend-guard: silent no-op with a live watcher lock and fresh beacon
ok - fm-turnend-guard: healthy non-Claude harness paths ignore Claude episode contention
ok - fm-turnend-guard: blocks on a live watcher lock with an ancient beacon
ok - fm-turnend-guard: blocks with the exact required reason in the primary when unhealthy
ok - fm-turnend-guard: blocks from active FM_HOME state, not only repo-root state
ok - fm-turnend-guard: X-mode repair reason sources the cadence config
ok - fm-turnend-guard: X-mode-only supervision remains guarded in default mode
ok - fm-turnend-guard: ignores stale repo-root state when FM_HOME is set
ok - fm-turnend-guard: uses FM_STATE_OVERRIDE ahead of FM_HOME/state
ok - fm-turnend-guard: stop_hook_active=true always allows the stop (never blocks twice in one turn)
ok - fm-turnend-guard: blocks a blind turn end in a secondmate's own home (.fm-secondmate-home no longer excludes it)
ok - fm-turnend-guard: idle-by-default - silent in a secondmate home with nothing in flight
ok - fm-turnend-guard: stop_hook_active=true allows the stop in a secondmate home (never blocks twice in one turn)
ok - fm-turnend-guard: secondmate deferred-death recovery - silent while watched, forces re-arm once the watcher exits
ok - fm-turnend-guard: inert in a secondmate's own child worktree (linked git worktree) even when unhealthy
ok - fm-turnend-guard: blocks a blind turn end in a treehouse-leased LINKED secondmate home (marker force-include)
ok - fm-turnend-guard: an invalid (empty) marker cannot spoof inclusion; linked worktree stays exempt
ok - fm-turnend-guard: a non-ASCII marker cannot spoof inclusion; linked worktree stays exempt
ok - fm-turnend-guard: inert in a crewmate/scout task worktree (linked git worktree) even when unhealthy
ok - fm-turnend-guard: fails open (never blocks) when jq is missing
ok - fm-turnend-guard: silent no-op on empty stdin
ok - fm-turnend-guard: runs well under the generous timing margin (0s)
ok - fm-turnend-guard-grok: forces one explicitly marked same-session resume when the shared predicate blocks
ok - fm-turnend-guard-grok: legacy environment loop guard prevents a nested resume loop
ok - fm-turnend-guard-grok: native false delegates blocking feedback with zero resume processes
ok - fm-turnend-guard-grok: native true remains bounded and starts no resume process
ok - fm-turnend-guard-grok: both spellings are typed and camelCase has deterministic precedence
ok - fm-turnend-guard-grok: malformed, invalidly typed, and missing-prerequisite payloads start neither path
ok - fm-turnend-guard-grok: missing jq and no-supervision-needed stops stay silent and bounded
ok - .codex/hooks.json: Stop hook uses hook process root when payload cwd is outside
ok - .codex/hooks.json: Stop hook ignores nested git root guard scripts
ok - .opencode primary plugin: guard path is anchored to worktree, not directory
ok - .pi primary extension: no-tool and multi-tool runs each inject exactly one guard follow-up
ok - .pi primary extension: delivery failure resets the logical-run latch
ok - fm-turnend-guard --claude: re-blocks a loop-guarded stop while unhealthy and unclaimed (incident regression)
ok - fm-turnend-guard --claude: X-mode-only homes re-block when auto-arm recovery is absent
ok - fm-turnend-guard --claude: a live arming epoch advances once and repeated observation is idempotent
ok - fm-turnend-guard --claude: repeated failed-to-arming races make bounded monotonic progress
ok - fm-turnend-guard --claude: terminal owner boundary excludes a concurrent start without deadlock
ok - fm-turnend-guard --claude: fresh rewake epoch prevents a duplicate continuation for the same event
ok - fm-turnend-guard --claude: fresh failed epochs preserve and advance monotonic fail-open progression
ok - fm-turnend-guard --claude: integrated fresh failures reach one bounded fail-open, stop continuation, and reset on recovery
ok - fm-turnend-guard --claude: reset contention preserves all episode state until retry
ok - fm-turnend-guard --claude: concurrent auto-arm and guard resets are idempotent and deadlock-free
ok - fm-turnend-guard --claude: stale rewake epoch does not allow a blind stop
ok - fm-turnend-guard --claude: budget exhaustion alone cannot permit a blind stop
ok - fm-turnend-guard --claude: verified fail-open is loud, bounded, attended, and non-repeating
ok - fm-turnend-guard --claude: fail-open requires both exhausted retries and consumed notice
ok - fm-turnend-guard --claude: away ownership excludes the Stop-autoarm fail-open
ok - fm-turnend-guard --claude: positive watcher recovery resets failure episode state
ok - fm-turnend-guard --claude: bounded claim wait avoids a token-consuming forced continuation
ok - fm-turnend-guard --claude: secondmate home re-blocks unclaimed and allows auto-arm-claimed stops
Evidence: Claude Stop auto-arm regressions
ok - auto-arm: inert in a linked child worktree even when in-flight
ok - auto-arm: inert with no session lock
ok - auto-arm: a demonstrably dead recorded session owner is reclaimed through fm-lock.sh before arming
ok - auto-arm: inert without arm, rewake, or lock replacement when another live harness owns the home
ok - auto-arm: inert while AFK owns supervision
ok - auto-arm: stale-owner recovery leaves the AFK and supervision-need gates unchanged
ok - auto-arm: resolves the outermost pid of a nested contiguous claude ancestry (bg-spare chain)
ok - auto-arm: inert with nothing in flight and no X-mode need
ok - auto-arm: actionable close translates to exactly one exit-2 rewake with reason
ok - auto-arm: actionable close survives a healthy successor without duplicate delivery
ok - auto-arm: bounded failure verification emits one automatic-mechanism alarm
ok - auto-arm: consecutive failures keep Stop-owned retry without repeating notice
ok - auto-arm: unverified clean close exhausts retries and fails closed
ok - auto-arm: post-alarm actionable outcomes cannot continue or reset failure state
ok - auto-arm: benign cycle end with a live watcher and fresh beacon stays silent across the next cycle
ok - auto-arm: budget contention preserves the episode and forces a reset retry
ok - auto-arm: X-mode poll need arms the cycle even with no tasks in flight
ok - auto-arm: concurrent firings admit one owner and one rewake translation
ok - auto-arm: need vanishing mid-cycle closes without a rewake
ok - auto-arm: mid-cycle AFK hands triage to the daemon with no rewake
ok - auto-arm: active in a marked secondmate home
ok - fm-lock: shared session-lock lib preserves the status path

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 2 issues found → auto-fixed ✅
  • 🚨 bin/fm-wake-lib.sh:136 - The requirement to “Preserve … all existing watcher lifecycle semantics” remains unmet for --restart. Canonical ownership is confined to this identity-strict helper: with case-differing spellings and a reused/wrong live PID, it correctly returns false, but fm-watch-arm.sh then calls clear_stale_recorded_watcher_lock, whose raw home/path comparisons reject the alias. The stale lock remains, so the replacement watcher cannot start—unlike the existing same-spelling reused-PID recovery. The new wrong-PID test checks only health rejection. Fixing this durably requires approval to exceed “Limit changes to bin/fm-wake-lib.sh…” by adding a shared path-only owner matcher used by restart cleanup, plus an end-to-end regression.
  • ⚠️ bin/fm-wake-lib.sh:159 - fm_watcher_healthy canonicalizes home and watcher here, then fm_watcher_lock_matches_pid immediately canonicalizes both again. In the arm’s 0.2-second confirmation and 0.5-second attachment loops, this adds two redundant realpath processes per iteration on a path whose MSYS fork cost is explicitly documented as significant. Pass the original arguments directly and let the matcher own canonicalization.

🔧 Fix: Fix aliased watcher restart recovery
✅ Re-checked - no issues remain.

✅ **Test** - passed

✅ No issues found.

  • git status --short --branch and git diff --stat --name-status 70aeba855527f7693082f6dd1bc731e334d0269f..5c9f4d0ff1502ffe3f1e1f1e1f18141c130cb79b
  • bash tests/fm-watcher-lock.test.sh
  • bash tests/fm-watch-arm.test.sh
  • bash tests/fm-turnend-guard.test.sh
  • bash tests/fm-claude-stop-autoarm.test.sh
  • Case-insensitive APFS end-to-end check: seeded canonical GitHub/FmHome lock metadata with an unrelated live PID and stale identity, then ran FM_HOME=.../github/fmhome .../github/firstmate/bin/fm-watch-arm.sh --restart
  • Verified the CLI-reported watcher PID was live and persisted, the unrelated PID remained alive, and realpath matched both stored owner paths to their canonical home and watcher
  • Rechecked git status --short --branch, evidence-file integrity, and cleanup with kill -0 70814 / kill -0 70871
✅ **Document** - passed

✅ No issues found.

⚠️ **Lint** - 1 warning
  • ⚠️ linter found issues (exit code 127)
✅ **Push** - passed

✅ No issues found.

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