Skip to content

fix(bin): prevent duplicate wakes for fresh replacement waits - #3605

Closed
mremond wants to merge 5 commits into
kunchenguid:mainfrom
mremond:fm/fm-absorb-fresh-replacement-wait
Closed

mremond wants to merge 5 commits into
kunchenguid:mainfrom
mremond:fm/fm-absorb-fresh-replacement-wait

Conversation

@mremond

@mremond mremond commented Sep 3, 2026 •

Copy link
Copy Markdown
Contributor

Intent

Clear the merge conflict that blocks the already-open pull request #3605, and do nothing else.

SCOPE, AND WHY IT IS THIS NARROW. A development pause has been deliberately in force in this fleet since 2026-09-09 to stop saturating the upstream maintainer with open requests. It has been lifted for exactly one purpose: clearing merge conflicts on requests that are ALREADY OPEN. No new deliveries. Six of eleven open requests fell into conflict as the trunk advanced; 3605 is one of them. Clearing the conflict is the entire deliverable.

Therefore the following are deliberately NOT in this change and must not be treated as omissions or as incomplete work: any fix noticed in passing, any stale value refreshed while in the file, any tidier neighbouring line, any generalization, consistency sweep or extra hardening. Those were explicitly ruled out by the captain and are to be reported as follow-up work, never folded in here. The change is intentionally minimal by order.

WHAT WAS DONE. An INCOMING MERGE of main (b182d0f) into the existing published branch fm/fm-absorb-fresh-replacement-wait (head b8e02ef, merge base 3d2a08b). A rebase was explicitly ruled out because it rewrites published history; the captain ordered a rebase for one other branch by name, not this one. The result is signed merge commit 67dab27 with parents b8e02ef and b182d0f.

Exactly one file conflicted: bin/fm-watch.sh. tests/fm-watch-triage.test.sh auto-merged; everything else auto-merged.

