Skip to content

fix(bin): prevent mid-session session-start reruns - #94

Merged
Amplify-Logic merged 5 commits into
mainfrom
fm/fm-continuity-gate-midsession-rerun-a3
Aug 23, 2026
Merged

fix(bin): prevent mid-session session-start reruns#94
Amplify-Logic merged 5 commits into
mainfrom
fm/fm-continuity-gate-midsession-rerun-a3

Conversation

@Amplify-Logic

Copy link
Copy Markdown
Owner

Intent

Close a real gap in the continuity gate of the firstmate public repo. bin/fm-continuity-pretool-check.sh is a PreToolUse guard that, while tasks are in flight and no live watcher holds the home lock, refuses most bin/fm-*.sh fleet commands but allows a short safe list, including the once-per-session bin/fm-session-start.sh. The known gap (documented as a deferred follow-up in docs/watcher-continuity.md 'Known limitation'): mid-session reruns of fm-session-start.sh were not distinguished from the legitimate first run, so the once-per-session allowance could be exploited or misfire mid-session. The decided technique, fixed in advance: reuse the lock-in-ancestry approach already implemented for bin/fm-sessionstart-nudge.sh so a session-start invocation is recognized as the legitimate first run only when no live session holds the home session lock, and a rerun after the session already completed session-start (live lock holder in this process's ancestry) - or an attempt while another live session holds the lock (ancestry mismatch / foreign holder) - is treated like any other gated fleet command and refused with the gate's canonical outage summary. Implementation decisions: the shared ancestry walk in bin/fm-primary-scope-lib.sh was generalized into a three-way fm_session_lock_relation() (free/ancestry/foreign) with fm_session_lock_in_ancestry() delegating to it so the nudge behavior is unchanged and there is one owner; the shell gate passes the relation to bin/fm-continuity-command-policy.mjs via a new optional --session-lock flag (absent or unrecognized value is treated as free, preserving old-caller behavior, and the classifier stays a pure function of its inputs); a live-held lock denies fm-session-start.sh with a distinct midsession-session-start reason code, rendered with the canonical outage summary and holder-specific wording; the generic deny guidance now names the once-per-session session-start entry point only when the lock relation is free. Deliberately unchanged: the gate's existing allowances (wake drain, watcher arm recovery, ordinary literal fail-closed teardown, exact sentinel enable) are untouched and are now explicitly asserted to remain allowed while a lock is held; a dead/stale lock holder (crash recovery) still allows the genuine first session-start. Acceptance criteria: first session-start in a lock-holding session allowed exactly as before; a second/mid-session session-start attempt while tasks are in flight with no live watcher refused with the canonical outage summary; no regression for other allowed commands; shellcheck-clean (bin/fm-lint.sh passes); colocated executable tests in tests/fm-continuity-pretool-check.test.sh covering first-run allowed, rerun refused (direct and nested), ancestry-mismatch refused, dead-holder first run allowed, and each existing allowance still permitted. Docs: the watcher-continuity.md 'Known limitation' section was rewritten as 'Session-start first-run scoping' since the gap is closed, and arm-pretool-check.md's claim that the ancestry check scopes guidance text only was updated to describe the classification contract. This changes firstmate's shared tracked material (bin/), so firstmate-coding-guidelines were followed: one-sentence-per-line markdown, plain dashes, no agent co-author, one-owner rule for the lock-relation decision.

What Changed

  • Scope the continuity gate’s fm-session-start.sh allowance to genuine first runs, denying same-session reruns and attempts under a foreign live session lock with holder-specific outage guidance.
  • Centralize harness-aware lock liveness and three-way lock classification (free, ancestry, or foreign) while preserving stale-lock recovery and existing recovery-command allowances.
  • Update continuity documentation and expand executable coverage for direct and nested reruns, foreign and stale holders, and unchanged allowances.

Risk Assessment

✅ Low: The follow-up centralizes harness-aware holder liveness, preserves the lock CLI contract, fixes stale PID reuse, and satisfies the stated continuity-gate acceptance criteria without introducing a material risk.

Testing

The full continuity-gate behavior suite passed, including direct and nested reruns, foreign and stale locks, and preserved recovery allowances; an isolated end-to-end transcript additionally demonstrates a genuine first run being silently allowed and a lock-held mid-session rerun returning the expected user-visible denial. No linters or static-analysis tools were run, per the testing rules.

Evidence: Continuity gate end-user transcript
CONTINUITY GATE END-USER TRANSCRIPT

Scenario 1: genuine first session-start, no live session lock
Command: bin/fm-session-start.sh
Exit: 0
stdout bytes: 0
stderr bytes: 0
Result: ALLOWED

Scenario 2: mid-session rerun, current harness ancestry holds lock
Command: bin/fm-session-start.sh
Exit: 2
PreToolUse response:
{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny"
  },
  "systemMessage": "[watcher-continuity] SUPERVISION OUTAGE: down for unknown duration (unknown since when; watcher beat file missing or unreadable); 1 task(s) in flight: task. No live watcher holds this home lock. This session's own ancestry already holds the home session lock, so the once-per-session bin/fm-session-start.sh has already run here and a mid-session re-run is not a recovery action. Drain wakes with bin/fm-wake-drain.sh, the safe mid-session action; use fail-closed bin/fm-teardown.sh for completed tasks when needed, then re-arm with bin/fm-watch-arm.sh as a tracked Claude background task before running other fleet commands (blocked: fm-session-start.sh)"
}

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 1 issue found → auto-fixed ✅
  • 🚨 bin/fm-primary-scope-lib.sh:57 - Intent requires that “a dead/stale lock holder (crash recovery) still allows the genuine first session-start,” but this new kill -0 check treats any reused live PID as a foreign session. That contradicts fm-lock.sh, which considers a holder stale when the PID is not a harness and would permit acquisition. A stale lock whose PID was reused by an unrelated process will therefore block recovery indefinitely; the new foreign-holder test even uses sleep, encoding this mismatch. Align relation liveness with the authoritative holder_alive semantics and test both a real foreign harness and a reused non-harness PID.

