Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
8256d25
feat: represent deliberately stood-down workers
AgardnerAU Aug 28, 2026
61e8d51
no-mistakes(review): harden stand-down authority; add worker-state re…
AgardnerAU Aug 28, 2026
f30de29
no-mistakes(review): share run attribution; refuse unprovable stand-down
AgardnerAU Aug 28, 2026
3cfdbaa
no-mistakes(review): refuse stand-down on unprovable run, worktree, o…
AgardnerAU Aug 28, 2026
fc7204b
no-mistakes(review): narrow stand-down exemption; check scouts; refus…
AgardnerAU Aug 28, 2026
5c355fb
no-mistakes(review): allow holds on finished runs and branchless work…
AgardnerAU Aug 28, 2026
389e702
no-mistakes(review): scan whole branch listing for live runs
AgardnerAU Aug 28, 2026
97c4d16
no-mistakes(review): corroborate terminal axi answers with branch lis…
AgardnerAU Aug 28, 2026
599f877
no-mistakes(review): treat a full run window as unproven
AgardnerAU Aug 28, 2026
f3d1426
refactor: unify branch run verdict
AgardnerAU Aug 28, 2026
6eed7f0
no-mistakes(review): carry run reporting through unknown verdicts and…
AgardnerAU Aug 28, 2026
9b5dd84
no-mistakes(review): hold unregistered projects and drop uncorroborat…
AgardnerAU Aug 28, 2026
c1ff3a8
no-mistakes(review): apply one corroboration rule to run reporting pr…
AgardnerAU Aug 28, 2026
9c17764
no-mistakes(review): rebuild branch-run verdict around the branch read
AgardnerAU Aug 28, 2026
b97a4bc
no-mistakes(review): skip corroboration for active reads and order re…
AgardnerAU Aug 28, 2026
4b40c58
no-mistakes(review): Gate lifecycle, corroborate runs, and repair van…
AgardnerAU Aug 29, 2026
dc5e313
no-mistakes(review): Fail closed and require run projection corrobora…
AgardnerAU Aug 29, 2026
9828e41
no-mistakes(review): Restore ordinary Herdr relaunch support
AgardnerAU Aug 29, 2026
8ec8712
no-mistakes(review): Preserve active run authority without redundant …
AgardnerAU Aug 29, 2026
86cb5d8
no-mistakes(document): Align worker-state and run verdict documentation
AgardnerAU Aug 29, 2026
90bd1b9
no-mistakes: apply CI fixes
AgardnerAU Aug 29, 2026
edb735f
no-mistakes: apply CI fixes
AgardnerAU Aug 29, 2026
2e56802
no-mistakes: apply CI fixes
AgardnerAU Aug 29, 2026
57addde
no-mistakes(review): Use newest terminal row for run projection
AgardnerAU Aug 30, 2026
5b472e2
no-mistakes(review): Keep missing endpoints under durable inbox super…
AgardnerAU Aug 30, 2026
562d760
no-mistakes(document): Document newest terminal projection rule
AgardnerAU Aug 30, 2026
970dfe9
no-mistakes: apply CI fixes
AgardnerAU Aug 30, 2026
9f68e8b
no-mistakes: apply CI fixes
AgardnerAU Aug 30, 2026
b387086
no-mistakes: apply CI fixes
AgardnerAU Aug 30, 2026
516bb2f
Merge origin/main into fm/fm-stooddown-worker-false-wedge-alarms
AgardnerAU Sep 18, 2026
f4a911d
no-mistakes(review): Keep meta removal atomic; live run outranks stoo…
AgardnerAU Sep 18, 2026
0b2e62e
no-mistakes(review): Drop dead run projection; narrow stood-down acti…
AgardnerAU Sep 18, 2026
b2aa524
no-mistakes(review): Remove unreachable stood-down guard from event w…
AgardnerAU Sep 18, 2026
d76405c
no-mistakes(review): Narrow unregistered matcher; drop duplicate runs…
AgardnerAU Sep 18, 2026
be81129
no-mistakes(review): Stop suppressing host git config in test harness
AgardnerAU Sep 18, 2026
264b581
no-mistakes(review): Reuse resolved task id in stood-down watch guard
AgardnerAU Sep 18, 2026
2804abb
no-mistakes(document): Document stand-down runs-limit knob and withhe…
AgardnerAU Sep 18, 2026
969d263
Merge remote-tracking branch 'origin/main' into fm/fm-stooddown-worke…
AgardnerAU Sep 20, 2026
3a34a9e
no-mistakes(review): accept already-stopped exit during stand-down
AgardnerAU Sep 20, 2026
3976eb1
no-mistakes(review): read only the branch's newest ledger row
AgardnerAU Sep 20, 2026
6dca6cc
no-mistakes(document): de-enumerate stale fm-control verb lists in docs
AgardnerAU Sep 20, 2026
04936a2
Merge remote-tracking branch 'origin/main' into fm/fm-stooddown-worke…
AgardnerAU Sep 22, 2026
392c766
test: remove the contribution budget test's dependence on fixture start
AgardnerAU Sep 22, 2026
17d14df
no-mistakes(review): Refuse invalid worker-state before stopping rela…
AgardnerAU Sep 22, 2026
4348aec
no-mistakes(document): Correct stood-down worker documentation
AgardnerAU Sep 22, 2026
ad78143
Merge upstream main into stood-down worker PR branch
AgardnerAU Sep 27, 2026
882ce51
Merge upstream main into stood-down worker PR branch
AgardnerAU Sep 27, 2026
9c15ae1
Merge upstream main into stood-down worker PR branch
AgardnerAU Sep 30, 2026
07165b2
Merge upstream main into stood-down worker PR branch
AgardnerAU Oct 5, 2026
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
1 change: 1 addition & 0 deletions .agents/skills/operational-home-layout/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ state/ runtime records and signals; gitignored
<id>.devin-config.json firstmate-owned per-task Devin config (mode 600 snapshot of the user config plus the busy-state and turn-end hooks) passed through --config so no user or project config is edited; bin/fm-devin-config.sh owns it; removed by teardown
<id>.muse-session muse busy-source binding (sessions root plus task worktree) written by fm-spawn; removed by teardown
<id>.cursor-session cursor busy-source binding (projects root, task worktree, prior conversations) written by fm-spawn; removed by teardown
<id>.worker-state exact record of a deliberately stood-down ship or scout worker; bin/fm-worker-state-lib.sh owns its format and the watcher admits no suppression unless the endpoint remains provably worker-free
<id>.git-hooks/ per-task git hooksPath that strips AI commit trailers at the commit object unless config/keep-ai-trailers is present; written by fm-spawn, removed by teardown (bin/fm-git-strip-ai-trailers.sh)
<id>.reconcile-nudged epoch second of the last inventory-reconcile nudge sent to this secondmate; bin/fm-secondmate-reconcile.sh owns its per-home cooldown window
<id>.backlog-close the exact backlog transition a teardown recorded before removing the task's record, so an interrupted cleanup can still be finished at the next session start; bin/fm-backlog-transition-lib.sh owns its format and replay, and a landed transition removes it
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ Steer a worker with ordinary text through fail-closed `fm-send`: the message bec
A remote secondmate steer rides the same durable-inbox model through the remote transport; after an unconfirmed delivery, only the exact `FM_PENDING_REPLY_EXISTING_CORR=<id>` resend command printed by `fm-send` is safe because it preserves the request body for remote enqueue deduplication (`bin/fm-send.sh` header).
When a steer answers an open keyed decision or blocker, pass `fm-send`'s `--resolve-key` so the answer itself closes that decision record at answer time, identically for local and remote workers (contract: `bin/fm-send.sh` header).
`fm-send` is the data plane for text the worker should read; never use its key or text paths for interrupt, exit, or other lifecycle control, because routing-marked lifecycle text becomes chat the worker reasons about instead of executing.
Drive a worker's lifecycle through `bin/fm-control.sh <task-id> interrupt|exit|relaunch`, which owns the per-runtime mechanics, verifies each action, and never tears down or discards anything ([`docs/agent-control.md`](docs/agent-control.md)).
Drive a worker's lifecycle through `bin/fm-control.sh <task-id> interrupt|exit|stand-down|repair-worker-state|relaunch`, which owns the per-runtime mechanics, verifies each action, and never tears down or discards anything ([`docs/agent-control.md`](docs/agent-control.md)).
A secondmate's routed reply returns through status or a document pointer, not by firstmate peeking into its chat.
For the parent-owned correlation, recovery, and escalation contract on marked secondmate requests, see `bin/fm-pending-reply-lib.sh`.
When the captain adds or changes an ask mid-task, append the captain's words without added speaker labels or direct address to that brief's `## Captain's intent` and relay those words to the worker; Firstmate build constraints stay in `## Firstmate spec` or the steer.
Expand Down
4 changes: 3 additions & 1 deletion bin/fm-control-lib.sh
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,15 @@ fm_control_verbs() {
cat <<'EOF'
interrupt
exit
stand-down
repair-worker-state
relaunch
EOF
}

