Skip to content

fix(bin): keep declared pauses off wedge cadence - #2738

Open
Omar-Nawaf wants to merge 1 commit into
kunchenguid:mainfrom
Omar-Nawaf:fm/firstmate-declared-pause-wedge-upstream-pr
Open

fix(bin): keep declared pauses off wedge cadence#2738
Omar-Nawaf wants to merge 1 commit into
kunchenguid:mainfrom
Omar-Nawaf:fm/firstmate-declared-pause-wedge-upstream-pr

Conversation

@Omar-Nawaf

Copy link
Copy Markdown

Intent

Publish the already-implemented declared-pause wedge correction as one upstream PR for kunchenguid/firstmate issue #2614 now that publication is authorized and the maintainer marked it ready for PR. On current origin/main, a current declared paused: external wait must use FM_PAUSE_RESURFACE_SECS and must never produce repeated idle/busy possible-wedge or demand-deep-inspection escalation in ordinary or away-mode supervision; a newer non-pause status must supersede the wait and restore normal bounded wedge detection so an old pause cannot hide a real wedge forever, while authoritative active-run precedence remains intact. Keep this policy in the existing shared classifier owner, preserve non-paused max-defer and wedge reliability, and do not broaden the change into speculative busy-pane damping. Preserve or improve executable regressions for paused suppression, pause supersession, ordinary escalation, away-mode watcher routing, and daemon rechecks; run focused classifier, watcher, daemon, and AFK suites, documentation audience checks, exact bin/fm-lint.sh, and the complete no-mistakes pipeline. Port only the intended correction from preserved commit d6f4659 onto current origin/main without altering the preserved source branch or old copy, and include no private data/state/config, credentials, local paths, project content, agent co-author, or unrelated changes. The external-healthy-watcher test was treated as branch-relevant, reproduced identically on this branch and clean origin/main, and proven to fail only under Node 22 TS import support while both pass unchanged under available Linux Node 24, so do not weaken or alter that unrelated assertion. Push only fm/firstmate-declared-pause-wedge-upstream-pr through the captain-authorized Omar-Nawaf/firstmate fork and open one PR against kunchenguid/firstmate:main referencing #2614; never merge.

What Changed

  • Added a status_pause_damps_wedge predicate to the shared bin/fm-classify-lib.sh and routed both supervision modes through it: a current paused: external wait now takes the bounded FM_PAUSE_RESURFACE_SECS recheck cadence — whether its pane is idle or busy in the wait's own poll loop — instead of repeated possible-wedge / demand-deep-inspection escalation, and a wedge-decorated watcher reason no longer overrides that pause when the daemon classifies the wake.
  • A newer non-pause status line supersedes the wait and restores ordinary bounded wedge detection, while authoritative run-step verdicts keep their existing precedence; daemon housekeeping no longer drops a pause marker for a busy pane — only a gone pane or a superseding status ends the recheck cadence.
  • Reworked tests/fm-daemon.test.sh and tests/fm-watch-triage.test.sh to cover busy declared waits, pause supersession, ordinary escalation, away-mode routing, and daemon rechecks, and updated docs/architecture.md, docs/configuration.md, and the AFK skill doc to describe the latest-status-line pause policy.

Risk Assessment

✅ Low: The change is a well-bounded, internally consistent port of the authorized declared-pause correction: both supervision modes route through the one shared classifier predicate, latest-status supersession and active-run precedence are verifiably preserved at every changed decision point, docs match the new behavior, and the rewritten tests are behavioral regressions that would fail under the old code.

Testing

Ran the focused daemon, watcher-triage, classifier, and documentation-audience suites at the target commit (all pass, including every preserved regression named in the intent), proved all four new/changed pause regression tests fail on the base commit with the exact pre-fix symptoms, and captured a manual before/after transcript of the real watcher and daemon showing pause suppression, the bounded awaiting-external re-surface, and wedge restoration on supersession; no failures or intent violations found.

Evidence: Manual before/after demo: base watcher wedge-escalates a declared busy wait; target watcher takes the pause cadence, daemon re-surfaces it bounded, newer status restores wedge detection

