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
49 changes: 31 additions & 18 deletions bin/backends/herdr.sh
Original file line number Diff line number Diff line change
Expand Up @@ -384,21 +384,15 @@ fm_backend_herdr_workspace_label() {
# compatible if a future herdr build honors it. Never used by
# fm_backend_herdr_version_check, which is intentionally session-independent
# (reads only .client.* fields).
# The long-lived `server` launch does not come through here; it is owned by
# fm_backend_herdr_server_ensure.
fm_backend_herdr_cli() { # <session> <herdr-subcommand-and-args...>
local session=$1 rc=0 err failed_bin selected_bin client_bin=herdr
local session=$1 rc=0 err failed_bin selected_bin client_bin
shift
if [ "${FM_BACKEND_HERDR_CLIENT_SESSION:-}" = "$session" ]; then
client_bin=$(fm_backend_herdr_bin)
fi
client_bin=$(fm_backend_herdr_session_client_bin "$session")
# stderr is buffered (stdout streams untouched) so a protocol_mismatch
# refusal can be recognized and retried once on a compatible client; see
# "client selection" below. A failed command's stderr is replayed verbatim.
# The long-lived `server` launch is exec'd straight through: buffering its
# stderr would hold this call open for the server's whole lifetime.
if [ "${1:-}" = server ]; then
HERDR_SESSION="$session" "$client_bin" "$@" --session "$session"
return $?
fi
failed_bin=$client_bin
{ err=$(HERDR_SESSION="$session" "$failed_bin" "$@" --session "$session" 2>&1 1>&3 3>&-) || rc=$?; } 3>&1
if [ "$rc" -ne 0 ]; then
Expand Down Expand Up @@ -447,6 +441,17 @@ fm_backend_herdr_bin() {
printf '%s' "${FM_BACKEND_HERDR_BIN:-herdr}"
}

# fm_backend_herdr_session_client_bin: the client every call for <session>
# starts with - the one already selected for that exact session, else the first
# `herdr` on PATH.
fm_backend_herdr_session_client_bin() { # <session>
if [ "${FM_BACKEND_HERDR_CLIENT_SESSION:-}" = "$1" ]; then
fm_backend_herdr_bin
else
printf 'herdr'
fi
}

# fm_backend_herdr_client_candidates: every distinct executable named herdr on
# PATH, one per line, in PATH order (builtins only - no fork).
fm_backend_herdr_client_candidates() {
Expand Down Expand Up @@ -1650,18 +1655,26 @@ fm_backend_herdr_projection_order_best_effort() { # <session> <created-workspac
# has-session || tmux new-session -d`. Verified: a bare socket CLI call does
# NOT auto-start the server, so this must run before any workspace/tab/pane
# call. The server outlives its launcher and passes its startup environment to
# every later pane, so remove home, harness identity, and supervision selection
# inherited from whichever agent happened to start it. Bounded poll for the
# server to report running.
# every later pane, so remove every Firstmate-private FM_* variable (home and
# directory overrides, supervision selection, and per-call overrides such as the
# fleet snapshot's FM_CREW_STATE_* paths) plus harness identity inherited from
# whichever caller happened to start it - often a passive state read. The
# launch execs the server in place of its subshell so no shell wrapper outlives
# the call holding the caller's descriptors (a function redirection's saved
# stdout would otherwise keep a caller's command substitution open for the
# server's lifetime). Bounded poll for the server to report running.
fm_backend_herdr_server_ensure() { # <session>
local session=$1 running out i
local session=$1 running i name client_bin
running=$(fm_backend_herdr_cli "$session" status --json 2>/dev/null | jq -r '.server.running // false' 2>/dev/null)
[ "$running" = "true" ] && return 0
client_bin=$(fm_backend_herdr_session_client_bin "$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 &
) || return 1
for name in $(compgen -e); do
case "$name" in FM_*) unset "$name" ;; esac
done
unset CURSOR_AGENT CURSOR_INVOKED_AS CLAUDECODE PI_CODING_AGENT GROK_AGENT
HERDR_SESSION="$session" exec "$client_bin" server --session "$session" </dev/null >/dev/null 2>&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)
[ "$running" = "true" ] && return 0
Expand Down
7 changes: 4 additions & 3 deletions docs/herdr-backend.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,10 +219,11 @@ Workspace and tab ids support verification and cleanup but are not inferred from
## Current transport behavior

The adapter starts and polls a named server before workspace, tab, pane, or agent calls.
Every Herdr invocation goes through `fm_backend_herdr_cli`, which sets the environment and passes an explicit trailing `--session <name>`.
Every Herdr invocation, including the server launch, sets the session environment and passes an explicit trailing `--session <name>`; every call except that launch goes through `fm_backend_herdr_cli`.
An environment variable alone is not reliable when another Herdr server is running.
When the selected named server is not running, the adapter launches it without inherited Firstmate home and directory overrides, harness identity markers, or the supervision-model override.
Herdr passes its server startup environment to every later pane, so retaining those values could misroute panes for another Firstmate home or harness.
When the selected named server is not running, the adapter launches it without any inherited Firstmate `FM_*` variable or harness identity marker.
Herdr passes its server startup environment to every later pane, and a passive state read can be what starts a stopped server, so retaining those values could misroute panes for another Firstmate home or harness or freeze a single call's overrides, such as the fleet snapshot's per-task state paths, into the primary session and every worker.
The server replaces its launching shell, so the call that starts it returns without holding the caller's output open for the server's lifetime.
An already-running server is reused without restart or environment changes.
Explicit named-session routing and unrelated launch environment remain intact.

Expand Down
Loading
Loading