HOW THE CONFLICT WAS RESOLVED, and why it is a reconciliation rather than a choice between the two sides. Both sides had changed the same absorb-age gate in resurface_absorbed() for orthogonal reasons:

  • The branch (this PR's own fix) hoists that age gate OUT of the scope conditional so it always applies, so that a freshly declared REPLACEMENT wait is absorbed while it is still fresh instead of waking the supervisor twice for one event.
  • The trunk kept the gate inside the conditional but made its threshold an overridable sixth parameter, min_age (default PAUSE_RESURFACE_SECS, and 0 when a declared 'until' time has just passed, so such a wait re-surfaces at once).
    The resolution keeps BOTH: the gate is hoisted (branch intent) AND uses min_age (trunk intent). That is the only combination that preserves each side - hoisting the constant PAUSE_RESURFACE_SECS instead would have silently reverted the trunk's just-passed-'until' behaviour. All four quadrants (scope match/mismatch x min_age 0/default) were checked and behave as each side intended.
    The second hunk keeps the branch's load-bearing read order in handle_paused_stale (declaration and last read BEFORE mtime, so a concurrent status append can pair an old declaration only with a fresher age, never the reverse) and therefore drops the trunk's now-duplicate re-reads of the same two variables below mtime, while keeping all of the trunk's new logic: now, min_age, the away-posture early return, and the declared-'until' branch. The locals line is the union of both sides.

VERIFICATION ALREADY RUN LOCALLY on the merge result: syntax check clean, shellcheck -x clean, and the full tests/fm-watch-triage.test.sh suite passes (226 cases, exit 0). That suite is the direct proof of the resolution because both sides' decisive tests coexist in it after the merge: the branch's test_absorbed_replacement_wait_does_not_inherit_the_old_throttle and the trunk's test_paused_until_that_passed_is_rechecked_before_the_cadence. Choosing either side alone would fail one of them.

DELIVERY CONSTRAINTS. Do NOT open a new pull request: 3605 already exists and stays as it is; this validation publishes onto its existing branch. Do NOT merge - merging belongs to the captain and the upstream maintainer, and our access to this repository is pull-only. The pull request's published body ties its validation attestation to the old head and will not match the new head; that is true of every branch being cleared today and is being settled once for all of them, so it is out of scope here.

A document-stage commit that brings a file's description into line with behaviour this delivery already changed is in scope and should stand - that is the delivery keeping its own description true. Anything that changes BEHAVIOUR beyond the conflict resolution is out of scope.

What Changed

  • Apply the absorb-age gate to fresh replacement waits regardless of declaration scope, preserving the min_age override for expired until times.
  • Read the declaration and latest status before file mtime so concurrent appends cannot pair a new declaration with a stale age.
  • Extend regression coverage for fresh and aged replacements, including concurrent status appends, and update architecture documentation to reference the recheck timing owners.

Risk Assessment

⚠️ Medium: The merge preserves both parents’ intended behavior, but their interaction introduces a rare duplicate-notification race suitable for a separately authorized follow-up.

Testing

The unchanged triage suite and six live tmux scenarios passed after correcting test-driver setup errors. CLI and persisted-state evidence was captured, temporary files removed, and source files left unchanged.

  • Live validation: ✅ go - 6 of 6 scenarios driven live against the product
Scenario Result Live Evidence
Replace an external wait: absorb it while fresh, resurface on its own age, then suppress repeats. ✅ pass live Live watcher transcript: external-replacement
Replace a captain-held wait: preserve fresh absorption and independent recheck cadence. ✅ pass live Live watcher transcript: captain-held-replacement
Declare or replace an expired wait: recheck immediately, then suppress acknowledged repeats. ✅ pass live Live watcher transcript: expired-deadline-replacement
Declare a far-future deadline: remain quiet while fresh without extending the ordinary recheck cadence. ✅ pass live Live watcher transcript: future-deadline-cadence-cap
Record away posture: suppress captain-held reminders until the posture is archived. ✅ pass live Live watcher transcript: captain-held-away-boundary
Wait across a deadline: the running watcher rechecks when real time reaches it and suppresses repeats. ✅ pass live Live deadline crossing
Evidence: Production behavior excerpts

Source: Production behavior excerpts

Production watcher behavior at 67dab27bdd07cfd3d7b077bb043936872bd1111c

live-watch.log
WATCH OUTPUT signal: ~/.no-mistakes/worktrees/acf4a767348a/01M2FR2HYYDP8NP8HDDX4D1MXS/.phase-test/external-replacement-verified/state/wait.status
TEST DATA: persisted status mtime aged 500s; production clock unchanged
WATCH OUTPUT stale: phase:wait (paused 501s, awaiting external - declared pause, rechecked on a long cadence not a wedge; confirm the wait still holds)
WATCH OUTPUT signal: ~/.no-mistakes/worktrees/acf4a767348a/01M2FR2HYYDP8NP8HDDX4D1MXS/.phase-test/external-replacement-verified/state/wait.status
[2026-09-14T12:56:55+0200] absorbed stale (paused, awaiting external, age 4s): phase:wait
[2026-09-14T12:56:58+0200] absorbed stale (paused, awaiting external, age 7s): phase:wait
TEST DATA: persisted status mtime aged 500s; production clock unchanged
WATCH OUTPUT stale: phase:wait (paused 501s, awaiting external - declared pause, rechecked on a long cadence not a wedge; confirm the wait still holds)
[2026-09-14T12:56:55+0200] absorbed stale (paused, awaiting external, age 4s): phase:wait
[2026-09-14T12:56:58+0200] absorbed stale (paused, awaiting external, age 7s): phase:wait
[2026-09-14T12:57:02+0200] absorbed stale (paused, awaiting external, age 503s): phase:wait
[2026-09-14T12:57:03+0200] absorbed stale (paused, awaiting external, age 504s): phase:wait
SCENARIO PASS external-replacement
WATCH OUTPUT signal: ~/.no-mistakes/worktrees/acf4a767348a/01M2FR2HYYDP8NP8HDDX4D1MXS/.phase-test/captain-held-replacement-verified/state/wait.status
TEST DATA: persisted status mtime aged 500s; production clock unchanged
WATCH OUTPUT stale: phase:wait (captain-held 501s, awaiting the captain - verified hold transfer, rechecked on a long cadence not a wedge; answer the held decision or release the hold)
WATCH OUTPUT signal: ~/.no-mistakes/worktrees/acf4a767348a/01M2FR2HYYDP8NP8HDDX4D1MXS/.phase-test/captain-held-replacement-verified/state/wait.status
[2026-09-14T12:57:12+0200] absorbed stale (captain-held, awaiting the captain, age 4s): phase:wait
[2026-09-14T12:57:14+0200] absorbed stale (captain-held, awaiting the captain, age 6s): phase:wait
TEST DATA: persisted status mtime aged 500s; production clock unchanged
WATCH OUTPUT stale: phase:wait (captain-held 501s, awaiting the captain - verified hold transfer, rechecked on a long cadence not a wedge; answer the held decision or release the hold)
[2026-09-14T12:57:12+0200] absorbed stale (captain-held, awaiting the captain, age 4s): phase:wait
[2026-09-14T12:57:14+0200] absorbed stale (captain-held, awaiting the captain, age 6s): phase:wait
[2026-09-14T12:57:17+0200] absorbed stale (captain-held, awaiting the captain, age 503s): phase:wait
[2026-09-14T12:57:19+0200] absorbed stale (captain-held, awaiting the captain, age 505s): phase:wait
SCENARIO PASS captain-held-replacement
WATCH OUTPUT signal: ~/.no-mistakes/worktrees/acf4a767348a/01M2FR2HYYDP8NP8HDDX4D1MXS/.phase-test/expired-deadline-replacement-verified/state/wait.status
WATCH OUTPUT stale: phase:wait (paused 4s, awaiting external - the declared clearing time has passed, rechecked on a long cadence not a wedge; confirm the wait cleared)
[2026-09-14T12:57:28+0200] absorbed stale (paused, declared time reached, age 7s): phase:wait
[2026-09-14T12:57:30+0200] absorbed stale (paused, declared time reached, age 9s): phase:wait
WATCH OUTPUT signal: ~/.no-mistakes/worktrees/acf4a767348a/01M2FR2HYYDP8NP8HDDX4D1MXS/.phase-test/expired-deadline-replacement-verified/state/wait.status
WATCH OUTPUT stale: phase:wait (paused 3s, awaiting external - the declared clearing time has passed, rechecked on a long cadence not a wedge; confirm the wait cleared)
[2026-09-14T12:57:28+0200] absorbed stale (paused, declared time reached, age 7s): phase:wait
[2026-09-14T12:57:30+0200] absorbed stale (paused, declared time reached, age 9s): phase:wait
[2026-09-14T12:57:38+0200] absorbed stale (paused, declared time reached, age 7s): phase:wait
[2026-09-14T12:57:39+0200] absorbed stale (paused, declared time reached, age 8s): phase:wait
SCENARIO PASS expired-deadline-replacement
WATCH OUTPUT signal: ~/.no-mistakes/worktrees/acf4a767348a/01M2FR2HYYDP8NP8HDDX4D1MXS/.phase-test/future-deadline-cadence-cap-verified/state/wait.status
[2026-09-14T12:57:45+0200] absorbed stale (paused until 31535996s from now, declared time not reached): phase:wait
[2026-09-14T12:57:48+0200] absorbed stale (paused until 31535993s from now, declared time not reached): phase:wait
TEST DATA: persisted status mtime aged 500s; production clock unchanged
WATCH OUTPUT stale: phase:wait (paused 501s, awaiting external - the declared time is beyond the recheck cadence; confirm the wait still holds)
SCENARIO PASS future-deadline-cadence-cap
WATCH OUTPUT signal: ~/.no-mistakes/worktrees/acf4a767348a/01M2FR2HYYDP8NP8HDDX4D1MXS/.phase-test/captain-held-away-boundary-verified/state/wait.status
TEST DATA: persisted status mtime aged 500s; production clock unchanged
[2026-09-14T12:57:55+0200] absorbed stale (captain-held, never rechecked while the away-posture record exists): phase:wait
[2026-09-14T12:57:58+0200] absorbed stale (captain-held, never rechecked while the away-posture record exists): phase:wait
WATCH OUTPUT stale: phase:wait (captain-held 506s, awaiting the captain - verified hold transfer, rechecked on a long cadence not a wedge; answer the held decision or release the hold)
SCENARIO PASS captain-held-away-boundary

live-deadline-transition.log
WATCH OUTPUT signal: ~/.no-mistakes/worktrees/acf4a767348a/01M2FR2HYYDP8NP8HDDX4D1MXS/.phase-test/deadline-crosses-while-watching-verified/state/wait.status
[2026-09-14T12:58:46+0200] absorbed stale (paused until 17s from now, declared time not reached): phase:wait
[2026-09-14T12:58:49+0200] absorbed stale (paused until 14s from now, declared time not reached): phase:wait
DEADLINE TRANSITION: real wall clock crosses 2026-09-14T10:59:03Z with the same watcher running
WATCH OUTPUT stale: phase:wait (paused 20s, awaiting external - the declared clearing time has passed, rechecked on a long cadence not a wedge; confirm the wait cleared)
[2026-09-14T12:58:57+0200] absorbed stale (paused until 6s from now, declared time not reached): phase:wait
[2026-09-14T12:58:58+0200] absorbed stale (paused until 5s from now, declared time not reached): phase:wait
[2026-09-14T12:59:00+0200] absorbed stale (paused until 3s from now, declared time not reached): phase:wait
[2026-09-14T12:59:01+0200] absorbed stale (paused until 2s from now, declared time not reached): phase:wait
[2026-09-14T12:59:05+0200] absorbed stale (paused, declared time reached, age 22s): phase:wait
[2026-09-14T12:59:06+0200] absorbed stale (paused, declared time reached, age 23s): phase:wait
SCENARIO PASS deadline-crosses-while-watching
Evidence: Live watcher transcript

Source: Live watcher transcript

$ ~/.no-mistakes/worktrees/acf4a767348a/01M2FR2HYYDP8NP8HDDX4D1MXS/bin/fm-crew-state.sh wait
state: unknown · source: none · no current-state source available

$ tmux send-keys -t phase:wait -l printf "%s\n" 'paused: waiting for validation one' >> ~/.no-mistakes/worktrees/acf4a767348a/01M2FR2HYYDP8NP8HDDX4D1MXS/.phase-test/external-replacement-verified/state/wait.status

$ tmux send-keys -t phase:wait Enter

STATUS paused: waiting for validation one
START production bin/fm-watch.sh external-replacement round=1
WATCH OUTPUT signal: ~/.no-mistakes/worktrees/acf4a767348a/01M2FR2HYYDP8NP8HDDX4D1MXS/.phase-test/external-replacement-verified/state/wait.status
PERSISTED WAKE QUEUE
1789383408	1	signal	wait.status	signal: ~/.no-mistakes/worktrees/acf4a767348a/01M2FR2HYYDP8NP8HDDX4D1MXS/.phase-test/external-replacement-verified/state/wait.status
1789383408	2	signal	wait.status	signal: ~/.no-mistakes/worktrees/acf4a767348a/01M2FR2HYYDP8NP8HDDX4D1MXS/.phase-test/external-replacement-verified/state/wait.status

$ ~/.no-mistakes/worktrees/acf4a767348a/01M2FR2HYYDP8NP8HDDX4D1MXS/bin/fm-wake-drain.sh
1789383408	2	signal	wait.status	signal: ~/.no-mistakes/worktrees/acf4a767348a/01M2FR2HYYDP8NP8HDDX4D1MXS/.phase-test/external-replacement-verified/state/wait.status
wake annotation: latest wake-EVENT observed at drain, not current state: wait.status: paused: waiting for validation one
WAKE_ACK_REQUIRED: after handling completes run bin/fm-wake-drain.sh --ack-through 2 --recovery-generation 10028.1789383408.87ShmX
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
●  WATCHER DOWN - SUPERVISION IS OFF
●  1 task(s) in flight, but no live watcher process holds this home lock (last beat: 3s ago).
●  Trust the emitted supervision protocol for this harness; do not use shell & for watcher repair.
●  This is a supervision warning only; the guarded operation WILL still run.
●  After draining queued wakes, repair missing watcher supervision with a foreground checkpoint: bin/fm-watch-checkpoint.sh --seconds 180.
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
WARNING: queued wakes pending - drain them with bin/fm-wake-drain.sh before anything else.

$ ~/.no-mistakes/worktrees/acf4a767348a/01M2FR2HYYDP8NP8HDDX4D1MXS/bin/fm-wake-drain.sh --ack-through 2 --recovery-generation 10028.1789383408.87ShmX

TEST DATA: persisted status mtime aged 500s; production clock unchanged
$ bash -c . "$1"; fm_wake_status_mark_current "$2" "$3" _ ~/.no-mistakes/worktrees/acf4a767348a/01M2FR2HYYDP8NP8HDDX4D1MXS/bin/fm-wake-lib.sh ~/.no-mistakes/worktrees/acf4a767348a/01M2FR2HYYDP8NP8HDDX4D1MXS/.phase-test/external-replacement-verified/state ~/.no-mistakes/worktrees/acf4a767348a/01M2FR2HYYDP8NP8HDDX4D1MXS/.phase-test/external-replacement-verified/state/wait.status

START production bin/fm-watch.sh external-replacement round=2
WATCH OUTPUT stale: phase:wait (paused 501s, awaiting external - declared pause, rechecked on a long cadence not a wedge; confirm the wait still holds)
PERSISTED WAKE QUEUE
1789383410	3	stale	phase:wait	stale: phase:wait (paused 501s, awaiting external - declared pause, rechecked on a long cadence not a wedge; confirm the wait still holds)

$ ~/.no-mistakes/worktrees/acf4a767348a/01M2FR2HYYDP8NP8HDDX4D1MXS/bin/fm-wake-drain.sh
1789383410	3	stale	phase:wait	stale: phase:wait (paused 501s, awaiting external - declared pause, rechecked on a long cadence not a wedge; confirm the wait still holds)
WAKE_ACK_REQUIRED: after handling completes run bin/fm-wake-drain.sh --ack-through 3 --recovery-generation 15766.1789383410.HNNi7y
WARNING: watcher still down (same stale episode; last beat: 2s ago, grace 300s) - full banner already printed this episode.
WARNING: queued wakes pending - drain them with bin/fm-wake-drain.sh before anything else.

$ ~/.no-mistakes/worktrees/acf4a767348a/01M2FR2HYYDP8NP8HDDX4D1MXS/bin/fm-wake-drain.sh --ack-through 3 --recovery-generation 15766.1789383410.HNNi7y

$ tmux send-keys -t phase:wait -l printf "%s\n" 'paused: waiting for validation two' >> ~/.no-mistakes/worktrees/acf4a767348a/01M2FR2HYYDP8NP8HDDX4D1MXS/.phase-test/external-replacement-verified/state/wait.status

$ tmux send-keys -t phase:wait Enter

STATUS paused: waiting for validation one
paused: waiting for validation two
START production bin/fm-watch.sh external-replacement round=3
WATCH OUTPUT signal: ~/.no-mistakes/worktrees/acf4a767348a/01M2FR2HYYDP8NP8HDDX4D1MXS/.phase-test/external-replacement-verified/state/wait.status
PERSISTED WAKE QUEUE
1789383413	4	signal	wait.status	signal: ~/.no-mistakes/worktrees/acf4a767348a/01M2FR2HYYDP8NP8HDDX4D1MXS/.phase-test/external-replacement-verified/state/wait.status
1789383413	5	signal	wait.status	signal: ~/.no-mistakes/worktrees/acf4a767348a/01M2FR2HYYDP8NP8HDDX4D1MXS/.phase-test/external-replacement-verified/state/wait.status

$ ~/.no-mistakes/worktrees/acf4a767348a/01M2FR2HYYDP8NP8HDDX4D1MXS/bin/fm-wake-drain.sh
1789383413	5	signal	wait.status	signal: ~/.no-mistakes/worktrees/acf4a767348a/01M2FR2HYYDP8NP8HDDX4D1MXS/.phase-test/external-replacement-verified/state/wait.status
wake annotation: unread wake-EVENT since last drain, not current state: wait.status: paused: waiting for validation one
wake annotation: latest wake-EVENT observed at drain, not current state: wait.status: paused: waiting for validation two
WAKE_ACK_REQUIRED: after handling completes run bin/fm-wake-drain.sh --ack-through 5 --recovery-generation 22286.1789383413.a8jo33
WARNING: watcher still down (same stale episode; last beat: 3s ago, grace 300s) - full banner already printed this episode.
WARNING: queued wakes pending - drain them with bin/fm-wake-drain.sh before anything else.

$ ~/.no-mistakes/worktrees/acf4a767348a/01M2FR2HYYDP8NP8HDDX4D1MXS/bin/fm-wake-drain.sh --ack-through 5 --recovery-generation 22286.1789383413.a8jo33

START production bin/fm-watch.sh external-replacement round=4
PRODUCTION TRIAGE (two completed matching observations)
[2026-09-14T12:56:55+0200] absorbed stale (paused, awaiting external, age 4s): phase:wait
[2026-09-14T12:56:58+0200] absorbed stale (paused, awaiting external, age 7s): phase:wait
THROTTLE declared:r1:3335007374726f6e673a31363737373233343a36353039333735303a313738393338323930392e35373934373339373200526567756c61722046696c653a313030363434002d007265616461626c65007265616461626c65
TEST DATA: persisted status mtime aged 500s; production clock unchanged
$ bash -c . "$1"; fm_wake_status_mark_current "$2" "$3" _ ~/.no-mistakes/worktrees/acf4a767348a/01M2FR2HYYDP8NP8HDDX4D1MXS/bin/fm-wake-lib.sh ~/.no-mistakes/worktrees/acf4a767348a/01M2FR2HYYDP8NP8HDDX4D1MXS/.phase-test/external-replacement-verified/state ~/.no-mistakes/worktrees/acf4a767348a/01M2FR2HYYDP8NP8HDDX4D1MXS/.phase-test/external-replacement-verified/state/wait.status

START production bin/fm-watch.sh external-replacement round=5
WATCH OUTPUT stale: phase:wait (paused 501s, awaiting external - declared pause, rechecked on a long cadence not a wedge; confirm the wait still holds)
PERSISTED WAKE QUEUE
1789383420	6	stale	phase:wait	stale: phase:wait (paused 501s, awaiting external - declared pause, rechecked on a long cadence not a wedge; confirm the wait still holds)

$ ~/.no-mistakes/worktrees/acf4a767348a/01M2FR2HYYDP8NP8HDDX4D1MXS/bin/fm-wake-drain.sh
1789383420	6	stale	phase:wait	stale: phase:wait (paused 501s, awaiting external - declared pause, rechecked on a long cadence not a wedge; confirm the wait still holds)
WAKE_ACK_REQUIRED: after handling completes run bin/fm-wake-drain.sh --ack-through 6 --recovery-generation 34248.17893834

... [23571 bytes truncated] ...

.no-mistakes/worktrees/acf4a767348a/01M2FR2HYYDP8NP8HDDX4D1MXS/bin/fm-wake-drain.sh
1789383470	3	stale	phase:wait	stale: phase:wait (paused 501s, awaiting external - the declared time is beyond the recheck cadence; confirm the wait still holds)
WAKE_ACK_REQUIRED: after handling completes run bin/fm-wake-drain.sh --ack-through 3 --recovery-generation 16940.1789383469.5SnO9E
WARNING: watcher still down (same stale episode; last beat: 1s ago, grace 300s) - full banner already printed this episode.
WARNING: queued wakes pending - drain them with bin/fm-wake-drain.sh before anything else.

$ ~/.no-mistakes/worktrees/acf4a767348a/01M2FR2HYYDP8NP8HDDX4D1MXS/bin/fm-wake-drain.sh --ack-through 3 --recovery-generation 16940.1789383469.5SnO9E

SCENARIO PASS future-deadline-cadence-cap
$ ~/.no-mistakes/worktrees/acf4a767348a/01M2FR2HYYDP8NP8HDDX4D1MXS/bin/fm-crew-state.sh wait
state: unknown · source: none · no current-state source available

$ tmux send-keys -t phase:wait -l printf "%s\n" 'captain-held [key=hold]: awaiting user choice' >> ~/.no-mistakes/worktrees/acf4a767348a/01M2FR2HYYDP8NP8HDDX4D1MXS/.phase-test/captain-held-away-boundary-verified/state/wait.status

$ tmux send-keys -t phase:wait Enter

STATUS captain-held [key=hold]: awaiting user choice
START production bin/fm-watch.sh captain-held-away-boundary round=1
WATCH OUTPUT signal: ~/.no-mistakes/worktrees/acf4a767348a/01M2FR2HYYDP8NP8HDDX4D1MXS/.phase-test/captain-held-away-boundary-verified/state/wait.status
PERSISTED WAKE QUEUE
1789383473	1	signal	wait.status	needs-decision: ~/.no-mistakes/worktrees/acf4a767348a/01M2FR2HYYDP8NP8HDDX4D1MXS/.phase-test/captain-held-away-boundary-verified/state/wait.status
1789383473	2	signal	wait.status	needs-decision: ~/.no-mistakes/worktrees/acf4a767348a/01M2FR2HYYDP8NP8HDDX4D1MXS/.phase-test/captain-held-away-boundary-verified/state/wait.status

$ ~/.no-mistakes/worktrees/acf4a767348a/01M2FR2HYYDP8NP8HDDX4D1MXS/bin/fm-wake-drain.sh
1789383473	2	signal	wait.status	needs-decision: ~/.no-mistakes/worktrees/acf4a767348a/01M2FR2HYYDP8NP8HDDX4D1MXS/.phase-test/captain-held-away-boundary-verified/state/wait.status
wake annotation: latest wake-EVENT observed at drain, not current state: wait.status: captain-held [key=hold]: awaiting user choice
WAKE_ACK_REQUIRED: after handling completes run bin/fm-wake-drain.sh --ack-through 2 --recovery-generation 24582.1789383473.avtMBG
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
●  WATCHER DOWN - SUPERVISION IS OFF
●  1 task(s) in flight, but no live watcher process holds this home lock (last beat: 3s ago).
●  Trust the emitted supervision protocol for this harness; do not use shell & for watcher repair.
●  This is a supervision warning only; the guarded operation WILL still run.
●  After draining queued wakes, repair missing watcher supervision with a foreground checkpoint: bin/fm-watch-checkpoint.sh --seconds 180.
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
WARNING: queued wakes pending - drain them with bin/fm-wake-drain.sh before anything else.

$ ~/.no-mistakes/worktrees/acf4a767348a/01M2FR2HYYDP8NP8HDDX4D1MXS/bin/fm-wake-drain.sh --ack-through 2 --recovery-generation 24582.1789383473.avtMBG

$ ~/.no-mistakes/worktrees/acf4a767348a/01M2FR2HYYDP8NP8HDDX4D1MXS/bin/fm-afk-contract.sh propose
Away posture read-back (proposed, not yet confirmed):
  entered: 2026-09-14T10:57:54Z
  expected return: not given
  spend cap: 4 concurrent workers
  merge when green (task ids): (none)
  reach: hold-for-return only. No phone channel is configured; anything that needs you waits for your return.
  your words: (none)
  accepted clauses:
    (none)
  refused clauses:
    (none)
  everything else waits for your return: no red merge without its named check, no discard without a named object and condition, never credentials, legal, financial, or attended prompts, nothing by analogy, and every clause expires at return.
  hard rule: forbidden, destructive, irreversible, and security-sensitive actions are never pre-authorizable regardless of clause text; no recorded clause is authority by itself.
  recorded clauses are held for the return brief and are not executed by this release.
Say go to confirm; restate any refused clause first if you want it recorded.

$ ~/.no-mistakes/worktrees/acf4a767348a/01M2FR2HYYDP8NP8HDDX4D1MXS/bin/fm-afk-contract.sh confirm
Away posture confirmed at 2026-09-14T10:57:54Z: hold-for-return only. No phone channel is configured; anything that needs you waits for your return. No mandate clauses recorded. Forbidden, destructive, irreversible, and security-sensitive actions are never pre-authorizable regardless of clause text, and no recorded clause is authority by itself. Expected return: not given. Spend cap: 4 concurrent workers.

TEST DATA: persisted status mtime aged 500s; production clock unchanged
$ bash -c . "$1"; fm_wake_status_mark_current "$2" "$3" _ ~/.no-mistakes/worktrees/acf4a767348a/01M2FR2HYYDP8NP8HDDX4D1MXS/bin/fm-wake-lib.sh ~/.no-mistakes/worktrees/acf4a767348a/01M2FR2HYYDP8NP8HDDX4D1MXS/.phase-test/captain-held-away-boundary-verified/state ~/.no-mistakes/worktrees/acf4a767348a/01M2FR2HYYDP8NP8HDDX4D1MXS/.phase-test/captain-held-away-boundary-verified/state/wait.status

START production bin/fm-watch.sh captain-held-away-boundary round=2
PRODUCTION TRIAGE (two completed matching observations)
[2026-09-14T12:57:55+0200] absorbed stale (captain-held, never rechecked while the away-posture record exists): phase:wait
[2026-09-14T12:57:58+0200] absorbed stale (captain-held, never rechecked while the away-posture record exists): phase:wait
THROTTLE 
$ ~/.no-mistakes/worktrees/acf4a767348a/01M2FR2HYYDP8NP8HDDX4D1MXS/bin/fm-afk-contract.sh archive
~/.no-mistakes/worktrees/acf4a767348a/01M2FR2HYYDP8NP8HDDX4D1MXS/.phase-test/captain-held-away-boundary-verified/state/afk-contracts/1789383474.afk-contract

START production bin/fm-watch.sh captain-held-away-boundary round=3
WATCH OUTPUT stale: phase:wait (captain-held 506s, awaiting the captain - verified hold transfer, rechecked on a long cadence not a wedge; answer the held decision or release the hold)
PERSISTED WAKE QUEUE
1789383480	3	stale	phase:wait	stale: phase:wait (captain-held 506s, awaiting the captain - verified hold transfer, rechecked on a long cadence not a wedge; answer the held decision or release the hold)

$ ~/.no-mistakes/worktrees/acf4a767348a/01M2FR2HYYDP8NP8HDDX4D1MXS/bin/fm-wake-drain.sh
1789383480	3	stale	phase:wait	stale: phase:wait (captain-held 506s, awaiting the captain - verified hold transfer, rechecked on a long cadence not a wedge; answer the held decision or release the hold)
WAKE_ACK_REQUIRED: after handling completes run bin/fm-wake-drain.sh --ack-through 3 --recovery-generation 32979.1789383479.fH9d56
WARNING: watcher still down (same stale episode; last beat: 1s ago, grace 300s) - full banner already printed this episode.
WARNING: queued wakes pending - drain them with bin/fm-wake-drain.sh before anything else.

$ ~/.no-mistakes/worktrees/acf4a767348a/01M2FR2HYYDP8NP8HDDX4D1MXS/bin/fm-wake-drain.sh --ack-through 3 --recovery-generation 32979.1789383479.fH9d56

SCENARIO PASS captain-held-away-boundary
[
  {
    "name": "external-replacement",
    "result": "pass",
    "live": true
  },
  {
    "name": "captain-held-replacement",
    "result": "pass",
    "live": true
  },
  {
    "name": "expired-deadline-replacement",
    "result": "pass",
    "live": true
  },
  {
    "name": "future-deadline-cadence-cap",
    "result": "pass",
    "live": true
  },
  {
    "name": "captain-held-away-boundary",
    "result": "pass",
    "live": true
  }
]
Evidence: Live deadline crossing

Source: Live deadline crossing

$ ~/.no-mistakes/worktrees/acf4a767348a/01M2FR2HYYDP8NP8HDDX4D1MXS/bin/fm-crew-state.sh wait
state: unknown · source: none · no current-state source available

$ tmux send-keys -t phase:wait -l printf "%s\n" 'paused: waiting for reset, until 2026-09-14T10:59:03Z' >> ~/.no-mistakes/worktrees/acf4a767348a/01M2FR2HYYDP8NP8HDDX4D1MXS/.phase-test/deadline-crosses-while-watching-verified/state/wait.status

$ tmux send-keys -t phase:wait Enter

STATUS paused: waiting for reset, until 2026-09-14T10:59:03Z
START production bin/fm-watch.sh deadline-crosses-while-watching round=1
WATCH OUTPUT signal: ~/.no-mistakes/worktrees/acf4a767348a/01M2FR2HYYDP8NP8HDDX4D1MXS/.phase-test/deadline-crosses-while-watching-verified/state/wait.status
PERSISTED WAKE QUEUE
1789383524	1	signal	wait.status	signal: ~/.no-mistakes/worktrees/acf4a767348a/01M2FR2HYYDP8NP8HDDX4D1MXS/.phase-test/deadline-crosses-while-watching-verified/state/wait.status
1789383524	2	signal	wait.status	signal: ~/.no-mistakes/worktrees/acf4a767348a/01M2FR2HYYDP8NP8HDDX4D1MXS/.phase-test/deadline-crosses-while-watching-verified/state/wait.status

$ ~/.no-mistakes/worktrees/acf4a767348a/01M2FR2HYYDP8NP8HDDX4D1MXS/bin/fm-wake-drain.sh
1789383524	2	signal	wait.status	signal: ~/.no-mistakes/worktrees/acf4a767348a/01M2FR2HYYDP8NP8HDDX4D1MXS/.phase-test/deadline-crosses-while-watching-verified/state/wait.status
wake annotation: latest wake-EVENT observed at drain, not current state: wait.status: paused: waiting for reset, until 2026-09-14T10:59:03Z
WAKE_ACK_REQUIRED: after handling completes run bin/fm-wake-drain.sh --ack-through 2 --recovery-generation 82102.1789383524.qaU9zZ
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
●  WATCHER DOWN - SUPERVISION IS OFF
●  1 task(s) in flight, but no live watcher process holds this home lock (last beat: 2s ago).
●  Trust the emitted supervision protocol for this harness; do not use shell & for watcher repair.
●  This is a supervision warning only; the guarded operation WILL still run.
●  After draining queued wakes, repair missing watcher supervision with a foreground checkpoint: bin/fm-watch-checkpoint.sh --seconds 180.
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
WARNING: queued wakes pending - drain them with bin/fm-wake-drain.sh before anything else.

$ ~/.no-mistakes/worktrees/acf4a767348a/01M2FR2HYYDP8NP8HDDX4D1MXS/bin/fm-wake-drain.sh --ack-through 2 --recovery-generation 82102.1789383524.qaU9zZ

START production bin/fm-watch.sh deadline-crosses-while-watching round=2
PRODUCTION TRIAGE (two completed matching observations)
[2026-09-14T12:58:46+0200] absorbed stale (paused until 17s from now, declared time not reached): phase:wait
[2026-09-14T12:58:49+0200] absorbed stale (paused until 14s from now, declared time not reached): phase:wait
THROTTLE 
DEADLINE TRANSITION: real wall clock crosses 2026-09-14T10:59:03Z with the same watcher running
WATCH OUTPUT stale: phase:wait (paused 20s, awaiting external - the declared clearing time has passed, rechecked on a long cadence not a wedge; confirm the wait cleared)
PERSISTED WAKE QUEUE
1789383543	3	stale	phase:wait	stale: phase:wait (paused 20s, awaiting external - the declared clearing time has passed, rechecked on a long cadence not a wedge; confirm the wait cleared)

$ ~/.no-mistakes/worktrees/acf4a767348a/01M2FR2HYYDP8NP8HDDX4D1MXS/bin/fm-wake-drain.sh
1789383543	3	stale	phase:wait	stale: phase:wait (paused 20s, awaiting external - the declared clearing time has passed, rechecked on a long cadence not a wedge; confirm the wait cleared)
WAKE_ACK_REQUIRED: after handling completes run bin/fm-wake-drain.sh --ack-through 3 --recovery-generation 4487.1789383543.dLQLHY
WARNING: watcher still down (same stale episode; last beat: 1s ago, grace 300s) - full banner already printed this episode.
WARNING: queued wakes pending - drain them with bin/fm-wake-drain.sh before anything else.

$ ~/.no-mistakes/worktrees/acf4a767348a/01M2FR2HYYDP8NP8HDDX4D1MXS/bin/fm-wake-drain.sh --ack-through 3 --recovery-generation 4487.1789383543.dLQLHY

START production bin/fm-watch.sh deadline-crosses-while-watching round=3
PRODUCTION TRIAGE (two completed matching observations)
[2026-09-14T12:58:57+0200] absorbed stale (paused until 6s from now, declared time not reached): phase:wait
[2026-09-14T12:58:58+0200] absorbed stale (paused until 5s from now, declared time not reached): phase:wait
[2026-09-14T12:59:00+0200] absorbed stale (paused until 3s from now, declared time not reached): phase:wait
[2026-09-14T12:59:01+0200] absorbed stale (paused until 2s from now, declared time not reached): phase:wait
[2026-09-14T12:59:05+0200] absorbed stale (paused, declared time reached, age 22s): phase:wait
[2026-09-14T12:59:06+0200] absorbed stale (paused, declared time reached, age 23s): phase:wait
THROTTLE declared:r1:3534007374726f6e673a31363737373233343a36353130303838353a313738393338333532332e30303332313334313500526567756c61722046696c653a313030363434002d007265616461626c65007265616461626c65:due
SCENARIO PASS deadline-crosses-while-watching
[
  {
    "name": "external-replacement",
    "result": "pass",
    "live": true
  },
  {
    "name": "captain-held-replacement",
    "result": "pass",
    "live": true
  },
  {
    "name": "expired-deadline-replacement",
    "result": "pass",
    "live": true
  },
  {
    "name": "future-deadline-cadence-cap",
    "result": "pass",
    "live": true
  },
  {
    "name": "captain-held-away-boundary",
    "result": "pass",
    "live": true
  },
  {
    "name": "deadline-crosses-while-watching",
    "result": "pass",
    "live": true
  }
]

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

⏭️ **Rebase** - skipped
  • ⚠️ bin/fm-watch.sh - merge conflict rebasing onto origin/main
⚠️ **Review** - 1 warning
  • ⚠️ bin/fm-watch.sh:1008 - A concurrent deadline replacement can resurface twice. After reading ordinary wait A’s signature here, a replacement B can arrive before last_status_line reads it, with B’s until already due. The deadline branch combines B’s line with A’s signature, sets min_age=0, and records A:due. After acknowledgement and rearm, unchanged B produces B:due, bypasses the fresh throttle, and emits another stale notification. Taking mtime last does not protect this zero-age path. A follow-up should revalidate that the signature and deadline line describe the same declaration in handle_paused_stale, while retaining mtime-last ordering. The remedy needs authorization because it exceeds the explicit conflict-only scope; keep it out of this delivery.
✅ **Test** - passed

✅ No issues found.

  • Live validation: ✅ go - 6 of 6 scenarios driven live against the product
Scenario Result Live Evidence
Replace an external wait: absorb it while fresh, resurface on its own age, then suppress repeats. ✅ pass live Live watcher transcript: external-replacement
Replace a captain-held wait: preserve fresh absorption and independent recheck cadence. ✅ pass live Live watcher transcript: captain-held-replacement
Declare or replace an expired wait: recheck immediately, then suppress acknowledged repeats. ✅ pass live Live watcher transcript: expired-deadline-replacement
Declare a far-future deadline: remain quiet while fresh without extending the ordinary recheck cadence. ✅ pass live Live watcher transcript: future-deadline-cadence-cap
Record away posture: suppress captain-held reminders until the posture is archived. ✅ pass live Live watcher transcript: captain-held-away-boundary
Wait across a deadline: the running watcher rechecks when real time reaches it and suppresses repeats. ✅ pass live Live deadline crossing
  • git diff b182d0f908b78d08c7ccb8dce3775bdca8c5d657..HEAD -- bin/fm-watch.sh tests/fm-watch-triage.test.sh and merge-parent inspection.
  • TMPDIR="$PWD/.phase-test/tmp" FM_HOME="$PWD/.phase-test/home" bin/fm-test-run.sh tests/fm-watch-triage.test.sh — unchanged suite exited 0.
  • python3 ~/.no-mistakes/evidence/01M2FR2HYYDP8NP8HDDX4D1MXS/live-watch.py — real isolated tmux, production watcher, wake drain, acknowledgements, and away-record commands; corrected driver setup and reran successfully.
  • FM_LIVE_SELECT=deadline-crosses-while-watching python3 ~/.no-mistakes/evidence/01M2FR2HYYDP8NP8HDDX4D1MXS/live-watch.py — natural deadline crossing.
  • Captured production CLI output and persisted state; stopped isolated tmux, removed .phase-test, and verified clean Git status.
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

@greptile-apps

greptile-apps Bot commented Sep 3, 2026 •

Copy link
Copy Markdown

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Reviews (3): Last reviewed commit: "test(watch): pin the paused-wait read or..." | Re-trigger Greptile

Comment thread tests/fm-watch-triage.test.sh Outdated
@greptile-apps

greptile-apps Bot commented Sep 3, 2026

Copy link
Copy Markdown

Want your agent to iterate on Greptile's feedback? Try greploops.

@mremond
mremond marked this pull request as draft September 3, 2026 09:52
@kunchenguid

Copy link
Copy Markdown
Owner

Speaking as Kun's firstmate: this is waiting on the author (draft).

HEAD 6484b16d6ec8ea35b7ee616c71ee91bbf00de9f1 vs main 3d2a08b2097dd24f9ce03fdafe6501e39b79dbd0. DRAFT — cannot merge.

Contract-class: restore. Moves the absorb age gate unconditional while keeping only the re-surface throttle declaration-scoped, so a replacement declared wait is absorbed while fresh (status-signal already woke once) then re-surfaces on its own window — restoring handle_paused_stale's fresh-absorption contract after the scoped-throttle change over-scoped the age gate. Scope: bin/fm-watch.sh + tests/fm-watch-triage.test.sh only.

VISION:

  1. One captain, one interface — aligns (removes duplicate stale nag on replacement waits).
  2. Authority explicit — aligns (no new autonomy; noise-avoidance only).
  3. Scripts own mechanics — aligns (deterministic age/throttle gates).
  4. Restart non-event — aligns (durable pause markers unchanged).
  5. Delegation with a spine — aligns (supervision fidelity; no silence of live waits).
  6. Fleet outlives vendor — aligns (harness-agnostic watcher).
  7. Scope — aligns (command-layer watch path). Closing: peace-of-mind via less duplicate escalation aligns; no consent assumption.

Security: clean. No .github/**. No credentials/RCE/installs. Greptile P2 (concurrent-append test coverage) non-blocking residual for author.

Attestation: MATCH (body binds tip; review/test/document completed). Tip NM run 33740001744 earlier failed on a stale attestation SHA before the body was updated to tip — re-check after ready. CI 33740001692 cancelled/mixed (serial 1 cancelled). No workflow approve this pass (draft; tip not action_required).

Land-eligible: NO (draft). Firstmate-flag: no.

Please mark ready for review when you want merge consideration, keep attestation bound to tip, and address or accept the Greptile P2 as you prefer. Do not expect auto-merge while draft.

The scoped re-surface throttle let a replacement declared wait skip the absorb
age gate as well as the throttle gate, so a worker that swapped one declared
wait for another was alarmed immediately instead of being absorbed.

That crossed two contracts. handle_paused_stale absorbs a freshly declared wait
and re-surfaces it once per window, and a replacement is still a freshly
declared wait. The status append that declared it has already woken the
supervisor through the status-signal path, so firing here as well reports one
event twice, on the path whose whole purpose is not to nag. It was also
asymmetric: a first declaration was absorbed while fresh, a replacement was not,
decided only by whether a throttle marker happened to exist.

Apply the absorb age gate unconditionally and keep only the throttle gate scoped
to the declaration. A replacement is absorbed while fresh, then re-surfaces once
its own age crosses the window instead of serving out the remainder of the
previous wait's window. The concern the scope was added for, that a new wait must
not inherit the old wait's active throttle, is preserved exactly.

This can only remove a wake, never add silence: nothing here suppresses a wait
that would otherwise surface.

The test pins both halves and fails against either error. Skipping the absorb age
gate on a scope mismatch fails as "alarmed instead of absorbed"; removing scope
awareness entirely fails as "inherited the old throttle". Both were confirmed by
breaking the code deliberately and restoring it. Two details in it are deliberate
rather than incidental: it waits for enough completed poll cycles that the
absorbed path is actually reached, so an absent wake is evidence about that path
rather than about timing, and it advances the seen marker so the status-signal
path stays quiet, because that path independently reports the same event and
without the isolation the assertion would pass whatever this path did.
The absorb-while-fresh guarantee depends on the order in which handle_paused_stale
reads the status file. Workers append to it without a lock, so an append landing
between the reads can pair values that describe different declarations, and only
one of the two orderings makes that pairing harmless.

Cover it deterministically rather than by chance. The case suspends the watcher at
the status-read boundary, appends the replacement declaration while it is held
there, and releases it, so the interleaving is forced rather than raced. It fails
as "alarmed instead of absorbed" if the mtime is read before the declaration, and
as "inherited the old throttle" if the declaration scope is dropped entirely.

The suspension itself is asserted: if the watcher never reaches that boundary the
case fails as "watcher did not reach the concurrent status-read boundary", so it
cannot pass without having exercised the interleaving.

No production code changes here.
@mremond
mremond force-pushed the fm/fm-absorb-fresh-replacement-wait branch from 6484b16 to b8e02ef Compare September 3, 2026 10:52
@mremond mremond changed the title fix(bin): preserve fresh absorption for replacement waits fix(bin): stop a duplicate supervision wake when one declared wait replaces another Sep 3, 2026
@mremond
mremond marked this pull request as ready for review September 3, 2026 11:23
@mremond

mremond commented Sep 3, 2026 •

Copy link
Copy Markdown
Contributor Author

The three conditions in the triage above were all met after it ran.

That triage was written at 10:20:44Z against head 6484b16d, while the PR was still a draft. Since then:

  • Ready for review — marked at 11:23:34Z.
  • Attestation bound to tip — head is now b8e02ef9e6faa2c6f0e337197efa58987f5d04c6. The required check's latest run on that commit completed successfully at 11:22:08Z. The failed entry still visible for the same check is a superseded 10:52:35Z run on the same commit, from before the body was updated to tip.
  • Greptile P2 (concurrent-append coverage) — addressed by the tip commit itself, b8e02ef9 "test(watch): pin the paused-wait read order against a concurrent append".

Three commits and a force-push landed between the triage and now, so its waiting-author outcome no longer reflects the current head. Ready for merge consideration whenever you next triage.

@kunchenguid

Copy link
Copy Markdown
Owner

Speaking as Kun's firstmate: this is a CAPTAIN-DECISION hold — it is waiting on Kun, not on you. There is nothing for you to fix and nothing to push; please do not rebase or force-push it. I have flagged it to him with the specifics below.

Thank you for the correction on the earlier stamp — you were right on all three points, and I re-verified each against tip rather than taking them on trust. This restamp supersedes the waiting-author outcome from 10:20Z.

Head b8e02ef9e6faa2c6f0e337197efa58987f5d04c6 vs main 3d2a08b2097dd24f9ce03fdafe6501e39b79dbd0. MERGEABLE / CLEAN, no longer draft.

Attestation: MATCH. The body's no-mistakes-pipeline-attestation:v1 binds head_sha b8e02ef9e6faa2c6f0e337197efa58987f5d04c6, which is exactly current HEAD, with review/test/document/lint completed.

Required checks on this exact head: green. CI run 33746634813 — conclusion success (Lint, Repo invariants, Test coverage guard, Behavior portable parallel 1-2, serial 1-5, Herdr, stock macOS Bash snapshot, Behavior timing aggregate all SUCCESS). Required no-mistakes: latest run 33753859638 — success (and 33749289164 success before it); the visible FAILURE for that check is the superseded 33746634796 from 10:52Z, before the body was bound to tip, exactly as you said. No workflow approval was needed from me this pass — nothing on this head was action_required.

Security: clean after full diff review. Two files only, no .github/**, no installers, no credentials, no network, no privilege change.

Contract-class: new-default — and this is the whole reason it is now with the captain rather than merged. I read resurface_absorbed() on both sides rather than relying on the description. On main the age gate [ "$age" -ge "$PAUSE_RESURFACE_SECS" ] || return 0 sits inside the scope-matching if, so a replacement declared wait (scope mismatch) skips both gates and surfaces one recheck immediately. Your change lifts that age gate out to be unconditional, so a replacement wait is now absorbed while fresh and only surfaces once its own age crosses the window.

That is a change to a specified default path, not a restoration of a broken one. main pins the current behavior explicitly in tests/fm-watch-triage.test.sh:2057-2112: the header comment says the old throttle "must not suppress the new wait's first inspection merely because its timestamp is still young", and the assertion is [ "$wakes" -eq 1 ] || fail "… produced $wakes wakes instead of one" with no aging step before it. Your diff rewrites that same test to assert [ "$wakes" -eq 0 ] while fresh and only surfaces after aging the status file 500s past the 240s window. Replacing a default path that the repo's own regression test specifies is new-default by definition here, and a bugfix motive does not convert it — so I cannot auto-merge it, however clean it is.

Why it is genuinely his call and not mine: the effect is that an unconfigured watcher now stays silent for up to PAUSE_RESURFACE_SECS (240s default) on a captain-held or paused wait that main surfaces once immediately. VISION.md's first rule says batching and silence "are presentation choices, and never hide a failure, a decision, or a risk" — a captain-held wait is precisely a pending decision. Whether one signal-path wake is sufficient notice for a replacement declaration, or whether the immediate recheck should stay, is a peace-of-mind judgment about his own supervision surface.

VISION verdict (per rule, fetched fresh from main):

  1. One captain, one interface — cannot tell. Removing a duplicate nag serves peace of mind; suppressing the one immediate recheck of a pending decision is the same rule pulling the other way. This is the ambiguity being escalated.
  2. Authority is explicit and never inferred — aligns. No new autonomy, no consent assumed; noise policy only.
  3. Scripts own the mechanics — aligns. Deterministic age and throttle gates; no judgment moved into the script, and the read-order fix (declaration and last line before mtime) is exactly the kind of exactness this rule wants.
  4. A restart is a non-event — aligns. Durable pause/throttle markers and their semantics are unchanged.
  5. Delegation with a spine — aligns. Live decision gates still surface; the wedge-escalation paths are untouched.
  6. The fleet outlives any vendor — aligns. Harness- and session-manager-agnostic watcher logic.
  7. Scope — aligns. bin/fm-watch.sh plus its own suite; command layer only.

Not merged. Not closed. Not stale (your push and comment are today). No competing PR from me. If Kun accepts the new default, this merges as-is with no further work from you.

@kunchenguid

Copy link
Copy Markdown
Owner

Speaking as Kun's firstmate: captain word on this hold — hold for now. Skip is not yes. Not waiting on the author.

HEAD still b8e02ef9e6faa2c6f0e337197efa58987f5d04c6. Class new-default unchanged (replacement declared-wait immediate recheck → silent absorb until wait age). Do not merge. Do not rebase.

Held with the same standing as #3440: Pi supervision is in active flux; the captain will revisit when it settles. Firstmate will not re-flag this unless the tip product surface changes or Firstmate asks.

@mremond mremond changed the title fix(bin): stop a duplicate supervision wake when one declared wait replaces another fix(bin): prevent duplicate wakes for fresh replacement waits Sep 14, 2026
@mremond

mremond commented Sep 23, 2026

Copy link
Copy Markdown
Contributor Author

Following up on the note that this would not be re-flagged "unless the tip product surface changes or
Firstmate asks" - asking, with a measurement taken on main since the hold.

The behaviour this PR changes is reachable without any replacement wait. The throttle scope is a
signature of the status file: status_observed_signature() in bin/fm-classify-lib.sh composes it
from size plus an identity that itself carries device, inode and mtime. So any append moves the key,
including one that does not change a single word of what is being waited on - and on a key mismatch
resurface_absorbed() skips both the age gate and the cadence gate.

Measured on main, default PAUSE_RESURFACE_SECS 14400s:

paused task, quiet and correctly absorbed        age 14776s
15:06:07Z  append one line to <task>.status      (same declared wait, no new fact)
15:07:38Z  stale wake: "paused 91s"              15:06:07 + 91 = 15:07:38

Reproduced on a second task, predicted before it was run:

15:44:02Z  append one line to <task>.status
15:46:35Z  stale wake: "paused 153s"             15:44:02 + 153 = 15:46:35

Both figures are recomputable from the status file mtime and the wake record; nothing here needs to
be taken on trust.

Independently, in a separate installation the same day, two captain-held: declarations re-fired within the
minute of being written and were read as the watcher settling rather than as the alarm being
re-armed.

That last part is what seems to speak to the stated reason for the hold. The concern was that this
change trades away the immediate notice of a genuinely new declaration. The measurement suggests the
current default is already spending that notice on writes that carry no new declaration at all, which
is what taught a reader to discount it.

The branch has not been rebased and nothing on it has changed since the hold. If any change from this side would help - a rebase, a narrower scope, a different approach, or splitting it - it will be done; it only needs to be known which.

@mremond

mremond commented Sep 25, 2026

Copy link
Copy Markdown
Contributor Author

Closing in favour of #4742, which replaces this change and keeps a newly declared wait surfacing immediately.

@mremond mremond closed this Sep 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants