Skip to content

feat(primary): add quota-aware orchestrator handoff with atomic lock rotation - #12

Merged
Amplify-Logic merged 6 commits into
mainfrom
fm/firstmate-auto-orchestrator-handoff-h4
Jul 21, 2026
Merged

Amplify-Logic merged 6 commits into
mainfrom
fm/firstmate-auto-orchestrator-handoff-h4

Conversation

@Amplify-Logic

Copy link
Copy Markdown
Owner

Intent

Build automated quota-aware orchestrator handoff (fully automated primary rotation) for firstmate. The captain wants a supervisor that monitors the active orchestrator's runtime quota and, when it crosses a threshold, cleanly hands the firstmate primary role off to a fallback runtime (e.g. Claude -> Pi -> Codex -> Kimi K3) with zero lost work.

HARD INVARIANT: two orchestrators must NEVER both hold the per-home session lock; the handoff must be atomic (outgoing releases the lock only after durable state is flushed; incoming acquires, runs session-start, reconciles, re-arms supervision). Build on bin/fm-primary.sh and the durable-state / restart-is-a-non-event design.

Acceptance: design notes covering the atomic-lock handoff protocol and failure modes (in PR body / docs); implementation with tests exercising the handoff including the never-two-locks invariant under failure injection; full relevant test suite passes; no change to existing primary behavior when the feature is not enabled. Feature is opt-in via local gitignored config/primary-handoff.

What Changed

  • Added an opt-in primary handoff supervisor (bin/fm-primary-handoff.sh plus fm-primary-handoff-lib.sh) that monitors the active orchestrator's remaining quota and, past a configurable threshold, atomically rotates the firstmate primary role to a fallback runtime — the outgoing holder flushes durable state and releases the per-home session lock before the incoming runtime acquires it, so two orchestrators never hold the lock simultaneously. Enabled only via gitignored config/primary-handoff; existing fm-primary.sh behavior is unchanged when disabled.
  • Hardened locking and correctness per review: fm-lock.sh release-stale now re-verifies lock contents before removal to close a TOCTOU race, the quota threshold comparison handles fractional JSON values, abort-path cleanup is factored into a helper, and the jq-1.6 cmux liveness probe and related test fixtures were fixed.
  • Added tests/fm-primary-handoff.test.sh (12 tests covering the never-two-holders invariant under failure injection, coordination-lock racing, threshold checks, and disabled-config no-op) plus docs: docs/primary-handoff.md protocol/failure-mode notes, configuration docs, an example config, and AGENTS.md state-file inventory updates.

Risk Assessment

✅ Low: The fix round cleanly resolves all six prior findings exactly as instructed (numeric threshold compare, narrowed release-stale race, doc corrections, documented unmonitored-provider limitation, seam doc, cleanup helper) with no new behavior or risks introduced.

Testing

Re-ran the three previously failing tests plus the primary-handoff and primary suites (all pass, RC=0) on top of the already-passing baseline full-suite run, and manually demonstrated the quota-triggered handoff end-to-end in a scratch home — threshold detection, atomic lock release, profile rotation claude-fable→pi, and a complete durable handoff record — with transcripts saved as evidence. No UI surface is involved (CLI/daemon feature), so evidence is CLI transcripts and persisted state files.

Evidence: Manual end-to-end handoff demo transcript (check → signal → release → launch pi → phase=complete)

$ fm-lock.sh status lock: held by live harness pid 40281 # quota feed: claude session window at 8% remaining (threshold 15%) $ fm-primary-handoff.sh check handoff: threshold crossed profile=claude-fable min_remaining=8 threshold=15 supervisor: signaling outgoing pid 40281 lock released: stale holder pid 40281 fm-primary-handoff: handed off primary claude-fable -> pi (reason=quota:min_remaining=8) handed_off: claude-fable -> pi $ fm-lock.sh status # after handoff lock: held by live harness pid 40527 # outgoing pid 40281 alive? no $ cat state/.primary-handoff phase=complete from=claude-fable to=pi reason=quota:min_remaining=8 outgoing_pid=40281 incoming_pid=40527

$ cat config/primary-handoff
{ "enabled": true, "threshold_percent_remaining": 15, "poll_seconds": 60, "cooldown_seconds": 300, "chain": ["claude-fable", "pi", "codex", "kimi-k3"] }

# live outgoing primary (claude) holds the session lock, pid 40281
$ fm-lock.sh status
lock: held by live harness pid 40281

# quota feed: claude session window at 8% remaining (threshold 15%)
$ fm-primary-handoff.sh check
handoff: threshold crossed profile=claude-fable min_remaining=8 threshold=15
supervisor: signaling outgoing pid 40281
lock released: stale holder pid 40281
fm-primary-handoff: handed off primary claude-fable -> pi (reason=quota:min_remaining=8)
handed_off: claude-fable -> pi

