diff --git a/docs/verification/runtime-backends.md b/docs/verification/runtime-backends.md index ddb6ebcd449..1cb48c25e14 100644 --- a/docs/verification/runtime-backends.md +++ b/docs/verification/runtime-backends.md @@ -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 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`. diff --git a/tests/fm-backend-herdr-respawn-idem-e2e.test.sh b/tests/fm-backend-herdr-respawn-idem-e2e.test.sh index 382fb72d979..2a63cdfca05 100755 --- a/tests/fm-backend-herdr-respawn-idem-e2e.test.sh +++ b/tests/fm-backend-herdr-respawn-idem-e2e.test.sh @@ -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" diff --git a/tests/fm-backend-herdr-smoke.test.sh b/tests/fm-backend-herdr-smoke.test.sh index 4e2d15a1096..3572a776e85 100755 --- a/tests/fm-backend-herdr-smoke.test.sh +++ b/tests/fm-backend-herdr-smoke.test.sh @@ -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 @@ -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 </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