fm_control_verb_allowed() { # <verb>
case "${1-}" in
interrupt|exit|relaunch) return 0 ;;
interrupt|exit|stand-down|repair-worker-state|relaunch) return 0 ;;
esac
return 1
}
Expand Down
240 changes: 236 additions & 4 deletions bin/fm-control.sh

Large diffs are not rendered by default.

91 changes: 91 additions & 0 deletions bin/fm-crew-state.sh
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,8 @@ STATE="${FM_STATE_OVERRIDE:-$FM_HOME/state}"
. "$SCRIPT_DIR/fm-classify-lib.sh"
# shellcheck source=bin/fm-busy-lib.sh
. "$SCRIPT_DIR/fm-busy-lib.sh"
# shellcheck source=bin/fm-worker-state-lib.sh
. "$SCRIPT_DIR/fm-worker-state-lib.sh"
# shellcheck source=bin/fm-nm-run-lib.sh
. "$SCRIPT_DIR/fm-nm-run-lib.sh"
# shellcheck source=bin/fm-pr-lib.sh
Expand Down Expand Up @@ -316,6 +318,82 @@ fi
TASK_BACKEND=$(fm_backend_of_meta "$META")
BACKEND_TARGET=$(fm_backend_target_of_meta "$META")
EXPECTED_LABEL="fm-$ID"