🔧 Fix: Unify harness-aware session lock liveness
✅ Re-checked - no issues remain.

✅ **Test** - passed

✅ No issues found.

  • Inspected git diff 3e81ed5f5fa3cce53ce3876198ff1458900802cc..8e72073affd98d0fc6c674a7b88d89b0b8a0c933 and the colocated behavior tests.
  • bash tests/fm-continuity-pretool-check.test.sh
  • Manually invoked bin/fm-continuity-pretool-check.sh --command 'bin/fm-session-start.sh' in an isolated lock-free fixture and confirmed exit 0 with no output.
  • Repeated the guard invocation beneath a harness-like Node lock holder and confirmed exit 2 with a PreToolUse deny, canonical outage summary, and ancestry-specific mid-session wording.
  • Verified git status --short remained clean after testing.
✅ **Document** - passed

✅ No issues found.

🔧 **Lint** - 1 issue found → auto-fixed ✅
  • ⚠️ linter found issues (exit code 1)

🔧 Fix: Silence deliberate ShellCheck single-quote warning
✅ Re-checked - no issues remain.

✅ **Push** - passed

✅ No issues found.

…first run

The watcher-continuity PreToolUse gate allowed bin/fm-session-start.sh
unconditionally, so a mid-session re-run while tasks were in flight with
no live watcher could re-acquire the home lock and run bootstrap's
mutating sweeps - the deferred follow-up documented in
docs/watcher-continuity.md.

Generalize the shared ancestry walk into fm_session_lock_relation()
(free / ancestry / foreign) in bin/fm-primary-scope-lib.sh, pass the
relation into bin/fm-continuity-command-policy.mjs, and deny a
session-start attempt whenever a live session holds the home lock: an
ancestry holder means session start already ran here, a foreign holder
means another session owns the home. A lock-free home - including a
dead holder after a crash - keeps session start a recovery command, and
the wake-drain, watcher-arm, literal teardown, and exact sentinel-enable
allowances are unchanged. The deny guidance also stops naming the
once-per-session entry point for a foreign-held lock.
@Amplify-Logic
Amplify-Logic merged commit eb56b58 into main Aug 23, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant