Skip to content

Captain-side firstmate机制修复 (22 commits) + upstream sync - #2046

Closed
Mr-G-9527 wants to merge 24 commits into
kunchenguid:mainfrom
Mr-G-9527:main
Closed

Mr-G-9527 wants to merge 24 commits into
kunchenguid:mainfrom
Mr-G-9527:main

Conversation

@Mr-G-9527

Copy link
Copy Markdown

Summary

22 captain-side firstmate机制修复 commits, kept local until now (captain-side workflow + worker产物). Upstreaming for visibility. Includes sync with upstream #2005 / #1997 / #1984 / #2029 (clean auto-merge, no conflicts).

Commits by area

Outbox / receipt:

  • 7d0b3ad per-task outbox receipt via fm-task-done.sh + auto-emit from fm-review-submit
  • 5cb81ca / 611d136 F16 modification-vs-new diff scaffolding for worker patches
  • b935f7a fm-review-submit consolidated diff base-sha HEAD + receipt-before-push
  • 9566430 flock on captain-outbox writers (row 17 corruption)
  • bcf6c47 restore fm-review-submit.sh + colocated test

AGENTS / docs:

  • 98d0d26 compress AGENTS.md hot/warm/cold layering 550→460 (-16.4%), hard rules 1-5 byte-identical

Quota / spawn / workcell:

  • 196e2a6 fm-minimax-quota.sh pre-flight + fm-spawn batch sizing
  • eee1c6b / bf7b074 workcell: separate admission from dispatch + remove false readiness gates
  • 6bce5b5 / 3008c28 research: allow worker cold starts + isolate workers + route revisions
  • 02f6ed1 P3 fresh-context worker spawn bypass treehouse
  • 690da87 FM_RESEARCH_WORKER_SKIP_SESSION_WAIT test hook

/fm-wake / drain:

  • eedfcf4 /fm-wake surfaces INPUT via two scripts (fm-captain-wake-drain + fm-wake-drain)
  • f46e2b6 Stop auto-arm: recognize inbox-drain actionable wake prefix
  • 16b8d87 P2 inbox-drain-on-signal + wake-drain helper + colocated tests
  • 5d4158d stop watcher firing stale: firstmate:0 self-wake loop
  • f1c50d3 wake backstop for daemon+argv topology + fix paused over-firing

Friction / hardening:

Caveat

Some commits are captain-workflow-specific (per-task outbox receipts surface back to a captain session, fm-captain outbox rows) and may be too captain-specific for upstream core. Happy to split, scope down, or drop any subset if reviewers prefer.

fm-captain and others added 24 commits August 5, 2026 14:38
state/captain-outbox.jsonl row 17 had two JSON objects glued
without a newline separator. Root cause: concurrent appends
from drain + LLM agent + manual retry interleave because no
writer held a lock.

Fix: new bin/fm-captain-outbox-append.sh helper with flock; all
writers migrate to it (drain.sh ack append; wake.md LLM agent
path). review-submit.sh keeps its own flock but references the
same lock path.

Regression test: tests/fm-outbox-concurrency.test.sh spawns 30
concurrent writers and asserts every line is valid single-line
JSON. Test passes after fix.
Restore the hash-bound submission helper lost in the cleanup of the
outbox-writer-lock worktree. The helper emits a kind=report-submission
done row to state/captain-outbox.jsonl on the shared flock and is the
only writer surface the controller's find_submission_events filter
recognises for enrolled review jobs.

Also restore bin/fm-captain-inbox-append.sh from the same canonical
staging source; the colocated test depends on it for the inbox --json
assertion and cannot run without it.

Both scripts gain the canonical `# shellcheck source=bin/fm-wake-lib.sh`
directive above their fm-wake-lib.sh source line, matching the convention
carried across bin/ (bin/fm-guard.sh:37). ShellCheck cannot resolve the
runtime $SCRIPT_DIR without it, so --external-sources alone leaves SC1091.

The test's ten `cond && ok || ko` assertions are rewritten as
`if cond; then ok; else ko; fi` to clear ShellCheck SC2015, matching the
single-line if/then/else assertion style used by sibling tests. Trigger
conditions are unchanged.
…over-firing

