From 438796a0e9a5314d22a0cbb4d3b3f71196c1d03f Mon Sep 17 00:00:00 2001 From: dnth Date: Fri, 16 Oct 2026 09:37:42 +0800 Subject: [PATCH 1/5] fix(omp): recover stale markers through relaunch owner --- bin/fm-control-lib.sh | 91 +++++-- bin/fm-spawn.sh | 2 + docs/configuration.md | 5 +- tests/fm-omp-secondmate.test.sh | 75 +++++- ...fm-remote-secondmate-lifecycle-e2e.test.sh | 187 ++++++++------ tests/fm-secondmate-restart.test.sh | 232 +++++++++++++++--- 6 files changed, 468 insertions(+), 124 deletions(-) diff --git a/bin/fm-control-lib.sh b/bin/fm-control-lib.sh index aac60ed3a42..2b89348d00a 100644 --- a/bin/fm-control-lib.sh +++ b/bin/fm-control-lib.sh @@ -197,32 +197,29 @@ fm_control_exit_command() { # fm_control_omp_home_owner_pid() { # local home_state=$1 pid="" if [ -f "$home_state/.lock" ] && [ ! -L "$home_state/.lock" ]; then - IFS= read -r pid < "$home_state/.lock" || pid="" + pid=$(cat "$home_state/.lock" 2>/dev/null) || return 1 fi case "$pid" in - ''|*[!0-9]*|0|1) return 1 ;; + ''|*[!0-9]*|0*|1) return 1 ;; esac printf '%s' "$pid" } -# Retire the session artifacts an OMP second mate leaves behind, so its -# replacement launch is not refused by its own predecessor's leftovers. +# Retire OMP session artifacts only after the prior session is absent and its +# primary marker is tied to this home, this runtime, and a known extension +# closure. A stale marker is not disposable just because its recorded PID is +# dead: foreign, malformed, or unexplained state must keep refusing relaunch. # -# The order is the contract. Both owners have to be proven gone FIRST - the -# recorded endpoint, and the process holding the home session - because an -# artifact removed under a live agent is indistinguishable from a fresh one, and -# the launch owner would then accept a duplicate against a still-running mate. -# -# FM_RESTART_STOP_CMD replaces the signal and the liveness probe for the home -# session owner (default: kill; invoked as " " to stop it and -# " -0 " to probe it, so one override covers both halves). It exists so -# tests/fm-secondmate-restart.test.sh can drive this sequence deterministically; -# unset, this is an ordinary kill and the behavior is unchanged. -# FM_RESTART_STOP_WAIT seconds to wait for the owner to go (default 30) +# FM_RESTART_STOP_CMD replaces the signal and liveness probe for the home lock +# owner (default: kill; invoked as " " to stop it and " -0 " +# to probe it). A distinct marker PID is probed but never signaled. This exists +# so tests/fm-secondmate-restart.test.sh can drive the sequence deterministically. +# FM_RESTART_STOP_WAIT seconds to wait for the lock owner to go (default 30) # FM_RESTART_STOP_POLL seconds between absence probes (default 1) fm_control_omp_secondmate_prepare_relaunch() { # local state=$1 id=$2 worktree=$3 backend=$4 target=$5 meta=$6 omp_state - local stop_cmd stop_wait stop_poll owner_pid elapsed proven + local stop_cmd stop_wait stop_poll owner_pid elapsed proven marker marker_pid + local expected_bun expected_bin current_version head committed_version lib_dir lock_present omp_state=$(fm_backend_agent_state "$backend" "$target" "$meta" 2>/dev/null || printf 'unreadable') case "$omp_state" in missing) ;; @@ -238,7 +235,61 @@ fm_control_omp_secondmate_prepare_relaunch() { # &2; return 1; } + + marker="$worktree/state/.omp-primary-extension-loaded" + marker_pid= + if [ -e "$marker" ] || [ -L "$marker" ]; then + [ -f "$marker" ] && [ ! -L "$marker" ] \ + || { printf 'error: OMP primary marker is not an ordinary file; refusing relaunch\n' >&2; return 1; } + lib_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P) || return 1 + # shellcheck source=bin/fm-primary-watch-version-lib.sh + . "$lib_dir/fm-primary-watch-version-lib.sh" || return 1 + # shellcheck source=bin/fm-omp-process-lib.sh + . "$lib_dir/fm-omp-process-lib.sh" || return 1 + fm_omp_primary_marker_read "$marker" \ + || { printf 'error: OMP primary marker is malformed; refusing relaunch\n' >&2; return 1; } + marker_pid=$FM_OMP_MARKER_PID + case "$marker_pid" in + 0|1|0*|*[!0-9]*) + printf 'error: OMP primary marker has an invalid owner PID; refusing relaunch\n' >&2 + return 1 + ;; + esac + expected_bun=$(fm_meta_get "$meta" omp_bun 2>/dev/null || true) + expected_bin=$(fm_meta_get "$meta" omp_bin 2>/dev/null || true) + if [ -z "$expected_bun" ] || [ -z "$expected_bin" ] \ + || [ "$FM_OMP_MARKER_BUN" != "$expected_bun" ] \ + || [ "$FM_OMP_MARKER_BIN" != "$expected_bin" ]; then + printf 'error: OMP primary marker runtime does not match the recorded home; refusing relaunch\n' >&2 + return 1 + fi + current_version=$(fm_primary_watch_version "$worktree/.omp/extensions/fm-primary-omp.ts" "$worktree" 2>/dev/null) \ + || { printf 'error: current OMP primary closure is unreadable; refusing relaunch\n' >&2; return 1; } + if [ "$FM_OMP_MARKER_VERSION" != "$current_version" ]; then + head=$(git -C "$worktree" rev-parse --verify 'HEAD^{commit}' 2>/dev/null) \ + || { printf 'error: OMP home HEAD is unverifiable; refusing relaunch\n' >&2; return 1; } + committed_version=$(fm_omp_primary_version_at_commit "$worktree" "$head" 2>/dev/null) \ + || { printf 'error: committed OMP primary closure is unreadable; refusing relaunch\n' >&2; return 1; } + [ "$committed_version" = "$current_version" ] \ + || { printf 'error: OMP primary closure differs from the recorded home revision; refusing relaunch\n' >&2; return 1; } + fm_omp_primary_version_fast_forwarded_from "$worktree" "$FM_OMP_MARKER_VERSION" >/dev/null 2>&1 \ + || { printf 'error: OMP primary marker version is not explained by recorded fast-forward history; refusing relaunch\n' >&2; return 1; } + fi + fi + + lock_present=0 + if [ -e "$worktree/state/.lock" ] || [ -L "$worktree/state/.lock" ]; then + lock_present=1 + fi if owner_pid=$(fm_control_omp_home_owner_pid "$worktree/state"); then + if [ -n "$marker_pid" ] && [ "$marker_pid" != "$owner_pid" ] \ + && "$stop_cmd" -0 "$marker_pid" >/dev/null 2>&1; then + printf 'error: OMP primary marker owner is still live; refusing relaunch\n' >&2 + return 1 + fi "$stop_cmd" "$owner_pid" >/dev/null 2>&1 || true proven=0 elapsed=0 @@ -252,6 +303,14 @@ fm_control_omp_secondmate_prepare_relaunch() { # /dev/null 2>&1; then + printf 'error: OMP primary marker owner is still live; refusing relaunch\n' >&2 + return 1 + fi + elif [ "$lock_present" -eq 1 ] || [ -n "$marker_pid" ]; then + printf 'error: OMP home lock owner is unverifiable; refusing relaunch\n' >&2 + return 1 fi rm -f -- "$state/$id.omp-ext.ts" "$state/$id.omp-ready" \ "$state/$id.omp-started" "$worktree/state/.omp-primary-extension-loaded" diff --git a/bin/fm-spawn.sh b/bin/fm-spawn.sh index 7bb3618ceb9..5a865c71913 100755 --- a/bin/fm-spawn.sh +++ b/bin/fm-spawn.sh @@ -3203,6 +3203,8 @@ if [ "$KIND" = secondmate ]; then if ! fm_omp_primary_marker_read "$OMP_PRIMARY_MARKER" \ || [ -z "$OMP_EXPECTED_MARKER_VERSION" ] \ || [ "$FM_OMP_MARKER_VERSION" != "$OMP_EXPECTED_MARKER_VERSION" ] \ + || [ "$FM_OMP_MARKER_BUN" != "$OMP_BUN_CANON" ] \ + || [ "$FM_OMP_MARKER_BIN" != "$OMP_BIN_CANON" ] \ || [ "$FM_OMP_MARKER_PID" != "$OMP_STALE_LOCK_PID" ]; then echo "error: OMP secondmate home has an unowned or malformed primary-integration marker at $OMP_PRIMARY_MARKER; refusing duplicate launch" >&2 exit 1 diff --git a/docs/configuration.md b/docs/configuration.md index 1ca43cae5c6..9ad3073dd55 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -326,7 +326,10 @@ For OMP secondmate launches, it explicitly loads that home's `.omp/extensions/fm After proving a failed launch's owned endpoint stopped, cleanup retires its generation artifacts while preserving the persistent home, endpoint metadata, durable inbox, and real session files. Without a live marker or lock owner, cleanup preserves an already-correct session binding or atomically repairs it to the pre-launch retained session, or to the unique saved session when no valid binding exists; an empty store permits pointer removal, while ambiguous saved sessions require explicit reconciliation. A relaunch whose recorded endpoint is proven dead or missing retires the prior generation's runtime markers (adapter, ready, started, doorbell markers and request receipts) only after the endpoint, session-lock, and integration-marker live-owner checks pass, so a wake-on-delivery restore after a successful generation is accepted. -[`tests/fm-omp-secondmate.test.sh`](../tests/fm-omp-secondmate.test.sh) and [`tests/fm-remote-secondmate-lifecycle-e2e.test.sh`](../tests/fm-remote-secondmate-lifecycle-e2e.test.sh) cover failed-bind recovery and post-generation restore relaunch for local and remote homes. +For OMP secondmates, the `fm-control.sh` relaunch owner parses the four-line primary marker and requires its Bun and entrypoint paths to match the recorded home runtime. It proves the `.lock` owner and any distinct marker PID absent before retiring the marker; if the marker hash is older, the current working closure must match `HEAD` and the hash must occur on that home's recorded fast-forward history. +The owner path removes only stale runtime markers, preserving `state/.omp-session` and saved session files; direct `fm-spawn.sh` still refuses an old-version marker, PID mismatch, or foreign runtime that the owner path has not safely reconciled. +Malformed, unverifiable, symlinked, non-file, foreign, live-owner, or unexplained-history state refuses recovery. +[`tests/fm-omp-secondmate.test.sh`](../tests/fm-omp-secondmate.test.sh), [`tests/fm-secondmate-restart.test.sh`](../tests/fm-secondmate-restart.test.sh), and [`tests/fm-remote-secondmate-lifecycle-e2e.test.sh`](../tests/fm-remote-secondmate-lifecycle-e2e.test.sh) cover direct-launch refusals, owner-path safety, retained-session preservation, and updated-extension relaunch in fixture homes. Firstmate's tracked `.omp/config.yml` keeps OMP's todo tool enabled but disables its end-of-turn incomplete-todo reminders because the session todo is a projection of long-lived fleet work rather than work that must finish within the current captain turn. This prevents OMP from scheduling a second model turn after a captain-facing answer solely because projected fleet work remains incomplete. The separately discovered `.omp/extensions/fm-fleet-hooks.ts` fails open while redacting credential-shaped values from text chunks in tool results, reporting valid `fm-todo-project.sh --check` drift through hidden next-turn context, and injecting a bounded roster, OPEN DECISIONS, in-flight PR, and Ready/In-flight backlog snapshot during compaction. diff --git a/tests/fm-omp-secondmate.test.sh b/tests/fm-omp-secondmate.test.sh index 4ddc9d33c1b..2ec7b1d69bf 100755 --- a/tests/fm-omp-secondmate.test.sh +++ b/tests/fm-omp-secondmate.test.sh @@ -892,7 +892,7 @@ test_legacy_launch_rejects_runtime_fallback() { } test_duplicate_recovery_states() { - local out before after mode expected version invalid_lock + local out before after mode expected version invalid_lock lock_pid marker_pid old_version for row in 'live already has a live agent' 'ambiguous has an ambiguous agent process' 'unreadable endpoint state is unreadable'; do mode=${row%% *} @@ -933,13 +933,78 @@ test_duplicate_recovery_states() { assert_contains "$out" 'unowned or malformed primary-integration marker' "trailing integration marker refusal was not actionable" [ "$(count_new_windows)" = 0 ] || fail "trailing integration marker refusal created another endpoint" - setup_case duplicate-owned-stale-marker + setup_case duplicate-old-version-marker-refused write_meta version=$(fm_primary_watch_version "$HOME_DIR/.omp/extensions/fm-primary-omp.ts" "$HOME_DIR") - printf '%s\n99999999\n%s\n%s\n' "$version" "$TEST_OMP_BUN" "$TEST_OMP_BIN" > "$HOME_DIR/state/.omp-primary-extension-loaded" + sleep 0.05 & + lock_pid=$! + sleep 0.05 & + marker_pid=$! + wait "$lock_pid" || fail "fixture lock owner did not exit" + wait "$marker_pid" || fail "fixture marker owner did not exit" + old_version="${version%?}0" + [ "$old_version" != "$version" ] || old_version="${version%?}1" + [ "$lock_pid" != "$marker_pid" ] || fail "fixture lock and marker owners unexpectedly share a PID" + if kill -0 "$lock_pid" 2>/dev/null || kill -0 "$marker_pid" 2>/dev/null; then + fail "fixture lock or marker owner is still alive" + fi + printf '%s\n%s\n%s\n%s\n' "$old_version" "$marker_pid" "$TEST_OMP_BUN" "$TEST_OMP_BIN" \ + > "$HOME_DIR/state/.omp-primary-extension-loaded" + printf '%s\n' "$lock_pid" > "$HOME_DIR/state/.lock" + out=$(FM_TEST_STATE_MODE=missing run_spawn 2>&1) \ + && fail "direct OMP launch bypassed the owner relaunch path for an old-version marker" + assert_contains "$out" 'unowned or malformed primary-integration marker' \ + "direct OMP launch did not preserve its refusal for an old-version marker" + [ "$(count_new_windows)" = 0 ] || fail "old-version marker refusal created another endpoint" + printf 'fixture stale marker: version=%s current=%s lock_pid=%s alive=no marker_pid=%s alive=no\n' \ + "$old_version" "$version" "$lock_pid" "$marker_pid" + + setup_case duplicate-live-marker-owner + write_meta + version=$(fm_primary_watch_version "$HOME_DIR/.omp/extensions/fm-primary-omp.ts" "$HOME_DIR") + printf '%s\n%s\n%s\n%s\n' "$version" "$AGENT_PID" "$TEST_OMP_BUN" "$TEST_OMP_BIN" \ + > "$HOME_DIR/state/.omp-primary-extension-loaded" + printf '%s\n' "$AGENT_PID" > "$HOME_DIR/state/.lock" + out=$(FM_TEST_STATE_MODE=missing run_spawn 2>&1) \ + && fail "OMP secondmate replaced a marker owned by a live process" + assert_contains "$out" 'already has a live session-lock owner' "live marker-owner refusal was not actionable" + [ "$(count_new_windows)" = 0 ] || fail "live marker refusal created another endpoint" + + setup_case duplicate-foreign-marker + write_meta + version=$(fm_primary_watch_version "$HOME_DIR/.omp/extensions/fm-primary-omp.ts" "$HOME_DIR") + printf '%s\n99999999\n%s\n%s\n' "$version" "$TEST_OMP_BUN" "$CASE/foreign-omp" \ + > "$HOME_DIR/state/.omp-primary-extension-loaded" printf '99999999\n' > "$HOME_DIR/state/.lock" - out=$(FM_TEST_STATE_MODE=missing run_spawn 2>&1) || fail "OMP secondmate did not recover from its proven stale integration marker: $out" - assert_contains "$(cat "$TMUX_LOG")" 'new-window' "proven stale integration marker did not permit recovery" + out=$(FM_TEST_STATE_MODE=missing run_spawn 2>&1) \ + && fail "OMP secondmate replaced a foreign integration marker" + assert_contains "$out" 'unowned or malformed primary-integration marker' \ + "foreign marker refusal was not actionable" + [ "$(count_new_windows)" = 0 ] || fail "foreign marker refusal created another endpoint" + + for marker_shape in symlink directory reserved-pid; do + setup_case "duplicate-marker-$marker_shape" + write_meta + version=$(fm_primary_watch_version "$HOME_DIR/.omp/extensions/fm-primary-omp.ts" "$HOME_DIR") + case "$marker_shape" in + symlink) + printf 'outside marker target\n' > "$CASE/marker-target" + ln -s "$CASE/marker-target" "$HOME_DIR/state/.omp-primary-extension-loaded" + ;; + directory) + mkdir "$HOME_DIR/state/.omp-primary-extension-loaded" + ;; + reserved-pid) + printf '%s\n1\n%s\n%s\n' "$version" "$TEST_OMP_BUN" "$TEST_OMP_BIN" \ + > "$HOME_DIR/state/.omp-primary-extension-loaded" + ;; + esac + printf '99999999\n' > "$HOME_DIR/state/.lock" + out=$(FM_TEST_STATE_MODE=missing run_spawn 2>&1) \ + && fail "OMP secondmate accepted $marker_shape primary marker" + assert_contains "$out" 'primary-integration marker' "$marker_shape marker refusal was not actionable" + [ "$(count_new_windows)" = 0 ] || fail "$marker_shape marker refusal created another endpoint" + done setup_case duplicate-dead write_meta diff --git a/tests/fm-remote-secondmate-lifecycle-e2e.test.sh b/tests/fm-remote-secondmate-lifecycle-e2e.test.sh index 68d8a2e8f46..d9a794b3c9e 100755 --- a/tests/fm-remote-secondmate-lifecycle-e2e.test.sh +++ b/tests/fm-remote-secondmate-lifecycle-e2e.test.sh @@ -1332,36 +1332,39 @@ pass "remote spawn refuses a retired route without republishing its metadata" # accepted independently, then a second route drives the real OMP launch across # the encoded SSH/job boundary into the remote Herdr pane and back into parent # metadata. -FALLBACK_REMOTE_HOME="$TMP_ROOT/remote-fallback-omp-home" +FALLBACK_ID="fallback-omp-$BASHPID" +FALLBACK_REMOTE_HOME="$TMP_ROOT/remote-$FALLBACK_ID-home" printf 'claude opus medium\n' > "$PARENT/config/secondmate-harness" printf 'omp test/model low\n' > "$PARENT/config/secondmate-harness-fallback" FM_SECONDMATE_CHARTER='Exercise an OMP remote fallback configuration.' \ FM_SECONDMATE_SCOPE='remote fallback validation' \ - remote_env "$ROOT/bin/fm-remote-home-seed.sh" fallback-omp remote-mac "$REMOTE_ROOT" \ + remote_env "$ROOT/bin/fm-remote-home-seed.sh" "$FALLBACK_ID" remote-mac "$REMOTE_ROOT" \ "$FALLBACK_REMOTE_HOME" --no-projects >/dev/null \ || fail "remote OMP fallback route seeding failed" -remote_env "$ROOT/bin/fm-spawn.sh" fallback-omp --secondmate > "$TMP_ROOT/fallback-omp.out" 2>&1 \ - || fail "remote spawn rejected OMP as the verified fallback harness: $(cat "$TMP_ROOT/fallback-omp.out")" -assert_grep 'harness=claude' "$PARENT/state/fallback-omp.meta" \ +remote_env "$ROOT/bin/fm-spawn.sh" "$FALLBACK_ID" --secondmate \ + > "$TMP_ROOT/$FALLBACK_ID.out" 2>&1 \ + || fail "remote spawn rejected OMP as the verified fallback harness: $(cat "$TMP_ROOT/$FALLBACK_ID.out")" +assert_grep 'harness=claude' "$PARENT/state/$FALLBACK_ID.meta" \ "fresh primary quota unexpectedly selected the OMP fallback" -assert_grep 'secondmate_model_source=primary' "$PARENT/state/fallback-omp.meta" \ +assert_grep 'secondmate_model_source=primary' "$PARENT/state/$FALLBACK_ID.meta" \ "accepted OMP fallback configuration lost the remote primary selection record" +OMP_ID="remote-omp-$BASHPID" OMP_REMOTE_HOME="$TMP_ROOT/remote-omp-home" printf 'omp test/model low\n' > "$PARENT/config/secondmate-harness" rm -f "$PARENT/config/secondmate-harness-fallback" FM_SECONDMATE_CHARTER='Exercise the remote OMP agent launch.' \ FM_SECONDMATE_SCOPE='remote OMP launch validation' \ - remote_env "$ROOT/bin/fm-remote-home-seed.sh" remote-omp remote-mac "$REMOTE_ROOT" \ + remote_env "$ROOT/bin/fm-remote-home-seed.sh" "$OMP_ID" remote-mac "$REMOTE_ROOT" \ "$OMP_REMOTE_HOME" --no-projects >/dev/null \ || fail "remote OMP route seeding failed" -if remote_env "$ROOT/bin/fm-spawn.sh" remote-omp --secondmate --harness 'raw-agent --flag' \ +if remote_env "$ROOT/bin/fm-spawn.sh" "$OMP_ID" --secondmate --harness 'raw-agent --flag' \ > "$TMP_ROOT/remote-omp-raw.out" 2>&1; then fail "remote secondmate accepted a raw launch command after OMP was allowlisted" fi assert_grep 'requires a verified harness adapter, not a raw launch command' \ "$TMP_ROOT/remote-omp-raw.out" "remote raw-command refusal weakened while allowlisting OMP" -if remote_env "$ROOT/bin/fm-spawn.sh" remote-omp --secondmate --harness omp \ +if remote_env "$ROOT/bin/fm-spawn.sh" "$OMP_ID" --secondmate --harness omp \ --prewalk-into test/model > "$TMP_ROOT/remote-omp-prewalk.out" 2>&1; then fail "remote secondmate accepted a Prewalk target that its control protocol cannot carry" fi @@ -1453,9 +1456,9 @@ setInterval(() => {}, 1_000); JS chmod +x "$REMOTE_ROOT/bin/omp" OMP_CONTROL_STATE="$OMP_REMOTE_HOME/state/parent-route" -OMP_READY="$OMP_CONTROL_STATE/remote-omp.omp-doorbell-ready" -OMP_INBOX="$OMP_CONTROL_STATE/remote-omp.inbox" -OMP_TURN_STARTED="$OMP_CONTROL_STATE/remote-omp.omp-started" +OMP_READY="$OMP_CONTROL_STATE/$OMP_ID.omp-doorbell-ready" +OMP_INBOX="$OMP_CONTROL_STATE/$OMP_ID.inbox" +OMP_TURN_STARTED="$OMP_CONTROL_STATE/$OMP_ID.omp-started" OMP_SENT="$TMP_ROOT/remote-omp-send-message.log" OMP_SKIP_HANDLED="$TMP_ROOT/remote-omp-skip-handled" OMP_DELAYED_HANDLED="$TMP_ROOT/remote-omp-delayed-handled" @@ -1483,21 +1486,21 @@ env -u OMP_SKIP_SETUP "$REMOTE_ROOT/bin/bun" "$REMOTE_ROOT/bin/omp" \ [ "$setup_rc" = 42 ] || fail 'remote OMP fixture accepted a launch without setup bypass' assert_absent "$TMP_ROOT/remote-omp-first-turn" 'setup refusal published a first turn' : > "$HERDR_LOG" -remote_env "$ROOT/bin/fm-spawn.sh" remote-omp --secondmate > "$TMP_ROOT/remote-omp-launch.out" 2>&1 \ +remote_env "$ROOT/bin/fm-spawn.sh" "$OMP_ID" --secondmate > "$TMP_ROOT/remote-omp-launch.out" 2>&1 \ || fail "verified remote OMP secondmate launch failed: $(cat "$TMP_ROOT/remote-omp-launch.out") $(cat "$TMP_ROOT/execute-omp-launch.out")" -assert_grep 'harness=omp' "$PARENT/state/remote-omp.meta" \ +assert_grep 'harness=omp' "$PARENT/state/$OMP_ID.meta" \ "parent metadata rejected the OMP harness returned by the remote launch" -assert_grep 'harness=omp' "$OMP_REMOTE_HOME/state/parent-route/remote-omp.meta" \ +assert_grep 'harness=omp' "$OMP_REMOTE_HOME/state/parent-route/$OMP_ID.meta" \ "remote endpoint metadata did not preserve its OMP harness" -assert_grep "omp_bun=$REMOTE_OMP_BUN" "$PARENT/state/remote-omp.meta" \ +assert_grep "omp_bun=$REMOTE_OMP_BUN" "$PARENT/state/$OMP_ID.meta" \ "parent route metadata did not preserve the remote OMP runtime identity" -assert_grep "omp_bin=$REMOTE_OMP_BIN" "$PARENT/state/remote-omp.meta" \ +assert_grep "omp_bin=$REMOTE_OMP_BIN" "$PARENT/state/$OMP_ID.meta" \ "parent route metadata did not preserve the remote OMP entrypoint identity" OMP_REMOTE_LAUNCH=$(grep -F 'FM_OMP_SESSION_POINTER=' "$HERDR_LOG" | tail -1 || true) assert_contains "$OMP_REMOTE_LAUNCH" \ "FM_OMP_BUN='$REMOTE_OMP_BUN' FM_OMP_BIN='$REMOTE_OMP_BIN'" \ "remote OMP pane did not receive the canonical runtime and entrypoint identities" -OMP_REMOTE_PANE=$(sed -n 's/^herdr_pane_id=//p' "$OMP_REMOTE_HOME/state/parent-route/remote-omp.meta") +OMP_REMOTE_PANE=$(sed -n 's/^herdr_pane_id=//p' "$OMP_REMOTE_HOME/state/parent-route/$OMP_ID.meta") assert_grep "pane run $OMP_REMOTE_PANE /bin/bash '" "$HERDR_LOG" \ "remote OMP pane did not stage its Bash-owned script invocation" assert_contains "$OMP_REMOTE_LAUNCH" "PATH='$REMOTE_ROOT/bin:" \ @@ -1511,13 +1514,13 @@ assert_contains "$OMP_REMOTE_LAUNCH" \ assert_contains "$OMP_REMOTE_LAUNCH" "--session-dir '$OMP_REMOTE_HOME/state/omp-sessions'" \ "remote OMP pane did not receive its home-owned durable session directory" assert_contains "$OMP_REMOTE_LAUNCH" \ - "FM_OMP_TASK_INBOX_DIR='$OMP_REMOTE_HOME/state/parent-route/remote-omp.inbox'" \ + "FM_OMP_TASK_INBOX_DIR='$OMP_REMOTE_HOME/state/parent-route/$OMP_ID.inbox'" \ "remote OMP extension did not receive its parent-route canonical inbox" assert_contains "$OMP_REMOTE_LAUNCH" \ - "FM_OMP_TASK_DOORBELL_READY='$OMP_REMOTE_HOME/state/parent-route/remote-omp.omp-doorbell-ready'" \ + "FM_OMP_TASK_DOORBELL_READY='$OMP_REMOTE_HOME/state/parent-route/$OMP_ID.omp-doorbell-ready'" \ "remote OMP extension did not receive its parent-route doorbell readiness marker" assert_contains "$OMP_REMOTE_LAUNCH" \ - "FM_OMP_TASK_TURN_STARTED='$OMP_REMOTE_HOME/state/parent-route/remote-omp.omp-started'" \ + "FM_OMP_TASK_TURN_STARTED='$OMP_REMOTE_HOME/state/parent-route/$OMP_ID.omp-started'" \ "remote OMP extension did not receive its parent-route turn-start marker" OMP_LISTENER_PID=$(cat "$OMP_ACTIVE_PID") @@ -1542,15 +1545,15 @@ PATH="$REMOTE_ROOT/bin:$PATH" FM_OMP_PROCESS_EXPECTED_BUN="$REMOTE_OMP_BUN" \ fm_omp_process_matches "$comm" "$args" "$2" ' _ "$REMOTE_ROOT" "$OMP_LISTENER_PID" \ || fail "remote OMP listener process did not satisfy the exact delivery identity:"$'\n'"$OMP_PROCESS" -[ "$(remote_env "$ROOT/bin/fm-on.sh" remote-omp fm-remote-secondmate-control.sh state remote-omp)" = alive ] \ +[ "$(remote_env "$ROOT/bin/fm-on.sh" "$OMP_ID" fm-remote-secondmate-control.sh state "$OMP_ID")" = alive ] \ || fail 'executed remote OMP launch did not bind its endpoint' [ "$(sed -n '2p' "$OMP_REMOTE_HOME/state/.omp-primary-extension-loaded")" = "$OMP_LISTENER_PID" ] \ || fail 'remote OMP bind retained a manufactured owner instead of the launched process' OMP_TARGET=$(FM_ROOT_OVERRIDE="$REMOTE_ROOT" /bin/bash -c \ '. "$1/bin/fm-backend.sh"; fm_backend_target_of_meta "$2"' _ "$REMOTE_ROOT" \ - "$OMP_CONTROL_STATE/remote-omp.meta") \ + "$OMP_CONTROL_STATE/$OMP_ID.meta") \ || fail "could not recover the legacy explicit remote OMP target" -OMP_PANE=$(sed -n 's/^herdr_pane_id=//p' "$OMP_CONTROL_STATE/remote-omp.meta") +OMP_PANE=$(sed -n 's/^herdr_pane_id=//p' "$OMP_CONTROL_STATE/$OMP_ID.meta") [ -n "$OMP_PANE" ] || fail "remote OMP endpoint metadata omitted its Herdr pane identity" "$REMOTE_ROOT/bin/herdr" agent get "$OMP_PANE" >/dev/null rm -f "$OMP_ACTIVE_PID" @@ -1576,7 +1579,7 @@ printf '%s\n' "$OMP_LISTENER_PID" > "$OMP_ACTIVE_PID" # /exit to fall through into the remote OMP inbox plane. : > "$HERDR_LOG" : > "$OMP_TYPED_INPUT" -remote_env "$ROOT/bin/fm-send.sh" fm-remote-omp /exit \ +remote_env "$ROOT/bin/fm-send.sh" "fm-$OMP_ID" /exit \ > "$TMP_ROOT/remote-omp-exit.out" 2>&1 || true assert_grep 'pane send-text' "$HERDR_LOG" \ "remote OMP /exit stopped using the typed lifecycle path" @@ -1590,7 +1593,7 @@ assert_grep 'pane send-text' "$HERDR_LOG" \ # worker's durable handled move as the acknowledgement. : > "$HERDR_LOG" : > "$OMP_SENT" -remote_env "$ROOT/bin/fm-send.sh" fm-remote-omp "ordinary remote OMP steer" \ +remote_env "$ROOT/bin/fm-send.sh" "fm-$OMP_ID" "ordinary remote OMP steer" \ > "$TMP_ROOT/remote-omp-delivery.out" 2>&1 \ || fail "bound remote OMP inbox delivery failed:"$'\n'"$(cat "$TMP_ROOT/remote-omp-delivery.out")"$'\n'"listener:"$'\n'"$(cat "$TMP_ROOT/execute-omp-launch.out")"$'\n'"programmatic sends:"$'\n'"$(cat "$OMP_SENT" 2>/dev/null)" grep -Eq '^request=[0-9a-f]{16} record=001 state=(recorded|handled)$' "$TMP_ROOT/remote-omp-delivery.out" \ @@ -1612,7 +1615,7 @@ assert_no_grep 'pane send-keys' "$HERDR_LOG" \ BAD_EXIT="[fm-from-firstmate]"$'\xE2\x81\xA3'"/exit" set +e -remote_env "$ROOT/bin/fm-on.sh" remote-omp fm-remote-secondmate-control.sh send remote-omp "$BAD_EXIT" \ +remote_env "$ROOT/bin/fm-on.sh" "$OMP_ID" fm-remote-secondmate-control.sh send "$OMP_ID" "$BAD_EXIT" \ > "$TMP_ROOT/remote-omp-bad-exit.out" 2>&1 bad_exit_rc=$? set -e @@ -1622,7 +1625,7 @@ assert_grep 'canonical parent correlation' "$TMP_ROOT/remote-omp-bad-exit.out" \ rm -f "$OMP_READY" set +e -remote_env "$ROOT/bin/fm-send.sh" fm-remote-omp "extension unavailable" \ +remote_env "$ROOT/bin/fm-send.sh" "fm-$OMP_ID" "extension unavailable" \ > "$TMP_ROOT/remote-omp-unavailable.out" 2>&1 unavailable_rc=$? set -e @@ -1638,7 +1641,7 @@ printf '%s\n' "$OMP_LISTENER_PID" > "$OMP_READY" # state=recorded, and the later handled/ move remains only an acknowledgement. touch "$OMP_DELAYED_HANDLED" set +e -remote_env "$ROOT/bin/fm-send.sh" fm-remote-omp "delayed acknowledgement" \ +remote_env "$ROOT/bin/fm-send.sh" "fm-$OMP_ID" "delayed acknowledgement" \ > "$TMP_ROOT/remote-omp-delayed.out" 2>&1 delayed_rc=$? set -e @@ -1654,7 +1657,7 @@ rm -f "$OMP_DELAYED_HANDLED" # one programmatic doorbell, never a replay of a prior request. sent_before_retry=$(wc -l < "$OMP_SENT" | tr -d ' ') set +e -remote_env "$ROOT/bin/fm-on.sh" remote-omp fm-remote-secondmate-control.sh send remote-omp \ +remote_env "$ROOT/bin/fm-on.sh" "$OMP_ID" fm-remote-secondmate-control.sh send "$OMP_ID" \ "[fm-from-firstmate]"$'\xE2\x81\xA3'"corr=0123456789abcdef durable contract" \ > "$TMP_ROOT/remote-omp-retry.out" 2>&1 control_rc=$? @@ -1668,23 +1671,23 @@ grep -Eq '^request=0123456789abcdef record=003 state=recorded$' "$TMP_ROOT/remot [ "$(wc -l < "$OMP_SENT" | tr -d ' ')" = "$((sent_before_retry + 1))" ] \ || fail "remote OMP control send replayed a prior programmatic request" -cp "$OMP_CONTROL_STATE/remote-omp.meta" "$TMP_ROOT/remote-omp.meta.before-stale" -meta_tmp="$OMP_CONTROL_STATE/remote-omp.meta.tmp" -sed 's/^endpoint_task_id=.*/endpoint_task_id=stale-endpoint/' "$OMP_CONTROL_STATE/remote-omp.meta" > "$meta_tmp" -mv "$meta_tmp" "$OMP_CONTROL_STATE/remote-omp.meta" +cp "$OMP_CONTROL_STATE/$OMP_ID.meta" "$TMP_ROOT/remote-omp.meta.before-stale" +meta_tmp="$OMP_CONTROL_STATE/$OMP_ID.meta.tmp" +sed 's/^endpoint_task_id=.*/endpoint_task_id=stale-endpoint/' "$OMP_CONTROL_STATE/$OMP_ID.meta" > "$meta_tmp" +mv "$meta_tmp" "$OMP_CONTROL_STATE/$OMP_ID.meta" set +e -remote_env "$ROOT/bin/fm-send.sh" fm-remote-omp "stale binding" \ +remote_env "$ROOT/bin/fm-send.sh" "fm-$OMP_ID" "stale binding" \ > "$TMP_ROOT/remote-omp-stale.out" 2>&1 stale_rc=$? set -e [ "$stale_rc" = 9 ] || fail "stale remote OMP identity did not return a binding refusal (rc=$stale_rc)" assert_grep 'remote-omp-binding-refused' "$TMP_ROOT/remote-omp-stale.out" \ "stale remote OMP identity did not report its named refusal" -mv "$TMP_ROOT/remote-omp.meta.before-stale" "$OMP_CONTROL_STATE/remote-omp.meta" +mv "$TMP_ROOT/remote-omp.meta.before-stale" "$OMP_CONTROL_STATE/$OMP_ID.meta" "$REMOTE_ROOT/bin/herdr" pane close "$OMP_PANE" set +e -remote_env "$ROOT/bin/fm-send.sh" fm-remote-omp "missing live endpoint" \ +remote_env "$ROOT/bin/fm-send.sh" "fm-$OMP_ID" "missing live endpoint" \ > "$TMP_ROOT/remote-omp-missing.out" 2>&1 missing_rc=$? set -e @@ -1714,7 +1717,7 @@ if ! kill -0 "$OMP_LISTENER_PID" 2>/dev/null; then printf '%s\n' "$OMP_LISTENER_PID" > "$OMP_REMOTE_HOME/state/.lock" fi set +e -remote_env "$ROOT/bin/fm-spawn.sh" remote-omp --secondmate \ +remote_env "$ROOT/bin/fm-spawn.sh" "$OMP_ID" --secondmate \ > "$TMP_ROOT/remote-omp-live-relaunch.out" 2>&1 live_relaunch_rc=$? set -e @@ -1743,18 +1746,18 @@ assert_present "$OMP_TURN_STARTED" \ # An awake compute-managed route takes the wake-on-delivery restore path: the # ordinary secondmate launch must accept the slept generation's leftovers. mkdir -p "$PARENT/data/boat" -printf 'lifecycle=ready\n' > "$PARENT/data/boat/remote-omp.meta" +printf 'lifecycle=ready\n' > "$PARENT/data/boat/$OMP_ID.meta" : > "$OMP_ACTIVE_PID" sent_before_restore=$(wc -l < "$OMP_SENT" | tr -d ' ') records_before_restore=$(find "$OMP_INBOX" -name '*.msg' | wc -l | tr -d ' ') -remote_env "$ROOT/bin/fm-send.sh" fm-remote-omp \ +remote_env "$ROOT/bin/fm-send.sh" "fm-$OMP_ID" \ "wake-on-delivery restore after a successful generation" \ > "$TMP_ROOT/remote-omp-restore.out" 2>&1 \ || fail "wake-on-delivery restore after a successful generation failed:"$'\n'"$(cat "$TMP_ROOT/remote-omp-restore.out")" grep -Eq '^request=[0-9a-f]{16} record=[0-9]+ state=(recorded|handled)$' \ "$TMP_ROOT/remote-omp-restore.out" \ || fail "restored remote OMP delivery did not report its durable machine line:"$'\n'"$(cat "$TMP_ROOT/remote-omp-restore.out")" -[ "$(remote_env "$ROOT/bin/fm-on.sh" remote-omp fm-remote-secondmate-control.sh state remote-omp)" = alive ] \ +[ "$(remote_env "$ROOT/bin/fm-on.sh" "$OMP_ID" fm-remote-secondmate-control.sh state "$OMP_ID")" = alive ] \ || fail "wake-on-delivery restore did not reach a live endpoint" OMP_RESTORED_PID=$(cat "$OMP_ACTIVE_PID") case "$OMP_RESTORED_PID" in @@ -1773,8 +1776,8 @@ assert_grep '"deliverAs":"steer","triggerTurn":true' "$OMP_SENT" \ "restored remote OMP delivery did not use programmatic triggerTurn" [ "$(cat "$OMP_TURN_STARTED")" != "$OMP_PRIOR_STARTED" ] \ || fail "restored remote OMP generation kept the prior generation's turn-start marker" -rm -f "$PARENT/data/boat/remote-omp.meta" -OMP_RESTORED_PANE=$(sed -n 's/^herdr_pane_id=//p' "$OMP_CONTROL_STATE/remote-omp.meta") +rm -f "$PARENT/data/boat/$OMP_ID.meta" +OMP_RESTORED_PANE=$(sed -n 's/^herdr_pane_id=//p' "$OMP_CONTROL_STATE/$OMP_ID.meta") [ -z "$OMP_RESTORED_PANE" ] || "$REMOTE_ROOT/bin/herdr" pane close "$OMP_RESTORED_PANE" pass "a successful remote OMP generation slept and restored on delivery retires its prior runtime markers and delivers exactly once" @@ -1788,6 +1791,7 @@ pass "a successful remote OMP generation slept and restored on delivery retires # and real session files untouched. rm -f "$TMP_ROOT/execute-omp-launch" +OMP_BROKEN_ID="remote-omp-broken-$BASHPID" OMP_BROKEN_HOME="$TMP_ROOT/remote-omp-broken-home" printf 'omp test/model low\n' > "$PARENT/config/secondmate-harness" rm -f "$PARENT/config/secondmate-harness-fallback" @@ -1818,7 +1822,7 @@ chmod +x "$REMOTE_ROOT/bin/omp" rm -f "$OMP_ACTIVE_PID" FM_SECONDMATE_CHARTER='Exercise a remote OMP bind that dies before acknowledgement.' \ FM_SECONDMATE_SCOPE='remote failed-bind cleanup validation' \ - remote_env "$ROOT/bin/fm-remote-home-seed.sh" remote-omp-broken remote-mac "$REMOTE_ROOT" \ + remote_env "$ROOT/bin/fm-remote-home-seed.sh" "$OMP_BROKEN_ID" remote-mac "$REMOTE_ROOT" \ "$OMP_BROKEN_HOME" --no-projects >/dev/null \ || fail "broken-bind remote OMP route seeding failed" OMP_BROKEN_STATE="$OMP_BROKEN_HOME/state" @@ -1826,7 +1830,7 @@ OMP_BROKEN_CONTROL="$OMP_BROKEN_STATE/parent-route" : > "$TMP_ROOT/omp-broken-ack" set +e -remote_env "$ROOT/bin/fm-spawn.sh" remote-omp-broken --secondmate \ +remote_env "$ROOT/bin/fm-spawn.sh" "$OMP_BROKEN_ID" --secondmate \ > "$TMP_ROOT/remote-omp-broken.out" 2>&1 broken_rc=$? set -e @@ -1840,19 +1844,19 @@ assert_absent "$OMP_BROKEN_STATE/.omp-primary-extension-loaded" \ "failed remote bind left a stale integration marker" assert_absent "$OMP_BROKEN_STATE/.lock" \ "failed remote bind left a dead session lock" -assert_absent "$OMP_BROKEN_CONTROL/remote-omp-broken.omp-ext.ts" \ +assert_absent "$OMP_BROKEN_CONTROL/$OMP_BROKEN_ID.omp-ext.ts" \ "failed remote bind left its generated adapter file" -assert_absent "$OMP_BROKEN_CONTROL/remote-omp-broken.omp-ready" \ +assert_absent "$OMP_BROKEN_CONTROL/$OMP_BROKEN_ID.omp-ready" \ "failed remote bind left a stale readiness marker" -assert_absent "$OMP_BROKEN_CONTROL/remote-omp-broken.omp-started" \ +assert_absent "$OMP_BROKEN_CONTROL/$OMP_BROKEN_ID.omp-started" \ "failed remote bind left a stale turn-start marker" -assert_absent "$OMP_BROKEN_CONTROL/remote-omp-broken.omp-doorbell-ready" \ +assert_absent "$OMP_BROKEN_CONTROL/$OMP_BROKEN_ID.omp-doorbell-ready" \ "failed remote bind left a stale doorbell marker" -assert_absent "$OMP_BROKEN_CONTROL/remote-omp-broken.omp-doorbell-failed" \ +assert_absent "$OMP_BROKEN_CONTROL/$OMP_BROKEN_ID.omp-doorbell-failed" \ "failed remote bind left a stale doorbell failure marker" -assert_absent "$OMP_BROKEN_CONTROL/remote-omp-broken.omp-doorbell-ready.requests" \ +assert_absent "$OMP_BROKEN_CONTROL/$OMP_BROKEN_ID.omp-doorbell-ready.requests" \ "failed remote bind left a stale request directory" -assert_present "$OMP_BROKEN_CONTROL/remote-omp-broken.meta" \ +assert_present "$OMP_BROKEN_CONTROL/$OMP_BROKEN_ID.meta" \ "failed remote bind dropped the endpoint recovery record" assert_present "$OMP_BROKEN_HOME/.fm-secondmate-home" \ "failed remote bind removed the persistent remote home" @@ -1862,11 +1866,11 @@ assert_present "$OMP_BROKEN_HOME/.fm-secondmate-home" \ # The next launch is accepted on the cleaned state rather than refusing on # the dead generation's leftovers. rm -f "$TMP_ROOT/omp-broken-ack" -remote_env "$ROOT/bin/fm-spawn.sh" remote-omp-broken --secondmate \ +remote_env "$ROOT/bin/fm-spawn.sh" "$OMP_BROKEN_ID" --secondmate \ > "$TMP_ROOT/remote-omp-relaunch.out" 2>&1 \ || fail "remote OMP relaunch after a failed bind was refused:"$'\n'"$(cat "$TMP_ROOT/remote-omp-relaunch.out")" -[ "$(remote_env "$ROOT/bin/fm-on.sh" remote-omp-broken \ - fm-remote-secondmate-control.sh state remote-omp-broken)" = alive ] \ +[ "$(remote_env "$ROOT/bin/fm-on.sh" "$OMP_BROKEN_ID" \ + fm-remote-secondmate-control.sh state "$OMP_BROKEN_ID")" = alive ] \ || fail "remote OMP relaunch after a failed bind did not reach a live endpoint" OMP_BROKEN_RETAINED=$(cat "$OMP_BROKEN_STATE/.omp-session") [ -f "$OMP_BROKEN_RETAINED" ] \ @@ -1876,7 +1880,7 @@ OMP_BROKEN_RETAINED=$(cat "$OMP_BROKEN_STATE/.omp-session") # rather than dropping or rewriting the binding. The live endpoint is closed # first: the second failure must come from a bind that died, not from a # refused duplicate launch against a live agent. -OMP_BROKEN_PANE=$(sed -n 's/^herdr_pane_id=//p' "$OMP_BROKEN_CONTROL/remote-omp-broken.meta") +OMP_BROKEN_PANE=$(sed -n 's/^herdr_pane_id=//p' "$OMP_BROKEN_CONTROL/$OMP_BROKEN_ID.meta") [ -n "$OMP_BROKEN_PANE" ] || fail "remote OMP endpoint metadata omitted its Herdr pane identity" "$REMOTE_ROOT/bin/herdr" pane close "$OMP_BROKEN_PANE" omp_dead_pid=$( { sleep 0.05 & echo "$!"; wait; } 2>/dev/null ) @@ -1887,7 +1891,7 @@ printf '%s\n%s\n%s\n%s\n' "$OMP_BROKEN_VERSION" "$omp_dead_pid" "$REMOTE_OMP_BUN > "$OMP_BROKEN_STATE/.omp-primary-extension-loaded" : > "$TMP_ROOT/omp-broken-ack" set +e -remote_env "$ROOT/bin/fm-spawn.sh" remote-omp-broken --secondmate \ +remote_env "$ROOT/bin/fm-spawn.sh" "$OMP_BROKEN_ID" --secondmate \ > "$TMP_ROOT/remote-omp-broken2.out" 2>&1 broken2_rc=$? set -e @@ -1902,21 +1906,21 @@ assert_absent "$OMP_BROKEN_STATE/.omp-primary-extension-loaded" \ "failed relaunch left a stale integration marker" assert_absent "$OMP_BROKEN_STATE/.lock" \ "failed relaunch left a dead session lock" -assert_absent "$OMP_BROKEN_CONTROL/remote-omp-broken.omp-started" \ +assert_absent "$OMP_BROKEN_CONTROL/$OMP_BROKEN_ID.omp-started" \ "failed relaunch left a stale turn-start marker" -assert_absent "$OMP_BROKEN_CONTROL/remote-omp-broken.omp-doorbell-ready" \ +assert_absent "$OMP_BROKEN_CONTROL/$OMP_BROKEN_ID.omp-doorbell-ready" \ "failed relaunch left a stale doorbell marker" rm -f "$TMP_ROOT/omp-broken-ack" -remote_env "$ROOT/bin/fm-spawn.sh" remote-omp-broken --secondmate \ +remote_env "$ROOT/bin/fm-spawn.sh" "$OMP_BROKEN_ID" --secondmate \ > "$TMP_ROOT/remote-omp-relaunch2.out" 2>&1 \ || fail "remote OMP launch after a failed retained-session bind was refused:"$'\n'"$(cat "$TMP_ROOT/remote-omp-relaunch2.out")" -[ "$(remote_env "$ROOT/bin/fm-on.sh" remote-omp-broken \ - fm-remote-secondmate-control.sh state remote-omp-broken)" = alive ] \ +[ "$(remote_env "$ROOT/bin/fm-on.sh" "$OMP_BROKEN_ID" \ + fm-remote-secondmate-control.sh state "$OMP_BROKEN_ID")" = alive ] \ || fail "remote OMP launch after a failed retained-session bind did not reach a live endpoint" pass "a failed remote OMP bind retires its generation artifacts, preserves the retained session, and accepts the next launch" for pointer_state in absent malformed; do - orphan_id="remote-omp-orphan-$pointer_state" + orphan_id="remote-omp-orphan-$pointer_state-$BASHPID" orphan_home="$TMP_ROOT/$orphan_id-home" FM_SECONDMATE_CHARTER='Exercise failed-bind session-pointer reconciliation.' \ FM_SECONDMATE_SCOPE='remote orphan-session validation' \ @@ -1958,28 +1962,63 @@ printf 'tmux\n' > "$OMP_BROKEN_BACKEND_CFG" # Relaunch on a dead endpoint skips graceful exit entirely, so the fixture's # missing exit semantics do not matter: close the live pane and model the # session lock the same way the failed-bind case does (dead pid). -OMP_BROKEN_LIVE_PANE=$(sed -n 's/^herdr_pane_id=//p' "$OMP_BROKEN_CONTROL/remote-omp-broken.meta") +OMP_BROKEN_LIVE_PANE=$(sed -n 's/^herdr_pane_id=//p' "$OMP_BROKEN_CONTROL/$OMP_BROKEN_ID.meta") [ -n "$OMP_BROKEN_LIVE_PANE" ] || fail "live remote OMP endpoint metadata omitted its Herdr pane identity" "$REMOTE_ROOT/bin/herdr" pane close "$OMP_BROKEN_LIVE_PANE" -omp_dead_pid=$( { sleep 0.05 & echo "$!"; wait; } 2>/dev/null ) -printf '%s\n' "$omp_dead_pid" > "$OMP_BROKEN_STATE/.lock" +sleep 0.05 & +omp_dead_lock_pid=$! +sleep 0.05 & +omp_dead_marker_pid=$! +wait "$omp_dead_lock_pid" || fail "fixture lock owner did not exit" +wait "$omp_dead_marker_pid" || fail "fixture marker owner did not exit" +OMP_BROKEN_OLD_VERSION=$(bash -c '. "$1/bin/fm-primary-watch-version-lib.sh"; fm_primary_watch_version "$2/.omp/extensions/fm-primary-omp.ts" "$2"' \ + _ "$REMOTE_ROOT" "$OMP_BROKEN_HOME") +# Bring the code root up to its origin, publish a tracked extension update, and +# use the host-local update owner to fast-forward this secondmate home. +git -C "$REMOTE_ROOT" checkout -- bin/omp +git -C "$REMOTE_ROOT" fetch -q origin main +git -C "$REMOTE_ROOT" merge --ff-only -q origin/main +printf '\n// relaunch fixture advances the loaded OMP closure\n' \ + >> "$REMOTE_SEED/.omp/extensions/fm-primary-omp.ts" +git -C "$REMOTE_SEED" add .omp/extensions/fm-primary-omp.ts +git -C "$REMOTE_SEED" commit -qm 'advance the OMP closure for relaunch recovery' +git -C "$REMOTE_SEED" push -q origin main +OMP_UPDATE_OUT=$(remote_env "$ROOT/bin/fm-on.sh" "$OMP_BROKEN_ID" \ + fm-remote-secondmate-control.sh update "$OMP_BROKEN_ID") +assert_contains "$OMP_UPDATE_OUT" 'synced:' \ + "remote home update did not fast-forward the OMP extension fixture" OMP_BROKEN_VERSION=$(bash -c '. "$1/bin/fm-primary-watch-version-lib.sh"; fm_primary_watch_version "$2/.omp/extensions/fm-primary-omp.ts" "$2"' \ _ "$REMOTE_ROOT" "$OMP_BROKEN_HOME") -printf '%s\n%s\n%s\n%s\n' "$OMP_BROKEN_VERSION" "$omp_dead_pid" "$REMOTE_OMP_BUN" "$REMOTE_OMP_BIN" \ +[ "$OMP_BROKEN_OLD_VERSION" != "$OMP_BROKEN_VERSION" ] \ + || fail "fixture update did not change the OMP primary closure" +[ "$omp_dead_lock_pid" != "$omp_dead_marker_pid" ] \ + || fail "fixture lock and marker owners unexpectedly share a PID" +if kill -0 "$omp_dead_lock_pid" 2>/dev/null || kill -0 "$omp_dead_marker_pid" 2>/dev/null; then + fail "fixture lock or marker owner is still alive" +fi +printf '%s\n%s\n%s\n%s\n' "$OMP_BROKEN_OLD_VERSION" "$omp_dead_marker_pid" "$REMOTE_OMP_BUN" "$REMOTE_OMP_BIN" \ > "$OMP_BROKEN_STATE/.omp-primary-extension-loaded" -remote_env "$ROOT/bin/fm-on.sh" remote-omp-broken \ - fm-remote-secondmate-control.sh relaunch remote-omp-broken omp - - \ +printf '%s\n' "$omp_dead_lock_pid" > "$OMP_BROKEN_STATE/.lock" +printf 'fixture marker contents:\n' +cat "$OMP_BROKEN_STATE/.omp-primary-extension-loaded" +printf 'fixture current closure=%s lock_pid=%s alive=no\n' "$OMP_BROKEN_VERSION" "$omp_dead_lock_pid" +remote_env "$ROOT/bin/fm-on.sh" "$OMP_BROKEN_ID" \ + fm-remote-secondmate-control.sh relaunch "$OMP_BROKEN_ID" omp - - \ > "$TMP_ROOT/remote-omp-broken-relaunch.out" 2>&1 \ || fail "remote OMP relaunch on a tmux-configured home was refused:"$'\n'"$(cat "$TMP_ROOT/remote-omp-broken-relaunch.out")" -assert_grep 'backend=herdr' "$OMP_BROKEN_CONTROL/remote-omp-broken.meta" \ +assert_grep 'backend=herdr' "$OMP_BROKEN_CONTROL/$OMP_BROKEN_ID.meta" \ "the relaunched remote OMP endpoint record does not name the herdr backend" -assert_grep 'herdr_session=fm-remote' "$OMP_BROKEN_CONTROL/remote-omp-broken.meta" \ +assert_grep 'herdr_session=fm-remote' "$OMP_BROKEN_CONTROL/$OMP_BROKEN_ID.meta" \ "the relaunched remote OMP endpoint record does not name the fm-remote session" -assert_no_grep 'window=firstmate:' "$OMP_BROKEN_CONTROL/remote-omp-broken.meta" \ +assert_no_grep 'window=firstmate:' "$OMP_BROKEN_CONTROL/$OMP_BROKEN_ID.meta" \ "the remote OMP relaunch landed on the home's configured tmux backend instead of herdr" -[ "$(remote_env "$ROOT/bin/fm-on.sh" remote-omp-broken \ - fm-remote-secondmate-control.sh state remote-omp-broken)" = alive ] \ +[ "$(remote_env "$ROOT/bin/fm-on.sh" "$OMP_BROKEN_ID" \ + fm-remote-secondmate-control.sh state "$OMP_BROKEN_ID")" = alive ] \ || fail "remote OMP relaunch on a tmux-configured home did not reach a live endpoint" +[ "$(cat "$OMP_BROKEN_STATE/.omp-session")" = "$OMP_BROKEN_RETAINED" ] \ + || fail "stale-marker recovery changed the retained session pointer" +[ -f "$OMP_BROKEN_RETAINED" ] \ + || fail "stale-marker recovery removed the retained session" if [ "$omp_broken_backend_prior" = 1 ]; then mv -f "$TMP_ROOT/omp-broken-backend.prior" "$OMP_BROKEN_BACKEND_CFG" else diff --git a/tests/fm-secondmate-restart.test.sh b/tests/fm-secondmate-restart.test.sh index c2300255279..170d159ae52 100755 --- a/tests/fm-secondmate-restart.test.sh +++ b/tests/fm-secondmate-restart.test.sh @@ -153,15 +153,32 @@ new_case() { } # add_local_mate [harness] [backend-line] -# A live LOCAL second mate: a real git worktree for its home, plus the durable -# record this home keeps for it. +# OMP local second mates use a persistent git home so the relaunch owner can +# verify the fixture's tracked extension closure against HEAD. +# add_local_mate() { local dir=$1 id=$2 harness=${3:-claude} backend=${4:-} local home="$dir/home" smhome="$dir/$id-home" - fm_git_worktree "$dir/$id-repo" "$smhome" "sm-$id" + if [ "$harness" = omp ]; then + local seed="$dir/local-seed" origin="$dir/local-origin.git" root="$dir/local-primary" + if [ ! -d "$origin" ]; then + fm_git_init_commit "$seed" + printf '/data/\n/state/\n/config/\n/projects/\n/.no-mistakes/\n.fm-secondmate-home\n' \ + > "$seed/.gitignore" + printf '# agents\n' > "$seed/AGENTS.md" + git -C "$seed" add .gitignore AGENTS.md + git -C "$seed" -c user.name='Firstmate Tests' -c user.email='tests@example.invalid' \ + commit -qm 'persistent home fixture layout' + fm_git_add_origin "$seed" "$origin" + git clone --quiet "$origin" "$root" + fi + git clone --quiet "$origin" "$smhome" + else + fm_git_worktree "$dir/$id-repo" "$smhome" "sm-$id" + printf '# agents\n' > "$smhome/AGENTS.md" + fi mkdir -p "$smhome/state" "$smhome/data" "$smhome/bin" "$home/data/$id" printf '%s\n' "$id" > "$smhome/.fm-secondmate-home" - printf '# agents\n' > "$smhome/AGENTS.md" printf '# charter\n' > "$home/data/$id/brief.md" { echo "window=fmses:fm-$id" @@ -247,8 +264,14 @@ arm_answer() { } run_restart() { # - local dir=$1; shift - env PATH="$dir/fakebin:$PATH" FM_HOME="$dir/home" FM_FAKE_DIR="$dir/fake" \ + local dir=$1 root="$ROOT" + shift + if [ -d "$dir/fmrepo/.git" ]; then + root="$dir/fmrepo" + elif [ -d "$dir/local-primary/.git" ]; then + root="$dir/local-primary" + fi + env PATH="$dir/fakebin:$PATH" FM_HOME="$dir/home" FM_ROOT_OVERRIDE="$root" FM_FAKE_DIR="$dir/fake" \ FM_SPAWN_NO_GUARD=1 FM_SECONDMATE_PERSIST_POLL=1 \ FM_SECONDMATE_PERSIST_WAIT="${FM_TEST_PERSIST_WAIT:-30}" \ FM_CONTROL_POLL=0.01 FM_CONTROL_EXIT_WAIT=0.05 FM_CONTROL_LAUNCH_WAIT=0.05 \ @@ -840,31 +863,151 @@ EOF } test_omp_secondmate_prepare_relaunch_clears_owned_markers() { - local dir smhome state meta log + local dir smhome state meta marker bash_bin version lock_pid marker_pid invalid_lock out path dir=$(new_case omp-prepare) - smhome="$dir/omp-home"; state="$dir/home/state"; meta="$state/omp1.meta"; log="$dir/fake/omp-log" + smhome="$dir/omp-home"; state="$dir/home/state"; meta="$state/omp1.meta" + marker="$smhome/state/.omp-primary-extension-loaded" mkdir -p "$smhome/state" - : > "$state/omp1.omp-ext.ts"; : > "$state/omp1.omp-ready"; : > "$state/omp1.omp-started" - : > "$smhome/state/.omp-primary-extension-loaded" - : > "$log" + bash -c '. "$1/bin/fm-primary-watch-version-lib.sh"; fm_omp_primary_closure_paths' \ + _ "$ROOT" > "$dir/closure-paths" + while IFS= read -r path; do + mkdir -p "$smhome/$(dirname "$path")" + cp "$ROOT/$path" "$smhome/$path" + done < "$dir/closure-paths" + bash_bin=$(fm_test_realpath "$(command -v bash)") + version=$(bash -c '. "$1/bin/fm-primary-watch-version-lib.sh"; fm_primary_watch_version "$2/.omp/extensions/fm-primary-omp.ts" "$2"' \ + _ "$ROOT" "$smhome") || fail "could not derive the fixture OMP closure" + lock_pid=99999998 + marker_pid=99999999 cat > "$meta" < "$FAKE_OMP_LOG"; rm -f "$FAKE_OMP_ALIVE"; } - fm_control_omp_secondmate_prepare_relaunch "$2" omp1 "$3" tmux fm:omp1 "$4" - ' _ "$ROOT" "$state" "$smhome" "$meta") || fail "OMP preparation rejected an absent endpoint" + : > "$state/omp1.omp-ext.ts"; : > "$state/omp1.omp-ready"; : > "$state/omp1.omp-started" + mkdir -p "$smhome/state/omp-sessions" + printf 'saved session\n' > "$smhome/state/omp-sessions/saved.jsonl" + printf '%s\n' "$smhome/state/omp-sessions/saved.jsonl" > "$smhome/state/.omp-session" + + write_fixture_marker() { + printf '%s\n%s\n%s\n%s\n' "$1" "$2" "$3" "$4" > "$marker" + } + reset_fixture() { + : > "$state/omp1.omp-ext.ts"; : > "$state/omp1.omp-ready"; : > "$state/omp1.omp-started" + rm -f "$marker" + printf '%s\n' "$lock_pid" > "$smhome/state/.lock" + write_fixture_marker "$version" "$marker_pid" "$bash_bin" "$bash_bin" + } + prepare_fixture() { + FM_RESTART_STOP_WAIT=0 bash -c ' + . "$1/bin/fm-backend.sh" || exit 1 + . "$1/bin/fm-control-lib.sh" || exit 1 + fm_backend_agent_state() { printf missing; } + fm_backend_kill() { return 0; } + fm_control_omp_secondmate_prepare_relaunch "$2" omp1 "$3" tmux fm:omp1 "$4" + ' _ "$ROOT" "$state" "$smhome" "$meta" 2>&1 + } + assert_preserved() { + [ -e "$marker" ] || [ -L "$marker" ] || fail "refused OMP relaunch removed the primary marker" + [ -f "$state/omp1.omp-ext.ts" ] && [ -f "$state/omp1.omp-ready" ] \ + && [ -f "$state/omp1.omp-started" ] \ + || fail "refused OMP relaunch removed session artifacts" + [ "$(cat "$smhome/state/.omp-session")" = "$smhome/state/omp-sessions/saved.jsonl" ] \ + || fail "refused OMP relaunch changed the saved-session pointer" + [ "$(cat "$smhome/state/omp-sessions/saved.jsonl")" = 'saved session' ] \ + || fail "refused OMP relaunch changed the saved session" + } + + reset_fixture + out=$(prepare_fixture) || fail "OMP preparation rejected two proven-dead marker owners: $out" [ "$out" = '' ] || fail "OMP preparation emitted unexpected output: $out" - [ ! -s "$log" ] || fail "OMP preparation killed an endpoint without a proven dead state" [ ! -e "$state/omp1.omp-ext.ts" ] || fail "OMP extension artifact survived preparation" [ ! -e "$state/omp1.omp-ready" ] || fail "OMP readiness artifact survived preparation" [ ! -e "$state/omp1.omp-started" ] || fail "OMP started marker survived preparation" - [ ! -e "$smhome/state/.omp-primary-extension-loaded" ] || fail "OMP primary marker survived preparation" - pass "OMP relaunch preparation proves stop and clears owned markers" + [ ! -e "$marker" ] || fail "OMP primary marker survived preparation" + [ "$(cat "$smhome/state/.omp-session")" = "$smhome/state/omp-sessions/saved.jsonl" ] \ + || fail "OMP preparation changed the exact saved-session pointer" + [ "$(cat "$smhome/state/omp-sessions/saved.jsonl")" = 'saved session' ] \ + || fail "OMP preparation changed the saved session" + + marker_pid=$BASHPID + reset_fixture + if out=$(prepare_fixture); then fail "OMP preparation accepted a live marker owner"; fi + assert_contains "$out" 'primary marker owner is still live' "live marker-owner refusal was not actionable" + assert_preserved + + marker_pid=99999999 + reset_fixture + write_fixture_marker "$version" "$marker_pid" "$bash_bin" "$dir/foreign-omp" + if out=$(prepare_fixture); then fail "OMP preparation accepted a foreign OMP runtime marker"; fi + assert_contains "$out" 'runtime does not match the recorded home' "foreign-runtime refusal was not actionable" + assert_preserved + + reset_fixture + printf 'sha256:unrecognized\n%s\n%s\n%s\n' "$marker_pid" "$bash_bin" "$bash_bin" > "$marker" + if out=$(prepare_fixture); then fail "OMP preparation accepted an unexplained marker version"; fi + assert_contains "$out" 'refusing relaunch' "unexplained marker-version refusal was not actionable" + assert_preserved + + reset_fixture + rm -f "$marker" + ln -s "$dir/absent-marker-target" "$marker" + if out=$(prepare_fixture); then fail "OMP preparation accepted a symlink marker"; fi + assert_contains "$out" 'not an ordinary file' "symlink marker refusal was not actionable" + assert_preserved + + reset_fixture + rm -f "$marker" + mkdir "$marker" + if out=$(prepare_fixture); then fail "OMP preparation accepted a non-file marker"; fi + assert_contains "$out" 'not an ordinary file' "non-file marker refusal was not actionable" + rmdir "$marker" + write_fixture_marker "$version" "$marker_pid" "$bash_bin" "$bash_bin" + assert_preserved + reset_fixture + rm -f "$marker" "$smhome/state/.lock" + mv "$smhome/state" "$dir/omp-home-state" + ln -s "$dir/omp-home-state" "$smhome/state" + if out=$(prepare_fixture); then fail "OMP preparation accepted a symlink home state directory"; fi + assert_contains "$out" 'OMP home state is not an ordinary directory' \ + "symlink home-state refusal was not actionable" + [ -f "$state/omp1.omp-ext.ts" ] && [ -f "$state/omp1.omp-ready" ] \ + && [ -f "$state/omp1.omp-started" ] \ + || fail "symlink home-state refusal removed the task's OMP artifacts" + rm "$smhome/state" + mv "$dir/omp-home-state" "$smhome/state" + + + for invalid_lock in not-a-pid 0 00 01 1; do + reset_fixture + printf '%s\n' "$invalid_lock" > "$smhome/state/.lock" + if out=$(prepare_fixture); then fail "OMP preparation accepted unverifiable lock owner $invalid_lock"; fi + assert_contains "$out" 'lock owner is unverifiable' \ + "unverifiable lock refusal was not actionable for PID '$invalid_lock'" + assert_preserved + done + reset_fixture + printf '%s\n%s\n' "$lock_pid" "$marker_pid" > "$smhome/state/.lock" + if out=$(prepare_fixture); then fail "OMP preparation accepted a multi-line lock PID"; fi + assert_contains "$out" 'lock owner is unverifiable' \ + "multi-line lock PID refusal was not actionable" + assert_preserved + + + cat > "$dir/fake-live-lock" <<'SH' +#!/usr/bin/env bash +if [ "${1:-}" = "-0" ] && [ "${2:-}" = "$FM_TEST_LOCK_PID" ]; then exit 0; fi +exit 1 +SH + chmod +x "$dir/fake-live-lock" + reset_fixture + if out=$(FM_RESTART_STOP_CMD="$dir/fake-live-lock" FM_TEST_LOCK_PID="$lock_pid" prepare_fixture); then + fail "OMP preparation accepted a live lock owner" + fi + assert_preserved + pass "OMP relaunch preparation accepts only owned dead markers and preserves sessions on every refusal" } # --- T17: an OMP mate's restart, driven end to end with both real side effects @@ -894,6 +1037,15 @@ omp_marker_list() { # # the composer. The process receipts each request and then answers the persist # question on the parent channel, which is the same modelling the tmux stub does # for every other adapter in this file. +copy_omp_primary_closure() { # + local dir=$1 home=$2 path + bash -c '. "$1/bin/fm-primary-watch-version-lib.sh"; fm_omp_primary_closure_paths' \ + _ "$ROOT" > "$dir/closure-paths" + while IFS= read -r path; do + mkdir -p "$home/$(dirname "$path")" + cp "$ROOT/$path" "$home/$path" + done < "$dir/closure-paths" +} OMP_LISTENER_PID= add_omp_mate() { # add_local_mate "$1" "$2" omp @@ -1025,15 +1177,32 @@ clear_omp_restart_stubs() { } test_omp_restart_stops_proves_cleans_then_relaunches() { - local dir out rc owner marker stop_first stop_probe spawn_line + local dir out rc owner marker primary_marker stop_first stop_probe spawn_line version bash_bin smhome dir=$(new_case omp) printf 'omp\n' > "$dir/home/config/secondmate-harness" add_omp_mate "$dir" sm1 + smhome="$dir/sm1-home" + copy_omp_primary_closure "$dir" "$dir/local-primary" + git -C "$dir/local-primary" add -A .omp bin/fm-primary-watch-core.ts + git -C "$dir/local-primary" -c user.name='Firstmate Tests' -c user.email='tests@example.invalid' \ + commit -qm 'install the OMP primary closure' + git -C "$dir/local-primary" push -q origin HEAD + git -C "$smhome" pull -q --ff-only owner=987654 make_omp_restart_stubs "$dir" sm1 "$owner" - # The session artifacts a live OMP mate is holding when the update lands. - while IFS= read -r marker; do printf 'live\n' > "$marker"; done < "$dir/markers" - + # Keep the task-owned artifacts live until the recorded session owner is + # proven absent; the primary marker itself has the real four-line contract. + while IFS= read -r marker; do + case "$marker" in + "$smhome/state/.omp-primary-extension-loaded") ;; + *) printf 'live\n' > "$marker" ;; + esac + done < "$dir/markers" + version=$(bash -c '. "$1/bin/fm-primary-watch-version-lib.sh"; fm_primary_watch_version "$2/.omp/extensions/fm-primary-omp.ts" "$2"' \ + _ "$ROOT" "$smhome") || fail "could not derive the current OMP marker version" + bash_bin=$(fm_test_realpath "$(command -v bash)") + primary_marker="$smhome/state/.omp-primary-extension-loaded" + printf '%s\n%s\n%s\n%s\n' "$version" "$owner" "$bash_bin" "$bash_bin" > "$primary_marker" out=$(run_restart "$dir" fm-sm1); rc=$? stop_omp_mate clear_omp_restart_stubs @@ -1078,7 +1247,7 @@ test_omp_restart_stops_proves_cleans_then_relaunches() { # placement's delivery binds no closure, so the same update must still reach the # running local session, have it persist, and only then stop and relaunch it. test_local_omp_restart_after_extension_closure_update() { - local dir out rc seed repo smhome marker bash_bin loaded tracked persisted_line stop_line + local dir out rc seed repo smhome marker bash_bin loaded tracked persisted_line stop_line owner dir=$(new_case local-omp-update) printf 'omp\n' > "$dir/home/config/secondmate-harness" add_repo_backed_mate "$dir" sm1 omp @@ -1097,13 +1266,14 @@ test_local_omp_restart_after_extension_closure_update() { git -C "$repo" pull -q --ff-only git -C "$smhome" checkout -q --detach main - make_omp_restart_stubs "$dir" sm1 987654 + owner=987654 + make_omp_restart_stubs "$dir" sm1 "$owner" FM_TEST_OMP_EVENTS="$dir/stop.log" start_omp_session "$dir" sm1 bash_bin=$(fm_test_realpath "$(command -v bash)") loaded=$(bash -c '. "$1/bin/fm-primary-watch-version-lib.sh"; fm_primary_watch_version "$2/.omp/extensions/fm-primary-omp.ts" "$2"' \ _ "$ROOT" "$smhome") || fail "could not derive the session's loaded closure" marker="$smhome/state/.omp-primary-extension-loaded" - printf '%s\n%s\n%s\n%s\n' "$loaded" "$OMP_LISTENER_PID" "$bash_bin" "$bash_bin" > "$marker" + printf '%s\n%s\n%s\n%s\n' "$loaded" "$owner" "$bash_bin" "$bash_bin" > "$marker" # The update crosses an extension change while that session keeps running. printf '// extension change shipped by the update\n' >> "$seed/bin/fm-primary-watch-core.ts" @@ -1127,7 +1297,7 @@ test_local_omp_restart_after_extension_closure_update() { assert_contains "$(cat "$dir/home/state/sm1.inbox"/*.msg)" "Open-record persistence" \ "the superseded local session never received the persist request" persisted_line=$(grep -n '^persisted corr=' "$dir/stop.log" | head -1 | cut -d: -f1) - stop_line=$(grep -n '^stopped 987654 ' "$dir/stop.log" | head -1 | cut -d: -f1) + stop_line=$(grep -n "^stopped $owner " "$dir/stop.log" | head -1 | cut -d: -f1) [ -n "$persisted_line" ] && [ -n "$stop_line" ] && [ "$persisted_line" -lt "$stop_line" ] \ || fail "the superseded local session was not asked to persist before it stopped"$'\n'"$(cat "$dir/stop.log")" pass "T18 a local OMP mate persists and restarts after an update changes its loaded closure" @@ -1196,6 +1366,12 @@ test_unlistable_state_still_refuses_restart() { assert_no_grep '^/exit$' "$dir/fake/literal" "an unlistable state refusal must not have stopped the agent" pass "T20 a mate home whose state cannot be listed still refuses the restart" } +if [ "${FM_TEST_OMP_RESTART_ONLY:-0}" = 1 ]; then + test_omp_secondmate_prepare_relaunch_clears_owned_markers + test_omp_restart_stops_proves_cleans_then_relaunches + test_local_omp_restart_after_extension_closure_update + exit 0 +fi test_persist_gates_and_asks_only_for_open_records test_persist_precedes_restart test_arrived_answer_precedes_deadline_check From 4e128e79cebb5da9b271bd7b19c23600e5a35bb7 Mon Sep 17 00:00:00 2001 From: dnth Date: Fri, 16 Oct 2026 09:47:23 +0800 Subject: [PATCH 2/5] no-mistakes(review): Refuse live OMP marker owners regardless of lock equality --- bin/fm-control-lib.sh | 4 ++-- tests/fm-secondmate-restart.test.sh | 27 +++++++++++++++++++++++++-- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/bin/fm-control-lib.sh b/bin/fm-control-lib.sh index 2b89348d00a..4b877139bfa 100644 --- a/bin/fm-control-lib.sh +++ b/bin/fm-control-lib.sh @@ -285,7 +285,7 @@ fm_control_omp_secondmate_prepare_relaunch() { # /dev/null 2>&1; then printf 'error: OMP primary marker owner is still live; refusing relaunch\n' >&2 return 1 @@ -303,7 +303,7 @@ fm_control_omp_secondmate_prepare_relaunch() { # /dev/null 2>&1; then printf 'error: OMP primary marker owner is still live; refusing relaunch\n' >&2 return 1 diff --git a/tests/fm-secondmate-restart.test.sh b/tests/fm-secondmate-restart.test.sh index 170d159ae52..57c6806b14f 100755 --- a/tests/fm-secondmate-restart.test.sh +++ b/tests/fm-secondmate-restart.test.sh @@ -78,6 +78,7 @@ case "${1:-}" in if [ -e "$D/remote-relaunch-start" ] && [ ! -e "$D/remote-relaunch-end" ]; then : > "$D/local-relaunch-during-remote" fi + if [ -n "${FM_TEST_OWNER_GONE:-}" ]; then : > "$FM_TEST_OWNER_GONE"; fi printf 'zsh' > "$D/command.$target" ;; "/bin/bash "*.fm-launch-*.sh\'*) cat "$D/becomes" > "$D/command.$target" ;; @@ -938,6 +939,26 @@ EOF assert_contains "$out" 'primary marker owner is still live' "live marker-owner refusal was not actionable" assert_preserved + cat > "$dir/stoppable-owner" <<'SH' +#!/usr/bin/env bash +if [ "${1:-}" = -0 ]; then + [ ! -e "$FM_TEST_SIGNAL_LOG" ] + exit $? +fi +printf '%s\n' "$1" >> "$FM_TEST_SIGNAL_LOG" +SH + chmod +x "$dir/stoppable-owner" + for marker_pid in "$lock_pid" 99999999; do + reset_fixture + if out=$(FM_RESTART_STOP_CMD="$dir/stoppable-owner" FM_TEST_SIGNAL_LOG="$dir/signals" prepare_fixture); then + fail "OMP preparation accepted a live stoppable marker owner $marker_pid" + fi + assert_contains "$out" 'primary marker owner is still live' \ + "live stoppable marker-owner refusal was not actionable" + assert_absent "$dir/signals" "OMP preparation signaled an owner despite a live primary marker" + assert_preserved + done + marker_pid=99999999 reset_fixture write_fixture_marker "$version" "$marker_pid" "$bash_bin" "$dir/foreign-omp" @@ -1213,8 +1234,10 @@ test_omp_restart_stops_proves_cleans_then_relaunches() { # 1 + 2. The owner was stopped, and its absence was PROVEN while every session # artifact was still on disk. - stop_first=$(sed -n 1p "$dir/stop.log") - stop_probe=$(sed -n 2p "$dir/stop.log") + stop_first=$(sed -n 2p "$dir/stop.log") + stop_probe=$(sed -n 3p "$dir/stop.log") + [ "$(sed -n 1p "$dir/stop.log")" = "probe $owner absent markers_present=4" ] \ + || fail "the graceful exit did not retire the marker owner before preparation" [ "$stop_first" = "stopped $owner markers_present=4" ] \ || fail "the restart did not stop the mate's session owner first: '$stop_first'"$'\n'"$(cat "$dir/stop.log")" [ "$stop_probe" = "probe $owner absent markers_present=4" ] \ From 0dbdb699958f1b922574310ba81869420c290ab8 Mon Sep 17 00:00:00 2001 From: dnth Date: Fri, 16 Oct 2026 09:55:18 +0800 Subject: [PATCH 3/5] no-mistakes(review): Refuse live lock owners and remove restart test shortcut --- bin/fm-control-lib.sh | 30 ++---------------- tests/fm-secondmate-restart.test.sh | 49 +++++++++++++++-------------- 2 files changed, 29 insertions(+), 50 deletions(-) diff --git a/bin/fm-control-lib.sh b/bin/fm-control-lib.sh index 4b877139bfa..25170c8a6a1 100644 --- a/bin/fm-control-lib.sh +++ b/bin/fm-control-lib.sh @@ -210,15 +210,9 @@ fm_control_omp_home_owner_pid() { # # closure. A stale marker is not disposable just because its recorded PID is # dead: foreign, malformed, or unexplained state must keep refusing relaunch. # -# FM_RESTART_STOP_CMD replaces the signal and liveness probe for the home lock -# owner (default: kill; invoked as " " to stop it and " -0 " -# to probe it). A distinct marker PID is probed but never signaled. This exists -# so tests/fm-secondmate-restart.test.sh can drive the sequence deterministically. -# FM_RESTART_STOP_WAIT seconds to wait for the lock owner to go (default 30) -# FM_RESTART_STOP_POLL seconds between absence probes (default 1) fm_control_omp_secondmate_prepare_relaunch() { # local state=$1 id=$2 worktree=$3 backend=$4 target=$5 meta=$6 omp_state - local stop_cmd stop_wait stop_poll owner_pid elapsed proven marker marker_pid + local stop_cmd owner_pid marker marker_pid local expected_bun expected_bin current_version head committed_version lib_dir lock_present omp_state=$(fm_backend_agent_state "$backend" "$target" "$meta" 2>/dev/null || printf 'unreadable') case "$omp_state" in @@ -231,10 +225,6 @@ fm_control_omp_secondmate_prepare_relaunch() { # &2; return 1; } @@ -290,22 +280,8 @@ fm_control_omp_secondmate_prepare_relaunch() { # &2 return 1 fi - "$stop_cmd" "$owner_pid" >/dev/null 2>&1 || true - proven=0 - elapsed=0 - while :; do - if ! "$stop_cmd" -0 "$owner_pid" >/dev/null 2>&1; then - proven=1 - break - fi - [ "$elapsed" -lt "$stop_wait" ] || break - sleep "$stop_poll" - elapsed=$((elapsed + stop_poll)) - done - [ "$proven" -eq 1 ] || return 1 - if [ -n "$marker_pid" ] \ - && "$stop_cmd" -0 "$marker_pid" >/dev/null 2>&1; then - printf 'error: OMP primary marker owner is still live; refusing relaunch\n' >&2 + if "$stop_cmd" -0 "$owner_pid" >/dev/null 2>&1; then + printf 'error: OMP home lock owner is still live; refusing relaunch\n' >&2 return 1 fi elif [ "$lock_present" -eq 1 ] || [ -n "$marker_pid" ]; then diff --git a/tests/fm-secondmate-restart.test.sh b/tests/fm-secondmate-restart.test.sh index 57c6806b14f..65ce8e4fe2c 100755 --- a/tests/fm-secondmate-restart.test.sh +++ b/tests/fm-secondmate-restart.test.sh @@ -902,7 +902,7 @@ EOF write_fixture_marker "$version" "$marker_pid" "$bash_bin" "$bash_bin" } prepare_fixture() { - FM_RESTART_STOP_WAIT=0 bash -c ' + bash -c ' . "$1/bin/fm-backend.sh" || exit 1 . "$1/bin/fm-control-lib.sh" || exit 1 fm_backend_agent_state() { printf missing; } @@ -1019,15 +1019,28 @@ SH cat > "$dir/fake-live-lock" <<'SH' #!/usr/bin/env bash -if [ "${1:-}" = "-0" ] && [ "${2:-}" = "$FM_TEST_LOCK_PID" ]; then exit 0; fi -exit 1 +if [ "${1:-}" = -0 ]; then + [ "${2:-}" = "$FM_TEST_LOCK_PID" ] && [ ! -e "$FM_TEST_SIGNAL_LOG" ] + exit $? +fi +printf '%s\n' "$1" >> "$FM_TEST_SIGNAL_LOG" SH chmod +x "$dir/fake-live-lock" - reset_fixture - if out=$(FM_RESTART_STOP_CMD="$dir/fake-live-lock" FM_TEST_LOCK_PID="$lock_pid" prepare_fixture); then - fail "OMP preparation accepted a live lock owner" - fi - assert_preserved + for path in present absent; do + reset_fixture + [ "$path" = present ] || rm -f "$marker" + if out=$(FM_RESTART_STOP_CMD="$dir/fake-live-lock" FM_TEST_LOCK_PID="$lock_pid" \ + FM_TEST_SIGNAL_LOG="$dir/lock-signals" prepare_fixture); then + fail "OMP preparation accepted a live stoppable lock owner with marker $path" + fi + assert_contains "$out" 'lock owner is still live' "live lock-owner refusal was not actionable" + assert_absent "$dir/lock-signals" "OMP preparation signaled the live lock owner" + if [ "$path" = absent ]; then + assert_absent "$marker" "refused OMP relaunch created a primary marker" + write_fixture_marker "$version" "$marker_pid" "$bash_bin" "$bash_bin" + fi + assert_preserved + done pass "OMP relaunch preparation accepts only owned dead markers and preserves sessions on every refusal" } @@ -1187,18 +1200,16 @@ SH export FM_TEST_NEW_WINDOW="reborn:fm-$id" export FM_RESTART_STOP_CMD="$sb/stop" export FM_RESTART_SPAWN_CMD="$sb/spawn" - export FM_RESTART_STOP_POLL=1 printf '%s\n' "$pid" > "$dir/$id-home/state/.lock" } clear_omp_restart_stubs() { unset FM_TEST_MARKERS FM_TEST_STOP_LOG FM_TEST_SPAWN_LOG FM_TEST_OWNER_GONE unset FM_TEST_META FM_TEST_NEW_WINDOW FM_RESTART_STOP_CMD FM_RESTART_SPAWN_CMD - unset FM_RESTART_STOP_POLL } test_omp_restart_stops_proves_cleans_then_relaunches() { - local dir out rc owner marker primary_marker stop_first stop_probe spawn_line version bash_bin smhome + local dir out rc owner marker primary_marker stop_probe spawn_line version bash_bin smhome dir=$(new_case omp) printf 'omp\n' > "$dir/home/config/secondmate-harness" add_omp_mate "$dir" sm1 @@ -1234,14 +1245,12 @@ test_omp_restart_stops_proves_cleans_then_relaunches() { # 1 + 2. The owner was stopped, and its absence was PROVEN while every session # artifact was still on disk. - stop_first=$(sed -n 2p "$dir/stop.log") - stop_probe=$(sed -n 3p "$dir/stop.log") + stop_probe=$(sed -n 2p "$dir/stop.log") [ "$(sed -n 1p "$dir/stop.log")" = "probe $owner absent markers_present=4" ] \ || fail "the graceful exit did not retire the marker owner before preparation" - [ "$stop_first" = "stopped $owner markers_present=4" ] \ - || fail "the restart did not stop the mate's session owner first: '$stop_first'"$'\n'"$(cat "$dir/stop.log")" [ "$stop_probe" = "probe $owner absent markers_present=4" ] \ - || fail "owner absence was not proven before anything was cleaned up: '$stop_probe'"$'\n'"$(cat "$dir/stop.log")" + || fail "lock-owner absence was not proven before cleanup: '$stop_probe'"$'\n'"$(cat "$dir/stop.log")" + assert_no_grep '^stopped ' "$dir/stop.log" "relaunch preparation must not signal an owner" # 3. Every session artifact the launch owner refuses to launch over is gone. while IFS= read -r marker; do @@ -1320,7 +1329,7 @@ test_local_omp_restart_after_extension_closure_update() { assert_contains "$(cat "$dir/home/state/sm1.inbox"/*.msg)" "Open-record persistence" \ "the superseded local session never received the persist request" persisted_line=$(grep -n '^persisted corr=' "$dir/stop.log" | head -1 | cut -d: -f1) - stop_line=$(grep -n "^stopped $owner " "$dir/stop.log" | head -1 | cut -d: -f1) + stop_line=$(grep -n "^probe $owner absent " "$dir/stop.log" | head -1 | cut -d: -f1) [ -n "$persisted_line" ] && [ -n "$stop_line" ] && [ "$persisted_line" -lt "$stop_line" ] \ || fail "the superseded local session was not asked to persist before it stopped"$'\n'"$(cat "$dir/stop.log")" pass "T18 a local OMP mate persists and restarts after an update changes its loaded closure" @@ -1389,12 +1398,6 @@ test_unlistable_state_still_refuses_restart() { assert_no_grep '^/exit$' "$dir/fake/literal" "an unlistable state refusal must not have stopped the agent" pass "T20 a mate home whose state cannot be listed still refuses the restart" } -if [ "${FM_TEST_OMP_RESTART_ONLY:-0}" = 1 ]; then - test_omp_secondmate_prepare_relaunch_clears_owned_markers - test_omp_restart_stops_proves_cleans_then_relaunches - test_local_omp_restart_after_extension_closure_update - exit 0 -fi test_persist_gates_and_asks_only_for_open_records test_persist_precedes_restart test_arrived_answer_precedes_deadline_check From 48154681e9b5080bd07b0375ad093cba854fe0f0 Mon Sep 17 00:00:00 2001 From: dnth Date: Fri, 16 Oct 2026 10:26:44 +0800 Subject: [PATCH 4/5] no-mistakes(test): Refuse unverifiable OMP owners before relaunch cleanup --- bin/fm-control-lib.sh | 28 ++++++++++++++++------- tests/fm-secondmate-restart.test.sh | 35 +++++++++++++++++++++++++++-- 2 files changed, 53 insertions(+), 10 deletions(-) diff --git a/bin/fm-control-lib.sh b/bin/fm-control-lib.sh index 25170c8a6a1..231869dc6c7 100644 --- a/bin/fm-control-lib.sh +++ b/bin/fm-control-lib.sh @@ -205,6 +205,23 @@ fm_control_omp_home_owner_pid() { # printf '%s' "$pid" } +# Only ESRCH proves absence. A failed probe can also mean EPERM or an +# unavailable probe command; keep those owners and their artifacts untouched. +fm_control_omp_owner_absent() { # + local probe_cmd=$1 pid=$2 description=$3 probe_output + if probe_output=$(LC_ALL=C "$probe_cmd" -0 "$pid" 2>&1); then + printf 'error: OMP %s owner is still live; refusing relaunch\n' "$description" >&2 + return 1 + fi + case "$probe_output" in + *'No such process'*) return 0 ;; + *) + printf 'error: OMP %s owner is unverifiable; refusing relaunch\n' "$description" >&2 + return 1 + ;; + esac +} + # Retire OMP session artifacts only after the prior session is absent and its # primary marker is tied to this home, this runtime, and a known extension # closure. A stale marker is not disposable just because its recorded PID is @@ -275,15 +292,10 @@ fm_control_omp_secondmate_prepare_relaunch() { # /dev/null 2>&1; then - printf 'error: OMP primary marker owner is still live; refusing relaunch\n' >&2 - return 1 - fi - if "$stop_cmd" -0 "$owner_pid" >/dev/null 2>&1; then - printf 'error: OMP home lock owner is still live; refusing relaunch\n' >&2 - return 1 + if [ -n "$marker_pid" ]; then + fm_control_omp_owner_absent "$stop_cmd" "$marker_pid" 'primary marker' || return 1 fi + fm_control_omp_owner_absent "$stop_cmd" "$owner_pid" 'home lock' || return 1 elif [ "$lock_present" -eq 1 ] || [ -n "$marker_pid" ]; then printf 'error: OMP home lock owner is unverifiable; refusing relaunch\n' >&2 return 1 diff --git a/tests/fm-secondmate-restart.test.sh b/tests/fm-secondmate-restart.test.sh index 65ce8e4fe2c..54a62e1dd30 100755 --- a/tests/fm-secondmate-restart.test.sh +++ b/tests/fm-secondmate-restart.test.sh @@ -933,6 +933,35 @@ EOF [ "$(cat "$smhome/state/omp-sessions/saved.jsonl")" = 'saved session' ] \ || fail "OMP preparation changed the saved session" + # Failed probes do not prove absence: permission errors, unknown errors, + # and a silent failure must preserve both generations of session artifacts. + cat > "$dir/uncertain-owner" <<'SH' +#!/usr/bin/env bash +if [ "${1:-}" = -0 ]; then + if [ "$2" = "$FM_TEST_UNCERTAIN_PID" ]; then + [ -z "$FM_TEST_PROBE_ERROR" ] || printf '%s\n' "$FM_TEST_PROBE_ERROR" >&2 + exit 1 + fi + printf 'kill: (%s) - No such process\n' "$2" >&2 + exit 1 +fi +printf '%s\n' "$*" >> "$FM_TEST_SIGNAL_LOG" +SH + chmod +x "$dir/uncertain-owner" + local uncertain_pid probe_error + for uncertain_pid in "$marker_pid" "$lock_pid"; do + for probe_error in 'Operation not permitted' 'Permission denied' 'probe unavailable' ''; do + reset_fixture + if out=$(FM_RESTART_STOP_CMD="$dir/uncertain-owner" FM_TEST_UNCERTAIN_PID="$uncertain_pid" \ + FM_TEST_PROBE_ERROR="$probe_error" FM_TEST_SIGNAL_LOG="$dir/uncertain-signals" prepare_fixture); then + fail "OMP preparation accepted an uncertain owner $uncertain_pid: $probe_error" + fi + assert_contains "$out" 'owner is unverifiable' "uncertain owner refusal was not actionable" + assert_absent "$dir/uncertain-signals" "OMP preparation signaled an uncertain owner" + assert_preserved + done + done + marker_pid=$BASHPID reset_fixture if out=$(prepare_fixture); then fail "OMP preparation accepted a live marker owner"; fi @@ -1020,8 +1049,9 @@ SH cat > "$dir/fake-live-lock" <<'SH' #!/usr/bin/env bash if [ "${1:-}" = -0 ]; then - [ "${2:-}" = "$FM_TEST_LOCK_PID" ] && [ ! -e "$FM_TEST_SIGNAL_LOG" ] - exit $? + if [ "${2:-}" = "$FM_TEST_LOCK_PID" ] && [ ! -e "$FM_TEST_SIGNAL_LOG" ]; then exit 0; fi + printf 'kill: (%s) - No such process\n' "${2:-}" >&2 + exit 1 fi printf '%s\n' "$1" >> "$FM_TEST_SIGNAL_LOG" SH @@ -1167,6 +1197,7 @@ while IFS= read -r m; do [ ! -e "$m" ] || present=$((present + 1)); done < "$FM_ if [ "${1:-}" = "-0" ]; then if [ -e "$FM_TEST_OWNER_GONE" ]; then printf 'probe %s absent markers_present=%d\n' "${2:-}" "$present" >> "$FM_TEST_STOP_LOG" + printf 'kill: (%s) - No such process\n' "${2:-}" >&2 exit 1 fi printf 'probe %s alive markers_present=%d\n' "${2:-}" "$present" >> "$FM_TEST_STOP_LOG" From b919fcc19f04e0b71d58999805d892a72b7176de Mon Sep 17 00:00:00 2001 From: dnth Date: Fri, 16 Oct 2026 10:42:37 +0800 Subject: [PATCH 5/5] no-mistakes(document): Clarify OMP relaunch recovery documentation --- bin/fm-control-lib.sh | 2 ++ docs/agent-control.md | 2 +- docs/configuration.md | 4 +++- tests/fm-secondmate-restart.test.sh | 2 +- 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/bin/fm-control-lib.sh b/bin/fm-control-lib.sh index 231869dc6c7..6921ed1205f 100644 --- a/bin/fm-control-lib.sh +++ b/bin/fm-control-lib.sh @@ -227,6 +227,8 @@ fm_control_omp_owner_absent() { # # closure. A stale marker is not disposable just because its recorded PID is # dead: foreign, malformed, or unexplained state must keep refusing relaunch. # +# FM_RESTART_STOP_CMD overrides kill for absence probes only, invoked as +# " -0 "; it never receives a stop signal request here. fm_control_omp_secondmate_prepare_relaunch() { # local state=$1 id=$2 worktree=$3 backend=$4 target=$5 meta=$6 omp_state local stop_cmd owner_pid marker marker_pid diff --git a/docs/agent-control.md b/docs/agent-control.md index 9b4517cf579..7381aec093a 100644 --- a/docs/agent-control.md +++ b/docs/agent-control.md @@ -70,7 +70,7 @@ It is not deterministic across the verified adapters: codex and grok resume only A secondmate relaunch does not require one and never rewrites its standing charter. 4. **Stop the old agent** through the `exit` verb, with its postcondition. An endpoint the backend authoritatively reports missing counts as already stopped - there is no agent to stop - so the transaction journals that verdict and continues instead of refusing. -5. **Retire an OMP second mate's session artifacts**, only for that case, after both its endpoint and its home session owner are proven gone; `bin/fm-control-lib.sh`'s `fm_control_omp_secondmate_prepare_relaunch` owns that sequence and the two overrides that let a test drive it. +5. **Retire an OMP second mate's session artifacts** through `bin/fm-control-lib.sh`'s `fm_control_omp_secondmate_prepare_relaunch`, subject to the recovery checks owned by [configuration's harness support](configuration.md#harness-support). Without it the launch owner would refuse the replacement over its own predecessor's leftovers, so an OMP second mate launches fresh with `--secondmate` and its new endpoint is revalidated from the record the launch published rather than the retired one. 6. **Launch the replacement** through its single owner, `bin/fm-spawn.sh --relaunch`, which adopts the recorded worktree and normally the endpoint - and recreates the endpoint inside that same worktree when the backend proves the recorded one gone - while allowing OMP secondmates to publish a fresh endpoint after clearing the previous harness's per-task wiring and arming a fresh busy generation. A ship relaunch also recovers the recorded mode and yolo contract when the caller does not repeat them. diff --git a/docs/configuration.md b/docs/configuration.md index 9ad3073dd55..2466e0bd680 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -326,7 +326,9 @@ For OMP secondmate launches, it explicitly loads that home's `.omp/extensions/fm After proving a failed launch's owned endpoint stopped, cleanup retires its generation artifacts while preserving the persistent home, endpoint metadata, durable inbox, and real session files. Without a live marker or lock owner, cleanup preserves an already-correct session binding or atomically repairs it to the pre-launch retained session, or to the unique saved session when no valid binding exists; an empty store permits pointer removal, while ambiguous saved sessions require explicit reconciliation. A relaunch whose recorded endpoint is proven dead or missing retires the prior generation's runtime markers (adapter, ready, started, doorbell markers and request receipts) only after the endpoint, session-lock, and integration-marker live-owner checks pass, so a wake-on-delivery restore after a successful generation is accepted. -For OMP secondmates, the `fm-control.sh` relaunch owner parses the four-line primary marker and requires its Bun and entrypoint paths to match the recorded home runtime. It proves the `.lock` owner and any distinct marker PID absent before retiring the marker; if the marker hash is older, the current working closure must match `HEAD` and the hash must occur on that home's recorded fast-forward history. +For OMP secondmates, the `fm-control.sh` relaunch owner parses the four-line primary marker and requires its Bun and entrypoint paths to match the recorded home runtime. +It proves both the `.lock` owner and the marker owner absent without signaling either process; only an explicit no-such-process probe result proves absence. +If the marker hash differs from the current closure, the current working closure must match `HEAD` and the marker hash must occur on that home's recorded fast-forward history. The owner path removes only stale runtime markers, preserving `state/.omp-session` and saved session files; direct `fm-spawn.sh` still refuses an old-version marker, PID mismatch, or foreign runtime that the owner path has not safely reconciled. Malformed, unverifiable, symlinked, non-file, foreign, live-owner, or unexplained-history state refuses recovery. [`tests/fm-omp-secondmate.test.sh`](../tests/fm-omp-secondmate.test.sh), [`tests/fm-secondmate-restart.test.sh`](../tests/fm-secondmate-restart.test.sh), and [`tests/fm-remote-secondmate-lifecycle-e2e.test.sh`](../tests/fm-remote-secondmate-lifecycle-e2e.test.sh) cover direct-launch refusals, owner-path safety, retained-session preservation, and updated-extension relaunch in fixture homes. diff --git a/tests/fm-secondmate-restart.test.sh b/tests/fm-secondmate-restart.test.sh index 54a62e1dd30..cf1618b16e4 100755 --- a/tests/fm-secondmate-restart.test.sh +++ b/tests/fm-secondmate-restart.test.sh @@ -1080,7 +1080,7 @@ SH # decision, real fm-send over the real OMP native doorbell, the parent-owned # persist gate, and bin/fm-control.sh's relaunch transaction with its OMP # preparation step. Only the two things that would touch this machine are -# replaced: the signal that stops the home session owner, and the launch owner. +# replaced: the home-owner absence probe, and the launch owner. # Both stubs record what the world looked like AT THE MOMENT they ran, which is # how the order below is asserted rather than assumed: the absence probe sees all # four session artifacts still on disk (so nothing was deleted while the owner