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
6 changes: 3 additions & 3 deletions bin/backends/herdr.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4112,7 +4112,8 @@ EOF
# primary-spawns-a-secondmate path in fm-spawn.sh. Read-only: a session/
# workspace that does not exist yet simply lists nothing. One
# "<session>:<pane_id>\t<label>" line per live task tab, followed by one per
# fm-<id>-labeled pane in each workspace carrying this home's crew label.
# fm-<id>-labeled pane in each crew workspace this home recorded (never another
# home's workspace that merely shares the crew label).
fm_backend_herdr_list_live() { # <session>
local session=$1 wsid tabs tab_id label pane_id
wsid=$(fm_backend_herdr_workspace_find "$session") || wsid=
Expand All @@ -4128,8 +4129,7 @@ fm_backend_herdr_list_live() { # <session>
[ -n "$wsid" ] || continue
fm_backend_herdr_cli "$session" pane list --workspace "$wsid" 2>/dev/null | jq -r --arg session "$session" \
'.result.panes[]? | select((.label // "") | startswith("fm-")) | "\($session):\(.pane_id)\t\(.label)"' 2>/dev/null
done < <(fm_backend_herdr_cli "$session" workspace list 2>/dev/null | jq -r --arg want "$(fm_backend_herdr_crew_workspace_label)" \
'.result.workspaces[]? | select(.label == $want) | .workspace_id' 2>/dev/null)
done < <(fm_backend_herdr_crew_workspace_recorded "${FM_STATE_OVERRIDE:-${FM_HOME:-}/state}" "$session" 2>/dev/null)
return 0
}

Expand Down
8 changes: 5 additions & 3 deletions docs/herdr-backend.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,8 @@ Rename it manually before expecting new tasks or recovery to use it.

### Recovery and existing tasks

Recovery and list-live still scan the first workspace matching the home label, because they address panes they already recorded rather than choosing where new work goes.
Recovery and list-live scan the first workspace matching the home label for ordinary tabs, while crew-pane discovery scans only this home's recorded crew workspace ids.
They address panes they already recorded rather than choosing where new work goes.
The one recovery that does place new work is the control plane's reclaim of a destroyed endpoint.
It mints a replacement tab through this section's ordinary placement rules while pinning the herdr session the task's record names ([`agent-control.md`](agent-control.md) "Reclaiming a task whose endpoint is gone").

Expand Down Expand Up @@ -514,9 +515,10 @@ Placement runs under the same per-session presentation lock as the projection, a
A live pane that already carries the task label refuses a duplicate launch, while an exited husk with that label is closed only after the replacement pane exists.
A failure after the new pane exists closes that exact pane, and a crew placement writes no presentation journal.
Cleanup closes only the task's exact pane through the ordinary exact-identity kill path and its focus-safe removal plan, so each crew workspace disappears with its last pane and a new one is created when a placement finds no room.
List-live reports the home's crew panes alongside its home-workspace tabs, and a bare selector that matches no tab label falls back to an exact pane label, so recovery, send, capture, and turn-end detection use the recorded pane endpoint exactly as they do for a tab.
List-live reports panes only from this home's recorded crew workspaces alongside its home-workspace tabs, so a same-labeled workspace belonging to another home in the shared session is never recovered as local work.
A bare selector that matches no tab label falls back to an exact pane label, so recovery, send, capture, and turn-end detection use the recorded pane endpoint exactly as they do for a tab.

`tests/fm-backend-herdr-crew.test.sh` pins parsing, first creation, grid fill, overflow, custom caps, cleanup rebalance, refill, recreation, record verification, duplicate refusal, list-live, and the no-layout-API fallback against a stateful fake.
`tests/fm-backend-herdr-crew.test.sh` pins parsing, first creation, grid fill, overflow, custom caps, cleanup rebalance, refill, recreation, record verification, duplicate refusal, home-scoped list-live, and the no-layout-API fallback against a stateful fake.
`tests/fm-backend-herdr-crew-workspace-e2e.test.sh` proves the same behavior and a real spawn and cleanup pass in a guarded named lab; [`verification/runtime-backends.md`](verification/runtime-backends.md#crew-workspace) owns the active versioned evidence.

## Default-tab prune safety
Expand Down
22 changes: 22 additions & 0 deletions tests/fm-backend-herdr-crew-workspace-e2e.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,28 @@ read -r WS TAB _ < <(place fm-t9) || fail 'could not place a crew task after the
|| fail 'the recreated crew workspace should again sit right after the home'
pass 'real herdr: each crew workspace disappears with its last pane and the next spawn recreates one'

# Separate homes can create identically labeled crew workspaces in one Herdr
# session. Recovery must use each home's recorded workspace ids, not that
# mutable shared label, so it never offers another home's pane as local work.
HOME_A=$HOME_DIR
OTHER_HOME="$TMP_ROOT/other-home"
mkdir -p "$OTHER_HOME/state"
HOME_DIR=$OTHER_HOME
read -r OTHER_WS _ < <(place fm-foreign) || fail 'could not place the foreign-home crew task'
[ -n "$OTHER_WS" ] && [ "$OTHER_WS" != "$WS" ] || fail 'the foreign home should create a distinct crew workspace'
HOME_DIR=$HOME_A
LIVE_A=$(adapter fm_backend_herdr_list_live "$HERDR_LAB_SESSION")
HOME_DIR=$OTHER_HOME
LIVE_B=$(adapter fm_backend_herdr_list_live "$HERDR_LAB_SESSION")
HOME_DIR=$HOME_A
[ "$(lab workspace list | jq '[.result.workspaces[] | select(.label == "firstmate-crew")] | length')" = 2 ] \
|| fail 'the shared session should contain two same-labeled crew workspaces'
printf '%s\n' "$LIVE_A" | grep -Fq $'\tfm-t9' || fail "home A did not list its recorded crew pane: $LIVE_A"
! printf '%s\n' "$LIVE_A" | grep -Fq $'\tfm-foreign' || fail "home A recovered foreign work from a same-labeled workspace: $LIVE_A"
printf '%s\n' "$LIVE_B" | grep -Fq $'\tfm-foreign' || fail "home B did not list its recorded crew pane: $LIVE_B"
! printf '%s\n' "$LIVE_B" | grep -Fq $'\tfm-t9' || fail "home B recovered foreign work from a same-labeled workspace: $LIVE_B"
pass 'real herdr: list-live scopes same-labeled crew workspaces to the recording home'

# The same layout through the real spawn and teardown scripts: a home whose
# config selects "crew" gets its crewmates as panes of one crew workspace, and
# teardown closes each exact pane.
Expand Down
21 changes: 21 additions & 0 deletions tests/fm-backend-herdr-crew.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,26 @@ test_list_live_reports_crew_panes() {
pass "herdr crew: list-live reports crew panes by their task labels"
}

test_list_live_ignores_another_homes_crew_workspace() {
local home_a live_a live_b
crew_fixture two-homes
home_a=$HOME_DIR
place fm-a >/dev/null || fail "home A crew placement failed"
HOME_DIR="$DIR/home-b"
mkdir -p "$HOME_DIR/state" "$HOME_DIR/config"
place fm-b >/dev/null || fail "home B crew placement failed"
assert_equals 2 "$(herdr_fake workspace list | jq '[.result.workspaces[] | select(.label == "firstmate-crew")] | length')" \
"both homes should own a same-labeled crew workspace in the shared session"
live_b=$(adapter fm_backend_herdr_list_live crewlab)
HOME_DIR=$home_a
live_a=$(adapter fm_backend_herdr_list_live crewlab)
assert_contains "$live_a" $'\tfm-a' "home A should list its own crew pane"
assert_not_contains "$live_a" $'\tfm-b' "home A must not list home B's crew pane"
assert_contains "$live_b" $'\tfm-b' "home B should list its own crew pane"
assert_not_contains "$live_b" $'\tfm-a' "home B must not list home A's crew pane"
pass "herdr crew: list-live lists only this home's recorded crew workspaces"
}

test_missing_layout_api_still_places_with_a_warning() {
local ws tab pane
crew_fixture no-layout-api
Expand All @@ -305,4 +325,5 @@ test_last_pane_removes_the_workspace_and_next_spawn_recreates_it
test_a_relabeled_recorded_workspace_is_never_adopted
test_a_live_same_labeled_pane_refuses
test_list_live_reports_crew_panes
test_list_live_ignores_another_homes_crew_workspace
test_missing_layout_api_still_places_with_a_warning
Loading