Closes the 2026-08-03 asyncRewake gap (data/handoff-2026-08-03-wake-and-lint.md
item 1; data/learnings.md 2026-08-03): when firstmate runs under the
daemon+argv topology, Claude Code Stop-hook asyncRewake does not re-invoke
the idle primary. The backstop is a second line of defense: the already-
armed watcher monitors state/.claude-autoarm-epoch, and after
FM_WATCH_BACKSTOP_GRACE (default 60s, which clears the synchronous
FM_CLAUDE_AUTOARM_EPOCH_FRESH 15s plus typical background-task notification
latency) it spawns a detached bash task that re-checks the epoch, consumes
the unconsumed rewake row, and injects the message into the primary pane
via the same channel the away-mode daemon uses (bin/fm-operational-input.sh
+ bin/fm-backend.sh fm_backend_send_text_submit). Background Bash task
completions re-invoke the idle session (observed 2/2); asyncRewake is
retained as the opportunistic fast path.

The inject path is bounded by timeout, not by positive confirmation: a
delivered rewake can only signal consumption by running the next turn, so
in the failure case the observation never happens. Idempotency is owned
by the bash task (not the watcher) so the re-check happens as close to
injection as possible: epoch is re-checked once after locating the row
and again immediately before consuming it; the consume step is the LAST
step so any earlier guard failure leaves the row in the queue. A
FM_WATCH_BACKSTOP_CONFIRM_INJECT=1 gate fail-closes the send so a probe
against a non-live state with an unresolved supervisor target cannot
deliver a real wake to the captain (the 2026-08-03 incident that
motivated the brief).

The secondary defect (item 1, "Secondary defect found in the same area")
folded into the same change: surface_nonterminal_stale was emitting a
bare "stale: <win>" wake every ~2 minutes for live agents on a declared
pause, bypassing handle_paused_stale. The fix routes that path through
handle_paused_stale with a first-sight fire flag so the LIVE-agent first
sight surfaces an annotated wake once (not hidden behind the long
PAUSE_RESURFACE_SECS cadence), while the DEAD-agent captain-held path
still absorbs on first sight. A successful surface in either branch
writes .paused-resurfaced-<key> so the long cadence governs every
subsequent recheck, not every pane change.

New files:
  bin/fm-watch-backstop.sh          the delivery helper (should-fire /
                                    fire / inject subcommands, with
                                    FM_WATCH_BACKSTOP_CONFIRM_INJECT
                                    test-isolation gate)
  tests/fm-watch-backstop.test.sh   15 hermetic cases covering each
                                    trigger gate, the confirm gate, the
                                    race-window idempotency, the
                                    watcher's per-cycle tick, and the
                                    paused-defect routing

Modified files:
  bin/fm-watch.sh                   the per-cycle fm_backstop_tick,
                                    the surface_nonterminal_stale
                                    paused-routing fix, and the
                                    handle_paused_stale first-sight
                                    fire flag
  docs/turnend-guard.md             the wake-backstop design contract,
                                    with the constraint self-check
                                    carried from the brief
  tests/fm-watch-triage.test.sh     the live-decision-gate assertion
                                    updated to expect the new
                                    first-sight annotated reason

Constraint self-check:
  - One supervision cycle ............. satisfied: the watcher is the
                                        cycle; the bash task is
                                        delivery plumbing, not an arm
  - Stop-hook ownership not duplicated  satisfied: fm-turnend-guard.sh
                                        keeps sole ownership of the
                                        Stop emit; the backstop only
                                        adds a delivery attempt after
                                        the preferred one
                                        demonstrably failed
  - AGENTS.md section 1 ............... satisfied: this is firstmate
                                        shared tracked material; the
                                        change ships through the
                                        repo's own delivery path (a
                                        local-only fast-forward
                                        merge), never by firstmate
                                        directly

Acceptance criteria demonstration:
  1. Backstop fires and delivers
     Demonstration: tests/fm-watch-backstop.test.sh
     test_fire_spawns_detached_inject_with_confirm_flag sets up an
     aged rewake in a scratch state, runs the backstop's fire
     subcommand, waits for the detached inject child to consume the
     row, write the consumed marker, and rewrite the epoch outcome
     to consumed. A fake tmux in PATH shadows the only subprocess
     the real inject touches, so the test is hermetic.
  2. Idempotency holds
     Demonstration: tests/fm-watch-backstop.test.sh
     test_inject_idempotency_epoch_outcome_changed_aborts_cleanly
     advances the epoch outcome to consumed between the should-fire
     pre-check and the inject's second re-check, then asserts the row
     is preserved (consume happens after the second re-check) and
     the consumed marker is NOT written.
  3. Paused defect fixed
     Demonstration: tests/fm-watch-backstop.test.sh
     test_surface_nonterminal_stale_paused_uses_long_cadence asserts
     the LIVE-agent first sight fires exactly one annotated wake
     (the bare text would be the bug this fix removes), and the
     existing test_nonterminal_stale_paused_absorbed_then_resurfaced
     in tests/fm-watch-triage.test.sh continues to verify the
     DEAD-agent captain-held path still absorbs on first sight.

