Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
e2fe5b7
fix: decide session-lock ownership by session, not by process tree
Aug 18, 2026
db797f4
docs: state the launch-marker scope limit where the contract lives
Aug 18, 2026
9560ddb
no-mistakes(review): make session-cohort proof symmetric and stop sam…
Aug 18, 2026
2932ca7
no-mistakes(review): narrow MainThread identity and report the real f…
Aug 18, 2026
af5014b
no-mistakes(review): isolate autoarm fixtures and guard the sample-ga…
Aug 18, 2026
503ff21
no-mistakes(review): refuse guessed MainThread scripts and own the si…
Aug 18, 2026
c2175bc
no-mistakes(review): scope the argv match claims and unify the yield …
Aug 19, 2026
a9425ca
no-mistakes(review): scope launch markers to the harness that exports…
Aug 19, 2026
789f22e
no-mistakes(review): narrow bare-interpreter identity to pre-flag arg…
Aug 19, 2026
fdbe22d
no-mistakes(review): type cohort acceptance from executable identity …
Aug 20, 2026
27af517
fix: type acceptance from the command name a real install reports
Aug 20, 2026
c67c1e2
no-mistakes(review): announce reclaims of live but unidentified lock …
Aug 24, 2026
a2ec599
no-mistakes(review): name the co-located holder and state the residua…
Aug 24, 2026
e241586
no-mistakes(review): defer the race statement and guard the ancestry …
Aug 24, 2026
9b81a43
no-mistakes(review): state the general cause of an announced lock rec…
Aug 24, 2026
03826bd
no-mistakes(review): order launch markers by process start time
Aug 24, 2026
66419f3
no-mistakes(test): register tests/session-signals.sh in changed-test map
Aug 24, 2026
96a879c
no-mistakes(document): widen FM_PROC_ROOT_OVERRIDE scope, add launch-…
Aug 24, 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/harness-adapters/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ Within the Pi family, only the exact launch-boundary marker `FM_PI_HARNESS=pi-si
On `unknown`, ask the captain instead of guessing.
A captain override always beats detection.
When verifying a new adapter, record its env marker and command name in `bin/fm-harness.sh`.
Also observe, in a real child of a real session of that harness, whether it exports a launch marker naming its own session pid: `FM_SESSION_LAUNCH_MARKERS` in `bin/fm-session-lock-lib.sh` has a verified row for Claude alone, and every other harness decides session-lock ownership by process ancestry until a row is verified for it, which [`docs/verification/runtime-backends.md`](../../../docs/verification/runtime-backends.md#session-lock-identity-and-the-suspended-holder) owns.

For stuck recovery, the target window's harness is recorded as `harness=` in `state/<id>.meta`.
Use that value for interrupt, exit, resume, and skill-invocation facts.
Expand Down
9 changes: 5 additions & 4 deletions bin/fm-claude-stop-autoarm.sh
Original file line number Diff line number Diff line change
Expand Up @@ -101,18 +101,19 @@ fm_hook_payload_is_foreign_host "$PAYLOAD" && exit 0
fm_primary_scope_matches "$FM_ROOT" "$STATE" || exit 0

# --- identity: only the lock-owning session's hooks may arm ------------------
# A prior session may have died after leaving its numeric harness pid in .lock.
# Use the shared liveness predicate to recognize only that stale-owner case.
# A prior session may have left its numeric harness pid in .lock after dying or
# being suspended. The shared holder predicate recognizes exactly those
# recoverable cases and keeps a genuinely competing session inert.
# Defer the mutating claim until after the unchanged AFK and need gates, so an
# idle or away home remains byte-for-byte inert. Missing or malformed locks are
# uncertainty rather than stale-owner evidence and remain inert.
# uncertainty rather than recoverable-owner evidence and remain inert.
RECOVER_SESSION_LOCK=0
if ! fm_session_lock_owned_by_self "$STATE"; then
LOCK_PID=$(cat "$STATE/.lock" 2>/dev/null || true)
case "$LOCK_PID" in
''|*[!0-9]*) exit 0 ;;
esac
fm_harness_pid_alive "$LOCK_PID" && exit 0
fm_session_lock_holder_competes "$LOCK_PID" && exit 0
RECOVER_SESSION_LOCK=1
fi

Expand Down
48 changes: 42 additions & 6 deletions bin/fm-lock.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,15 @@
# Writes the harness (agent) process PID found by walking the shell's ancestry,
# which lives as long as the firstmate session - unlike the transient subshell
# PID of any one tool call, which is dead moments after it is written.
#
# Acquisition yields ONLY to a genuinely competing session, decided by
# fm_session_lock_holder_competes in bin/fm-session-lock-lib.sh: a recorded
# holder that is this same session in another process tree, or a durably
# suspended one, does not block. Every acquisition then converges the lock onto
# the acquiring session's own pid, so repeated runs are idempotent, and any live
# holder it converged onto, took over from, or could not identify is named on
# stdout rather than being silent.
#
# Usage: fm-lock.sh acquire; exit 1 unless ownership is verified
# fm-lock.sh status print holder and liveness; always exits 0
set -u
Expand All @@ -29,7 +38,13 @@ if [ "${1:-}" = "status" ]; then
echo "lock: unreadable"
exit 0
}
if fm_harness_pid_alive "$old"; then echo "lock: held by live harness pid $old"; else echo "lock: stale (pid $old dead or not a harness)"; fi
if ! fm_harness_pid_alive "$old"; then
echo "lock: stale (pid $old dead or not a harness)"
elif fm_harness_pid_suspended "$old"; then
echo "lock: held by SUSPENDED harness pid $old (reclaimable: a stopped session is not holding this home)"
else
echo "lock: held by live harness pid $old"
fi
exit 0
fi

Expand All @@ -55,13 +70,17 @@ release_claim_lock() {
trap release_claim_lock EXIT
trap 'exit 1' HUP INT TERM

# Why the yield reason is captured rather than printed here: the fast path
# below is only a pre-check, and the authoritative decision is retaken under the
# claim lock. Printing it once, at the end, keeps one acquisition to one line.
YIELDED=
if [ -f "$LOCK" ] && [ ! -L "$LOCK" ]; then
old=$(cat "$LOCK" 2>/dev/null || true)
if [ "$old" = "$me" ]; then
echo "lock acquired: harness pid $me"
exit 0
fi
if fm_harness_pid_alive "$old"; then
if fm_session_lock_holder_competes "$old"; then
echo "error: another live firstmate session holds the lock (pid $old); operate read-only until resolved" >&2
exit 1
fi
Expand All @@ -86,9 +105,22 @@ if [ -e "$LOCK" ] || [ -L "$LOCK" ]; then
echo "error: session lock is unreadable; operate read-only until resolved" >&2
exit 1
}
if [ "$old" != "$me" ] && fm_harness_pid_alive "$old"; then
echo "error: another live firstmate session holds the lock (pid $old); operate read-only until resolved" >&2
exit 1
if [ "$old" != "$me" ]; then
if fm_session_lock_holder_competes "$old"; then
echo "error: another live firstmate session holds the lock (pid $old); operate read-only until resolved" >&2
exit 1
fi
# Reclaiming an owner that is gone has always been silent and stays that
# way. The three holders that are alive and still yield are the ones worth
# naming - this session's own holder in another tree, a durably suspended
# session, and a live process the identity rules cannot type as a harness -
# so name them rather than moving the lock out from under a visible process
# quietly. None of the three is exceptional; the last is the ordinary
# reading of a stale lock whose pid an unrelated process now occupies.
# The predicate supplies the whole clause and leaves it empty for the silent
# case, so this is one assignment rather than a second liveness question that
# could disagree with the classification that just ran.
YIELDED=$FM_SESSION_HOLDER_YIELD_REASON
fi
fi
if ! { printf '%s\n' "$me" > "$LOCK"; } 2>/dev/null; then
Expand All @@ -104,4 +136,8 @@ if [ ! -f "$LOCK" ] || [ -L "$LOCK" ] || [ "$written" != "$me" ]; then
exit 1
fi
release_claim_lock
echo "lock acquired: harness pid $me"
if [ -n "$YIELDED" ]; then
echo "lock acquired: harness pid $me ($YIELDED)"
else
echo "lock acquired: harness pid $me"
fi
Loading
Loading