# A proven intentional stand-down outranks a HISTORICAL terminal validation
# result: that run describes the last worker incarnation, while this record
# describes the task's current deliberate absence of a worker. It never
# outranks an ACTIVE run, which still owns the branch and still has a gate or a
# step to report - that reporting is the authoritative current state, and
# dropping it would hide actionable work. So the record is resolved here but
# reported only at the two points below where nothing more current exists.
WORKER_LIFECYCLE=$(fm_worker_state_status "$STATE" "$ID" "$BACKEND_TARGET")

# Report the worker-state verdict, or return 1 to let the caller carry on.
# Recheck the endpoint first so a stale record can never hide a live worker
# that must remain eligible for wedge detection.
#
# Mode `proven-only` reports just the one verdict this record can prove - the
# task really is worker-free - and stays silent otherwise, so a record that
# does NOT describe reality never masks a source that does. Mode `full` also
# surfaces the discrepancies, and is used only where the alternative is a
# guess from a pane read or a stale status log.
emit_worker_state_if_current() { # <proven-only|full>
local mode=$1
case "$WORKER_LIFECYCLE" in
stood-down)
case "$(fm_backend_agent_state "$TASK_BACKEND" "$BACKEND_TARGET" 2>/dev/null || true)" in
dead)
# A hold is only healthy while there is no work in flight. A worker-
# state record describes the absence of a worker, never the absence
# of work, so a live run on the preserved branch is reported with its
# details withheld rather than as a healthy park. The other arms below
# are not healthy holds and keep reporting on their own evidence.
if [ "$mode" = full ] && branch_run_verdict_is_active; then
emit working run-step "active run (details withheld)${FM_NM_BRANCH_RUN_ID:+${SEP}run: $FM_NM_BRANCH_RUN_ID}"
fi
emit parked worker-state "worker deliberately stood down"
;;
# A VANISHED endpoint is not the hold the operator declared. `dead` is
# the declared state: the endpoint is still there, still holds the
# worktree and the uncommitted work, and a relaunch restores the worker
# in place. `missing` means that endpoint is gone, so the hold can no
# longer be resumed where it was declared and something must be done
# about it - reporting it as a healthy park would hide exactly that.
# Only a human declaration ever makes an absent worker healthy here;
# this branch is why no absence is ever inferred to be one.
missing)
[ "$mode" = full ] || return 1
emit unknown worker-state "stood down but its endpoint is gone: $BACKEND_TARGET (relaunch cannot restore it in place)"
;;
alive)
[ "$mode" = full ] || return 1
emit unknown worker-state "record says stood down but endpoint has a live worker"
;;
*)
[ "$mode" = full ] || return 1
emit unknown worker-state "stood-down record cannot prove the endpoint remains worker-free"
;;
esac
;;
invalid)
[ "$mode" = full ] || return 1
emit unknown worker-state "invalid worker-state record; reconcile it with 'fm-control $ID repair-worker-state'"
;;
esac
return 1
}