Source: Manual before/after demo: base watcher wedge-escalates a declared busy wait; target watcher takes the pause cadence, daemon re-surfaces it bounded, newer status restores wedge detection

=== Run 1: BASE watcher (a0cec26, pre-fix) on the declared wait === watcher surfaced a wake and exited; the wake reason handed to supervision: stale: demo:fm-wait (idle 501s, possible wedge, escalation 3, demand-deep-inspection: same pane has wedge-escalated 3 times in a row - do not re-absorb on the run-step/pane state alone) === Run 2: TARGET watcher (2c35a93, fixed) on the identical fixture === watcher completed a full poll cycle without surfacing any wake. pause-cadence marker .paused-demo_fm-wait installed (FM_PAUSE_RESURFACE_SECS recheck owns it now). wedge timer .stale-since-demo_fm-wait cleared. deep-inspection escalation counter .wedge-escalations-demo_fm-wait cleared. === Run 3: TARGET daemon housekeeping past FM_PAUSE_RESURFACE_SECS (busy pane) === escalation digest the captain receives (state/.subsuper-escalations): paused 5000s (awaiting external, recheck whether the wait still holds): sess:fm-held-w12 pause marker reset, so the bounded recheck window repeats. === Run 4: TARGET watcher after a NEWER non-pause status supersedes the wait === watcher surfaced a wake and exited; ordinary bounded wedge detection is restored: stale: demo:fm-wait (idle 502s, possible wedge, escalation 3, demand-deep-inspection: ...)

##########################################################################
## Fixture: crew status 'paused: awaiting the external confirmation run',
## pane busy in the wait's poll loop, away mode, wedge timer pre-armed
## with 2 prior escalations.
##########################################################################

=== Run 1: BASE watcher (a0cec26, pre-fix) on the declared wait ===
watcher surfaced a wake and exited; the wake reason handed to supervision:
    stale: demo:fm-wait (idle 501s, possible wedge, escalation 3, demand-deep-inspection: same pane has wedge-escalated 3 times in a row - do not re-absorb on the run-step/pane state alone)

=== Run 2: TARGET watcher (2c35a93, fixed) on the identical fixture ===
watcher completed a full poll cycle without surfacing any wake.
pause-cadence marker .paused-demo_fm-wait installed (FM_PAUSE_RESURFACE_SECS recheck owns it now).
wedge timer .stale-since-demo_fm-wait cleared.
deep-inspection escalation counter .wedge-escalations-demo_fm-wait cleared.

=== Run 3: TARGET daemon housekeeping past FM_PAUSE_RESURFACE_SECS (busy pane) ===
escalation digest the captain receives (state/.subsuper-escalations):
    paused 5000s (awaiting external, recheck whether the wait still holds): sess:fm-held-w12
pause marker reset, so the bounded recheck window repeats.

=== Run 4: TARGET watcher after a NEWER non-pause status supersedes the wait ===
watcher surfaced a wake and exited; ordinary bounded wedge detection is restored:
    stale: demo:fm-wait (idle 502s, possible wedge, escalation 3, demand-deep-inspection: same pane has wedge-escalated 3 times in a row - do not re-absorb on the run-step/pane state alone)
Evidence: New watcher regression tests fail on base a0cec26 (pre-fix symptoms reproduced)

Source: New watcher regression tests fail on base a0cec26 (pre-fix symptoms reproduced)

=== watch-afk-busy-pause (target-commit test against BASE code a0cec26) === not ok - away-mode busy declared pause wedge-escalated: stale: test:fm-afk-wait (idle 501s, possible wedge, escalation 3, demand-deep-inspection: same pane has wedge-escalated 3 times in a row - do not re-absorb on the run-step/pane state alone) exit=1 === watch-live-pause-bounded (target-commit test against BASE code a0cec26) === not ok - a live declared pause surfaced immediately: stale: test:fm-gate exit=1

