Skip to content

fix(bin): absorb routine attended supervision wakes in bash - #74

Merged
withally merged 3 commits into
mainfrom
fm/fm-wake-turn-reduction-t1
Aug 24, 2026
Merged

fix(bin): absorb routine attended supervision wakes in bash#74
withally merged 3 commits into
mainfrom
fm/fm-wake-turn-reduction-t1

Conversation

@withally

Copy link
Copy Markdown
Owner

Intent

Reduce actual LLM turns spent on routine attended supervision wakes, implementing the captain's 2026-08-24 directive that healthy waiting remain pure code and not silently spend tokens. In attended mode, absorb only recognized nonterminal routine status shapes such as working and resolved echoes when every referenced recorded worker has positive live/busy current-work proof; keep terminal done, needs-decision, blocked, and failed statuses, authenticated check results, stale or wedge suspicion, dead or unreadable endpoints, secondmate status replies, malformed configuration, and unparseable signals as immediate fail-toward-model wakes. Keep one shared status-shape policy in bin/fm-classify-lib.sh for both the watcher and AFK daemon, with a conservative default-on home-local config/attended-routine-status-absorb knob plus exact on/off environment override. An absorbed status append must be recorded before signal suppression, survive crashes, and appear exactly once in the existing UNREAD STATUS section on the next genuine model turn. Absorption must not reset or bypass stale detection, and regression coverage must prove absorption, no-loss presentation, a subsequently quiet worker still wedge-escalates, and terminal statuses still wake. Add a short authoritative docs update and colocated behavioral tests, then ship through no-mistakes without merging. The PR body must report the evidence replay estimate: 16 of the review's defensible 60 primary-wake lower bound, or 26.7%, were routine-only attended signal wakes expected to be absorbed, with the caveat that live endpoint proof is evaluated at event time. The PR body must distinguish existing upstream work from this addition and link it: issue https://github.com/withally/firstmate/issues/2356 already proposes quiet attended supervision via the AFK bash-triage pattern; issue https://github.com/withally/firstmate/issues/2374 and PR https://github.com/withally/firstmate/pull/2877 preserve positive-work evidence for turn-end absorption; issue https://github.com/withally/firstmate/issues/2227 quantifies paid benign watcher turns; PR https://github.com/withally/firstmate/pull/2801 adds bounded context to actionable wakes; current architecture/configuration already provide attended bash classification, signal-grace coalescing, durable presentation cursors, and batched AFK digests. This change adds conservative recognized-shape gating for attended routine status appends plus identity-and-byte-bound delayed presentation receipts, without introducing a second supervisor or classification dialect.

What Changed

  • bin/fm-classify-lib.sh gains one shared attended absorption policy — signal_reason_is_routine_nonterminal (recognized nonterminal shapes only: working, resolved, captain-held, the paused verb, note:, and bare turn-end markers), attended_routine_status_absorb_enabled (default-on config/attended-routine-status-absorb, exact on/off via FM_ATTENDED_ROUTINE_STATUS_ABSORB, any other value or unreadable/symlinked file disables it), and attended_signal_is_absorbable, which additionally requires no captain-relevant status and positive live/busy current-work proof for every referenced crew. bin/fm-watch.sh uses that policy in place of its old no-verb triage; bin/fm-supervise-daemon.sh reuses the same shape boundary as its syntax gate, escalating unrecognized shapes and treating a signal path teardown already removed as self-handled. Terminal done/needs-decision/blocked/failed, authenticated checks, secondmate status replies, dead or unreadable endpoints, malformed config, and unparseable signals all still wake immediately, and absorption does not touch stale/wedge timers.
  • Delayed presentation is durable: a new per-task state/.status-absorbed-<id> receipt binds the status file's identity and byte endpoint and is written before the watcher advances signal suppression, so a crash replays rather than loses the line. New status_lines_between_offsets lets scan_unread_surface_snapshot print the absorbed span exactly once in the existing UNREAD STATUS section on the next genuine turn; cursor commit retires the receipt only after those bytes print, and a receipt that no longer describes the current file (identity changed, endpoint past EOF, malformed) is dropped and read as absent so it can never suppress the drain's no-loss sections. status_retire_presentation_task retires the receipt on teardown, and bin/fm-wake-drain.sh now reports a failed status presentation on stderr instead of silently swallowing it.
  • Docs (docs/architecture.md, docs/configuration.md, AGENTS.md) describe the recognized-shape policy, the config knob, the receipt, and the state/config files. Tests cover config parsing, absorption plus exactly-once presentation on a later unrelated turn, live note: absorption, terminal statuses still waking over a live worker, absorb-off surfacing the routine signal, unparseable live status surfacing, daemon escalation on unrecognized shape, vanished-path self-handling, and unverifiable/out-of-range receipts still presenting every section.

Evidence replay estimate: 16 of the review's defensible 60 primary-wake lower bound — 26.7% — were routine-only attended signal wakes expected to be absorbed. Caveat: live endpoint proof is evaluated at event time, so the realized rate depends on worker liveness at each wake.

Relation to existing upstream work. This is additive to, not a replacement for, work already in flight: issue #2356 proposes quiet attended supervision via the AFK bash-triage pattern; issue #2374 and PR #2877 preserve positive-work evidence for turn-end absorption; issue #2227 quantifies paid benign watcher turns; PR #2801 adds bounded context to actionable wakes. Current architecture/configuration already provide attended bash classification, signal-grace coalescing, durable presentation cursors, and batched AFK digests. What this change adds on top is conservative recognized-shape gating for attended routine status appends plus identity-and-byte-bound delayed presentation receipts — no second supervisor and no second classification dialect.

Risk Assessment

✅ Low: The absorption gate, the identity-and-byte-bound receipt lifecycle, and the no-loss presentation contract all held under direct tracing — absorbed bytes are recorded before suppression, surfaced exactly once, and not replayed — and every prior-round fix is genuinely present, leaving only informational duplication and error-reporting nits.

Testing

Ran the three targeted suites covering this change (fm-watch-triage, fm-wake-drain-unread-status, fm-daemon) — all 186 assertions pass with no failures — and then drove the real watcher and wake-drain binaries in a scripted end-user scenario to capture a CLI transcript showing a routine live-worker status being absorbed with no LLM turn, its durable receipt on disk, the absorbed line appearing exactly once in UNREAD STATUS on the next genuine drain after the watcher was SIGKILLed, no replay on the following drain, and immediate wakes preserved for terminal done:, unparseable status text, and the home-local off switch. This change is CLI/supervision-layer only with no rendered UI surface, so the reviewer-visible evidence is the CLI transcript rather than a screenshot.

Evidence: Attended routine-absorb end-to-end CLI transcript

Source: Attended routine-absorb end-to-end CLI transcript

=== SCENARIO A: routine 'working:' status from a provably-live worker (attended, absorb default on) === worker wrote: working: compiling step 2 live-work proof: state: working · source: run-step · validating (running) watcher: still blocking -> NO LLM turn spent watcher stdout: '' durable wake queue: '' triage log: [...] absorbed benign signal: .../state/task.status delayed-presentation receipt on disk (survives a crash): .../state/.status-absorbed-task -> 16777234:108892383\t26 (the watcher process was then SIGKILLed - the drain below runs against a crashed watcher) === SCENARIO A cont.: the next GENUINE model turn (unrelated check wake) drains === | 1787582390\t1\tcheck\tunrelated.check.sh\tcheck: unrelated genuine model turn | UNREAD STATUS (new since last drain, not re-printed after this presentation): | task working: compiling step 2 receipt after presentation: removed === SCENARIO A cont.: a second drain must NOT replay the same line === | 1787582390\t1\tcheck\tunrelated.check.sh\tcheck: unrelated genuine model turn === SCENARIO B: terminal 'done:' status, same live worker -> must still wake immediately === watcher: EXITED -> model turn taken (correct) === SCENARIO C: unparseable status line, same live worker -> must still wake === watcher: EXITED -> model turn taken (correct) === SCENARIO D: home-local off switch (config/attended-routine-status-absorb = off) === watcher: EXITED -> model turn taken (absorption disabled, correct)


=== SCENARIO A: routine 'working:' status from a provably-live worker (attended, absorb default on) ===
worker wrote:      working: compiling step 2
live-work proof:   state: working · source: run-step · validating (running)
watcher:           still blocking -> NO LLM turn spent
watcher stdout:    ''
durable wake queue: ''
triage log:        [2026-08-24T22:39:32+0800] absorbed benign signal: /var/folders/3n/3wfcplrn3clf44hjjkfgq8t00000gn/T//fm-absorb-e2e.hJ8r9y/demo-absorb/state/task.status
delayed-presentation receipt on disk (survives a crash):
  /var/folders/3n/3wfcplrn3clf44hjjkfgq8t00000gn/T//fm-absorb-e2e.hJ8r9y/demo-absorb/state/.status-absorbed-task -> 16777234:108892383	26
(the watcher process was then SIGKILLed - the drain below runs against a crashed watcher)

=== SCENARIO A cont.: the next GENUINE model turn (unrelated check wake) drains ===
  | 1787582390	1	check	unrelated.check.sh	check: unrelated genuine model turn
  | UNREAD STATUS (new since last drain, not re-printed after this presentation):
  | task working: compiling step 2
receipt after presentation: removed

=== SCENARIO A cont.: a second drain must NOT replay the same line ===
  | 1787582390	1	check	unrelated.check.sh	check: unrelated genuine model turn

=== SCENARIO B: terminal 'done:' status, same live worker -> must still wake immediately ===
watcher:           EXITED -> model turn taken (correct)
watcher stdout:    signal: /var/folders/3n/3wfcplrn3clf44hjjkfgq8t00000gn/T//fm-absorb-e2e.hJ8r9y/demo-terminal/state/task.status
durable wake queue: 1787582392	1	signal	task.status	signal: /var/folders/3n/3wfcplrn3clf44hjjkfgq8t00000gn/T//fm-absorb-e2e.hJ8r9y/demo-terminal/state/task.status
1787582392	2	signal	task.status	signal: /var/folders/3n/3wfcplrn3clf44hjjkfgq8t00000gn/T//fm-absorb-e2e.hJ8r9y/demo-terminal/state/task.status