# 0 when this branch provably owns a live no-mistakes run. Consulted only where
# a proven hold would otherwise answer, so ordinary run attribution below
# remains the single owner of run reporting.
branch_run_verdict_is_active() {
FM_NM_BRANCH_RUN_ID=
[ "$KIND" = ship ] && [ -n "$CREW_BRANCH" ] || return 1
fm_nm_branch_run_verdict "$WT" "$CREW_BRANCH" "$NM_TIMEOUT"
[ "$FM_NM_BRANCH_RUN_VERDICT" = active ]
}

pane_readable() { # <target>
case "$TASK_BACKEND" in
tmux) tmux display-message -p -t "$1" '#{pane_id}' >/dev/null 2>&1 ;;
Expand Down Expand Up @@ -1221,6 +1299,14 @@ if [ "$HAVE_RUN" = 1 ]; then
;;
esac

# A terminal run is history; an intentional stand-down published after it is
# the newer statement about this task, so it outranks the terminal outcome
# only. Anything the run still reports as live (working, parked at a gate)
# stays authoritative.
case "$RUN_STATE" in
done|failed) emit_worker_state_if_current proven-only || true ;;
esac

[ -z "$SELECTED_RUN_ID" ] || RUN_DETAIL="$RUN_DETAIL${SEP}run: $SELECTED_RUN_ID"
emit "$RUN_STATE" run-step "$RUN_DETAIL"
fi
Expand All @@ -1233,6 +1319,11 @@ fi
# both classifier-backed backends (tmux and herdr) - and every death-class
# verdict reports unknown rather than trusting a possibly-stale status log as
# the current state.
# With no run to consult, a worker-state record is the most current statement
# there is about this task - including the discrepancies, whose only remaining
# alternative is a guess from the pane or a stale status log.
emit_worker_state_if_current full || true

[ -n "$BACKEND_TARGET" ] || emit unknown none "no backend target recorded"
if ! pane_readable "$BACKEND_TARGET"; then
# A failed probe is not itself evidence the pane is gone: the herdr CLI can
Expand Down
178 changes: 178 additions & 0 deletions bin/fm-nm-run-lib.sh
Original file line number Diff line number Diff line change
Expand Up @@ -507,3 +507,181 @@ fm_nm_runs_status_for_worktree() { # <worktree> <branch> <runs-list-output> [ex
printf '%s' "$decided"
return 0
}