=== watch-afk-busy-pause (target-commit test against BASE code a0cec26) ===
not ok - away-mode busy declared pause wedge-escalated: stale: test:fm-afk-wait (idle 501s, possible wedge, escalation 3, demand-deep-inspection: same pane has wedge-escalated 3 times in a row - do not re-absorb on the run-step/pane state alone)
exit=1

=== watch-live-pause-bounded (target-commit test against BASE code a0cec26) ===
not ok - a live declared pause surfaced immediately: stale: test:fm-gate
exit=1
Evidence: New daemon regression tests fail on base a0cec26 (pre-fix symptoms reproduced)

Source: New daemon regression tests fail on base a0cec26 (pre-fix symptoms reproduced)

=== daemon-wedge-decoration (target-commit test against BASE code a0cec26) === not ok - an enriched wedge overrode a declared external wait: sess:fm-suffix-paused (idle 500s, possible wedge, escalation 3, demand-deep-inspection: ...) exit=1 === daemon-busy-pause-recheck (target-commit test against BASE code a0cec26) === not ok - a busy declared wait was not re-surfaced as an awaiting-external recheck exit=1

=== daemon-wedge-decoration (target-commit test against BASE code a0cec26) ===
not ok - an enriched wedge overrode a declared external wait: sess:fm-suffix-paused (idle 500s, possible wedge, escalation 3, demand-deep-inspection: same pane has wedge-escalated 3 times in a row - do not re-absorb on the run-step/pane state alone)
exit=1

=== daemon-busy-pause-recheck (target-commit test against BASE code a0cec26) ===
not ok - a busy declared wait was not re-surfaced as an awaiting-external recheck
exit=1
Evidence: fm-watch-triage suite at target (51 ok)

Source: fm-watch-triage suite at target (51 ok)

ok - signal_reason_is_actionable: benign absorbed, captain verbs and coalesced batches surfaced
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, and paused is not captain-relevant
ok - crew_absorb_class: working/paused/none from one read; crew_is_paused and crew_is_provably_working agree
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 - 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 - away mode routes a busy declared wait to the long pause cadence instead of 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 - current declared pauses use the bounded pause cadence regardless of agent liveness, while exited captain-held panes remain bounded
ok - a declared paused 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 - 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 2273700.1787298381.8n2QVm
ok - queue revalidation, proactive output, and marker commit serialize with drain
/home/omarn/.no-mistakes/worktrees/79daa7753079/01M0HKXWQ37T7VVNTW9692TSX5/bin/fm-push-transition-lib.sh: line 96: echo: write error: Broken pipe
tests/wake-helpers.sh: line 281: 2276650 Killed                     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 281: 2278881 Killed                     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
Evidence: fm-daemon suite at target (99 ok)

Source: fm-daemon suite at target (99 ok)

ok - fm-afk-start.sh fails before daemon startup when the afk flag cannot be written
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 preserve ordinary escalation but never override a declared external wait
ok - stale + terminal status escalates immediately
ok - paused reasons with captain phrases remain pause-classified
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 busy declared wait on the long cadence instead of dropping it
ok - housekeeping clears a paused marker once the crew is no longer declaring the pause
ok - housekeeping moves an 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
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
Evidence: fm-classify-decision-key suite at target (13 ok)

Source: fm-classify-decision-key suite at target (13 ok)

ok - a stated [key=X] opens X whether it precedes or follows the verb colon
ok - a keyless needs-decision still opens and closes the default key
ok - a resolution closes its decision regardless of either line's key position
ok - blocked [key=X] opens X in both key positions
ok - two colon-form keyed decisions never collapse into one shared bucket
ok - a [key=x] mentioned mid-note is prose, never an opened or closed key
ok - a malformed stated key is rejected in both positions, never folded as default
ok - status_line_verb strips every bracket tag before the colon, in any order, and recovers the bare verb
ok - a [corr=...] tag ahead of [key=...] no longer swallows the verb: opens and closes under the stated key
ok - a [corr=...] tag with no stated key opens under 'default', exactly like a bare needs-decision line
ok - a [key=x] tag alone (no corr tag) still opens x - no regression from the tag-stripping fix
ok - blocked/resolved parse their bare verb with any bracket-tag order preceding the colon
ok - the incremental fold matches the full fold across appends in both key positions
Evidence: fm-documentation-audiences suite at target (4 ok)