Co-Authored-By: Claude <noreply@anthropic.com>
fm-research-worker-spawn.sh + test (P3, 2026-08-04):

- bypass treehouse/fm-spawn (treehouse get hung on git fetch origin, 60s timeout
  in this env); spawn fresh worker via direct tmux new-window + claude -p in
  the spec temporary project_dir (firstmate home).
- preallocate UUID session_id, pass to claude --session-id, wait for the
  exact session .jsonl (removes concurrent-worker shared-directory race).
- stdin-based brief (claude -p ... < brief) not shell interpolation.
- fail-closed on tmux-create / send / session-capture failure: no .meta
  written, inbox retry boundary preserved.
- --permission-mode bypassPermissions per P3 decision source (not
  --dangerously-skip-permissions).

Verified: test 12/12 pass; bash -n OK; live smoke 2 concurrent harmless
report_research rows -> 2 distinct session_ids (50a5aace / 83553418); P2
drain tests intact; old fm-fresh-context-executor test (treehouse path)
recorded incompatible, not a P3 defect.

Codex repaired the two files; puti reviewed + verified + committed.

Per CLAUDE.md commit-attribution Option A.
The watcher's hash-stable stale detection fires on firstmate:0 every cycle
when claude -p processes a long tool call (the rendered pane footer does
not change), the auto-arm treats that as actionable, rewakes the owner, and
a fresh watcher cycle sees the same primary again — an endless
self-triggering Stop-hook loop.

This fix compares each window against the canonical target recorded in
state/firstmate.meta and skips stale/wedge classification for that one
pane only. Worker panes retain normal supervision. The Stop/SessionEnd
hooks remain the authoritative liveness channel for the primary owner.

Added a focused regression (tests/fm-watch-triage.test.sh) that proves a
stable primary neither emits a wake nor enqueues a record while a stable
child pane still surfaces, plus a docs/verification/supervision.md entry
dated 2026-08-05 with the live-fix evidence.

bin/fm-watch.sh was already dirty before this fix (P2 inbox-drain hunks
that remain uncommitted). This commit includes only the new primary-exclusion
hunk; the P2 hunks stay in the working tree for a separate commit.
…ests

Sprint WIP 0805:
- P2 live-drain: when captain-inbox surfaces a signal wake, drain inbox
  immediately so the agent sees the message in this turn rather than next.
  Hunks in fm-captain-inbox-drain.sh (P2 hunks), fm-watch.sh,
  fm-research-worker-spawn.sh, fm-claude-stop-autoarm.sh.
- Wake-drain helper: bin/fm-captain-wake-drain.sh surfaces FIRSTMATE
  CAPTAIN INPUT v1 blocks as parsed captain instructions with corr/kind/seq.
- Proxy env helper: bin/fm-proxy-env.sh (WIP, unverified in this commit).
- Outbox append hardening: bin/fm-captain-inbox-append.sh flock already in;
  session-start / watch-arm small follow-ups.
- Colocated tests: tests/fm-captain-wake-drain.test.sh,
  tests/fm-fresh-context-executor.test.sh, tests/fm-mid-session-drain.test.sh,
  tests/fm-p2-live-drain.test.sh.

