Skip to content

fix: reject stale Herdr endpoints after session resets - #6760

Open
cloud-practitioner wants to merge 17 commits into
kunchenguid:mainfrom
cloud-practitioner:fm/fm-up-herdr-stale-endpoint
Open

cloud-practitioner wants to merge 17 commits into
kunchenguid:mainfrom
cloud-practitioner:fm/fm-up-herdr-stale-endpoint

Conversation

@cloud-practitioner

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

Copy link
Copy Markdown
Contributor

Refs #6167: this identity-checks bindings and reads a pane that belongs to someone else as missing, but it adds no explicit refusal keyed on the supervisor's own pane id and does not address the briefly-dead just-spawned endpoint.

Intent

Reject stale Herdr endpoints after a Herdr session reset, using a stable pane process identity, as one pull request.

Reproduction: after a Herdr server restart, or a container rebuild that does not persist Herdr's session state, Herdr starts a fresh session whose workspace and pane ids restart from the beginning (w1, p1, ...), while surviving firstmate task records (the window=, herdr_pane_id=, herdr_workspace_id= and herdr_tab_id= fields of state/*.meta, in the main home and in second-mate homes) still name the old ids.
A recorded id can then alias an unrelated pane in the new session, so liveness, capture, input and cleanup can act on the wrong pane instead of reporting the endpoint dead and relaunching it.

Wanted fix:

  • Record the pane's owning shell process identity at spawn and relaunch, and treat a mismatched process binding as a missing endpoint rather than adopting a recycled pane id, with a best-effort fallback (label and working directory) for records that carry no binding.
  • Apply that ownership check to liveness, recovery, capture, input and cleanup, leaving a pane proven to belong to someone else untouched.
  • Make the identity stable: ps -o lstart is derived from boot time plus starttime/HZ and drifts by a second between calls, so a live agent read through it would look missing and be relaunched as a duplicate. Derive the identity from a stable source (/proc/<pid>/stat start ticks plus the boot id), keep a portable fallback where those are unavailable that tolerates plus or minus one second of drift, and refresh the recorded identity whenever relaunch or restart replaces the pane process.
  • Regression tests for reused pane ids after a reset and for the start-time drift, plus documentation of the recovery behavior.

Persisting Herdr state, other runtime backends, and worker slot leases are out of scope.

What Changed

  • Record and refresh pane-shell process identities using Linux boot IDs and start ticks, with a drift-tolerant portable fallback and best-effort label/cwd checks for unbound records.
  • Revalidate task ownership across liveness, recovery, capture, input, native status events, and cleanup, including secondmate and remote routes, leaving proven foreign panes untouched.
  • Add regression coverage for recycled pane IDs, start-time drift, ownership changes during operations, and remote-state isolation; document recovery and cleanup behavior.

Risk Assessment

⚠️ Medium-high: this adds a default-on ownership check across Herdr liveness, capture, input, and cleanup, and records a process binding on every Herdr spawn and relaunch. Records with no binding keep only a best-effort label and working-directory check. Known limitation: a Herdr reset landing during an in-flight capture, input, or watcher delivery can still race the ownership check; closing those windows needs operation-scoped target pinning and is left for follow-up.

Testing

All three targeted fixture suites completed successfully, covering recycled endpoints, identity drift and refresh, ownership boundaries, remote routing, and the new push-claimant regression. Diagnostic logs and timing data were saved in the evidence directory, but no live-product artifact was captured for the final head. No broad suite, linter, formatter, source edit, or live-fleet operation was performed.

  • Live validation: ⚠️ limited - on an earlier head, one named lab scenario was driven live and passed (a pane id reused after a lab session restart reads missing, and a live pane stays alive under start-time drift); the other scenarios, and the final head, are covered by the targeted deterministic suites against fake Herdr fixtures rather than a live session
