Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions docs/verification/runtime-backends.md
Original file line number Diff line number Diff line change
Expand Up @@ -1707,6 +1707,22 @@ herdr pane process-info --pane w1:p1 --session "$LAB" | jq -c '.result.process_i
Before the fix `fm_backend_agent_state herdr` read that second state as `alive`, so `bin/fm-control.sh <id> relaunch` and `bin/fm-spawn.sh --relaunch` were refused for as long as the registration lived, which is hours.
The registration is still present after the wait, and a `pane report-agent` registration whose agent process exits under a nested shell leaves the same shape behind on any pane, which is what the lifecycle-control guard uses; with no nested shell, Herdr releases that registration within about a second.

Measured 2026-10-01 on Linux x86_64 against Herdr 0.9.3 in an isolated `fm-lab-` session: a registration on a pane idling at its own top shell is released within about a second, while a nested shell in the foreground keeps it.
A fixture that needs a lasting registration therefore backs it with a running foreground process, as the live-duplicate case in `tests/fm-backend-herdr-smoke.test.sh` does.

```sh
herdr pane report-agent w1:p1 --source fm-probe --agent probe-agent --state idle --session "$LAB"; sleep 2
herdr agent get w1:p1 --session "$LAB" 2>&1
herdr pane run w1:p1 bash --session "$LAB"; sleep 1
herdr pane report-agent w1:p1 --source fm-probe --agent probe-agent --state idle --session "$LAB"; sleep 2
herdr agent get w1:p1 --session "$LAB" | jq -c '.result.agent | {agent, agent_status}'
```

```text
{"error":{"code":"agent_not_found","message":"agent target w1:p1 not found"},"id":"cli:agent:get"}
{"agent":"probe-agent","agent_status":"idle"}
```

Two vendor facts the fix rests on, both read from the outputs above and from `fm_backend_herdr_pane_process_state`'s `pane process-info` parse:

- Pi's process presents with kernel name `node` and argv0 `pi` (its foreground group also carries Pi's child `node` helpers with argv0 such as `npm view ... version`), so a running Pi is attributed by argv[0] exactly as the tmux probe attributes it; a symlink named `claude` to `sleep` presents as name `sleep`, argv0 `claude`.
Expand Down
25 changes: 24 additions & 1 deletion tests/fm-backend-herdr-respawn-idem-e2e.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -163,9 +163,32 @@ pass "fixed: the workspace holds exactly the 2 replacement tabs after both respa
# the freshly-respawned panes, then confirm a further same-labeled spawn
# attempt refuses exactly as before - the husk fix must never touch a pane
# that actually has something registered in it.

# The registration is backed by a running agent-named foreground process
# (a `claude` symlink to `sleep`, as tests/fm-backend-herdr-smoke.test.sh
# does): Herdr 0.9.3 releases a report-agent registration within about a
# second once the pane's foreground is its own top shell, so a report on an
# idle shell pane is not a live agent and would race that release
# (docs/verification/runtime-backends.md "Stale agent registration").

LIVE_SLEEP=$(command -v sleep) || fail "sleep not found"
ln -s "$LIVE_SLEEP" "$SCRATCH/claude"
printf -v LIVE_AGENT_Q '%q' "$SCRATCH/claude"
fm_backend_herdr_send_text_line "$SESSION:$NEW_CREW_PANE_ID" "$LIVE_AGENT_Q 900" \
|| fail "could not start the agent-named foreground process on the respawned crewmate-shaped pane"
LIVE_TRIES=0
until [ "$(fm_backend_herdr_pane_process_state "$SESSION" "$NEW_CREW_PANE_ID")" = agent ]; do
LIVE_TRIES=$((LIVE_TRIES + 1))
[ "$LIVE_TRIES" -lt 50 ] \
|| fail "the respawned crewmate-shaped pane's agent-named foreground process reads '$(fm_backend_herdr_pane_process_state "$SESSION" "$NEW_CREW_PANE_ID")' rather than 'agent' through pane process-info"
sleep 0.1
done
herdr pane report-agent "$NEW_CREW_PANE_ID" --source fm-respawn-e2e --agent fm-respawn-live-agent --state idle --session "$SESSION" >/dev/null 2>&1 \
|| fail "could not register a live agent on the respawned crewmate-shaped pane"
# Prove the scenario is the live case rather than passing on a stale or
# unreadable registration, which the husk check also refuses.
LIVE_STATE=$(fm_backend_herdr_pane_agent_state "$SESSION" "$NEW_CREW_PANE_ID")
[ "$LIVE_STATE" = live ] \
|| fail "live-duplicate setup is wrong: the respawned crewmate-shaped pane should classify live, got '$LIVE_STATE'"

