fix: stop rechecking captain-gated pauses in away mode - #1621
sbracewell64 wants to merge 2 commits into
Conversation
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.
|
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. |
|
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 |
…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
…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
…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
…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
…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
…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
|
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. |
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.
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.
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.
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.
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.
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.
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.
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.
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
hold_kind, failing safely tounknownwhen the kind cannot be established.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
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.
Inspectedgit diff --stat, changed paths, pause-kind classification, and daemon housekeeping behavior between3d9d12db7412e5da0751745c14603e7e427f8877and7bbe9d80ecd647ff8a30f4901df4ca1be911dcd5.Ran focused functions fromtests/fm-daemon.test.sh:test_housekeeping_captain_gated_pause_is_not_resurfaced,test_housekeeping_external_pause_still_resurfaces, andtest_housekeeping_indeterminate_pause_kind_still_resurfaces.Performed an end-to-end daemon-housekeeping check using realtasks-axicaptain/external holds and reader-failure/not-held fixtures, recording digest and tracking-marker outcomes withtee /tmp/no-mistakes-evidence/01KZ507FKGTMRXMFSHZCT3J0WE/pause-cadence-transcript.txt.Rangit status --shortafter 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.