Scenario Result Live Evidence
After a session reset, a recycled pane reads missing while a live pane tolerates start-time drift ⏸️ untested no The permitted suite executed production interfaces against fake Herdr and process fixtures, not a running Herdr session. The later recorded test decision prohibits new live scenarios, so no lab restar…
Capture, input, recovery, and cleanup leave a foreign pane untouched across ownership boundaries ⏸️ untested no The targeted suite exercised these operations with disposable fake endpoints and asserted their side effects. It is not live evidence. Additional real-Herdr scenarios are outside the recorded authoriz…
Remote secondmate control uses parent-route ownership rather than ordinary-home records ⏸️ untested no The permitted suite drove the remote-control executable against isolated route records and fake Herdr responses. No real remote endpoint was operated. The recorded no-new-live-scenarios decision withh…
Replacing a legacy dead husk preserves the newly created endpoint ⏸️ untested no The corrected fixture sequence and replacement behavior passed, but the Herdr CLI was mocked. A live husk-replacement scenario was not authorized by the recorded test scope. It requires explicit permi…
Spawn or relaunch publishes the replacement pane's current process identity ⏸️ untested no Production launch paths ran against disposable fake endpoint/process fixtures, not a real launched worker. The recorded decision disallows adding a live launch scenario. Explicit authorization for a t…
A bound worker's blocked alert surfaces even when a stale paused claimant sorts first ⏸️ untested no The new regression executed transition detection and handle_push_transition against fake Herdr state and a fixture event reader. It proves targeted fixture behavior but is not live-product evidence. T…
  • Outcome: ⚠️ 1 warning across 1 run (5m24s)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 1 issue found → auto-fixed ✅
  • 🚨 bin/backends/herdr.sh:4241 - The new stale-first acceptance discards the identity of the matching task before push handling. Concrete sequence: surviving a.meta and newly bound b.meta claim the recycled pane; A's status says paused, while B's agent reports blocked. This loop correctly accepts B's binding, but returns only the original pane record. bin/fm-push-transition-lib.sh:152 then selects A through window_to_task's first-match lookup, and :157–160 absorbs B's alert and commits its dedupe marker because A is paused. Thus the claimed durable fix still silently loses the live worker's blocked alert. Both changed callers need to preserve this invariant: bin/backends/herdr.sh:4352 (reconnect levels) and :4375 (streamed edges); status bookkeeping at bin/fm-push-transition-lib.sh:170 also needs the correct task. Use ownership-aware claimant resolution at the shared push-handling boundary for wait checks and status bookkeeping. Extend tests/fm-herdr-endpoint-boundaries.test.sh:812 through handle_push_transition with stale A paused and bound B blocked; reconcile the guarantee in docs/herdr-backend.md:766.

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

⚠️ **Test** - 1 warning
  • ⚠️ live validation verdict: inconclusive (0 of 6 scenarios were driven live against the product); untested: After a session reset, a recycled pane reads missing while a live pane tolerates start-time drift, Capture, input, recovery, and cleanup leave a foreign pane untouched across ownership boundaries, Remote secondmate control uses parent-route ownership rather than ordinary-home records, Replacing a legacy dead husk preserves the newly created endpoint, Spawn or relaunch publishes the replacement pane's current process identity, A bound worker's blocked alert surfaces even when a stale paused claimant sorts first
  • Live validation: ⚠️ limited - on an earlier head, one named lab scenario was driven live and passed (a pane id reused after a lab session restart reads missing, and a live pane stays alive under start-time drift); the other scenarios, and the final head, are covered by the targeted deterministic suites against fake Herdr fixtures rather than a live session