# The bounded corroboration window read from `no-mistakes runs`. The listing is
# repo-wide and has neither pagination nor an end-of-list marker, so it can add
# a run this branch owns but can never be asked to prove the absence of one.
fm_nm_runs_limit() {
local n=${FM_CREW_STATE_RUNS_LIMIT:-200}
case "$n" in ''|*[!0-9]*|0) n=200 ;; esac
printf '%s' "$n"
}

# `no-mistakes` answers a repository it holds no registration for with a
# not-initialized error and no rows at all. A repository with no registration
# owns no run, so that is an answer, not a failure to answer - the same
# reasoning the branchless worktree rests on. firstmate supports whole project
# modes (direct-PR, local-only) that never run `no-mistakes init`.
fm_nm_says_unregistered() { # <response-text>...
local response line
for response in "$@"; do
while IFS= read -r line; do
case "$line" in
"repo not initialized (run 'no-mistakes init' first)"|\
"error: repo not initialized (run 'no-mistakes init' first)") return 0 ;;
esac
done <<< "$response"
done
return 1
}

# Bounded `no-mistakes` call in $1 whose stdout and stderr are written into the
# variables named by $2 and $3 rather than stderr being discarded, so a caller
# can inspect the CLI's complete response when it explains a failure on either
# stream. Both variables are assigned in the caller's scope - the call must NOT
# be wrapped in a command
# substitution - and the CLI's exit status is returned, or 1 when no scratch
# file could be opened for the error stream.
fm_nm_run_capturing_stderr() { # <dir> <stdout-var> <stderr-var> <timeout_secs> <args...>
local dir=$1 outvar=$2 errvar=$3 timeout=$4 err='' rc=0 out=''
shift 4
printf -v "$outvar" '%s' ''
printf -v "$errvar" '%s' ''
if command -v mktemp >/dev/null 2>&1; then
err=$(mktemp "${TMPDIR:-/tmp}/fm-nm-err.XXXXXX" 2>/dev/null) || err=
fi
if [ -z "$err" ]; then
err="${TMPDIR:-/tmp}/fm-nm-err.$$.${RANDOM}${RANDOM}"
( set -o noclobber; : > "$err" ) 2>/dev/null || return 1
fi
if out=$(fm_nm_run_bounded "$dir" "$timeout" "$@" 2>"$err"); then rc=0; else rc=$?; fi
printf -v "$outvar" '%s' "$out"
printf -v "$errvar" '%s' "$(<"$err")"
rm -f "$err" 2>/dev/null || :
return "$rc"
}