=== SCENARIO C: unparseable status line, same live worker -> must still wake ===
watcher:           EXITED -> model turn taken (correct)
watcher stdout:    signal: /var/folders/3n/3wfcplrn3clf44hjjkfgq8t00000gn/T//fm-absorb-e2e.hJ8r9y/demo-unparseable/state/task.status

=== SCENARIO D: home-local off switch (config/attended-routine-status-absorb = off) ===
home config:       /var/folders/3n/3wfcplrn3clf44hjjkfgq8t00000gn/T//fm-absorb-e2e.hJ8r9y/demo-off/config/attended-routine-status-absorb = off
watcher:           EXITED -> model turn taken (absorption disabled, correct)
watcher stdout:    signal: /var/folders/3n/3wfcplrn3clf44hjjkfgq8t00000gn/T//fm-absorb-e2e.hJ8r9y/demo-off/state/task.status

=== SCENARIO E: absorbed worker then goes quiet -> stale detection still wedge-escalates ===
(covered by tests/fm-watch-triage.test.sh::test_nonterminal_stale_provably_working_absorbed_then_escalated,
 which now enters via the attended signal-absorb path)
Evidence: Driver script used for the end-to-end transcript

Source: Driver script used for the end-to-end transcript

#!/usr/bin/env bash
# End-to-end demo of attended routine-status absorption, driving the REAL
# bin/fm-watch.sh watcher process and the REAL bin/fm-wake-drain.sh the model reads.
set -u
REPO=${REPO:?set REPO}
. "$REPO/tests/wake-helpers.sh"
. "$REPO/bin/fm-classify-lib.sh"
WATCH="$ROOT/bin/fm-watch.sh"; DRAIN="$ROOT/bin/fm-wake-drain.sh"
TMP_ROOT=$(fm_test_tmproot fm-absorb-e2e)

run_watcher() { # <state> <fakebin> <out> [env...]
  local state=$1 fakebin=$2 out=$3; shift 3
  PATH="$fakebin:$PATH" FM_STATE_OVERRIDE="$state" FM_CREW_STATE_BIN="$fakebin/fm-crew-state.sh" \
    FM_POLL=1 FM_SIGNAL_GRACE=1 FM_CHECK_INTERVAL=999999 FM_HEARTBEAT=999999 \
    env "$@" "$WATCH" > "$out" &
  local pid=$! i=0
  while [ $i -lt 60 ]; do kill -0 "$pid" 2>/dev/null || return 0; sleep 0.2; i=$((i+1)); done
  kill -9 "$pid" 2>/dev/null; wait "$pid" 2>/dev/null; return 1
}

hr() { printf '\n=== %s ===\n' "$1"; }
export FM_FAKE_CREW_STATE='state: working · source: run-step · validating (running)'

hr "SCENARIO A: routine 'working:' status from a provably-live worker (attended, absorb default on)"
dirA=$(make_case demo-absorb); stA="$dirA/state"
printf 'working: compiling step 2\n' > "$stA/task.status"
echo "worker wrote:      $(cat "$stA/task.status")"
echo "live-work proof:   $FM_FAKE_CREW_STATE"
if run_watcher "$stA" "$dirA/fakebin" "$dirA/watch.out"; then
  echo "watcher:           EXITED (would spend an LLM turn)"
else
  echo "watcher:           still blocking -> NO LLM turn spent"
fi
echo "watcher stdout:    '$(cat "$dirA/watch.out")'"
echo "durable wake queue: '$(cat "$stA/.wake-queue" 2>/dev/null)'"
echo "triage log:        $(grep -F 'absorbed benign signal' "$stA/.watch-triage.log")"
echo "delayed-presentation receipt on disk (survives a crash):"
echo "  $stA/.status-absorbed-task -> $(cat "$stA/.status-absorbed-task")"

echo "(the watcher process was then SIGKILLed - the drain below runs against a crashed watcher)"

hr "SCENARIO A cont.: the next GENUINE model turn (unrelated check wake) drains"
append_wake "$stA" check unrelated.check.sh 'check: unrelated genuine model turn' >/dev/null
FM_STATE_OVERRIDE="$stA" "$DRAIN" 2>/dev/null | sed 's/^/  | /'
echo "receipt after presentation: $([ -e "$stA/.status-absorbed-task" ] && echo present || echo removed)"

hr "SCENARIO A cont.: a second drain must NOT replay the same line"
FM_STATE_OVERRIDE="$stA" "$DRAIN" 2>/dev/null | sed 's/^/  | /'

hr "SCENARIO B: terminal 'done:' status, same live worker -> must still wake immediately"
dirB=$(make_case demo-terminal); stB="$dirB/state"
printf 'done: shipped clean\n' > "$stB/task.status"
if run_watcher "$stB" "$dirB/fakebin" "$dirB/watch.out"; then
  echo "watcher:           EXITED -> model turn taken (correct)"
else echo "watcher:           still blocking (WRONG)"; fi
echo "watcher stdout:    $(cat "$dirB/watch.out")"
echo "durable wake queue: $(cat "$stB/.wake-queue" 2>/dev/null)"

hr "SCENARIO C: unparseable status line, same live worker -> must still wake"
dirC=$(make_case demo-unparseable); stC="$dirC/state"
printf 'progress text without a status verb\n' > "$stC/task.status"
if run_watcher "$stC" "$dirC/fakebin" "$dirC/watch.out"; then
  echo "watcher:           EXITED -> model turn taken (correct)"
else echo "watcher:           still blocking (WRONG)"; fi
echo "watcher stdout:    $(cat "$dirC/watch.out")"

hr "SCENARIO D: home-local off switch (config/attended-routine-status-absorb = off)"
dirD=$(make_case demo-off); stD="$dirD/state"
mkdir -p "$dirD/config"; printf 'off\n' > "$dirD/config/attended-routine-status-absorb"
echo "home config:       $dirD/config/attended-routine-status-absorb = $(cat "$dirD/config/attended-routine-status-absorb")"
printf 'working: compiling step 2\n' > "$stD/task.status"
if run_watcher "$stD" "$dirD/fakebin" "$dirD/watch.out" FM_HOME="$dirD" FM_ROOT_OVERRIDE="$dirD"; then
  echo "watcher:           EXITED -> model turn taken (absorption disabled, correct)"
else echo "watcher:           still blocking (WRONG)"; fi
echo "watcher stdout:    $(cat "$dirD/watch.out")"

hr "SCENARIO E: absorbed worker then goes quiet -> stale detection still wedge-escalates"
echo "(covered by tests/fm-watch-triage.test.sh::test_nonterminal_stale_provably_working_absorbed_then_escalated,"
echo " which now enters via the attended signal-absorb path)"
Evidence: Targeted suite output (fm-watch-triage, fm-wake-drain-unread-status, fm-daemon)

Source: Targeted suite output (fm-watch-triage, fm-wake-drain-unread-status, fm-daemon)

