Skip to content

fix(bin): wait for the Herdr presentation lock during recovery - #6496

Open
cloud-practitioner wants to merge 5 commits into
kunchenguid:mainfrom
cloud-practitioner:fm/fm-up-herdr-recovery-lock
Open

cloud-practitioner wants to merge 5 commits into
kunchenguid:mainfrom
cloud-practitioner:fm/fm-up-herdr-recovery-lock

Conversation

@cloud-practitioner

@cloud-practitioner cloud-practitioner commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

Intent

Closes #6454.
On the Herdr runtime, when two session recoveries run at once, the second one gives up after about 5 seconds with "refusing a concurrent resume" instead of waiting for the first recovery's presentation-order lock.
Recovery now waits for that lock for a bounded time (up to 120 seconds), while ordinary new spawns keep the existing short bounded try and flat-layout fallback.

Why the wait is bounded and recovery-only

A clean projected create can fall back to the flat layout when the session lock is contended, so its five-second try stays as is.
Recovery has no such fallback, so a holder that keeps the lock past five seconds (for example a concurrent spawn on a loaded runner) made recovery refuse purely on timing.
The wait is deliberately bounded by an elapsed-time deadline rather than unbounded: a holder that keeps the lock for the whole 120 seconds is treated as stuck, and recovery refuses clearly before any Herdr mutation.
It is also not a new default for other paths: only recovery of an existing presentation journal waits longer, so the earlier unbounded-wait attempts (#5405, #5962) and the opt-in flag proposed in #5989 address a different trade-off, and this change does not add a flag.

Test note

tests/fm-backend-herdr-presentation-e2e.test.sh has a pre-existing failure in the duplicate-token agent check when run against Herdr 0.9.3 (CI pins an older Herdr); it is unrelated to this change, which does not touch that code.

What Changed

Risk Assessment

✅ Low: The change is narrowly scoped, enforces the recovery deadline at acquisition, retains ordinary spawn fallback, and preserves identity checks and mutation safeguards after waiting.

Testing

After correcting disposable-driver setup issues, real Herdr/Treehouse checks passed for concurrent recovery, ordinary fallback, the production deadline, and expired acquisition refusal. The baseline failure was reproduced; CLI/API transcripts and timing evidence were saved. Labs were torn down. No full suite, lint, or delivery phase ran; screenshots were unnecessary for this CLI wait-policy change.

  • Live validation: ✅ go - 4 of 4 scenarios driven live against the product
Scenario Result Live Evidence
Two cross-home recoveries wait for the presentation lock and both resume ✅ pass live live-lock-transcript.log, bravo-acquisition-attempts.log, serialized-replacement-events.log; baseline reproduction in regression/live-lock-transcript.log
A stuck lock holder causes bounded recovery refusal without mutation ✅ pass live live-lock-transcript.log, stuck-holder.err, stuck-acquisition-attempts.log
An ordinary new spawn keeps its short wait and flat-layout fallback ✅ pass live live-lock-transcript.log, ordinary-new.out, ordinary-new.err, ordinary-acquisition-attempts.log
Releasing the holder after the deadline cannot authorize an expired recovery ✅ pass live regression/live-lock-transcript.log, regression/late-acquisition.err
Evidence: Live-validation results, isolation, and cleanup

Source: Live-validation results, isolation, and cleanup

# Herdr recovery-lock live validation

Target: `44eef00296b31462f361625f9b3898dbc7b2b39e`
Baseline: `1f3e769616fdf9f31f85f4c3e6a9f71606634238`
Runtime: existing Herdr 0.9.3 / protocol 22 and real Treehouse.

## Results

| User action | Observed product behavior | Evidence |
| --- | --- | --- |
| Start an ordinary new worker while another process holds the session presentation lock | Acquisition attempts spanned 4.524 seconds, then the public spawn command succeeded with the flat-layout warning. Metadata pointed to the existing primary workspace, no presentation journal was created, and focus stayed unchanged. | `ordinary-new.out`, `ordinary-new.err`, `ordinary-acquisition-attempts.log`, `live-lock-transcript.log` |
| Recover behind a holder that never releases | With the **unmodified production 120-second budget**, recovery refused 119.663 seconds after the first observed attempt. Metadata remained byte-identical, the recorded real Herdr operations contained no create/close/focus/move mutation, and focus was unchanged. | `stuck-holder.err`, `stuck-acquisition-attempts.log`, `live-lock-transcript.log` |
| Recover two workers from different homes after a real named-session restart | The second recovery remained pending for more than eight seconds after reaching acquisition, made 61 attempts (exceeding the former 50-attempt limit), and did not reach replacement creation while the first held the lock. After release, both public spawn commands succeeded, retained their exact workspace identities, replaced their own panes, removed the old husks, and preserved focus. | `bravo-acquisition-attempts.log`, `serialized-replacement-events.log`, `primary-resume.out`, `bravo-resume.out`, endpoint API responses in `live-lock-transcript.log` |
| Release a holder only after the acquisition deadline has elapsed | A six-second delayed acquisition operation used a shortened five-second recovery budget. The holder was released 5.4 seconds after the attempt began; recovery still refused, did not mutate Herdr or metadata, and left no acquired lock behind. | `regression/late-acquisition.err`, `regression/live-lock-transcript.log` |

## Before/after regression

The baseline's unmodified `bin/fm-spawn.sh` was materialized as a disposable executable beside the existing scripts, without changing tracked source or using a fleet bypass. Under the same synchronized real-session contention, the baseline second recovery exited with `refusing a concurrent resume` before the first holder was released. The target remained pending under that contention and completed both recoveries successfully.

See `regression/bravo-resume.err` and `regression/live-lock-transcript.log`. The disposable baseline executable was removed afterward.

## Isolation and cleanup

- All fixture homes were minted with `bin/fm-lab-home.sh create`; lifecycle calls used those marked homes, no `FM_*_OVERRIDE` relocation, and no gate-refusal bypass.
- Herdr lifecycle and API calls went through `bin/fm-herdr-lab.sh` with generated non-default `fm-lab-lock6454-*` names. Provision performed its own prepare contract; teardown succeeded and verified the default-session tripwire.
- Fixture homes, shell configuration, Herdr session data, git projects, origins, and Treehouse pools were all workspace-local. A short `/proc/<driver-pid>/fd/9` directory alias avoided the Unix socket pathname limit. The real workspace mover used that same real lab socket through a path-shortening adapter.
- The helper's required default-session tripwire used a read-only socket symlink; no pane or lifecycle operation was directed at the default session.
- The idle-shell workers exercised the actual public Firstmate spawn interface; no harness CLI, authentication, Herdr response, or Treehouse response was faked. Instrumentation only logged real calls, coordinated contention, and delayed the explicitly adversarial acquisition operation.
- Initial disposable-driver setup problems (duplicate prepare, shell selection, an overlong interactive pool lock, and a stale release marker) were corrected and scenarios re-driven. Their transcripts remain in `setup-attempt-*.log` for transparency; they were not product failures.
- Both completed proof labs were torn down in their evidence turn. All transient worktree files, pools, and the baseline executable were removed; the worktree has no test-created changes.

Commands: `python3 .gate6454/live.py` and `python3 .gate6454/regression.py`. Exact disposable drivers are retained as `live-driver.py` and `regression-driver.py` alongside this report; product CLI/API output is in the transcripts. No full repository suite, lint, formatter, static analysis, push, PR, or CI phase was run. This is a CLI wait-policy change, not a rendered UI change; CLI output, timing, endpoint state, and mutation audit are the relevant evidence rather than screenshots.
Evidence: Target product CLI and API transcript

Source: Target product CLI and API transcript

$ ~/.no-mistakes/worktrees/c98b859efde5/01M410E7F7G38F8N1PVBSA2B64/bin/fm-herdr-lab.sh name lock6454
fm-lab-lock6454-1889621-19434
$ ~/.no-mistakes/worktrees/c98b859efde5/01M410E7F7G38F8N1PVBSA2B64/bin/fm-herdr-lab.sh provision fm-lab-lock6454-1889621-19434
$ ~/.no-mistakes/worktrees/c98b859efde5/01M410E7F7G38F8N1PVBSA2B64/bin/fm-herdr-lab.sh run fm-lab-lock6454-1889621-19434 status --json
{"client":{"version":"0.9.3","channel":"stable","protocol":22,"endpoint_protocol_generation":1,"endpoint_capabilities":["surface_interest","presentation_effects_fence","health_check"],"remote_host_bridge":true,"remote_bridge_idle_timeout":true,"binary":"~/.local/bin/herdr","session":"fm-lab-lock6454-1889621-19434"},"server":{"status":"running","running":true,"version":"0.9.3","protocol":22,"capabilities":{"live_handoff":true,"detached_server_daemon":false,"endpoint_protocol_generation":1,"surface_interest":true,"health_check":true,"ssh_agent_registration":true},"compatible":true,"endpoint_compatible":true,"socket":"/proc/1889588/fd/9/herdr/sessions/fm-lab-lock6454-1889621-19434/herdr.sock","session":"fm-lab-lock6454-1889621-19434","restart_needed":false,"server_binary_stale":false},"update":{"restart_needed":false,"server_binary_stale":false}}
$ ~/.no-mistakes/worktrees/c98b859efde5/01M410E7F7G38F8N1PVBSA2B64/bin/fm-herdr-lab.sh stop fm-lab-lock6454-1889621-19434
{"session":{"default":false,"name":"fm-lab-lock6454-1889621-19434","running":false,"session_dir":"/proc/1889588/fd/9/herdr/sessions/fm-lab-lock6454-1889621-19434","socket_path":"/proc/1889588/fd/9/herdr/sessions/fm-lab-lock6454-1889621-19434/herdr.sock"},"stopped":true}
$ ~/.no-mistakes/worktrees/c98b859efde5/01M410E7F7G38F8N1PVBSA2B64/bin/fm-herdr-lab.sh provision fm-lab-lock6454-1889621-19434
$ ~/.no-mistakes/worktrees/c98b859efde5/01M410E7F7G38F8N1PVBSA2B64/bin/fm-lab-home.sh create ~/.no-mistakes/worktrees/c98b859efde5/01M410E7F7G38F8N1PVBSA2B64/.gate6454/home-primary
~/.no-mistakes/worktrees/c98b859efde5/01M410E7F7G38F8N1PVBSA2B64/.gate6454/home-primary
$ ~/.no-mistakes/worktrees/c98b859efde5/01M410E7F7G38F8N1PVBSA2B64/bin/fm-lab-home.sh create ~/.no-mistakes/worktrees/c98b859efde5/01M410E7F7G38F8N1PVBSA2B64/.gate6454/home-bravo
~/.no-mistakes/worktrees/c98b859efde5/01M410E7F7G38F8N1PVBSA2B64/.gate6454/home-bravo
$ git -C ~/.no-mistakes/worktrees/c98b859efde5/01M410E7F7G38F8N1PVBSA2B64/.gate6454/project init -q -b main
$ git -C ~/.no-mistakes/worktrees/c98b859efde5/01M410E7F7G38F8N1PVBSA2B64/.gate6454/project add README.md
$ git -C ~/.no-mistakes/worktrees/c98b859efde5/01M410E7F7G38F8N1PVBSA2B64/.gate6454/project -c user.name=Firstmate Tests -c user.email=tests@example.invalid commit -qm initial
$ git clone --quiet --bare ~/.no-mistakes/worktrees/c98b859efde5/01M410E7F7G38F8N1PVBSA2B64/.gate6454/project ~/.no-mistakes/worktrees/c98b859efde5/01M410E7F7G38F8N1PVBSA2B64/.gate6454/project.origin.git
$ git -C ~/.no-mistakes/worktrees/c98b859efde5/01M410E7F7G38F8N1PVBSA2B64/.gate6454/project remote add origin file://~/.no-mistakes/worktrees/c98b859efde5/01M410E7F7G38F8N1PVBSA2B64/.gate6454/project.origin.git
$ ~/.no-mistakes/worktrees/c98b859efde5/01M410E7F7G38F8N1PVBSA2B64/bin/fm-herdr-lab.sh run fm-lab-lock6454-1889621-19434 workspace create --cwd ~/.no-mistakes/worktrees/c98b859efde5/01M410E7F7G38F8N1PVBSA2B64/.gate6454/project --label firstmate --no-focus
{"id":"cli:workspace:create","result":{"root_pane":{"agent_status":"unknown","cwd":"~/.no-mistakes/worktrees/c98b859efde5/01M410E7F7G38F8N1PVBSA2B64/.gate6454/project","focused":true,"foreground_cwd":"~/.no-mistakes/worktrees/c98b859efde5/01M410E7F7G38F8N1PVBSA2B64/.gate6454/project","pane_id":"w1:p1","revision":0,"scroll":{"max_offset_from_bottom":0,"offset_from_bottom":0,"viewport_rows":40},"tab_id":"w1:t1","terminal_id":"term_65cf090884dbe1","workspace_id":"w1"},"tab":{"agent_status":"unknown","focused":true,"label":"1","number":1,"pane_count":1,"tab_id":"w1:t1","workspace_id":"w1"},"type":"workspace_created","workspace":{"active_tab_id":"w1:t1","agent_status":"unknown","focused":true,"label":"firstmate","number":1,"pane_count":1,"tab_count":1,"workspace_id":"w1"}}}
$ ~/.no-mistakes/worktrees/c98b859efde5/01M410E7F7G38F8N1PVBSA2B64/bin/fm-herdr-lab.sh run fm-lab-lock6454-1889621-19434 workspace create --cwd ~/.no-mistakes/worktrees/c98b859efde5/01M410E7F7G38F8N1PVBSA2B64/.gate6454/project --label 2ndmate-bravo --focus
{"id":"cli:workspace:create","result":{"root_pane":{"agent_status":"unknown","cwd":"~/.no-mistakes/worktrees/c98b859efde5/01M410E7F7G38F8N1PVBSA2B64/.gate6454/project","focused":true,"foreground_cwd":"~/.no-mistakes/worktrees/c98b859efde5/01M410E7F7G38F8N1PVBSA2B64/.gate6454/project","pane_id":"w2:p1","revision":0,"scroll":{"max_offset_from_bottom":0,"offset_from_bottom":0,"viewport_rows":40},"tab_id":"w2:t1","terminal_id":"term_65cf090887f3e2","workspace_id":"w2"},"tab":{"agent_status":"unknown","focused":true,"label":"1","number":1,"pane_count":1,"tab_id":"w2:t1","workspace_id":"w2"},"type":"workspace_created","workspace":{"active_tab_id":"w2:t1","agent_status":"unknown","focused":true,"label":"2ndmate-bravo","number":2,"pane_count":1,"tab_count":1,"workspace_id":"w2"}}}
$ FM_HOME=~/.no-mistakes/worktrees/c98b859efde5/01M410E7F7G38F8N1PVBSA2B64/.gate6454/home-primary bin/fm-spawn.sh primary-recover <disposable-project> <idle-shell> --mode no-mistakes --yolo off --backend herdr
primary-initial exit=0
spawned primary-recover harness=sh kind=ship mode=no-mistakes yolo=off window=fm-lab-lock6454-1889621-19434:w3:p2 worktree=~/.no-mistakes/worktrees/c98b859efde5/01M410E7F7G38F8N1PVBSA2B64/.gate6454/treehouse/.treehouse/project-708f13/1/project
fm-gate-refuse: gate agent lifecycle permitted only against lab home ~/.no-mistakes/worktrees/c98b859efde5/01M410E7F7G38F8N1PVBSA2B64/.gate6454/home-primary
warning: ~/.no-mistakes/worktrees/c98b859efde5/01M410E7F7G38F8N1PVBSA2B64/.gate6454/home-primary/data/primary-recover/launch-brief.md records no ship branch; defaulting to legacy branch fm/primary-recover
warning: ~/.no-mistakes/worktrees/c98b859efde5/01M410E7F7G38F8N1PVBSA2B64/.gate6454/home-primary/data/primary-recover/launch-brief.md records no delivery contract line (scaffolded before ship briefs recorded one); launching on the explicit --mode no-mistakes - confirm its definition of done matches
$ FM_HOME=~/.no-mistakes/worktrees/c98b859efde5/01M410E7F7G38F8N1PVBSA2B64/.gate6454/home-bravo bin/fm-spawn.sh bravo-recover <disposable-project> <idle-shell> --mode no-mistakes --yolo off --backend herdr
bravo-initial exit=0
spawned bravo-recover harness=sh kind=ship mode=no-mistakes yolo=off window=fm-lab-lock6454-1889621-19434:w4:p2 worktree=~/.no-mistakes/worktrees/c98b859efde5/01M410E7F7G38F8N1PVBSA2B64/.gate6454/treehouse/.treehouse/project-708f13/2/project
fm-gate-refuse: gate agent lifecycle permitted only against lab home ~/.no-mistakes/worktrees/c98b859efde5/01M410E7F7G38F8N1PVBSA2B64/.gate6454/home-bravo
warning: ~/.no-mistakes/worktrees/c98b859efde5/01M410E7F7G38F8N1PVBSA2B64/.gate6454/home-bravo/data/bravo-recover/launch-brief.md records no ship branch; defaulting to legacy branch fm/bravo-recover
warning: ~/.no-mistakes/worktrees/c98b859efde5/01M410E7F7G38F8N1PVBSA2B64/.gate6454/home-bravo/data/bravo-recover/launch-brief.md records no delivery contract line (scaffolded before ship briefs recorded one); launching on the explicit --mode no-mistakes - confirm its definition of done matches
$ bash -c . "$1/bin/backends/herdr.sh"; fm_backend_herdr_presentation_session_lock_path "$2" bash ~/.no-mistakes/worktrees/c98b859efde5/01M410E7F7G38F8N1PVBSA2B64 fm-lab-lock6454-1889621-19434
/tmp/firstmate-herdr-presentation/order-c7224ec8c52fa61b4076f4afd62aa20b.lock
$ ~/.no-mistakes/worktrees/c98b859efde5/01M410E7F7G38F8N1PVBSA2B64/bin/fm-herdr-lab.sh run fm-lab-lock6454-1889621-19434 worksp

... [10567 bytes truncated] ...

pane:get","result":{"pane":{"agent_status":"unknown","cwd":"~/.no-mistakes/worktrees/c98b859efde5/01M410E7F7G38F8N1PVBSA2B64/.gate6454/project","focused":false,"foreground_cwd":"~/.no-mistakes/worktrees/c98b859efde5/01M410E7F7G38F8N1PVBSA2B64/.gate6454/treehouse/.treehouse/project-708f13/2/project","pane_id":"w4:p3","revision":0,"scroll":{"max_offset_from_bottom":0,"offset_from_bottom":0,"viewport_rows":40},"tab_id":"w4:t3","terminal_id":"term_65cf09a9366b67","workspace_id":"w4"},"type":"pane_info"}}
$ ~/.no-mistakes/worktrees/c98b859efde5/01M410E7F7G38F8N1PVBSA2B64/bin/fm-herdr-lab.sh run fm-lab-lock6454-1889621-19434 workspace list
{"id":"cli:workspace:list","result":{"type":"workspace_list","workspaces":[{"active_tab_id":"w1:t1","agent_status":"unknown","focused":false,"label":"firstmate","number":1,"pane_count":2,"tab_count":2,"workspace_id":"w1"},{"active_tab_id":"w3:t3","agent_status":"unknown","focused":false,"label":"└ primary-recover · p:_nOMi6MnrWUpuAC3CAO2Yg","number":2,"pane_count":1,"tab_count":1,"workspace_id":"w3"},{"active_tab_id":"w2:t1","agent_status":"unknown","focused":true,"label":"2ndmate-bravo","number":3,"pane_count":1,"tab_count":1,"workspace_id":"w2"},{"active_tab_id":"w4:t3","agent_status":"unknown","focused":false,"label":"└ bravo-recover · p:sqYi08oh7mIwEhBWWfYGPw","number":4,"pane_count":1,"tab_count":1,"workspace_id":"w4"}]}}
CONCURRENT RECOVERY: both public spawn commands succeeded; exact workspace identities retained; old husks absent; new panes present; focus unchanged.
$ ~/.no-mistakes/worktrees/c98b859efde5/01M410E7F7G38F8N1PVBSA2B64/bin/fm-teardown.sh ordinary-new --force
A new version of treehouse is available: v3.1.1 → v3.1.2
Run "treehouse update" to update

🌳 Worktree returned to pool.
~/.no-mistakes/worktrees/c98b859efde5/01M410E7F7G38F8N1PVBSA2B64/.gate6454/project: already current
teardown ordinary-new complete (window fm-lab-lock6454-1889621-19434:w1:p2, worktree ~/.no-mistakes/worktrees/c98b859efde5/01M410E7F7G38F8N1PVBSA2B64/.gate6454/treehouse/.treehouse/project-708f13/3/project)
Backlog: ordinary-new just finished (this home keeps no markdown backlog at ~/.no-mistakes/worktrees/c98b859efde5/01M410E7F7G38F8N1PVBSA2B64/.gate6454/home-primary/data/backlog.md). Update ~/.no-mistakes/worktrees/c98b859efde5/01M410E7F7G38F8N1PVBSA2B64/.gate6454/home-primary/data/backlog.md - move ordinary-new to Done, keep Done to the 10 most recent, then re-scan Queued and dispatch only work whose blockers are gone and date is due.
fm-gate-refuse: gate agent lifecycle permitted only against lab home ~/.no-mistakes/worktrees/c98b859efde5/01M410E7F7G38F8N1PVBSA2B64/.gate6454/home-primary
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
●  WATCHER DOWN - SUPERVISION IS OFF
●  2 task(s) in flight, but no live watcher process holds this home lock (last beat: 176s ago).
●  Trust the emitted supervision protocol for this harness; do not use shell & for watcher repair.
●  This is a supervision warning only; the guarded operation WILL still run.
●  repair a missing or failed watcher cycle with the Pi tool fm_watch_arm_pi, or restart Pi with -e ~/.no-mistakes/worktrees/c98b859efde5/01M410E7F7G38F8N1PVBSA2B64/.pi/extensions/fm-primary-turnend-guard.ts -e ~/.no-mistakes/worktrees/c98b859efde5/01M410E7F7G38F8N1PVBSA2B64/.pi/extensions/fm-primary-pi-watch.ts if the extensions are not loaded.
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
WARNING: watcher still down (same stale episode; last beat: 180s ago, grace 300s) - full banner already printed this episode.
$ ~/.no-mistakes/worktrees/c98b859efde5/01M410E7F7G38F8N1PVBSA2B64/bin/fm-teardown.sh primary-recover --force
A new version of treehouse is available: v3.1.1 → v3.1.2
Run "treehouse update" to update

🌳 Worktree returned to pool.
~/.no-mistakes/worktrees/c98b859efde5/01M410E7F7G38F8N1PVBSA2B64/.gate6454/project: already current
teardown primary-recover complete (window fm-lab-lock6454-1889621-19434:w3:p3, worktree ~/.no-mistakes/worktrees/c98b859efde5/01M410E7F7G38F8N1PVBSA2B64/.gate6454/treehouse/.treehouse/project-708f13/1/project)
Backlog: primary-recover just finished (this home keeps no markdown backlog at ~/.no-mistakes/worktrees/c98b859efde5/01M410E7F7G38F8N1PVBSA2B64/.gate6454/home-primary/data/backlog.md). Update ~/.no-mistakes/worktrees/c98b859efde5/01M410E7F7G38F8N1PVBSA2B64/.gate6454/home-primary/data/backlog.md - move primary-recover to Done, keep Done to the 10 most recent, then re-scan Queued and dispatch only work whose blockers are gone and date is due.
fm-gate-refuse: gate agent lifecycle permitted only against lab home ~/.no-mistakes/worktrees/c98b859efde5/01M410E7F7G38F8N1PVBSA2B64/.gate6454/home-primary
WARNING: watcher still down (same stale episode; last beat: 182s ago, grace 300s) - full banner already printed this episode.
teardown: reaping leaked worktree process(es) for primary-recover: 2040145 2046892 2046896 2062642
teardown: force-killing leaked worktree process(es) for primary-recover: 2040145
$ ~/.no-mistakes/worktrees/c98b859efde5/01M410E7F7G38F8N1PVBSA2B64/bin/fm-teardown.sh bravo-recover --force
A new version of treehouse is available: v3.1.1 → v3.1.2
Run "treehouse update" to update

🌳 Worktree returned to pool.
~/.no-mistakes/worktrees/c98b859efde5/01M410E7F7G38F8N1PVBSA2B64/.gate6454/project: already current
teardown bravo-recover complete (window fm-lab-lock6454-1889621-19434:w4:p3, worktree ~/.no-mistakes/worktrees/c98b859efde5/01M410E7F7G38F8N1PVBSA2B64/.gate6454/treehouse/.treehouse/project-708f13/2/project)
Backlog: bravo-recover just finished (this home keeps no markdown backlog at ~/.no-mistakes/worktrees/c98b859efde5/01M410E7F7G38F8N1PVBSA2B64/.gate6454/home-bravo/data/backlog.md). Update ~/.no-mistakes/worktrees/c98b859efde5/01M410E7F7G38F8N1PVBSA2B64/.gate6454/home-bravo/data/backlog.md - move bravo-recover to Done, keep Done to the 10 most recent, then re-scan Queued and dispatch only work whose blockers are gone and date is due.
fm-gate-refuse: gate agent lifecycle permitted only against lab home ~/.no-mistakes/worktrees/c98b859efde5/01M410E7F7G38F8N1PVBSA2B64/.gate6454/home-bravo
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
●  WATCHER DOWN - SUPERVISION IS OFF
●  1 task(s) in flight, but no live watcher process holds this home lock (last beat: 191s ago).
●  Trust the emitted supervision protocol for this harness; do not use shell & for watcher repair.
●  This is a supervision warning only; the guarded operation WILL still run.
●  repair a missing or failed watcher cycle with the Pi tool fm_watch_arm_pi, or restart Pi with -e ~/.no-mistakes/worktrees/c98b859efde5/01M410E7F7G38F8N1PVBSA2B64/.pi/extensions/fm-primary-turnend-guard.ts -e ~/.no-mistakes/worktrees/c98b859efde5/01M410E7F7G38F8N1PVBSA2B64/.pi/extensions/fm-primary-pi-watch.ts if the extensions are not loaded.
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
teardown: reaping leaked worktree process(es) for bravo-recover: 2050420 2055441 2055443 2071958
teardown: force-killing leaked worktree process(es) for bravo-recover: 2050420
$ ~/.no-mistakes/worktrees/c98b859efde5/01M410E7F7G38F8N1PVBSA2B64/bin/fm-herdr-lab.sh teardown fm-lab-lock6454-1889621-19434
LAB TEARDOWN: status=0 (includes default-session tripwire verification)
Evidence: Serialized recovery replacement events

Source: Serialized recovery replacement events

fm-primary-recover start 1791038297
fm-primary-recover end 1791038306
fm-bravo-recover start 1791038315
fm-bravo-recover end 1791038315
Evidence: Second recovery acquisition attempts

Source: Second recovery acquisition attempts

1791038298.428718973
1791038298.551059111
1791038298.672451609
1791038298.795925496
1791038298.917689851
1791038299.040799801
1791038299.162627204
1791038299.281797794
1791038299.398069183
1791038299.517661375
1791038299.635868184
1791038299.753774777
1791038299.870195142
1791038299.984857888
1791038300.099589142
1791038300.220183859
1791038300.338032232
1791038300.455828132
1791038300.572821318
1791038300.694558825
1791038300.821485539
1791038300.952170570
1791038301.079470869
1791038301.201972652
1791038301.322262092
1791038301.447966545
1791038301.575903178
1791038301.713293318
1791038301.865382475
1791038302.052457143
1791038302.208223365
1791038302.371130117
1791038302.551584556
1791038302.717381160
1791038302.869979248
1791038303.031153769
1791038303.185976468
1791038303.328657769
1791038303.490208461
1791038303.649618521
1791038303.786802083
1791038303.922178434
1791038304.048355239
1791038304.170159852
1791038304.298241708
1791038304.447717891
1791038304.583612765
1791038304.703253732
1791038304.823924645
1791038304.946051480
1791038305.078030204
1791038305.203855793
1791038305.332015828
1791038305.463603944
1791038305.591703055
1791038305.719463628
1791038305.847650318
1791038305.973595633
1791038306.096263202
1791038306.220495366
1791038306.346047190
1791038306.467024195
1791038306.601532146
1791038306.729006775
1791038306.855206873
1791038306.988869696
1791038307.113024502
1791038307.243154181
1791038307.367783342
1791038307.489416832
1791038307.610342594
1791038307.731912051
1791038307.853619373
1791038307.985973159
1791038308.116805620
1791038308.238392667
1791038308.357608546
1791038308.477083895
1791038308.596364505
1791038308.714875430
1791038308.833728667
1791038308.955147291
1791038309.075552624
1791038309.196789049
1791038309.316320321
1791038309.434204666
1791038309.550885805
1791038309.667748053
1791038309.784818828
1791038309.906114609
1791038310.027263985
1791038310.152235638
1791038310.278663879
1791038310.407873123
1791038310.539787053
1791038310.664392397
1791038310.794967586
1791038310.928455439
1791038311.075294774
1791038311.197549102
1791038311.320610941
1791038311.454411089
1791038311.579667704
1791038311.704562480
1791038311.843636888
1791038311.993909128
1791038312.120901403
1791038312.241842943
1791038312.363976412
1791038312.486772084
1791038312.624414854
1791038312.763223362
1791038312.908687455
1791038313.060568355
1791038313.211167270
1791038313.349991642
1791038313.484418471
1791038313.611721157
1791038313.742766110
1791038313.875434981

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 2 issues found → auto-fixed (2) ✅
  • 🚨 bin/fm-spawn.sh:1401 - The intent requires a bounded wait "up to 120 seconds", but limit=$(( ${2:-5} * 10 )) bounds sleep iterations, not elapsed time. With a stuck live holder, recovery performs 1,200 sleeps totaling 120 seconds plus 1,201 lock attempts, including filesystem operations and subprocesses. On the loaded runners this fix targets, it can therefore wait materially longer and even acquire a lock released after the promised deadline. Related changed sites: bin/fm-spawn.sh:1405-1408, iteration-based timeout; bin/fm-spawn.sh:3665-3666, recovery call and claimed deadline; docs/herdr-backend.md:366-367, upper-bound guarantee. Correct the recovery path to enforce an elapsed-time deadline at the acquisition boundary while preserving ordinary spawns' existing short try/fallback.
  • ⚠️ tests/fm-backend-herdr-presentation-e2e.test.sh:1390 - The regression can pass without exercising the formerly failing wait. If the second recovery is delayed during the serialized Treehouse calls or other preparation until the first releases the presentation lock, the original five-second implementation succeeds too. Both timing assertions pass: the tab creates are non-overlapping and the second starts at least eight seconds after suite launch. Related changed sites: tests/fm-backend-herdr-presentation-e2e.test.sh:153 and :161, tab-create timestamps; :1377-1380, unsynchronized recovery launches; :1388-1390, ordering and elapsed-time assertions. Coordinate the slow holder with the second recovery reaching lock acquisition, then verify that recovery remains pending beyond the former budget before releasing the holder.

🔧 Fix applied.
2 warnings still open:

  • ⚠️ tests/fm-backend-herdr-presentation-e2e.test.sh:1390 - The regression can pass without exercising the formerly failing wait. If the second recovery is delayed during the serialized Treehouse calls or other preparation until the first releases the presentation lock, the original five-second implementation succeeds too. Both timing assertions pass: the tab creates are non-overlapping and the second starts at least eight seconds after suite launch. Related changed sites: tests/fm-backend-herdr-presentation-e2e.test.sh:153 and :161, tab-create timestamps; :1377-1380, unsynchronized recovery launches; :1388-1390, ordering and elapsed-time assertions. Coordinate the slow holder with the second recovery reaching lock acquisition, then verify that recovery remains pending beyond the former budget before releasing the holder.
  • ⚠️ tests/fm-backend-herdr-presentation-e2e.test.sh:1380 - Round 1’s deadline fix introduces a false failure in the stuck-holder regression. The one-second budget at :1366 now starts before lock-path resolution (bin/fm-spawn.sh:1399-1403). If the real session-list, hashing, and filesystem work crosses the next SECONDS tick, acquisition correctly refuses without attempting the lock. The mktemp wrapper at tests/fm-backend-herdr-presentation-e2e.test.sh:277 therefore writes no attempt log, but :1378-1380 require nonempty first/last timestamps and fail despite correct bounded refusal. This is especially reachable on the loaded runners targeted by the fix. Give the shortened timeout and injected delay enough headroom for real resolution, and handle expiry before the first attempt without interpreting a missing log as a timeout overrun.

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

✅ **Test** - passed

✅ No issues found.

  • Live validation: ✅ go - 4 of 4 scenarios driven live against the product
Scenario Result Live Evidence
Two cross-home recoveries wait for the presentation lock and both resume ✅ pass live live-lock-transcript.log, bravo-acquisition-attempts.log, serialized-replacement-events.log; baseline reproduction in regression/live-lock-transcript.log
A stuck lock holder causes bounded recovery refusal without mutation ✅ pass live live-lock-transcript.log, stuck-holder.err, stuck-acquisition-attempts.log
An ordinary new spawn keeps its short wait and flat-layout fallback ✅ pass live live-lock-transcript.log, ordinary-new.out, ordinary-new.err, ordinary-acquisition-attempts.log
Releasing the holder after the deadline cannot authorize an expired recovery ✅ pass live regression/live-lock-transcript.log, regression/late-acquisition.err
  • python3 .gate6454/live.py: real Herdr/Treehouse checks of ordinary-spawn fallback, the production 120-second recovery deadline, and synchronized cross-home recovery.
  • python3 .gate6454/regression.py: late-acquisition refusal and reproduction using the unmodified baseline fm-spawn.sh.
  • bin/fm-spawn.sh primary-recover &lt;disposable-project&gt; &#34;sh -c &#39;while :; do sleep 60; done&#39;&#34; --mode no-mistakes --yolo off --backend herdr, with equivalent launches for bravo-recover and ordinary-new, against marked lab homes.
  • Inspected real Herdr workspace and pane API responses, acquisition timestamps, mutation logs, metadata identity, and preserved focus.
  • Ran guarded bin/fm-herdr-lab.sh provision, restart, and teardown operations; teardown verified the default-session tripwire.
  • Corrected disposable-driver setup issues and re-drove the affected checks; removed transient fixtures and the baseline executable, then confirmed a clean worktree.
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

Herdr presentation recovery took the session-scoped presentation-order lock
with the same five-second bounded try that clean projected creates use.
A create can fall back to the flat layout when the lock is contended, but
recovery has no fallback, so a concurrent spawn or recovery that held the
lock past five seconds made it refuse with "refusing a concurrent resume".
On a loaded runner that hold can exceed five seconds, so the outcome
depended only on machine speed.

spawn_herdr_presentation_order_lock_acquire now takes an optional wait in
seconds (default 5, unchanged for ordinary spawns and the abort path).
Recovery waits up to 120 seconds, still bounded so a stuck holder produces
a clear refusal before any Herdr mutation.
FM_TEST_HERDR_RECOVERY_LOCK_WAIT shortens that bound for tests.

The real-Herdr presentation e2e now stretches each concurrent recovery's
husk replacement past the former budget and asserts the recoveries
serialize, and asserts a stuck holder is refused without mutating Herdr.

Closes kunchenguid#6454
@greptile-apps

greptile-apps Bot commented Oct 3, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5

[High risk] Changes lock-wait behavior in session recovery startup.

The PR appears safe to merge based on the reviewed changes.

Reviews (2) · Last reviewed commit: "no-mistakes(ci): Fixed ci-1 in tests/fm-..."

Comment thread tests/fm-backend-herdr-presentation-e2e.test.sh
…e.test.sh by canonicalizing the expected presentation-lock path at its shared producer using fm_lock_abs_path(), matching the resolver used before mktemp. This fixes attempt logging for both recovery tests when /tmp is symlinked, without changing production behavior. The behavioral probe failed before the fix and passed afterward; physical and symlinked paths passed acquisition, contention, reacquisition, and unrelated-mktemp checks. Bash syntax, canonical lint, and git diff --check passed. The real Herdr E2E run stopped at its safety preflight because the isolated environment lacks the required running default session, so full E2E verification remains blocked
@kunchenguid

Copy link
Copy Markdown
Owner

Speaking as Kun's firstmate: RELOOK vs tip 1f3e7696. Diff reviewed (Herdr presentation-lock wait for recovery up to 120s; new spawns keep short try + flat fallback). Contract-class: restore — concrete concurrent secondmate recovery path already promised to resume; the 5s give-up under a longer lock holder is the broken path (#6454). No new default-on captain surface. No security risk in the tip.

VISION (per rule):

  • One captain, one interface — aligns: flaky recovery refusals should not become captain noise.
  • Authority is explicit — aligns: wait is bounded and recovery-only; no silent widen of spawn policy.
  • Scripts own the mechanics — aligns: spawn owns the lock try/wait.
  • A restart is a non-event — aligns: recovery must complete under concurrent spawn load.
  • Delegation with a spine — aligns: secondmate recoverability is the spine.
  • The fleet outlives any vendor — aligns: Herdr session-lock timing is a backend surprise to bound.
  • Scope — aligns: Herdr backend spawn/recovery + docs/tests.

Blockers: Fork CI + Require no-mistakes approved this pass (37131844769 / 37133863007 / 37131844764) — waiting on those checks. Attestation MATCH 8b7dcd63. Greptile Review SUCCESS (prior P1 on lock-path canonicalize resolved; no open threads). Not escalating.

Does close #6454 when CI is green.

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.

Herdr presentation recovery gives up after 5s while a concurrent spawn holds the session lock longer (flaky concurrent secondmate recovery)

2 participants