$ fm-lock.sh status   # after handoff: new live holder, old pid gone
lock: held by live harness pid 40527
# outgoing pid 40281 alive? no; lock now held by pid 40527

$ cat state/.primary-handoff
schema=fm-primary-handoff.v1
phase=complete
from=claude-fable
to=pi
reason=quota:min_remaining=8
token=1784590096-40281
outgoing_pid=40281
incoming_pid=40527
started_at=1784590096
updated_at=1784590096
error=
completed_at=1784590096
cooldown_until=1784590396
Evidence: Test transcript: previously failing tests + fm-primary-handoff + fm-primary (all pass, RC=0)
tmux 3.6a
== tests/fm-backend-cmux.test.sh ==
ok - fm_backend_cmux_version_check: accepts the verified minimum (0.64.17)
ok - fm_backend_cmux_version_check: accepts a newer version (0.70.0)
ok - fm_backend_cmux_version_check: refuses an old version loudly
ok - fm_backend_cmux_version_check: refuses loudly when cmux is not found on PATH or at the bundle path
ok - fm_backend_cmux_password: reads the first non-empty line of config/cmux-socket-password
ok - fm_backend_cmux_password: preserves spaces and tabs in config/cmux-socket-password
ok - fm_backend_cmux_password: respects FM_CONFIG_OVERRIDE
ok - fm_backend_cmux_password: empty when config/cmux-socket-password is absent
ok - fm_backend_cmux_cli: exports CMUX_SOCKET_PASSWORD when config/cmux-socket-password is set
ok - fm_backend_cmux_parse_target: splits '<workspace_uuid>:<surface_uuid>' on the first colon
ok - fm_backend_cmux_normalize_key: Enter/Escape/C-c map to cmux's verified enter/escape/ctrl-c
ok - fm_backend_cmux_scoped_title: scopes a primary task title with firstmate plus root hash
ok - fm_backend_cmux_scoped_title: scopes a secondmate task title with the home marker plus root hash
ok - fm_backend_cmux_scoped_title: includes the resolved FM_ROOT hash in the home label
ok - fm_backend_validate: cmux is a known backend
ok - fm_backend_busy_state: cmux (no native primitive) always reports unknown, same as tmux/zellij/orca
ok - fm_backend_composer_state: routes cmux to the cmux composer classifier
ok - fm_backend_cmux_ping_state: reports 'ok' on PONG
ok - fm_backend_cmux_ping_state: reports 'denied' when socketControlMode=cmuxOnly rejects the connection
ok - fm_backend_cmux_ping_state: reports 'unauth' when password mode rejects a missing/wrong password
ok - fm_backend_cmux_ping_state: reports 'unauth' when password mode rejects a wrong password (Invalid password)
ok - fm_backend_cmux_ping_state: reports 'down' when the app is not running yet
ok - fm_backend_cmux_ensure_running: returns immediately when cmux is already reachable
ok - fm_backend_cmux_ensure_running: fails fast on a denied socket without attempting to launch, naming every viable mode
ok - fm_backend_cmux_ensure_running: fails fast on an unauthenticated socket, naming the password config and the Automation mode alternative
ok - fm_backend_cmux_create_task: refuses a duplicate workspace title (cmux's own new-workspace has no uniqueness check)
ok - fm_backend_cmux_create_task: creates a workspace and parses workspace_id/surface_id from list responses
ok - fm_backend_cmux_target_ready: fails when the workspace/surface is not found (list-panes structural check)
ok - fm_backend_cmux_target_ready: verifies the workspace title against the expected label first
ok - fm_backend_cmux_target_ready: rejects a workspace id reused under a different title
ok - fm_backend_cmux_capture: fetches generously and trims to N lines locally
ok - fm_backend_cmux_capture: propagates a read-screen failure even when stdout is empty
ok - fm_backend_cmux_capture: fails when the target surface is absent
ok - fm_backend_cmux_send_key: normalizes the key (Escape -> escape) and targets the explicit workspace/surface
ok - fm_backend_cmux_send_key: recovers stale workspace/surface ids by expected label
ok - fm_backend_cmux_send_literal: calls send with an explicit workspace/surface and a -- separator
ok - fm_backend_cmux_current_path: actively probes with marked begin/end lines (zellij-shape frozen cwd)
ok - fm_backend_cmux_composer_state: a bare '❯' composer row reads empty
ok - fm_backend_cmux_composer_state: the ghost placeholder text reads empty, not pending
ok - fm_backend_cmux_composer_state: real composer text reads pending
ok - fm_backend_cmux_composer_state: a slash-command popup's argument-hint placeholder still reads pending (the incident fix)
ok - fm_backend_cmux_composer_state: reports unknown when the surface cannot be captured
ok - fm_backend_cmux_composer_state: reports unknown when no border-delimited composer row is found
ok - fm_backend_cmux_send_text_submit: reports 'empty' once the composer row reads empty after one Enter
ok - fm_backend_cmux_send_text_submit: reports 'pending' when the composer never clears after retried Enters (swallowed)
ok - fm_backend_cmux_send_text_submit: retries past a popup-placeholder-fill Enter and lands the real second Enter (the incident fix)
ok - fm_backend_cmux_send_text_submit: reports 'send-failed' when the target workspace/surface is absent
ok - fm_backend_cmux_window_of_workspace: walks windows and counts the membership-confirming workspace list
ok - fm_backend_cmux_window_of_workspace: echoes nothing when no window holds the workspace
ok - fm_backend_cmux_kill: closes the task workspace directly when it is not the last in its window
ok - fm_backend_cmux_kill: adds a throwaway sibling then closes the target when it is the last workspace in its window
ok - fm_backend_cmux_kill: never fails even when close-workspace fails
ok - fm_backend_cmux_kill: recovers stale workspace/surface ids by expected label
ok - fm_backend_cmux_list_live: lists only this home's scoped task workspaces using plain fm-<id> labels
ok - fm-spawn.sh: refuses backend=cmux for --secondmate spawns (mirrors Orca's refusal; no secondmate launch design exists yet)
== tests/fm-secondmate-liveness.test.sh ==
ok - fm_backend_tmux_agent_alive: alive/dead/unknown classification
ok - fm_backend_herdr_agent_alive: dead/no-agent->dead, live->alive, unknown->unknown
ok - fm_backend_agent_alive: routes tmux/herdr correctly, unknown for an unverified backend
ok - sweep: a confirmed-dead secondmate endpoint is killed and respawned
ok - sweep: cursor is accepted as a verified secondmate harness
ok - sweep: an already-live secondmate is left untouched (no kill, no respawn)
ok - sweep: a transient/unknown probe reading is reported but never acted on
ok - sweep: an unverified harness makes a dead-looking probe inconclusive
ok - sweep: idempotent by construction - a live secondmate is never re-touched on a later run
ok - sweep: skipped entirely under FM_BOOTSTRAP_DETECT_ONLY=1, exactly like the other mutating sweeps
ok - sweep: a silent no-op with no kind=secondmate meta present (a secondmate home's own natural scoping)
# all fm-secondmate-liveness tests passed
== tests/fm-spawn-herdr-presentation.test.sh ==
ok - fm-spawn fake Herdr E2E: single, batch, projects, axes, human labels, outcomes, states, and hidden ids converge
ok - fm-spawn fake Herdr E2E: a protocol-14 build spawns through the prior label-based flow untouched by presentation
== tests/fm-primary-handoff.test.sh ==
ok - disabled config is a no-op and leaves the live session lock alone
ok - happy-path handoff flushes, releases outgoing, launches incoming, one live holder
ok - flush failure aborts without releasing lock or launching incoming
ok - signal failure aborts with outgoing still the sole live holder
ok - wait_dead failure never launches and never dual-holds
ok - fm-lock release-stale refuses while a live harness holds the lock
ok - pre_launch failure leaves zero live holders and never dual-holds
ok - launch failure never creates two live holders
ok - check hands off when quota is at or below threshold
ok - check is a no-op when remaining quota is above threshold
ok - fm-primary behavior unchanged when handoff is disabled; marker only when enabled
ok - coordination lock serializes supervisors without dual session-lock holders
All primary-handoff tests passed.
== tests/fm-primary.test.sh ==
ok - fm-primary: profiles expand exact flags and always launch from the tracked root
ok - fm-primary: unknown profiles, missing CLIs, and missing integrations fail closed
ok - fm-primary: a live Firstmate lock is refused without killing or replacing it
ok - fm-primary: exec preserves root, stable child marker, role, and CLI exit status
ok - fm-primary: visible role metadata is scoped to the current pane or window
ok - fm-primary: opt-in shim is idempotent only for the exact safe symlink
ok - fm-primary: Kimi is pinned, isolated, lifecycle-integrated, and remains primary-only
ok - fm-primary: Kimi gets a scoped tmux companion without replacing native controls
ok - fm-primary: Kimi version, doctor, and managed-path checks fail closed
ok - fm-primary: a corrupt source Kimi registry fails closed and leaves no temp files
ok - fm-primary: LAB role cannot appear as FIRSTMATE or run in default Herdr
RC=0
- Outcome: 🔧 1 issue found → auto-fixed ✅ across 2 runs (1h21m53s)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 6 issues found → auto-fixed ✅
  • ⚠️ bin/fm-primary-handoff-lib.sh:314 - fm_handoff_over_threshold uses integer test [ &#34;$remaining&#34; -le &#34;$threshold&#34; ], but percentRemaining is a JSON number and jq min can emit fractional values (e.g. 12.5). [ errors on non-integers and the error is treated as not-over-threshold, so automated handoff silently never triggers on fractional low-quota values. Compare via jq/awk numeric comparison instead. Tests only cover integer fixtures.
  • ⚠️ bin/fm-lock.sh:63 - TOCTOU in fm-lock.sh release-stale: between holder_alive($old) returning false and rm -f &#34;$LOCK&#34;, an independently launched primary (not serialized by the handoff coordination lock) can acquire the lock; the rm then deletes that live holder's lock file, permitting a second acquire and violating the never-two-holders invariant. Re-read the lock and verify its content still equals $old immediately before removal (or use an atomic rename) to narrow the race.
  • ⚠️ docs/primary-handoff.md:90 - docs/primary-handoff.md states the state/.primary-active marker is written on real launches even 'when handoff is disabled', but bin/fm-primary.sh:439 writes it only when config/primary-handoff exists and enabled==true — which is the behavior the user intent requires ('no change to existing primary behavior when the feature is not enabled'). Correct the doc sentence to match the code.
  • ⚠️ bin/fm-primary-handoff-lib.sh:117 - fm_handoff_profile_provider maps only claude/codex/grok to quota providers; pi, kimi-k3, and opencode return empty, so min_remaining is 'na' and cmd_check never crosses the threshold for them. The intent's example chain Claude -> Pi -> Codex -> Kimi K3 therefore stops auto-rotating once pi becomes primary (manual execute --force still works). Confirm whether this is an accepted limitation of the 'fully automated primary rotation' goal or whether those providers need quota sources / a fallback trigger.
  • ℹ️ bin/fm-primary-handoff.sh:31 - Header test-seam list omits the post_launch FM_HANDOFF_INJECT_FAIL value handled at fm-primary-handoff.sh:267 (list shows flush|signal|wait_dead|release|pre_launch|launch).
  • ℹ️ bin/fm-primary-handoff.sh:220 - cmd_execute repeats the fm_lock_release &#34;$coord&#34;; trap - EXIT; return 1 cleanup sequence ~12 times across abort paths; a single cleanup helper (or relying on the already-installed EXIT trap) would simplify control flow without behavior change.

🔧 Fix: fix float threshold, release-stale race, docs, cleanup helper
✅ Re-checked - no issues remain.

🔧 **Test** - 1 issue found → auto-fixed ✅
  • 🚨 tests failed with exit code 1
  • command -v tmux >/dev/null || { echo "tmux is required for e2e tests" >&2; exit 1; }; tmux -V; rc=0; for t in tests/*.test.sh; do echo "== $t =="; bash "$t" || rc=1; done; exit "$rc"

🔧 Fix: fix jq-1.6 cmux liveness probe and test fixtures
✅ Re-checked - no issues remain.

  • command -v tmux >/dev/null || { echo "tmux is required for e2e tests" >&2; exit 1; }; tmux -V; rc=0; for t in tests/*.test.sh; do echo "== $t =="; bash "$t" || rc=1; done; exit "$rc"
  • Baseline: configured full suite (for t in tests/*.test.sh; do bash $t; done) already ran successfully before this round
  • bash tests/fm-primary-handoff.test.sh — 12 tests including never-two-holders invariant under failure injection (flush, signal, wait_dead, pre_launch, launch), coordination-lock racing, threshold check, and disabled-config no-op
  • bash tests/fm-primary.test.sh — existing primary behavior unchanged
  • bash tests/fm-backend-cmux.test.sh, bash tests/fm-secondmate-liveness.test.sh, bash tests/fm-spawn-herdr-presentation.test.sh — the three round-1 failures, all now passing (RC=0)
  • Manual end-to-end demo: enabled config/primary-handoff, live fake claude holder on state/.lock, quota feed at 8% remaining → bin/fm-primary-handoff.sh check performed the full handoff to pi with one live holder throughout; transcript captured as evidence
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

Amplify-Logic and others added 4 commits July 21, 2026 01:36
Automate primary rotation when provider quota crosses a threshold, with an
atomic session-lock transfer that never allows two live holders, and keep
the feature inert unless config/primary-handoff is enabled.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Amplify-Logic
Amplify-Logic force-pushed the fm/firstmate-auto-orchestrator-handoff-h4 branch from 9dbc38b to c449342 Compare July 20, 2026 23:38
@Amplify-Logic Amplify-Logic reopened this Jul 20, 2026
Amplify-Logic and others added 2 commits July 21, 2026 02:02
CI runs test scripts directly; the file was committed with mode 100644,
so the Behavior tests job failed with exit 126 (Permission denied).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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