$ bash tests/fm-watch-triage.test.sh
ok - signal_reason_is_actionable: benign absorbed, captain verbs and coalesced batches surfaced
ok - attended routine absorption defaults on, supports explicit on/off, and fails closed on invalid values
ok - stale_is_terminal: terminal status surfaces, non-terminal and no-status are benign
ok - scan_captain_relevant_statuses lists only captain-relevant statuses
ok - classifier primitives: keyed decisions and activity phases, captain relevance, window-to-task, and overrides
ok - crew_is_provably_working: only working+run-step/pane is provable; idle/finished/parked/failed/unknown surface
ok - status_is_paused: only the leading paused verb matches, paused is not captain-relevant, and the two declared-wait verbs stay separable
ok - crew_absorb_class: working/paused/none from one read; crew_is_paused and crew_is_provably_working agree
ok - crew_worktree_written_since: real writes are evidence; no worktree, no anchor, quiet trees, .git churn and a mate's own home are not
ok - an empty FM_WORKTREE_WRITE_PRUNE widens the probe to the whole depth-bounded tree instead of disabling it
ok - an empty FM_WORKTREE_WRITE_PRUNE exported into the environment prunes nothing, widening the probe
ok - the worktree write probe is wall-clock bounded, and hitting the bound reads as no write evidence
ok - signal_crew_provably_working: benign only when every referenced crew is provably working
ok - a secondmate's status signal is never absorbed as provably working; crewmates are unaffected
ok - a no-verb signal whose crew is provably working is absorbed (no exit, no queue, suppressor advanced, beacon present)
ok - an absorbed status line is presented once in UNREAD STATUS on the next unrelated genuine model turn
ok - a note: signal from a provably-live worker is absorbed and presented once
ok - a terminal done: status wakes immediately even when the worker is positively live
ok - the attended routine-absorb off switch restores an immediate model wake
ok - an unparseable status always wakes even when the worker is positively live
ok - a bare turn-end whose crew is provably working (busy pane) is absorbed
ok - a bare turn-end whose crew is not provably working is surfaced (the swallowed-finish fix)
ok - a no-verb working: note whose crew is idle with no running pipeline is surfaced
ok - a secondmate's status note surfaces even while its own agent is busy
ok - a self-announced close never wakes its own home, and the next real note still does
ok - captain-relevant signal is surfaced (queue + exit) and marked surfaced
ok - a stale pane sitting on a terminal status is surfaced (queue + exit)
ok - a stale terminal-looking status is overridden and absorbed while a run is actively working, then wedge-escalated
ok - provably-working non-terminal stale is absorbed on first sight, then wedge-escalated past the threshold
ok - consecutive wedge escalations on the same pane accumulate and demand deep inspection at the threshold
ok - a pane becoming active again resets the consecutive wedge-escalation counter
ok - a busy worker below the turn-age bound remains working with no escalation
ok - a busy worker with a stable pane hash still escalates once its completed-turn age reaches the bound
ok - a busy worker whose pane hash changes every poll still escalates once its completed-turn age reaches the bound
ok - touching a busy worker's completed-turn marker resets the age and prevents an old-age escalation
ok - repeated busy turn-age escalations reuse the existing escalation counter and demand deep inspection at the threshold
ok - the production default busy-turn-age bound is 3600s (5min under does not wedge, 66min over does)
ok - a busy pane under a declared pause is rechecked on the long cadence, and lifting the pause restores the wedge escalation
ok - a not-provably-working non-terminal stale is surfaced immediately (never left to wait out the timer)
ok - a declared pause is absorbed on first sight, then re-surfaced as a recheck past the threshold, never wedge-escalated
ok - exited declared-pause and captain-held panes use bounded pause cadence while a live decision gate still surfaces once
ok - a current declared wait holds its long cadence across pane churn, declaring appends and watcher restarts
ok - needs-decision, blocked and done still wake immediately through an armed declared-wait cadence
ok - a declared paused secondmate re-surfaces on the bounded normal-mode cadence
ok - a captain-held secondmate re-surfaces on the bounded normal-mode cadence
ok - a non-paused secondmate retains normal stale suppression
ok - a resumed secondmate clears pause and stale tracking before stale exemption
ok - unchanged stale hashes reclassify when a crew enters or leaves pause
ok - a declared pause is periodically rechecked against authoritative active-run state
ok - a paused status overridden by authoritative working preserves its wedge timer and escalates
ok - matching non-terminal stale suppressors repair missing or corrupt stale-since timers
ok - a quiet pane writing its own worktree is deferred, while one writing nothing still wedge-escalates on the unchanged schedule
ok - a write deferral re-surfaces once on the bounded pause cadence, so a churning worktree cannot stay invisible
ok - a secondmate's own home supervision churn is not crew write evidence, so a pane recording that home keeps the unchanged escalation schedule
ok - an idle-window timer repair drops a finished write-deferral chain, so the next deferral gets a fresh re-surface window
ok - both first-sight paths through a captain-relevant status drop a finished write-deferral chain with the idle window
ok - triage log capping handles wc byte counts with leading spaces
ok - a captured process-event result wakes a healthy watcher proactively, with no manual drain
ok - an unacknowledged process-event result re-drains until handling is acknowledged
ok - complete process-event queue keys map to distinct seen markers
WAKE_ACK_REQUIRED: after handling completes run bin/fm-wake-drain.sh --ack-through 1 --recovery-generation 85002.1787582620.fiB8oF
ok - queue revalidation, proactive output, and marker commit serialize with drain
/Users/ivan/.no-mistakes/worktrees/37852af5566c/01M0T052SJ8JAA69K4MG9G5ZFJ/bin/fm-push-transition-lib.sh: line 96: echo: write error: Broken pipe
tests/wake-helpers.sh: line 282: 93862 Killed: 9               PATH="$dir/fakebin:$PATH" FM_HOME="$dir" FM_PROCEVENT_CLAIM_ROOT="$dir/claims" FM_CREW_STATE_BIN="$dir/fakebin/fm-crew-state.sh" FM_POLL=0.2 FM_SIGNAL_GRACE=1 FM_CHECK_INTERVAL=999999 FM_HEARTBEAT=999999 "$WATCH" > "$out"
tests/wake-helpers.sh: line 282: 97981 Killed: 9               PATH="$dir/fakebin:$PATH" FM_HOME="$dir" FM_PROCEVENT_CLAIM_ROOT="$dir/claims" FM_CREW_STATE_BIN="$dir/fakebin/fm-crew-state.sh" FM_POLL=0.2 FM_SIGNAL_GRACE=1 FM_CHECK_INTERVAL=999999 FM_HEARTBEAT=999999 "$WATCH" > "$out"
ok - surfacing failures replay until post-handling acknowledgement
ok - marker failure exits through the shared wake owner, releases its lock, and replays later
ok - a heartbeat with no captain-relevant change is absorbed and backs off the cadence
ok - heartbeat backstop fail-safe surfaces a captain-relevant status the per-wake path missed
ok - the liveness beacon stays fresh while the watcher absorbs benign wakes (fm-guard never false-alarms)
ok - with .afk present the watcher reverts to one-shot so the daemon owns triage (no double-triage)
ok - AFK changed paused panes hand off plain stale identities for daemon-owned pause triage

$ bash tests/fm-wake-drain-unread-status.test.sh
ok - a note: answer buried under a later routine note: is surfaced with both lines
ok - already-presented note: lines are not re-surfaced on the next drain
ok - a brand-new note: after presentation is surfaced without replaying handled lines
ok - a queued status signal annotates every unread note, not only the newest
WAKE_ACK_REQUIRED: after handling completes run bin/fm-wake-drain.sh --ack-through 1 --recovery-generation 32612.1787582651.PRBsMP
WAKE_ACK_REQUIRED: after handling completes run bin/fm-wake-drain.sh --ack-through 1 --recovery-generation 32612.1787582651.PRBsMP
WAKE_ACK_REQUIRED: aft

... [1244 bytes truncated] ...

