Skip to content

fix: stop rechecking captain-gated pauses in away mode - #1621

Closed
sbracewell64 wants to merge 2 commits into
kunchenguid:mainfrom
sbracewell64:fm/afk-rechecks-captain-gated-pauses
Closed

sbracewell64 wants to merge 2 commits into
kunchenguid:mainfrom
sbracewell64:fm/afk-rechecks-captain-gated-pauses

Conversation

@sbracewell64

@sbracewell64 sbracewell64 commented Aug 3, 2026 •

Copy link
Copy Markdown

Intent

Stop rechecking waits that only the captain can clear.

WHAT JUSTIFIES THIS. The resurface cadence performs fixed-interval rechecks of captain-gated waits whose state cannot change without the captain acting. That behaviour is reproducible directly from the code path and is covered by the regression evidence in this change. Each recheck costs a coordinator turn to answer "no, still waiting." A bounded direct observation on 2026-08-04 recorded one such batch arriving, and then repeating about an hour later, against waits that only the captain could clear. The historical prevalence, rate, and total cost of this behaviour are UNKNOWN and are not claimed here.

THE DISTINCTION THE DAEMON WAS MISSING. The resurface cadence (FM_PAUSE_RESURFACE_SECS, default 3600s) treated every declared pause the same. It should not. The question is not "paused versus not paused". It is: can the thing being waited on change without the captain acting? A genuine external wait CAN clear on its own and rechecking it is correct - proven the same day, when luna-max-only-routing and three siblings sat on upstream maintainer workflow approval, the maintainer approved, and the wait cleared; those rechecks earned their keep. A captain-gated wait CANNOT: it clears only when the captain acts, and the captain acting is already the away-mode exit signal, which runs the full return sequence. So the recheck can never surface anything the exit does not surface anyway. It is strictly wasted.

WHAT WAS ASKED FOR. Classify a captain-gated pause distinctly from a declared external wait, and exclude ONLY the captain-gated kind from the resurface cadence. Leave external-wait rechecks exactly as they are - they work, and they demonstrably paid off that same day.

DELIBERATE DESIGN DECISIONS A REVIEWER READING ONLY THE DIFF WOULD NOT KNOW.

  1. Reading the backlog's existing hold_kind vocabulary was an explicit instruction, not an incidental choice. The task said to establish from source how a pause's kind is currently determined before designing, and to check whether the backlog's existing hold_kind vocabulary (captain|external|load|parked|future) can be read rather than inventing a parallel one, with an explicit "prefer using what exists". task_hold_kind therefore shells out to tasks-axi and reads the same hold_kind field bin/fm-decision-hold.sh already verifies a captain hold with. Parsing the pause line's prose was considered and deliberately rejected as the parallel vocabulary the instruction ruled out.

  2. task_hold_kind requires held: yes in addition to a kind. This is deliberate: a stale kind on an item whose hold has been cleared must not keep suppressing the recheck. It fails toward rechecking.

  3. An indeterminate kind is deliberately NOT a captain-gated kind. This was an explicit requirement: if a pause's kind cannot be determined it must fall back to being rechecked, not silently dropped. No reader on PATH, an unreadable or absent backlog item, and an item carrying no active hold all return the token "unknown", which never equals the captain kind. The looks-unnecessary defensive branches in task_hold_kind are that requirement, not over-engineering.

  4. Suppression is a CADENCE decision only, and the marker is deliberately KEPT and its window deliberately RESET rather than dropped. Another explicit requirement was not to over-correct: a captain-gated pause must still be visible, just not re-asked on a timer, and nothing may make it harder to see at session start, in a fleet view, or in the away-mode return catch-up. Keeping the marker keeps the wait tracked; resetting the window means it resumes ordinary rechecking within one window if its recorded kind later stops being captain-gated. Dropping the marker would have been simpler and was rejected for exactly this reason.

  5. The suppression check is placed INSIDE the existing escalate branch, after the busy-or-gone checks, rather than earlier where it would have skipped a pane capture. That is intentional: it leaves the pause marker lifecycle (busy -> clear, window gone -> clear, no longer paused -> clear) completely unchanged, so the only behavioral delta is whether a digest line is appended.

  6. The new predicate is placed in the pause section of bin/fm-classify-lib.sh, deliberately away from crew_absorb_class and crew_is_provably_working. A sibling task, wedge-detection-ignores-child-processes, is live on that same file at the same time extending the provably-working predicate. Keeping this change in the pause path and out of the working path was an explicit instruction so the eventual rebase stays cheap.

  7. Documentation follows the repo's one-owner rule. The away-mode policy statement lives in the /afk skill, which already owns the pause classification bullet; bin/fm-supervise-daemon.sh's comments own the mechanism; docs/configuration.md and docs/architecture.md carry one-line cross-references rather than restating the contract. AGENTS.md was deliberately not touched, because its away-mode stub keeps only the marker format, ownership transfer, and exit condition inline by design.

  8. The three new tests use real tasks-axi for the captain and external directions (self-skipping with a visible skip line where the tool is absent; CI installs it) and PATH-level reader stubs for the two indeterminate shapes, so the indeterminate guarantee holds even where the backlog tool is missing. No test asserts implementation source bytes.