# shellcheck disable=SC2034 # FM_NM_BRANCH_RUN_* is this function's documented output tuple.
# `fm_nm_branch_run_verdict` is the one authoritative answer to whether a
# branch owns a live no-mistakes run. It writes `active`, `quiet`, or `unknown`
# to FM_NM_BRANCH_RUN_VERDICT, with one diagnostic reason in
# FM_NM_BRANCH_RUN_REASON when the answer is unknown.
#
# THE QUESTION IS ALWAYS "DOES THIS BRANCH HAVE AN ACTIVE RUN?", AND THE BRANCH
# READ ANSWERS IT. `no-mistakes axi status` reports the repository's ACTIVE run
# and only falls back to the most recent one when nothing is in flight, so a
# successful call that places no non-terminal run on this branch is an answer:
# this branch is quiet, whether the CLI named another branch's run or had
# nothing to report at all. Only a branch read that could not be made - the CLI
# failed or timed out, named an active run without a placeable branch identity,
# or named a live run on this branch whose head cannot be placed against this
# worktree - leaves the question open, and an open question refuses the caller
# that needs it proven.
#
# The repo-wide `runs` listing is CORROBORATION ONLY. The NEWEST row for this
# branch is a second way to establish `active` when it is non-terminal (the
# listing's status column is each run's current status, so it catches a run a
# stale `axi status` answer missed). Only that newest row is read: the listing
# is ordered newest first, so an older live row never displaces the newer
# terminal result that superseded it. Its absence proves nothing: a full window, an unreadable row, a
# failed call, an unregistered repo or an absent CLI must never turn a readable
# quiet branch read into a refusal. That is why widening FM_CREW_STATE_RUNS_LIMIT
# is a reporting nicety rather than a safety setting.
#
# The verdict carries no run detail: it answers the safety question and names
# the active run's id when the branch read placed one. Callers that need run
# detail read it through their own attribution. A direct active answer skips
# the optional listing entirely.
fm_nm_branch_run_verdict() { # <worktree> <branch> <timeout_secs> [limit]
local wt=$1 branch=$2 timeout=$3 limit=${4:-}
local status_out='' status_rc status_stderr='' status_body='' direct_status=''
local run_branch_raw='' run_branch='' run_head=''
local branch_state=unknown branch_reason='' active_id=''
local inventory row st rest br sha listing_live=''
FM_NM_BRANCH_RUN_VERDICT=unknown
FM_NM_BRANCH_RUN_REASON=
FM_NM_BRANCH_RUN_ID=
case "$limit" in ''|*[!0-9]*|0) limit=$(fm_nm_runs_limit) ;; esac
[ -d "$wt" ] || {
FM_NM_BRANCH_RUN_REASON="worktree '$wt' is not readable, so whether branch '$branch' has a run in flight could not be read"
return 0
}
[ -n "$branch" ] || {
FM_NM_BRANCH_RUN_VERDICT=quiet
return 0
}
command -v no-mistakes >/dev/null 2>&1 || {
FM_NM_BRANCH_RUN_VERDICT=quiet
return 0
}
if fm_nm_run_capturing_stderr "$wt" status_out status_stderr "$timeout" axi status; then status_rc=0; else status_rc=$?; fi
if [ "$status_rc" = 0 ]; then
branch_state=quiet
status_body=$(fm_nm_trim "$status_out")
if [ -n "$status_body" ]; then
direct_status=$(fm_nm_strip_quotes "$(fm_nm_field "$status_out" status)")
run_branch_raw=$(fm_nm_trim "$(fm_nm_field "$status_out" branch)")
run_branch=$(fm_nm_strip_quotes "$run_branch_raw")
run_head=$(fm_nm_strip_quotes "$(fm_nm_field "$status_out" head)")
if [ -z "$direct_status" ]; then
branch_state=unknown
branch_reason="'no-mistakes axi status' returned a non-empty result without a readable run status for branch $branch"
elif fm_nm_run_is_active "$status_out"; then
case "$run_branch_raw" in
\"*\") ;;
*\"*) run_branch= ;;
esac
if [ -z "$run_branch" ] \
|| ! git check-ref-format --branch "$run_branch" >/dev/null 2>&1; then
branch_state=unknown
branch_reason="the active run 'no-mistakes axi status' reports has no placeable branch identity, so whether it belongs to branch $branch cannot be proved"
elif [ "$run_branch" = "$branch" ]; then
if fm_nm_head_matches_worktree "$wt" "$run_head" \
|| fm_nm_run_is_pipeline_owned_active "$status_out"; then
branch_state=active
active_id=$(fm_nm_strip_quotes "$(fm_nm_field "$status_out" id)")
else
branch_state=unknown
branch_reason="the run 'no-mistakes axi status' reports on branch $branch (head ${run_head:-unknown}) cannot be placed against this worktree's HEAD"
fi
fi
fi
fi
elif [ "$status_rc" != 0 ] \
&& fm_nm_says_unregistered "$status_out" "$status_stderr"; then
branch_state=quiet
else
branch_reason="'no-mistakes axi status' could not answer whether branch $branch has a run in flight"
fi
if [ "$branch_state" != active ] \
&& inventory=$(fm_nm_run_checked "$wt" "$timeout" runs --limit "$limit"); then
while IFS= read -r row; do
row=$(fm_nm_trim "$row")
[ -n "$row" ] || continue
case "$row" in *" "*) ;; *) continue ;; esac
st=${row%% *}
rest=$(fm_nm_trim "${row#* }")
[ -n "$rest" ] || continue
br=${rest%% *}
rest=$(fm_nm_trim "${rest#* }")
sha=${rest%% *}
{ [ -n "$br" ] && [ -n "$sha" ]; } || continue
[ "$br" = "$branch" ] || continue
case "$st" in
completed|failed|cancelled) ;;
*) listing_live=$st ;;
esac
break
done <<< "$inventory"
fi
if [ "$branch_state" = active ]; then
FM_NM_BRANCH_RUN_VERDICT=active
FM_NM_BRANCH_RUN_ID=$active_id
elif [ -n "$listing_live" ]; then
FM_NM_BRANCH_RUN_VERDICT=active
elif [ "$branch_state" = quiet ]; then
FM_NM_BRANCH_RUN_VERDICT=quiet
else
FM_NM_BRANCH_RUN_REASON=$branch_reason
fi
}
22 changes: 21 additions & 1 deletion bin/fm-send.sh
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@
# durably sent (recorded); nonzero = nothing was confirmed delivered and a
# resend is appropriate (unresolvable target, an endpoint that cannot be
# locked and revalidated or that retired or changed, an unwritable record, a
# failed or lost remote transport) or a decision-close append failed after
# failed or lost remote transport, or a task deliberately recorded as having
# no worker) or a decision-close append failed after
# delivery (the error then carries the exact manual close). The remote enqueue
# is idempotent: the remote leg deduplicates an exact re-run of the same
# request onto the existing record (bin/fm-task-inbox-lib.sh), so after a lost
Expand Down Expand Up @@ -242,6 +243,8 @@ fi
. "$SCRIPT_DIR/fm-backend.sh"
# shellcheck source=bin/fm-control-lib.sh
. "$SCRIPT_DIR/fm-control-lib.sh"
# shellcheck source=bin/fm-worker-state-lib.sh
. "$SCRIPT_DIR/fm-worker-state-lib.sh"
# shellcheck source=bin/fm-marker-lib.sh
. "$SCRIPT_DIR/fm-marker-lib.sh"
# shellcheck source=bin/fm-pending-reply-lib.sh
Expand Down Expand Up @@ -443,6 +446,23 @@ fm_send_resolve_target "$RAW_TARGET" || exit 1
T=$RESOLVED_TARGET
shift

# A held task with a proven stood-down worker in its recorded endpoint has no
# receiver for either inbox or typed-plane input. Refuse instead of leaving an
# unread durable instruction. Missing or unprovable endpoints remain under
# ordinary supervision, and a stale record never blocks a live endpoint.
if [ -n "$TARGET_META" ] && [ "$TARGET_BACKEND" != remote ]; then
TARGET_WORKER_ID=$(fm_send_id_from_meta "$TARGET_META")
if [ "$(fm_worker_state_status "$STATE" "$TARGET_WORKER_ID" "$T")" = stood-down ]; then
TARGET_WORKER_AGENT_STATE=$(fm_backend_agent_state "$TARGET_BACKEND" "$T" 2>/dev/null || true)
case "$TARGET_WORKER_AGENT_STATE" in
dead)
echo "error: task $TARGET_WORKER_ID deliberately has no worker; relaunch it before sending an instruction" >&2
exit 1
;;
esac
fi
fi

# Supervision lease guard: a steer is overlap territory between the two Pi
# supervision actors, so refuse while the OTHER actor holds this task's live
# lease. A home with no supervision branch has no lease files and passes
Expand Down
Loading