Source: fm-documentation-audiences suite at target (4 ok)

ok - documentation inventory classifies every maintained prose surface exactly once
ok - classification, setup routing, and maintained-prose scope fail safely
ok - required documentation owner pointers cannot silently disappear
ok - local links resolve while dates, versions, commands, and incident prose remain semantically reviewed

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 1 info
  • ℹ️ bin/fm-supervise-daemon.sh:1046 - Daemon housekeeping (2b) no longer retires a pause marker when the pane is busy, so a crew that genuinely resumed work but never appended a newer non-pause status line will keep receiving the bounded 'awaiting external' recheck digest once per FM_PAUSE_RESURFACE_SECS until a newer status supersedes the pause. This is the documented, intended trade-off (busyness cannot distinguish a wait's poll loop from resumed work), and the recheck explicitly asks firstmate to confirm the wait still holds - noting it here as the one place future maintainers might expect busy-implies-resumed.
✅ **Test** - passed

✅ No issues found.

  • bash tests/fm-daemon.test.sh at target 2c35a93 (99 ok, includes busy-pause recheck, wedge-decoration-never-overrides-pause, ordinary escalation, max-defer, afk presence-gating)
  • bash tests/fm-watch-triage.test.sh at target 2c35a93 (51 ok, includes away-mode busy declared pause, liveness-independent pause cadence, pause supersession restoring wedge escalation, active-run precedence recheck, secondmate exemptions)
  • bash tests/fm-classify-decision-key.test.sh at target (13 ok, shared classifier suite)
  • bash tests/fm-documentation-audiences.test.sh at target (4 ok, covers the docs/SKILL.md edits)
  • Regression proof: ran target-commit test_stale_diagnostic_wedge_survives_busy_housekeeping and test_housekeeping_paused_busy_pane_still_rechecks against base a0cec26 code — both fail pre-fix (declared wait escalated as possible wedge / busy wait silently dropped)
  • Regression proof: ran target-commit test_afk_busy_declared_pause_uses_pause_cadence and test_declared_pause_and_exited_captain_hold_are_bounded against base a0cec26 code — both fail pre-fix (away-mode wedge escalation with demand-deep-inspection / live pause surfaced immediately)
  • Manual end-to-end demo driving real bin/fm-watch.sh and daemon housekeeping on identical fixtures: base watcher emits possible wedge, escalation 3, demand-deep-inspection for a declared busy wait; target watcher absorbs it into the pause cadence and clears wedge state; daemon re-surfaces it past FM_PAUSE_RESURFACE_SECS as paused 5000s (awaiting external, recheck whether the wait still holds) and resets the window; a newer working: status restores the wedge escalation
  • Hygiene: verified status_pause_damps_wedge is defined once in bin/fm-classify-lib.sh and called by both supervision modes; commit author is Omar Nawaf with no Co-Authored-By, and the diff contains no credentials, local paths, or private data
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2c35a9345f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread bin/fm-watch.sh
Comment on lines +377 to 379
if status_pause_damps_wedge "$last" \
|| { ! afk_present && status_is_paused_or_captain_held "$last"; }; then
handle_paused_stale "$win" "$task" "$h"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reconcile the run before damping away-mode wedges

Captain, when AFK is active and a task starts an authoritative run without appending a new status event, an older paused: line still makes this new first clause call handle_paused_stale before any fm-crew-state.sh read. That clears the short wedge timer and leaves a hung busy run on repeated hour-long pause rechecks, so the old pause can mask exactly the busy-run wedge this bound detects; gate the pause on current-state reconciliation so run-step working retains precedence.

AGENTS.md reference: AGENTS.md:L133-L133

Useful? React with 👍 / 👎.

@Omar-Nawaf
Omar-Nawaf force-pushed the fm/firstmate-declared-pause-wedge-upstream-pr branch from 1427342 to bdf8b07 Compare August 21, 2026 19:01
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