EXPLICIT SCOPE EXCLUSIONS, ALL HONORED. Do not change what counts as a wedge. Do not touch the stale-escalation threshold (FM_STALE_ESCALATE_SECS). Do not alter how any pause is displayed. The always-on watcher path in bin/fm-watch.sh was named as the always-on owner to read from, and is deliberately unchanged - only the away-mode daemon cadence changes.

VERIFICATION DISCIPLINE THAT WAS REQUIRED AND PERFORMED. The negative control had to be witnessed failing first, with the red run recorded, and BOTH directions had to be proven, because a change that suppressed everything would look like a fix and a test that only proves the suppression proves the dangerous half. Against unchanged production code the captain-gated test failed red with "a captain-gated pause was re-surfaced on the cadence", while the external and indeterminate tests passed. Against a deliberately over-suppressing build the external test failed red with "a declared external wait stopped being re-surfaced on the cadence" and the indeterminate test failed with it, proving both are load-bearing. The predicate was additionally spot-checked read-only against the live fleet data that produced the measurement.

This is firstmate's own shared tracked material, so the firstmate-coding-guidelines contract applies: one sentence per line in tracked Markdown, plain dash rather than em dash, no agent name as a commit co-author, shellcheck-clean bin scripts via bin/fm-lint.sh, and tests colocated in tests/ extending the existing suite rather than a new runner.

DELIVERY CONTEXT. Every push to this cross-fork pull request re-gates it to action_required awaiting maintainer approval, which we cannot grant, so zero checks executed is the expected state. A checks-passed reported off that empty check set is a known false green and must not be accepted or reported as green.

What Changed

  • Classify active paused tasks by the backlog’s existing hold_kind, failing safely to unknown when the kind cannot be established.
  • Skip away-mode cadence rechecks only for captain-gated pauses while retaining and resetting their tracking markers; external and indeterminate waits continue to resurface.
  • Document the pause-kind behavior and add daemon coverage for captain, external, and indeterminate hold cases.

Risk Assessment

✅ Low: The change is well-bounded, conforms to the stated intent, fails safely toward rechecking when hold classification is indeterminate, and preserves external-wait cadence and pause-marker lifecycle behavior.

Testing

After inspecting the change, I ran the three focused pause-cadence scenarios and an end-to-end housekeeping transcript: captain-gated waits were suppressed only from the timed digest while external and unknown waits still resurfaced, all markers stayed tracked and reset, and the worktree remained clean.

Evidence: Pause cadence end-to-end transcript
ok - housekeeping does not re-surface a captain-gated pause, and keeps tracking it
ok - housekeeping still re-surfaces a declared external wait, unchanged
ok - an indeterminate pause kind keeps being re-surfaced, never silently suppressed