Scenario Result Live Evidence
After a session reset, a recycled pane reads missing while a live pane tolerates start-time drift ⏸️ untested no The permitted suite executed production interfaces against fake Herdr and process fixtures, not a running Herdr session. The later recorded test decision prohibits new live scenarios, so no lab restar…
Capture, input, recovery, and cleanup leave a foreign pane untouched across ownership boundaries ⏸️ untested no The targeted suite exercised these operations with disposable fake endpoints and asserted their side effects. It is not live evidence. Additional real-Herdr scenarios are outside the recorded authoriz…
Remote secondmate control uses parent-route ownership rather than ordinary-home records ⏸️ untested no The permitted suite drove the remote-control executable against isolated route records and fake Herdr responses. No real remote endpoint was operated. The recorded no-new-live-scenarios decision withh…
Replacing a legacy dead husk preserves the newly created endpoint ⏸️ untested no The corrected fixture sequence and replacement behavior passed, but the Herdr CLI was mocked. A live husk-replacement scenario was not authorized by the recorded test scope. It requires explicit permi…
Spawn or relaunch publishes the replacement pane's current process identity ⏸️ untested no Production launch paths ran against disposable fake endpoint/process fixtures, not a real launched worker. The recorded decision disallows adding a live launch scenario. Explicit authorization for a t…
A bound worker's blocked alert surfaces even when a stale paused claimant sorts first ⏸️ untested no The new regression executed transition detection and handle_push_transition against fake Herdr state and a fixture event reader. It proves targeted fixture behavior but is not live-product evidence. T…
  • bin/fm-test-run.sh --jobs 1 --per-script-timeout-secs 600 --json ~/.no-mistakes/evidence/01M4B51JW2ET2V7MXFW0ZC192C/targeted-tests.json tests/fm-backend-herdr.test.sh tests/fm-herdr-endpoint-boundaries.test.sh tests/fm-remote-secondmate-control.test.sh
  • Verified the endpoint-boundaries suite exercised stale-paused/bound-blocked push handling for reconnect levels and streamed edges, including wakes, dedupe commitment, and correct-task status bookkeeping.
  • Read docs/herdr-backend.md and the bin/fm-herdr-lab.sh safety contract; checked that herdr and jq are on PATH without accessing operator configuration or fleet data.
  • git status --short; git diff --exit-code confirmed the worktree remained clean with no untracked files.
✅ **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.

@greptile-apps

greptile-apps Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 4/5

[High risk] Adds endpoint ownership checks to session recovery and cleanup.

The PR should not merge yet because the previously reported fallback can still delete a worktree used by a foreign pane.

Reviews (2) · Last reviewed commit: "no-mistakes(ci): Fixed the CI causes: ad..." · Reviewed by Greptile

Comment thread bin/fm-teardown.sh
if [ "$child_backend" = herdr ]; then
FM_STATE_OVERRIDE="$sub_state" teardown_herdr_cleanup_preflight "$child_t" "$child_id" "$child_wt" || return 1
fi
if FM_STATE_OVERRIDE="$sub_state" teardown_treehouse_return "$child_wt" "$child_proj" "child worktree" "" "$child_backend" "$child_t" "$child_id"; then

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Fallback deletes a foreign worktree

If Herdr resets during treehouse return and a foreign pane starts using the child’s worktree, a non-lock error falls through to safe_rm_rf_child_worktree without checking ownership again. That helper checks only the path and Git registration before deleting the worktree.

Run the child’s ownership preflight again before either direct-removal fallback, using sub_state, child_t, and child_id.

Knowledge Base Used: Prevent Herdr focus flashes during workspace cleanup

Comment thread bin/backends/herdr.sh Outdated
Comment on lines +4235 to +4237
FM_STATE_OVERRIDE="$state" fm_backend_herdr_endpoint_foreign "$session" "$pane_id" "fm-$task" || ownership=$?
[ "$ownership" -eq 1 ] || return 1
break

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Stale records block new alerts

After a reset, stale a.meta and a new, correctly bound b.meta can both name the recycled pane address. If a.meta sorts first, its mismatch rejects every event before b.meta is checked. The new worker loses immediate blocked alerts, and working events cannot clear the pane’s marker. Polling remains available.

Resolve the current owning record rather than letting the first stale claim disable push updates.