if fm_backend_herdr_create_task "$CONTAINER" "$CREW_LABEL" "$PROJ_CWD" >/dev/null 2>&1; then
fail "REGRESSION: create_task should refuse a same-labeled tab whose pane hosts a genuinely live registered agent"
Expand Down
26 changes: 26 additions & 0 deletions tests/fm-backend-herdr-smoke.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,10 @@ herdr_forget_inherited_pane
SESSION="fm-lab-backend-smoke-$$"
export HERDR_SESSION="$SESSION"
SM_SCRATCH=
LIVE_DUP_SCRATCH=
cleanup_all() {
[ -n "$SM_SCRATCH" ] && rm -rf "$SM_SCRATCH"
[ -n "$LIVE_DUP_SCRATCH" ] && rm -rf "$LIVE_DUP_SCRATCH"
herdr_safe_stop_and_delete "$SESSION"
}
trap cleanup_all EXIT
Expand Down Expand Up @@ -133,6 +135,12 @@ pass "real herdr: create_task prunes the freshly-created workspace's seeded defa

# 1. A genuinely LIVE duplicate (a real registered agent, via herdr's own
# `pane report-agent`) must still refuse exactly as before.
# The registration is backed by a running agent-named foreground process
# (a `claude` symlink to `sleep`, as tests/fm-control-herdr-smoke.test.sh
# does): Herdr 0.9.3 releases a report-agent registration within about a
# second once the pane's foreground is its own top shell, so a report on an
# idle shell pane is not a live agent and would race that release
# (docs/verification/runtime-backends.md "Stale agent registration").
LIVE_DUP_LABEL="fm-smoke-livedup"
LIVE_DUP_IDS=$(fm_backend_herdr_create_task "$CONTAINER" "$LIVE_DUP_LABEL" /tmp) || fail "could not create the live-duplicate scenario's tab"
read -r LIVE_DUP_TAB_ID LIVE_DUP_PANE_ID <<EOF
Expand All @@ -141,8 +149,26 @@ EOF
if [ -z "$LIVE_DUP_TAB_ID" ] || [ -z "$LIVE_DUP_PANE_ID" ]; then
fail "live-duplicate scenario tab creation did not return ids"
fi
LIVE_DUP_SCRATCH=$(mktemp -d "${TMPDIR:-/tmp}/fm-herdr-smoke-livedup.XXXXXX")
LIVE_DUP_SLEEP=$(command -v sleep) || fail "sleep not found"
ln -s "$LIVE_DUP_SLEEP" "$LIVE_DUP_SCRATCH/claude"
printf -v LIVE_DUP_AGENT_Q '%q' "$LIVE_DUP_SCRATCH/claude"
fm_backend_herdr_send_text_line "$SESSION:$LIVE_DUP_PANE_ID" "$LIVE_DUP_AGENT_Q 900" \
|| fail "could not start the agent-named foreground process in the live-duplicate scenario's pane"
LIVE_DUP_TRIES=0
until [ "$(fm_backend_herdr_pane_process_state "$SESSION" "$LIVE_DUP_PANE_ID")" = agent ]; do
LIVE_DUP_TRIES=$((LIVE_DUP_TRIES + 1))
[ "$LIVE_DUP_TRIES" -lt 50 ] \
|| fail "the live-duplicate scenario's agent-named foreground process reads '$(fm_backend_herdr_pane_process_state "$SESSION" "$LIVE_DUP_PANE_ID")' rather than 'agent' through pane process-info"
sleep 0.1
done
herdr pane report-agent "$LIVE_DUP_PANE_ID" --source fm-smoke-test --agent fm-smoke-live-agent --state idle --session "$SESSION" >/dev/null 2>&1 \
|| fail "could not register a live agent on the live-duplicate scenario's pane"
# Prove the scenario is the live case rather than passing on a stale or
# unreadable registration, which the husk check also refuses.
LIVE_DUP_STATE=$(fm_backend_herdr_pane_agent_state "$SESSION" "$LIVE_DUP_PANE_ID")
[ "$LIVE_DUP_STATE" = live ] \
|| fail "live-duplicate scenario setup is wrong: the pane should classify live, got '$LIVE_DUP_STATE'"
if fm_backend_herdr_create_task "$CONTAINER" "$LIVE_DUP_LABEL" /tmp >/dev/null 2>&1; then
fail "REGRESSION: create_task should refuse a duplicate label whose pane hosts a genuinely live registered agent (idle counts as live)"
fi
Expand Down
Loading