Operator-visible cadence outcome
paused-captain-gated | digest=(none) | tracking-marker=kept-and-reset
paused-external-kind | digest=paused 5000s (awaiting external, recheck whether the wait still holds): sess:fm-held-ext1  | tracking-marker=kept-and-reset
paused-kind-reader-fails | digest=paused 5000s (awaiting external, recheck whether the wait still holds): sess:fm-held-unk  | tracking-marker=kept-and-reset
paused-kind-not-held | digest=paused 5000s (awaiting external, recheck whether the wait still holds): sess:fm-held-unk  | tracking-marker=kept-and-reset

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

✅ **Review** - passed

✅ No issues found.

✅ **Test** - passed

✅ No issues found.

  • Inspected git diff --stat, changed paths, pause-kind classification, and daemon housekeeping behavior between 3d9d12db7412e5da0751745c14603e7e427f8877 and 7bbe9d80ecd647ff8a30f4901df4ca1be911dcd5.
  • Ran focused functions from tests/fm-daemon.test.sh: test_housekeeping_captain_gated_pause_is_not_resurfaced, test_housekeeping_external_pause_still_resurfaces, and test_housekeeping_indeterminate_pause_kind_still_resurfaces.
  • Performed an end-to-end daemon-housekeeping check using real tasks-axi captain/external holds and reader-failure/not-held fixtures, recording digest and tracking-marker outcomes with tee /tmp/no-mistakes-evidence/01KZ507FKGTMRXMFSHZCT3J0WE/pause-cadence-transcript.txt.
  • Ran git status --short after testing to confirm no working-tree artifacts remained.
  • An initial focused extraction command had an incomplete function range; it executed the captain and external cases, then failed during harness parsing. The corrected focused command reran and passed all intended scenarios.
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

The pause re-surface cadence treated every declared pause the same, but the
question it actually asks is whether the thing being waited on can change
without the captain acting. An external wait can, so rechecking it is real
work. A captain-gated wait cannot: it clears only when the captain acts, and
the captain acting is already the away-mode exit signal, which runs the full
return catch-up, so the hourly recheck could never surface anything the exit
does not. Measured over one away-mode stretch, 32 of 38 false-positive wakes
were exactly that recheck.

The backlog already records the distinction per work item as hold_kind
(captain|external|load|parked|future), the same field fm-decision-hold.sh
verifies a captain hold with, so task_hold_kind/pause_is_captain_gated read
that existing vocabulary rather than parsing pause prose or introducing a
parallel one.

Suppression is a cadence decision only. The marker is kept and the window is
still reset, so the wait stays tracked, stays as visible as before in the
backlog digest, the fleet view, and the return catch-up, and resumes ordinary
rechecking within one window if its recorded kind stops being captain-gated.
An indeterminate kind is deliberately not a captain-gated kind: a missing
reader, an unreadable or absent item, or an item with no active hold all read
as unknown and keep being rechecked.

The always-on watcher path, the wedge definition, and the stale-escalation
threshold are untouched.