Scope: 7 modified bin/*.sh (141 insertions) + 6 untracked (2 bin + 4 tests).
Origin push deferred (proxy+凭证). Merge to main deferred for captain review.
… stale fresh-context-executor test

- helper: wrap the real-claude-p session-file wait in an if-else guard;
  tests set FM_RESEARCH_WORKER_SKIP_SESSION_WAIT=1 to skip it (fake tmux
  cannot simulate claude -p writing the session file). production never sets it.
- test: fm-fresh-context-executor.test.sh is stale — it fakes fm-spawn via
  FM_SPAWN_BIN, but the helper was rewritten to call claude -p directly via
  tmux send-text. Skip with reason until re-architected to fake claude-p.
  (puti review of fm WIP — fm blind spot: committed WIP with a failing test.)
…puti)

#1 fm-spawn.sh: accept --effort default (profile sentinel) + translate to high
   (concrete) instead of hard-rejecting. Callers passing the profile-level
   value no longer hit the enum error.
#3 fm-decision-hold.sh: add early parens check on --reason with a clear hint
   to rephrase (cite line ranges as L513-517, not (controller:513-517)) rather
   than failing inscrutably downstream in tasks_axi.
   (puti acting on fm friction log — Alex directive: puti出手改 fm, not fm self-mod.)
bin/fm-watch-arm.sh's watch_output_has_wake learned the inbox-drain prefix in
the 2026-08-04 P2 live-fix, but bin/fm-claude-stop-autoarm.sh's actionable
loop was not updated to match. When the watcher's auto-drain path surfaces
an inbox-drain wake:

  - fm-watch-arm.sh prints the wake (actionable-inbox-drain) and returns 0
  - the Stop auto-arm's grep on the arm output finds no signal:/stale:/check:/
    heartbeat match, so ACTIONABLE stays 0
  - the next loop iteration's fm_watcher_healthy probe fails (the watcher
    has exited), so HEALTHY stays 0
  - after the bounded 2-attempt loop converges with no actionable and no
    healthy watcher, the FAILED notice path fires even though the watcher
    just delivered its wake and rewake is the correct outcome

The cycle log confirms this is the dominant close reason in this home:
every recent actionable cycle closed with reason=actionable-inbox-drain,
so the Stop hook routinely hit the false-failure path.

Fix: mirror bin/fm-watch-arm.sh's actionable regex in the auto-arm loop and
the rewake banner grep, so an inbox-drain wake short-circuits both into the
rewake exit-2 path.

Co-Authored-By: Claude <noreply@anthropic.com>
…s 1-5 + yolo + precedence byte-identical (worker 125 redo, base b935f7a)
…onstant in fm-claude-stop-autoarm.sh + fm-watch-arm.sh, invariant test pins drift (F5 watcher drop fix per w2-02 brief + captain seq 145)
Wave4 brief fm-0808-w4-02. A worker that runs `git diff HEAD~1..HEAD` over a brand-new file produces a new-file diff from /dev/null, which fails to apply on a captain checkout that already has the file committed (F16, observed on fm-0808-w3-02). This adds bin/fm-worker-diff.sh which inspects each changed path in the worktree: tracked files get `git diff HEAD` (modification diff), untracked files under data/ get `git diff --no-index /dev/null` (new-file diff). A colocated tests/fm-worker-diff.test.sh round-trips every emitted patch through `git apply` on a fresh clone, including the exact F16 scenario and an out-of-scope untracked filter.
Non-FF: branch fork (2d2be63) is not an ancestor of current main (29135f1); a rebase would require resolving AGENTS.md conflicts that are out of scope for F16 (the brief explicitly excludes AGENTS.md). Trade-off: a merge commit joins two independent fork lines, but the joined commit only adds the two new files (bin/fm-worker-diff.sh + tests/fm-worker-diff.test.sh), which no main commit touches, so the substantive change is identical to a rebased FF.

# Conflicts:
#	AGENTS.md
…om fm-review-submit

Closes the puti 监控漏 (P1 fm-side) gap: workers that only knew the
canonical submit helper had no automatic per-task done row in the
captain outbox, so the controller lost completions whenever the
worker forgot to append a chat row.

What lands:
  - bin/fm-task-done.sh: new wrapper that builds the canonical
    "<task-id> done at <artifact path>" row and routes it through
    bin/fm-captain-outbox-append.sh with --idempotent. The helper
    never writes to the outbox directly, so the row 17 atomicity
    contract is preserved.
  - bin/fm-captain-outbox-append.sh: adds --idempotent, which
    dedupes on (corr, kind) under the same flock as the append.
    Two concurrent callers cannot both insert a duplicate row.
  - bin/fm-review-submit.sh: when --task-id is supplied, the
    helper also fires fm-task-done.sh with the submitted report
    as the artifact, so a worker that only calls the submit
    helper now emits the per-task done row for free. The auto
    emit is best-effort; the report-submission row remains the
    contract.
  - tests/fm-task-done.test.sh: colocated test (16 cases) that
    covers the new helper, the --idempotent flag on the outbox
    appender, and the auto-emit path through fm-review-submit.

Existing behavior preserved: the unchanged-outbox-row surface
(kind=report-submission), the receipt.diff write, and the
existing tests all still pass.
@Mr-G-9527

Copy link
Copy Markdown
Author

Closing — these are captain-side workflow commits (outbox receipts back to a captain session, fm-captain outbox surface, worker产物) specific to our firstmate fleet operation, not general-purpose contributions to firstmate core. Wrong target. Apologies for the noise.

@Mr-G-9527 Mr-G-9527 closed this Aug 10, 2026
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