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
15 changes: 12 additions & 3 deletions bin/backends/herdr.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1672,7 +1672,7 @@ fm_backend_herdr_server_ensure() { # <session>
(
unset FM_HOME FM_ROOT_OVERRIDE FM_STATE_OVERRIDE FM_DATA_OVERRIDE FM_PROJECTS_OVERRIDE FM_CONFIG_OVERRIDE \
CURSOR_AGENT CURSOR_INVOKED_AS CLAUDECODE PI_CODING_AGENT FM_PI_HARNESS GROK_AGENT FM_SUPERVISION_MODEL
fm_backend_herdr_cli "$session" server >/dev/null 2>&1 &
fm_backend_herdr_cli "$session" server </dev/null >/dev/null 2>&1 &
) || return 1
for i in $(seq 1 20); do
running=$(fm_backend_herdr_cli "$session" status --json 2>/dev/null | jq -r '.server.running // false' 2>/dev/null)
Expand Down Expand Up @@ -3047,6 +3047,15 @@ fm_backend_herdr_parse_target() { # <target>
[ -n "$FM_BACKEND_HERDR_SESSION" ] && [ -n "$FM_BACKEND_HERDR_PANE" ] && [ "$FM_BACKEND_HERDR_PANE" != "$target" ]
}

# fm_backend_herdr_target_observable: verify <target> is well-formed and its
# recorded session server is currently running, WITHOUT autostarting the server.
# Passive read-only probes (e.g. composer inspection) must stay observable-only
# and fail fast on inactive or bad targets without starting a new server.
fm_backend_herdr_target_observable() { # <target>
fm_backend_herdr_parse_target "$1" || return 1
[ "$(fm_backend_herdr_server_running_state "$FM_BACKEND_HERDR_SESSION")" = running ]
}

fm_backend_herdr_target_ready() { # <target>
fm_backend_herdr_parse_target "$1" || return 1
fm_backend_herdr_server_ensure "$FM_BACKEND_HERDR_SESSION" || return 1
Expand Down Expand Up @@ -3150,12 +3159,12 @@ fm_backend_herdr_capture() { # <target> <lines>
# workaround above - the bound is the pane itself, and asking for a line count
# is what triggers the empty-read bug.
fm_backend_herdr_visible_capture() { # <target>
fm_backend_herdr_target_ready "$1" || return 1
fm_backend_herdr_target_observable "$1" || return 1
fm_backend_herdr_cli "$FM_BACKEND_HERDR_SESSION" pane read "$FM_BACKEND_HERDR_PANE" --source visible 2>/dev/null
}

fm_backend_herdr_visible_capture_ansi() { # <target>
fm_backend_herdr_target_ready "$1" || return 1
fm_backend_herdr_target_observable "$1" || return 1
fm_backend_herdr_cli "$FM_BACKEND_HERDR_SESSION" pane read "$FM_BACKEND_HERDR_PANE" --source visible --format ansi 2>/dev/null
}

Expand Down
26 changes: 26 additions & 0 deletions tests/fm-backend-herdr.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3957,6 +3957,30 @@ test_composer_state_unknown_on_capture_failure() {
pass "fm_backend_herdr_composer_state: reports unknown when the pane cannot be captured"
}

test_composer_state_inactive_session_fails_fast_without_server_autostart() {
local dir log resp fb out status
dir="$TMP_ROOT/composer-inactive-session"; mkdir -p "$dir/responses"; log="$dir/log"; resp="$dir/responses"; : > "$log"
# status --json reports stopped server
printf '{"client":{"version":"0.7.1","protocol":14},"server":{"running":false}}\n' > "$resp/1.out"
fb=$(make_herdr_fakebin "$dir")
out=$( PATH="$fb:$PATH" FM_HERDR_LOG="$log" FM_HERDR_RESPONSES="$resp" FM_HERDR_SCRIPT_STATUS=1 \
bash -c '. "$0/bin/backends/herdr.sh"; fm_backend_herdr_composer_state nonexistent:w1:p2' "$ROOT" )
status=$?
[ "$status" -eq 0 ] || fail "composer_state should exit 0 on inactive session"
[ "$out" = unknown ] || fail "composer_state on inactive session should read unknown, got '$out'"
assert_not_contains "$(cat "$log")" $'\x1f''server' "composer probe on inactive session must never autostart a herdr server"
pass "fm_backend_herdr_composer_state: fails fast to unknown for inactive session without starting server"
}

# shellcheck disable=SC2016
test_composer_state_piped_reader_does_not_hang() {
local out rc=0
out=$( timeout 3s bash -c '. "$0/bin/fm-backend.sh"; ( fm_backend_composer_state herdr nonexistent-session:p2 ) 2>&1 | head -20' "$ROOT" ) || rc=$?
[ "$rc" -eq 0 ] || fail "piped composer probe on bad target hung or failed with rc=$rc"
[ "$out" = unknown ] || fail "piped composer probe should print unknown, got '$out'"
pass "fm_backend_composer_state (herdr): piped probe on bad target does not hang EOF-sensitive readers"
}

test_composer_state_unknown_when_no_composer_row_found() {
local dir log resp fb out glyph idx=1
dir="$TMP_ROOT/composer-no-row"; mkdir -p "$dir/responses"; log="$dir/log"; resp="$dir/responses"; : > "$log"
Expand Down Expand Up @@ -6002,6 +6026,8 @@ test_composer_state_real_text_is_pending
test_composer_state_grok_oversized_title_preserves_safe_verdicts
test_composer_state_popup_placeholder_fill_is_pending
test_composer_state_unknown_on_capture_failure
test_composer_state_inactive_session_fails_fast_without_server_autostart
test_composer_state_piped_reader_does_not_hang
test_composer_state_unknown_when_no_composer_row_found
test_composer_state_pi_parked_prompt_is_not_empty
test_composer_state_pi_separator_idle_is_empty
Expand Down
Loading