Verified in both directions, since a change that suppressed everything would
look like a fix. Against unchanged code the captain-gated test failed
("a captain-gated pause was re-surfaced on the cadence") while the external
and indeterminate tests passed; against a deliberately over-suppressing build
the external test failed ("a declared external wait stopped being re-surfaced
on the cadence") and the indeterminate test failed with it.
@sbracewell64

Copy link
Copy Markdown
Author

Correction: The quantitative wake-ledger evidence previously cited in this PR was invalid. The cited counts came from outcome records containing fabricated placeholder sequence identifiers that could not be joined to genuine wakes. I have removed those measurements and the associated prevalence claims. The underlying recheck behavior remains reproducible from the implementation and regression evidence, but its historical frequency and cost are unknown. Please reassess the PR on that corrected basis.

@kunchenguid

kunchenguid commented Aug 5, 2026 •

Copy link
Copy Markdown
Owner

Automated reminder: thanks for the PR! This branch currently has a merge conflict with the base branch.

When you get a chance, please rebase onto (or merge) the latest base branch, resolve the conflict, and push. After that, checks will re-run and the PR will get looked at again.

Noted for firstmate#1621 at 23b825a7.

sbracewell64 added a commit to sbracewell64/firstmate that referenced this pull request Aug 6, 2026
…eam kunchenguid#1621) (#42)

* fix(bin): stop rechecking captain-gated pauses on the away-mode cadence

The pause re-surface cadence treated every declared pause the same, but the
question it actually asks is whether the thing being waited on can change
without the captain acting. An external wait can, so rechecking it is real
work. A captain-gated wait cannot: it clears only when the captain acts, and
the captain acting is already the away-mode exit signal, which runs the full
return catch-up, so the hourly recheck could never surface anything the exit
does not. Measured over one away-mode stretch, 32 of 38 false-positive wakes
were exactly that recheck.

The backlog already records the distinction per work item as hold_kind
(captain|external|load|parked|future), the same field fm-decision-hold.sh
verifies a captain hold with, so task_hold_kind/pause_is_captain_gated read
that existing vocabulary rather than parsing pause prose or introducing a
parallel one.

Suppression is a cadence decision only. The marker is kept and the window is
still reset, so the wait stays tracked, stays as visible as before in the
backlog digest, the fleet view, and the return catch-up, and resumes ordinary
rechecking within one window if its recorded kind stops being captain-gated.
An indeterminate kind is deliberately not a captain-gated kind: a missing
reader, an unreadable or absent item, or an item with no active hold all read
as unknown and keep being rechecked.

The always-on watcher path, the wedge definition, and the stale-escalation
threshold are untouched.

Verified in both directions, since a change that suppressed everything would
look like a fix. Against unchanged code the captain-gated test failed
("a captain-gated pause was re-surfaced on the cadence") while the external
and indeterminate tests passed; against a deliberately over-suppressing build
the external test failed ("a declared external wait stopped being re-surfaced
on the cadence") and the indeterminate test failed with it.

* no-mistakes(document): Correct stale pause-cadence comments
@kunchenguid kunchenguid removed the wheelhouse:pending-contributor-action Managed by Wheelhouse label Aug 6, 2026
sbracewell64 added a commit to sbracewell64/firstmate that referenced this pull request Aug 9, 2026
…eam kunchenguid#1621) (#42)

* fix(bin): stop rechecking captain-gated pauses on the away-mode cadence

The pause re-surface cadence treated every declared pause the same, but the
question it actually asks is whether the thing being waited on can change
without the captain acting. An external wait can, so rechecking it is real
work. A captain-gated wait cannot: it clears only when the captain acts, and
the captain acting is already the away-mode exit signal, which runs the full
return catch-up, so the hourly recheck could never surface anything the exit
does not. Measured over one away-mode stretch, 32 of 38 false-positive wakes
were exactly that recheck.

The backlog already records the distinction per work item as hold_kind
(captain|external|load|parked|future), the same field fm-decision-hold.sh
verifies a captain hold with, so task_hold_kind/pause_is_captain_gated read
that existing vocabulary rather than parsing pause prose or introducing a
parallel one.

Suppression is a cadence decision only. The marker is kept and the window is
still reset, so the wait stays tracked, stays as visible as before in the
backlog digest, the fleet view, and the return catch-up, and resumes ordinary
rechecking within one window if its recorded kind stops being captain-gated.
An indeterminate kind is deliberately not a captain-gated kind: a missing
reader, an unreadable or absent item, or an item with no active hold all read
as unknown and keep being rechecked.

The always-on watcher path, the wedge definition, and the stale-escalation
threshold are untouched.

Verified in both directions, since a change that suppressed everything would
look like a fix. Against unchanged code the captain-gated test failed
("a captain-gated pause was re-surfaced on the cadence") while the external
and indeterminate tests passed; against a deliberately over-suppressing build
the external test failed ("a declared external wait stopped being re-surfaced
on the cadence") and the indeterminate test failed with it.

* no-mistakes(document): Correct stale pause-cadence comments
sbracewell64 added a commit to sbracewell64/firstmate that referenced this pull request Aug 9, 2026
…eam kunchenguid#1621) (#42)

* fix(bin): stop rechecking captain-gated pauses on the away-mode cadence

The pause re-surface cadence treated every declared pause the same, but the
question it actually asks is whether the thing being waited on can change
without the captain acting. An external wait can, so rechecking it is real
work. A captain-gated wait cannot: it clears only when the captain acts, and
the captain acting is already the away-mode exit signal, which runs the full
return catch-up, so the hourly recheck could never surface anything the exit
does not. Measured over one away-mode stretch, 32 of 38 false-positive wakes
were exactly that recheck.

The backlog already records the distinction per work item as hold_kind
(captain|external|load|parked|future), the same field fm-decision-hold.sh
verifies a captain hold with, so task_hold_kind/pause_is_captain_gated read
that existing vocabulary rather than parsing pause prose or introducing a
parallel one.

Suppression is a cadence decision only. The marker is kept and the window is
still reset, so the wait stays tracked, stays as visible as before in the
backlog digest, the fleet view, and the return catch-up, and resumes ordinary
rechecking within one window if its recorded kind stops being captain-gated.
An indeterminate kind is deliberately not a captain-gated kind: a missing
reader, an unreadable or absent item, or an item with no active hold all read
as unknown and keep being rechecked.

The always-on watcher path, the wedge definition, and the stale-escalation
threshold are untouched.

Verified in both directions, since a change that suppressed everything would
look like a fix. Against unchanged code the captain-gated test failed
("a captain-gated pause was re-surfaced on the cadence") while the external
and indeterminate tests passed; against a deliberately over-suppressing build
the external test failed ("a declared external wait stopped being re-surfaced
on the cadence") and the indeterminate test failed with it.

* no-mistakes(document): Correct stale pause-cadence comments
sbracewell64 added a commit to sbracewell64/firstmate that referenced this pull request Aug 10, 2026
…eam kunchenguid#1621) (#42)

* fix(bin): stop rechecking captain-gated pauses on the away-mode cadence

The pause re-surface cadence treated every declared pause the same, but the
question it actually asks is whether the thing being waited on can change
without the captain acting. An external wait can, so rechecking it is real
work. A captain-gated wait cannot: it clears only when the captain acts, and
the captain acting is already the away-mode exit signal, which runs the full
return catch-up, so the hourly recheck could never surface anything the exit
does not. Measured over one away-mode stretch, 32 of 38 false-positive wakes
were exactly that recheck.

The backlog already records the distinction per work item as hold_kind
(captain|external|load|parked|future), the same field fm-decision-hold.sh
verifies a captain hold with, so task_hold_kind/pause_is_captain_gated read
that existing vocabulary rather than parsing pause prose or introducing a
parallel one.

Suppression is a cadence decision only. The marker is kept and the window is
still reset, so the wait stays tracked, stays as visible as before in the
backlog digest, the fleet view, and the return catch-up, and resumes ordinary
rechecking within one window if its recorded kind stops being captain-gated.
An indeterminate kind is deliberately not a captain-gated kind: a missing
reader, an unreadable or absent item, or an item with no active hold all read
as unknown and keep being rechecked.

The always-on watcher path, the wedge definition, and the stale-escalation
threshold are untouched.

Verified in both directions, since a change that suppressed everything would
look like a fix. Against unchanged code the captain-gated test failed
("a captain-gated pause was re-surfaced on the cadence") while the external
and indeterminate tests passed; against a deliberately over-suppressing build
the external test failed ("a declared external wait stopped being re-surfaced
on the cadence") and the indeterminate test failed with it.

* no-mistakes(document): Correct stale pause-cadence comments
sbracewell64 added a commit to sbracewell64/firstmate that referenced this pull request Aug 11, 2026
…eam kunchenguid#1621) (#42)

* fix(bin): stop rechecking captain-gated pauses on the away-mode cadence

The pause re-surface cadence treated every declared pause the same, but the
question it actually asks is whether the thing being waited on can change
without the captain acting. An external wait can, so rechecking it is real
work. A captain-gated wait cannot: it clears only when the captain acts, and
the captain acting is already the away-mode exit signal, which runs the full
return catch-up, so the hourly recheck could never surface anything the exit
does not. Measured over one away-mode stretch, 32 of 38 false-positive wakes
were exactly that recheck.

The backlog already records the distinction per work item as hold_kind
(captain|external|load|parked|future), the same field fm-decision-hold.sh
verifies a captain hold with, so task_hold_kind/pause_is_captain_gated read
that existing vocabulary rather than parsing pause prose or introducing a
parallel one.

Suppression is a cadence decision only. The marker is kept and the window is
still reset, so the wait stays tracked, stays as visible as before in the
backlog digest, the fleet view, and the return catch-up, and resumes ordinary
rechecking within one window if its recorded kind stops being captain-gated.
An indeterminate kind is deliberately not a captain-gated kind: a missing
reader, an unreadable or absent item, or an item with no active hold all read
as unknown and keep being rechecked.

The always-on watcher path, the wedge definition, and the stale-escalation
threshold are untouched.

Verified in both directions, since a change that suppressed everything would
look like a fix. Against unchanged code the captain-gated test failed
("a captain-gated pause was re-surfaced on the cadence") while the external
and indeterminate tests passed; against a deliberately over-suppressing build
the external test failed ("a declared external wait stopped being re-surfaced
on the cadence") and the indeterminate test failed with it.

* no-mistakes(document): Correct stale pause-cadence comments
sbracewell64 added a commit to sbracewell64/firstmate that referenced this pull request Aug 11, 2026
…eam kunchenguid#1621) (#42)

* fix(bin): stop rechecking captain-gated pauses on the away-mode cadence

The pause re-surface cadence treated every declared pause the same, but the
question it actually asks is whether the thing being waited on can change
without the captain acting. An external wait can, so rechecking it is real
work. A captain-gated wait cannot: it clears only when the captain acts, and
the captain acting is already the away-mode exit signal, which runs the full
return catch-up, so the hourly recheck could never surface anything the exit
does not. Measured over one away-mode stretch, 32 of 38 false-positive wakes
were exactly that recheck.

The backlog already records the distinction per work item as hold_kind
(captain|external|load|parked|future), the same field fm-decision-hold.sh
verifies a captain hold with, so task_hold_kind/pause_is_captain_gated read
that existing vocabulary rather than parsing pause prose or introducing a
parallel one.

Suppression is a cadence decision only. The marker is kept and the window is
still reset, so the wait stays tracked, stays as visible as before in the
backlog digest, the fleet view, and the return catch-up, and resumes ordinary
rechecking within one window if its recorded kind stops being captain-gated.
An indeterminate kind is deliberately not a captain-gated kind: a missing
reader, an unreadable or absent item, or an item with no active hold all read
as unknown and keep being rechecked.

The always-on watcher path, the wedge definition, and the stale-escalation
threshold are untouched.

Verified in both directions, since a change that suppressed everything would
look like a fix. Against unchanged code the captain-gated test failed
("a captain-gated pause was re-surfaced on the cadence") while the external
and indeterminate tests passed; against a deliberately over-suppressing build
the external test failed ("a declared external wait stopped being re-surfaced
on the cadence") and the indeterminate test failed with it.

* no-mistakes(document): Correct stale pause-cadence comments
@kunchenguid

Copy link
Copy Markdown
Owner

Speaking as Kun's firstmate: closing this as stale. It has been waiting on a contributor update for 14+ days with no author push or comment. Reopen if you want to pick it back up.

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.

2 participants