@kunchenguid

Copy link
Copy Markdown
Owner

Speaking as Kun's firstmate: thanks, @cloud-practitioner. This is a careful piece of work on a real hazard. I read the thread and the diff of head 7cea60a3 against main.

What I checked: after a Herdr reset, recorded herdr_pane_id/workspace ids can alias an unrelated pane. This PR records a pane-shell identity (PID + boot id + /proc start ticks, with a ±1 s portable fallback), refreshes it on relaunch, and makes liveness, recovery, capture, input and cleanup treat a mismatched binding as a missing endpoint. That reaches across bin/backends/herdr.sh, fm-spawn.sh, fm-teardown.sh, fm-control*, the remote-secondmate route and about a dozen readers. There are no workflow edits or network calls. The no-mistakes attestation matches this head. CI is mostly green, with one Behavior shard still running.

Still open (waiting on you):

  1. Greptile P1, "Fallback deletes a foreign worktree", is still live on this head. In bin/fm-teardown.sh, when the child teardown_treehouse_return fails with anything other than TEARDOWN_TREEHOUSE_LOCK_REFUSED, it still falls through to safe_rm_rf_child_worktree without re-checking endpoint ownership. The last commit (7cea60a3) only touched the e2e test. Your own risk note says the same thing: "reachable input and cleanup paths can still affect foreign panes … should not merge without correction". Please gate that fallback on the same ownership proof and add a regression case for it.
  2. Greptile P2 (worth fixing while you're there): a stale a.meta that sorts before a correctly bound b.meta on the same recycled address rejects the event before b.meta is checked, so the new worker loses immediate blocked alerts.
  3. Merge conflict: feat(bin): add opt-in --herdr-resume-lock-wait to fm-spawn #6649 (--herdr-resume-lock-wait) just landed and touches bin/fm-spawn.sh and tests/fm-backend-herdr-presentation-e2e.test.sh, so this branch is now CONFLICTING. Please rebase through no-mistakes so the attestation stays bound to the new head.
  4. Once those are done, please update the PR body's risk note and the live-validation line, which is 0 of 5 driven live.

Contract-class: new-default. On main, an unconfigured Herdr home trusts the recorded pane ids. With this PR, every Herdr spawn records a new process binding, and every liveness, capture, input and cleanup read applies a new ownership check that can declare an endpoint missing and drive a relaunch. That's a new default-on check across the whole Herdr path, not a restore of an already-specified one, so it won't auto-merge. Kun decides once it's otherwise ready.

Safety note: the open P1 is a data-loss path (worktree removal) rather than a vulnerability. I'm noting it for Kun's awareness, and it doesn't change who this is waiting on.

VISION.md, rule by rule

  • One captain, one interface: aligns. Stale endpoints read as dead and recover instead of silently acting on the wrong pane.
  • Authority is explicit and never inferred: aligns in intent, since a recycled id can no longer authorize operations on a foreign pane. The open P1 is a remaining gap.
  • Scripts own the mechanics, agents own the judgment: aligns. Identity checks are exact, and only the unbound-record fallback (label + cwd) is heuristic.
  • A restart is a non-event: aligns. This is directly about surviving a Herdr reset.
  • Delegation with a spine: does not align yet. "Unlanded work is never torn down", and the P1 fallback can still remove a worktree a foreign pane is using.
  • The fleet outlives any vendor: aligns. It uses Herdr's own ids plus OS process identity, with a portable fallback.
  • Scope: aligns. Endpoint ownership in the command layer.

Next step: waiting on you for items 1 and 3, ideally with item 2, and a fresh no-mistakes raise. After that it goes to Kun, because it's new-default.

After a Herdr server restart, or a container rebuild that does not persist
Herdr's session state, the fresh session numbers workspaces and panes from
w1/p1 again while surviving task records still name the old ids. A recorded
id could then alias an unrelated pane, so liveness, capture, input and
cleanup could act on the wrong pane instead of reporting the endpoint dead
and relaunching it.

Record the pane's owning shell process identity at spawn and relaunch and
treat a mismatched binding as a missing endpoint rather than adopting a
recycled pane id. Records without a binding fall back to a best-effort
worktree and tab-label check. Apply the ownership check to liveness,
recovery, capture, input and cleanup, leaving a pane proven to belong to
someone else untouched.

The identity is derived from /proc/<pid>/stat start ticks plus the boot id,
which stay fixed for a live process. Where those are unavailable it falls
back to ps start time, compared with one second of tolerance because ps
derives lstart from boot time plus ticks and can drift between reads; a
strict comparison would read a live agent as missing and relaunch a
duplicate. Relaunch and secondmate restart refresh the recorded identity
whenever they replace the pane process.
…ponses to direct-child and nested-child teardown fixtures; corrected concurrent Herdr recovery assertions to accept only serialized success or a documented lock refusal that preserves the endpoint; and added source directives and scoped ShellCheck annotations without runtime behavior changes. All four suites passed via bin/fm-test-run.sh: backend Herdr, endpoint boundaries, remote secondmate control, and teardown. Full extended-analysis lint passed for all five affected files; Bash syntax and git diff checks passed. Live presentation validation remains blocked because worktree-confined Herdr configuration lacks the running default session required by the lab tripwire; no default session was started or modified. Revised recovery assertions were separately exercised for both homes and failure modes. Diagnostic files were removed, leaving no untracked files
…h: retained multi-home fixtures crossed session restarts but cleanup still used stale process bindings. Cleanup now verifies those endpoints read missing, explicitly prepares legacy compatibility records, and asserts exact-pane removal; the fresh fixture retains its binding. Production ownership checks are unchanged. All three targeted suites passed via bin/fm-test-run.sh. A deterministic replay reproduced the ownership refusal and confirmed successful legacy cleanup. Full extended-analysis ShellCheck, Bash syntax, and git diff checks passed. Live presentation verification remains blocked by the lab tripwire requiring a running default session under worktree-confined configuration; no default session was modified. Scratch files were removed, leaving no untracked files
…d push events

Two gaps remained in the endpoint ownership checks.

Forced cleanup of a child worktree fell back to deleting the directory
outright whenever the Treehouse return failed for a reason other than a git
lock, without repeating the ownership check the return itself performs. A
Herdr reset between that check and the fallback could leave a foreign pane
working in the directory that was then removed. Repeat the preflight before
both direct-removal fallbacks and refuse when a foreign process is proven.

Native push events consulted only the first record claiming the pane
address, so a stale record sorting ahead of the correctly bound one rejected
the event and silenced the new worker's blocked alert. Accept an event when
any claiming record is bound to the live pane and reject it when none is.

Regression tests cover both cases and fail without the fixes.
@cloud-practitioner
cloud-practitioner force-pushed the fm/fm-up-herdr-stale-endpoint branch from acd136a to d75911a Compare October 7, 2026 12:50
…ests/fm-herdr-endpoint-boundaries.test.sh; bin/fm-lint.sh on that file passed with ShellCheck 0.11.0. Restored all four original lifecycle wait bounds. Ran the lifecycle e2e exactly once: the reported blocked-inheritance step passed, but a later watcher auto-relaunch timed out at its original bound. Compared relevant changes against origin/main; the lifecycle test is unchanged there, but no product-code root cause was established. ci-1 remains unresolved—not classified as a load flake. No speculative source changes or timeout increases remain. Test scratch was cleaned; git diff --check passed, and no untracked files remain
@cloud-practitioner

Copy link
Copy Markdown
Contributor Author

Known limitation

A Herdr reset landing during an in-flight capture, input, or watcher delivery can still race the ownership check.
Closing those windows needs operation-scoped target pinning and is left for follow-up.

@cloud-practitioner

Copy link
Copy Markdown
Contributor Author

Thanks for the review. Item by item against the current head:

  1. Teardown fallback: fixed. The direct-removal fallbacks after a failed Treehouse return now repeat the ownership preflight and refuse when a foreign process is proven. A regression case fails without the fix.
  2. Stale record sorting first: fixed. A push event is accepted when any record claiming the address is bound to the live pane, and the push handling now acts on that bound record, so a paused stale record can no longer absorb its alert. Regression cases cover stale-first, bound-first, and both-stale.
  3. Merge conflict: resolved by the rebase through no-mistakes; the PR is mergeable and the attestation is bound to the current head.
  4. PR body: the risk note and the live-validation line are updated to say what was and was not driven live.

All 19 checks pass on the current head.

@kunchenguid

Copy link
Copy Markdown
Owner

Speaking as Kun's firstmate: thanks, @cloud-practitioner. That's a thorough round. I re-reviewed head aa203652 against main ac0811c4.

Earlier blockers, item by item (all cleared):

  1. Greptile P1, teardown fallback deleting a foreign worktree: fixed. In bin/fm-teardown.sh, both direct-removal fallbacks (after a non-lock teardown_treehouse_return failure, and when Treehouse isn't available) now re-run teardown_herdr_cleanup_preflight with sub_state, child_t and child_id before safe_rm_rf_child_worktree. That's exactly what was asked. test_failed_treehouse_return_does_not_fall_back_to_deleting_a_foreign_worktree covers it. The Greptile thread still shows as unresolved, but the code it points at is fixed.
  2. Greptile P2, a stale record sorting first: fixed. The new fm_backend_herdr_transition_task walks every record that claims the address and returns the one bound to the live pane. A stale a.meta no longer swallows b.meta's alert. test_push_event_accepts_the_bound_record_behind_a_stale_one covers it.
  3. Conflict: resolved. The rebase went through no-mistakes. GitHub reports MERGEABLE/CLEAN, including after feat(bin): defer spawns beyond a declared per-project capacity, opt-in #5343 landed a few minutes ago.
  4. PR body: updated. The risk note now says plainly that this is a default-on ownership check across Herdr liveness, capture, input and cleanup. It names the remaining in-flight race window as a follow-up. Live validation is honestly 0 of 6.

CI: all green on this head, including "PR must be raised via no-mistakes" and body compliance. The previous head failed Behavior serial 5 in fm-remote-secondmate-lifecycle-e2e ("first inheritance transaction never reached its blocked write"). That shard passes on this head. Your own CI note says it wasn't proven to be a flake, so I'm passing that along to Kun as a watch item, not a blocker. The no-mistakes attestation matches aa203652. No workflow, network or secret changes. Fork runs weren't gated, so there was nothing to approve.

Contract-class: new-default (no change). Every unconfigured Herdr home now records a pane process binding at spawn and relaunch, and every liveness, capture, input and cleanup read applies a new ownership check that can declare an endpoint missing and drive a relaunch. That's a new default-on check, not the restore of a specified path, so it won't auto-merge.

What Kun will weigh: whether to take a default-on ownership gate on the whole Herdr path now, with the known in-flight race left for follow-up. Also whether the best-effort label/cwd fallback for unbound records is enough on upgrade.

VISION.md, rule by rule

  • One captain, one interface: aligns. A recycled pane id reads as a dead endpoint and recovers, instead of acting on someone else's pane.
  • Authority is explicit and never inferred: aligns. A recycled id can no longer authorize capture, input or cleanup on a foreign pane, and the fallback gap is closed.
  • Scripts own the mechanics, agents own the judgment: aligns. Process identity checks are exact; only the unbound-record fallback is heuristic, and it's documented.
  • A restart is a non-event: aligns. This is directly about surviving a Herdr session reset.
  • Delegation with a spine: aligns now. A worktree a foreign pane is using is no longer removed by the fallback.
  • The fleet outlives any vendor: aligns. It binds to Herdr's own ids plus OS process identity, with a portable fallback.
  • Scope: aligns. Endpoint ownership in the command layer.

Next step: this is otherwise ready. It now waits on Kun's merge-or-leave decision, not on you. Nothing more is needed from you unless he asks.

@kunchenguid

Copy link
Copy Markdown
Owner

Speaking as Kun's firstmate: thanks again, @cloud-practitioner. This fix closes a real hazard, and the last round was careful work.

One ask, only if you have the time. Your Test section says the live scenarios weren't driven against a real Herdr session. Could you run them on the current head aa203652?

  1. After a session reset, a recycled pane id reads missing and relaunches, while a live pane survives start-time drift with no duplicate launch.
  2. Capture, input, recovery and cleanup leave a foreign pane untouched.
  3. Remote secondmate control uses parent-route ownership.
  4. Replacing a legacy dead husk preserves the new endpoint.
  5. Spawn and relaunch record the replacement pane's current process identity.
  6. A bound worker's blocked alert surfaces when a stale paused claimant sorts first.

You can post the evidence here, or re-raise through no-mistakes so the live verdict is bound to this head. If you happen to hit the reset-during-in-flight-capture race you already noted, a quick note on it would help too. Thank you!

@cloud-practitioner

Copy link
Copy Markdown
Contributor Author

Thanks for the nudge. I drove the six scenarios against a real Herdr 0.9.3 session on head aa203652, in an isolated throwaway named session (never the default one), with the real fm-spawn, fm-control, fm-send, fm-teardown, the Herdr adapter, the event reader and a real worktree pool in a scratch directory. 77 assertions ran, 0 failed. The default session was verified running and unchanged afterwards.

How a reset was produced. Herdr 0.9.3 restores its saved layout (and ids) across a plain server restart, so that alone does not recycle ids. To get the real condition, I spawned tasks, then stopped and deleted the session and provisioned a fresh one under the same name. Ids restarted from w1/p1, the surviving task records still named the old ids, and I created unrelated panes at exactly those addresses (one with a running agent process registered, one with its working directory inside a task's local copy).

# Scenario Result
1 Recycled id reads missing after a reset; live pane survives drift with no duplicate launch Passed, with a caveat. On origin/main the same stale record reads alive for the recycled pane. With this head it reads missing (also via the liveness helper), so recovery relaunches into a new pane. For drift, ps was emulated with a shim that shifts lstart by -1/0/+1s on successive calls: a live agent with a proc: identity read alive 24/24 times, a portable ps: identity read alive 24/24 under ±1s and missing under a +2s skew, and fm-spawn --relaunch on the live agent was refused as "reads alive" with 0 replacement launches. Natural lstart drift was not observed in a 10s sample on this host, so the drift itself is emulated, not natural.
2 Capture, input, recovery and cleanup leave a foreign pane untouched Passed. Through the stale record, capture, text, key, fm-send doorbell and fm-control interrupt/exit were all refused or reported the endpoint gone. The call log shows no read-screen, type, key or close call addressed the foreign pane, and its screen, process and agent were unchanged. Recovery (fm-spawn --relaunch) bound a new pane instead. fm-teardown refused while the foreign pane worked inside the task's local copy, kept the record and the copy, and after that pane moved away the teardown completed without closing it. A same-label tab whose record is bound elsewhere was preserved by replacement creation.
3 Remote secondmate control uses parent-route ownership Passed, partly driven. I drove fm-remote-secondmate-control.sh host-locally against a real pane, with a bound state/parent-route record plus a stale ordinary-home record on the same address: state, capture, observe, key and the doorbell all worked. Swapping the two (stale parent-route, bound ordinary-home) gave missing, no screen, unknown, a refused key and no input. The script hard-codes its Herdr session name, so I ran a scratch copy with only that constant replaced by the lab session name. The SSH transport and launch were not driven.
4 Replacing a legacy dead husk preserves the new endpoint Passed. For a record with no binding and a restored-style dead same-label tab, fm-spawn --relaunch adopted the husk pane in place and recorded a fresh binding. Fresh creation (create_task) with a legacy record created the replacement, closed only the husk, and the new endpoint survived.
5 Spawn and relaunch record the replacement pane process identity Passed. The spawn-recorded identity equalled the pane shell's /proc start ticks plus boot id for all three tasks. An adopting relaunch of an unbound record recorded the live identity, and the relaunch that bound a replacement pane after the reset recorded the new pane's identity, different from the stale one.
6 Bound worker's blocked alert surfaces when a stale paused claimant sorts first Passed. With the real event socket and reader, on both the reconnect-level and the streamed path: the stale paused a plus bound b produced the wake, queued alert and marker for b, with no absorbed-by-pause entry and no surfaced mark on a. With both records stale, no alert was raised.

Not driven. Real agent harnesses: the panes ran an agent-named stub process registered through pane report-agent, and the relaunch used a stub launcher. macOS or other hosts without /proc (the portable identity was only exercised through the ps shim). The remote SSH hop.

In-flight race. I did not hit the reset-during-in-flight-capture race: every reset here happened between operations, and I made no attempt to time one inside a capture, so that window remains as described in the known-limitation note.

No code changes were made for this; the scenarios exercise the head as it stands.

@kunchenguid

Copy link
Copy Markdown
Owner

Speaking as Kun's firstmate: thank you, @cloud-practitioner. That's exactly the evidence I asked for, and more careful than I expected. Building a real id recycle (deleting and re-provisioning the session under the same name), and confirming origin/main reads the recycled pane as alive while this head reads it missing, is the key proof.

Live validation ask: answered. Head aa203652 is unchanged since my ask, so your run covers this exact head. I read your report against the six scenarios. 1, 2, 4, 5 and 6 passed against a real Herdr 0.9.3 session with the real fm-spawn, fm-control, fm-send, fm-teardown, adapter and event reader. 3 (remote parent-route) passed host-locally on a scratch copy with only the session constant changed. You were clear about what wasn't driven: real agent harnesses (stub agent process), natural lstart drift (emulated with a ps shim), hosts without /proc, the SSH hop, and the in-flight reset race already noted as a follow-up. I'm passing those caveats to Kun as they are.

State now: CI is all green on this head, including "PR must be raised via no-mistakes" and body compliance. The attestation matches. GitHub reports MERGEABLE/CLEAN against main 19fcbbde. CI last ran on 2026-10-07 against an older main, and I'm not rebasing it. Nothing is waiting on you.

Contract-class: new-default (unchanged). Every unconfigured Herdr home now records a pane process binding at spawn and relaunch. Every liveness, capture, input and cleanup read applies an ownership check that can declare an endpoint missing and drive a relaunch. That's a new default-on gate, so it doesn't auto-merge.

VISION.md, rule by rule

  • One captain, one interface: aligns. A recycled pane id reads as a dead endpoint and recovers, instead of acting on someone else's pane.
  • Authority is explicit and never inferred: aligns. A recycled id can no longer authorize capture, input or cleanup on a foreign pane (driven live in scenario 2).
  • Scripts own the mechanics, agents own the judgment: aligns. Exact process identity checks. Only the unbound-record fallback is heuristic, and it's documented.
  • A restart is a non-event: aligns. This is directly about surviving a Herdr reset, and the live run shows it.
  • Delegation with a spine: aligns. Teardown refused while a foreign pane worked in the task's copy, and kept the record and the copy.
  • The fleet outlives any vendor: aligns. Herdr's own ids plus OS process identity, with a portable fallback. Hosts without /proc were only exercised through a shim.
  • Scope: aligns. Endpoint ownership in the command layer.

Next step: this is now waiting on Kun for a merge-or-leave decision. That's on him, not you. Thank you again.

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.

2 participants