out hiding a live terminal status
ok - fm-afk-start.sh ignores stale pidfile-only live pids
ok - fm-afk-start.sh reclaims stale daemon locks whose live pid identity no longer matches
ok - supervise daemon state root is scoped by FM_HOME
ok - routine signal self-handles
ok - captain-relevant status verbs escalate
ok - check + unknown escalate; heartbeat self-handles
ok - transient stale self-handles and records a persistence marker
ok - enriched stale wedges bypass status absorption without disturbing busy workers
ok - stale + terminal status escalates immediately
ok - paused reasons with captain phrases remain pause-classified
ok - a captain-held transfer classifies as pause, not as a wedge candidate
ok - handle_wake on a paused stale records a pause marker, drops the wedge marker, and does not escalate
ok - handle_wake records a declared pause from a routine signal for long-cadence rechecks
ok - a terminal signal clears pause and stale tracking across both supervisors
ok - housekeeping migrates a normal-watcher's declared pause into daemon tracking
ok - housekeeping clears an already-resumed watcher pause across both supervisors
ok - housekeeping seeds pause tracking from status without a watcher marker
ok - persistent stale escalates after threshold and clears its marker
ok - resumed (busy) stale clears its marker without escalating
ok - housekeeping re-surfaces a stale declared pause on the long cadence and resets its window
ok - housekeeping re-surfaces a forgotten captain hold on the long cadence and resets its window
ok - housekeeping clears a paused marker whose pane became busy again, without escalating
ok - housekeeping clears a paused marker once the crew is no longer declaring the pause
ok - housekeeping clears the pause marker once a captain hold is answered
ok - housekeeping moves an existing stale marker to pause before wedge escalation
ok - housekeeping moves a captain hold's existing stale marker to pause before wedge escalation
ok - housekeeping clears tracking when a crew leaves pause
ok - persistent herdr stale resolves the target from metadata and escalates
ok - herdr idle busy-footer stale clears through capture corroboration
ok - resumed herdr stale clears through backend-aware busy state
ok - persistent Orca stale resolves the terminal from metadata
ok - multiple escalations flush as a single batched digest
ok - batch flush measures max-delay from the first append, not the last
ok - catch-all scan escalates a missed terminal once, not twice
ok - handle_wake routes routine->self and captain->escalate
ok - INJECT_SKIP forces self-handle, bypassing captain-relevant classification
ok - is_wake_reason distinguishes watcher wake reasons from singleton-status stdout
ok - terminal-stale escalate removes its marker so housekeeping does not re-escalate
ok - captain signal escalate marks seen so the catch-all scan does not re-fire
ok - _collapse_newlines replaces newlines with literal separator
ok - afk flag absent: daemon does not inject, buffer preserved
ok - busy-guard defers injection when supervisor pane is busy
ok - marker detection: marker -> stay afk, no marker -> exit afk
ok - /afk invocation is exempt from afk exit (no self-cancel)
ok - should_exit_afk returns false when afk is not active
ok - strip_injection_marker removes the sentinel marker cleanly
ok - pane_input_pending detects partial input on the cursor line
ok - pane_input_pending: a blank unidentified cursor row defers (strict container-proof rule)
ok - pane_input_pending: only proven empty agent prompts pass
ok - fm_tmux_composer_state: a bare shell prompt ($/%/#/>) reads unknown, never empty (dead-shell injection safety)
ok - fm_tmux_composer_state: a bordered composer box and bare agent glyphs (❯/›) still read empty
ok - fm_tmux_composer_state: only matching edge borders form a composer box
ok - pane_input_pending preserves bright placeholder-like drafts in styled captures
ok - classify_signal dedupes against the catch-all scan seen marker
ok - classify_stale dedupes against the signal path seen marker
ok - AFK nonterminal working:+merged keeps wedge aging and re-escalates at bound
ok - genuine done: and merge-check events still escalate
ok - pane_input_pending: an idle bordered composer is NOT pending (afk-invx-i5)
ok - pane_input_pending: text inside a bordered composer is still pending
ok - submit-ACK confirms a submit when the composer returns to a bordered-empty box
ok - submit-ACK reports pending on a persistently swallowed Enter (type-once)
ok - max-defer on an empty stuck pane types once, alarms, and preserves the buffer
ok - max-defer flushes and clears the buffer on an empty bordered pane
ok - max-defer on a pending composer alarms without typing
ok - normal flush clears a stale wedge marker
ok - below MAX_DEFER: no inject, no alarm, buffer preserved
ok - max-defer does not flush or alarm while afk is inactive
ok - library mode: sourcing the daemon defaults FM_WEDGE_ALARM_EXEC to discard (no test can fire a real notification)
ok - wake helpers replace inherited notifier overrides with the safe recorder
ok - the discard seam suppresses every notifier, including command: (fires nothing)
ok - direct notifier helpers honor the discard seam, including command:
ok - osascript channel routes through the notifier seam with the summary (never a real notification)
ok - herdr channel routes through the notifier seam with the summary (never a real notification)
ok - command channel runs the captain command with the summary on $1 and on stdin
ok - command channel failures redact configured commands while logging their exit status
ok - unknown channel directives are redacted while the alarm keeps running
ok - off disables every active alert regardless of directive position (marker and tmux flash are unaffected)
ok - auto resolves to the macOS osascript notifier on Darwin (default-on)
ok - auto on a non-macOS platform selects no built-in OS channel (the marker or a configured command carries it)
ok - config/wedge-alarm selects every configured channel and skips comment and blank lines
ok - a failing channel logs and falls back to the next channel, never crashing the alarm
ok - a hung notifier is bounded, logged, and falls through to the next channel
ok - a backgrounded command notifier remains bounded until its process group is reaped
ok - a hung notifier override is bounded, logged, and proceeds to the next channel
/Users/ivan/.no-mistakes/worktrees/37852af5566c/01M0T052SJ8JAA69K4MG9G5ZFJ/bin/fm-supervise-daemon.sh: line 778: 84218 Terminated: 15          sh -c 'sleep 30 & printf "%s" "$!" > "$1"; wait' sh "$child_file"
ok - daemon shutdown stops and reaps the active notifier process group
ok - inject_wedge_alarm writes the marker AND emits the active alert even with no tmux status-line (herdr backend)
ok - in-process wedge throttle prevents alert spam when the marker cannot persist
ok - fm-send returns 3 with a non-error no-resend warning when confirmation stays pending
ok - fm-send exits non-zero when initial text send fails
ok - fm-send exits non-zero unless delivery is proven empty
ok - discover_supervisor_backend: override > TMUX_PANE > HERDR_ENV+HERDR_PANE_ID > tmux fallback
ok - discover_supervisor_target: override > TMUX_PANE > herdr '<session>:<pane-id>' composition > firstmate:0 fallback
ok - pane_is_busy: herdr native busy_state='busy' short-circuits without a capture fallback
ok - primary busy guard isolates rendered signatures by detected harness
ok - pane_is_busy: omitted backend defaults to tmux for Grok's isolated fallback
ok - pane_input_pending: dispatches through fm_backend_composer_state for backend=herdr
ok - inject_msg: herdr busy-guard defers before ever attempting a submit
ok - inject_msg: herdr composer-guard defers before ever attempting a submit
ok - inject_msg: herdr pane-gone check defers before any busy/composer/submit call
ok - inject_msg: dispatches busy-guard/composer-guard/submit through the herdr backend and succeeds on a confirmed empty composer
ok - inject_msg: defers on a dead-shell/unreadable composer (unknown), never typing the escalation into a shell
ok - inject_msg: unrecognized composer states defer by default

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 3 infos
  • ⚠️ bin/fm-classify-lib.sh:1322 - note: status lines are no longer absorbable and now cost a turn in BOTH modes, which regresses the very metric this change targets. Concrete trace: state/task.status last line is note: captain said use REST not RPC and the crew is provably working (state: working · source: run-step). Before this change the attended watcher ran signal_reason_is_actionable (false — note is not captain-relevant and does not match FM_CAPTAIN_RE) then signal_crew_provably_working (true) and absorbed the wake. Now signal_reason_is_routine_nonterminal only accepts working|resolved|captain-held|paused, so note returns 1, attended_signal_is_absorbable fails, and the watcher enqueues and wakes the model. In away mode the same verb hits the new early return in bin/fm-supervise-daemon.sh:346 and escalates as unrecognized signal shape where it previously classified self|routine signal. note is a first-class informational verb elsewhere in this same library (status_line_is_unread_surface at bin/fm-classify-lib.sh:1147 treats it as the canonical UNREAD STATUS shape), so this looks like an omission rather than a deliberate wake, and the intent's wake list (terminal done/needs-decision/blocked/failed, checks, stale, dead endpoints, secondmate replies, malformed config, unparseable signals) does not include notes. Confirm whether note: should join the routine set (its bytes would still be surfaced once via the receipt path) or intentionally wake.
  • ⚠️ bin/fm-classify-lib.sh:912 - An unverifiable absorbed-receipt permanently and silently disables the entire status presentation, including the no-loss surfaces this change is supposed to protect. status_absorbed_receipt_endpoint returns 1 on identity mismatch, malformed content, or an out-of-range endpoint; status_acknowledge_presented_snapshot (line 912) and scan_unread_surface_snapshot (line 1196) both propagate that with || return 1, print_status_sections propagates it to print_status_presentation, and bin/fm-wake-drain.sh:350 / bin/fm-wake-drain.sh:402 call it as (print_status_presentation) || true. Failure trace: task t is absorbed (receipt dev:inode\t120 written), then state/t.status is replaced out of band with a new inode (id reuse without going through status_retire_presentation_task, a restore/sync, or a manual repair). Every later drain: ack fails at line 912 -> UNREAD STATUS, OPEN DECISIONS and RECORD DIVERGENCE all print nothing, the presentation cursor never commits, and the exit status is swallowed, so a buried needs-decision stops surfacing with no error anywhere. Nothing clears the receipt on this path (commit at line 964 is never reached), so it does not self-heal — unlike the pre-existing analogous case, where status_presentation_cursor_offset resets to offset 0 on an identity mismatch. Same class: status_lines_between_offsets returns 1 for a missing/symlinked status file at line 1211, where the previous status_new_lines_since_cursor returned 0. Suggest distinguishing 'receipt no longer applies to this file' (identity mismatch / endpoint > size -> drop the receipt and continue; the absorbed bytes are gone with the old inode anyway) from a transient read error, and making any remaining hard failure visible rather than swallowed.
  • ℹ️ bin/fm-supervise-daemon.sh:346 - The new away-mode gate escalates signals whose referenced files no longer exist. signal_reason_is_routine_nonterminal requires [ -f &#34;$f&#34; ] for every path, so a queued reason like signal: state/x.status whose task was torn down before the daemon classified it now prints escalate|unrecognized signal shape: state/x.status, whereas the loop below (which starts with [ -e &#34;$f&#34; ] || continue) previously yielded self|routine signal: and stayed in the digest. The escalation carries no content (the file is gone), so it adds a captain-visible digest entry with nothing to act on. Confirm whether a vanished signal path should escalate or be treated as already-resolved.
  • ℹ️ bin/fm-classify-lib.sh:1210 - Dead clamp. absorbed is already clamped to &gt;= offset at line 1199, and tail_start=$absorbed at line 1209, so [ &#34;$tail_start&#34; -ge &#34;$offset&#34; ] || tail_start=$offset can never fire. Dropping it (and using absorbed directly) removes a branch that reads as if a third invariant were in play.

🔧 Fix: absorb live note statuses and fail safe on stale receipts
3 infos still open:

  • ℹ️ bin/fm-classify-lib.sh:1220 - The receipt records only an endpoint, so the "absorbed span" is reconstructed as [presentation-cursor, receipt-endpoint) and printed UNFILTERED. That span can contain bytes that were never absorbed by the watcher, which then appear in UNREAD STATUS even though they belong to another section. Verified live: with state/t.status = needs-decision [key=k]: pick REST or RPC followed by working: still exploring, and a receipt written at EOF (66), scan_unread_surface_snapshot emits BOTH t needs-decision [key=k]: pick REST or RPC and t working: still exploring, while scan_open_decisions_snapshot emits the same needs-decision as t k needs-decision pick REST or RPC — the decision is rendered twice in one drain, in a section documented as carrying only verbs that "never enter the OPEN DECISIONS fold". Reachable whenever a decision line and a routine line land inside one FM_SIGNAL_GRACE window (only the last line is classified, so the pair is absorbed together). No loss occurs — OPEN DECISIONS still holds the decision until resolved, and surfacing the extra bytes is arguably better than the pre-change silence — so this is duplication/noise, not a defect. A start offset in the receipt would bound the span precisely if the noise matters.
  • ℹ️ bin/fm-classify-lib.sh:1212 - The new guard [ -f &#34;$f&#34; ] &amp;&amp; [ -r &#34;$f&#34; ] &amp;&amp; [ ! -L &#34;$f&#34; ] || continue in scan_unread_surface_snapshot claims to "skip it rather than failing the whole presentation", but it cannot deliver that: print_status_sections (bin/fm-wake-drain.sh:244) calls status_acknowledge_presented_snapshot FIRST, and for any task not in fully_presented that function calls status_presentation_cursor_offset, which hard-fails on a missing/symlinked status file. Verified: with the status file removed after the snapshot, scan_unread_surface_snapshot returns 0 (skips) while status_acknowledge_presented_snapshot returns 1, so print_status_presentation still fails and now prints the new stderr warning. status_commit_presentation_snapshot (line 963) rejects the same condition. The guard only fires for fully_presented tasks, where commit then fails anyway. Either make ack/commit tolerant of a vanished row the same way, or drop the guard and its comment so the code does not read as resilient when it is not.
  • ℹ️ bin/fm-classify-lib.sh:979 - status_commit_presentation_snapshot's new receipt-retirement loop runs AFTER mv -f &#34;$tmp&#34; &#34;$state/.status-presentation-cursor&#34;, and status_absorbed_receipt_endpoint &#34;$f&#34; || return 1 / rm -f -- &#34;$receipt&#34; || return 1 can return 1 there. If the status file becomes unreadable or the state dir becomes non-writable between the two loops, the cursor is already durably committed but print_status_sections returns 1, so the drain now emits wake drain: status presentation failed; UNREAD STATUS, OPEN DECISIONS and RECORD DIVERGENCE may be incomplete on a presentation that in fact fully succeeded. Practically hard to hit (the first loop validates the same files under the same lock), and the failure direction is safe (the receipt survives and replays), but the reported outcome is wrong. Treating a post-commit receipt-cleanup failure as non-fatal, or moving the cleanup before the mv, would keep the exit status honest.
✅ **Test** - passed

✅ No issues found.

  • bash tests/fm-watch-triage.test.sh (includes new test_attended_routine_absorb_configuration, test_absorbed_signal_surfaces_on_next_unrelated_model_turn, test_live_note_signal_absorbed_and_presented_once, test_terminal_status_still_wakes_over_a_live_worker, test_attended_absorb_off_surfaces_routine_signal, test_unparseable_live_status_still_surfaces, and the wedge-escalation regression now entering via the absorb path)
  • bash tests/fm-wake-drain-unread-status.test.sh (delayed-presentation receipt no-loss cases: stale identity and out-of-range endpoint)
  • bash tests/fm-daemon.test.sh (away-mode shared shape policy: unparseable signal escalates, retired signal path self-handles)
  • Manual end-to-end driver /tmp/fm-absorb-e2e.sh (copied to evidence) spawning the real bin/fm-watch.sh and bin/fm-wake-drain.sh: absorb + no queue entry + on-disk receipt, SIGKILL of the watcher, next genuine drain prints the line once in UNREAD STATUS, second drain does not replay, terminal done: wakes, unparseable status wakes, home-local config/attended-routine-status-absorb=off wakes
  • git status --porcelain to confirm no transient artifacts left in the worktree
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

@withally
withally merged commit d982d45 into main Aug 24, 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