Repository navigation
fix(bin): auto-acknowledge no-longer-actionable wakes - #70
Conversation
…x re-ring from in-flight handling Adds bin/fm-wake-autoack.sh behind config/wake-autoack: when an arm closes with an actionable wake, the Claude Stop hook re-asks crew_is_provably_working for turn-end and plain stale rows and, only when every queued row is cleared, runs the drain's own generation-bound acknowledgement and re-arms instead of ringing. Anything else rings as before; a presentation the drain consumed is carried into the banner and every acknowledged row is logged. The steering-inbox ladder records busy sightings after a ring and paces the next ring or escalation from the latest sighting, so a handling turn in flight no longer draws a re-ring at the first idle poll. Closes #45, closes #46, closes #48
Scope record for this PRIssues closed here: #45 and #46 only. Premise check against the post-sync HEAD
Behavior notes
Scope boundaries
Test evidenceThe pipeline's live validation verdict was inconclusive for one scenario: an unsafe or mixed wake state being handed back with the queue preserved. |
Intent
The queued fork work was approved to go ahead; this is its second ship. It builds the fork's wake-noise fixes, because upstream shows no sign of merging the tracked fixes (kunchenguid#1692 with PR kunchenguid#2801, and kunchenguid#4228 with PR kunchenguid#4859):
One branch, one PR, closing all three. Background evidence: data/fm-wake-noise-origin-scout/report.md in the firstmate home (sections R1, R2 and the per-issue sections).
What Changed
Risk Assessment
🚨 High: The change leaves required wake-noise behavior opt-in and adapter-specific, and contains a reachable timing bug that defeats immediate re-ring on a new inbox state transition.
Testing
Live disposable tmux labs confirmed qualifying wakes are acknowledged without a model turn, while unproven wakes remain queued; focused auto-ack and Claude auto-arm tests also passed.
Evidence: Live qualifying wake auto-ack
Source: Live qualifying wake auto-ack
Evidence: Live fail-closed actionable wake
Source: Live fail-closed actionable wake
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
bin/fm-wake-autoack.sh:71- The required behavior is not enabled by default: this helper exits unless config/wake-autoack exists, so eligible turn-ended or plain-stale wakes still consume a model turn in normal homes. Even when enabled, the changed documentation limits the seam to Claude and leaves Pi, omp, OpenCode, and the away daemon unchanged (docs/configuration.md:604-605,608,619), contradicting the required behavior that ack-only wakes must not consume turns and non-actionable watcher wakes must not be surfaced. This is an unapproved containment/scope reduction; confirm that policy or enforce classification at the shared wake-delivery boundary.bin/fm-task-inbox-lib.sh:444- A fresh new oldest record is returned as quiet before the new-oldest transition is detected at lines 454-461. For example, after record 1 has rung and gained an in-flight sighting, moving it to handled and writing fresh record 2 causes the next watcher poll to wait one grace interval instead of ringing immediately, violating the state-transition requirement. The added test backdates record 2 at tests/fm-task-inbox-inflight.test.sh:91, so it does not cover this real sequence. Detect and reset the transition before the age gate and ring immediately for it.🔧 Fix applied.
2 errors still open:
bin/fm-task-inbox-lib.sh:444- A fresh new oldest record is returned as quiet before the new-oldest transition is detected at lines 454-461. For example, after record 1 has rung and gained an in-flight sighting, moving it to handled and writing fresh record 2 causes the next watcher poll to wait one grace interval instead of ringing immediately, violating the state-transition requirement. The added test backdates record 2 at tests/fm-task-inbox-inflight.test.sh:91, so it does not cover this real sequence. Detect and reset the transition before the age gate and ring immediately for it.bin/fm-claude-stop-autoarm.sh:448- The required criterion is “an ack-only wake must not consume a model turn,” but after 25 successful auto-ack cycles (AUTOACK_MAX, lines 399-401) this condition stops calling auto-ack and falls through to the rewake banner. A 26th qualifying turn-ended or plain-stale wake therefore consumes a Claude turn; docs/configuration.md:616 explicitly documents this behavior. Confirm that this deliberate cap is authorized; otherwise remove it or prevent it from ringing an ack-only queue.🔧 Fix applied.
3 issues (1 error, 2 warnings) still open:
bin/fm-task-inbox-lib.sh:444- A fresh new oldest record is returned as quiet before the new-oldest transition is detected at lines 454-461. For example, after record 1 has rung and gained an in-flight sighting, moving it to handled and writing fresh record 2 causes the next watcher poll to wait one grace interval instead of ringing immediately, violating the state-transition requirement. The added test backdates record 2 at tests/fm-task-inbox-inflight.test.sh:91, so it does not cover this real sequence. Detect and reset the transition before the age gate and ring immediately for it.bin/fm-task-inbox-lib.sh:510- Round 2's in-flight marker write suppresses all failures and returns success. If the state directory is temporarily unwritable after a ring, a busy handling turn leaves no.inflight; the next poll can re-ring or escalate while that turn is still active. The callers also ignore this result at bin/fm-watch.sh:552 and bin/fm-watch.sh:582. Propagate the write failure through the existing watcher error path.bin/fm-wake-autoack.sh:136- Simplification: the new verbatim durable.wake-autoack.logcopy andFM_WAKE_AUTOACK_LOG_LINESretention option (line 139) are not required by the three stated wake-noise criteria; they add persistent duplicated wake data and log-rotation behavior. Remove this component and its related banner/docs/tests unless an audit trail is explicitly authorized.🔧 Fix applied.
4 issues (2 errors, 2 warnings) still open:
bin/fm-task-inbox-lib.sh:444- A fresh new oldest record is returned as quiet before the new-oldest transition is detected at lines 454-461. For example, after record 1 has rung and gained an in-flight sighting, moving it to handled and writing fresh record 2 causes the next watcher poll to wait one grace interval instead of ringing immediately, violating the state-transition requirement. The added test backdates record 2 at tests/fm-task-inbox-inflight.test.sh:91, so it does not cover this real sequence. Detect and reset the transition before the age gate and ring immediately for it.bin/fm-task-inbox-lib.sh:510- Round 2's in-flight marker write suppresses all failures and returns success. If the state directory is temporarily unwritable after a ring, a busy handling turn leaves no.inflight; the next poll can re-ring or escalate while that turn is still active. The callers also ignore this result at bin/fm-watch.sh:552 and bin/fm-watch.sh:582. Propagate the write failure through the existing watcher error path.bin/fm-task-inbox-lib.sh:451- The Round 3 transition fix only detects a changed.ring-state. If record A was escalated for a dead/missing pane,.escalatedis written without.ring-state; after A is handled and fresh record B arrives before the next poll, lines 451-468 retain the old marker and age-gate B, delaying its first ring by the grace period instead of ringing on the state transition. Treat a marker naming a former oldest record as a transition and clear it before the age gate; the added test at tests/fm-task-inbox-inflight.test.sh:87 covers only the.ring-statepath.bin/fm-watch.sh:529- The Round 3 fix introduced an unwritable-.inflightstale path that queues a wake but never records.escalated. With the concrete failure fixture used at tests/fm-task-inbox-inflight.test.sh:164, every subsequent busy poll repeats the stale wake and grows the durable queue. The same missing deduplication remains in the sibling.ring-statefailure path at bin/fm-watch.sh:611-614. After queuing the bookkeeping failure, mark the current record escalated through the shared marker-owning path so it surfaces once.🔧 Fix applied.
6 issues (3 errors, 3 warnings) still open:
bin/fm-task-inbox-lib.sh:444- A fresh new oldest record is returned as quiet before the new-oldest transition is detected at lines 454-461. For example, after record 1 has rung and gained an in-flight sighting, moving it to handled and writing fresh record 2 causes the next watcher poll to wait one grace interval instead of ringing immediately, violating the state-transition requirement. The added test backdates record 2 at tests/fm-task-inbox-inflight.test.sh:91, so it does not cover this real sequence. Detect and reset the transition before the age gate and ring immediately for it.bin/fm-task-inbox-lib.sh:510- Round 2's in-flight marker write suppresses all failures and returns success. If the state directory is temporarily unwritable after a ring, a busy handling turn leaves no.inflight; the next poll can re-ring or escalate while that turn is still active. The callers also ignore this result at bin/fm-watch.sh:552 and bin/fm-watch.sh:582. Propagate the write failure through the existing watcher error path.bin/fm-task-inbox-lib.sh:451- The Round 3 transition fix only detects a changed.ring-state. If record A was escalated for a dead/missing pane,.escalatedis written without.ring-state; after A is handled and fresh record B arrives before the next poll, lines 451-468 retain the old marker and age-gate B, delaying its first ring by the grace period instead of ringing on the state transition. Treat a marker naming a former oldest record as a transition and clear it before the age gate; the added test at tests/fm-task-inbox-inflight.test.sh:87 covers only the.ring-statepath.bin/fm-watch.sh:529- The Round 3 fix introduced an unwritable-.inflightstale path that queues a wake but never records.escalated. With the concrete failure fixture used at tests/fm-task-inbox-inflight.test.sh:164, every subsequent busy poll repeats the stale wake and grows the durable queue. The same missing deduplication remains in the sibling.ring-statefailure path at bin/fm-watch.sh:611-614. After queuing the bookkeeping failure, mark the current record escalated through the shared marker-owning path so it surfaces once.bin/fm-claude-stop-autoarm.sh:573- The hand-back path truncates AUTOACK_CARRY to 4000 bytes even though the drain has already committed unread status presentation. If a qualifying wake has over 4KB of status or decision text, the next model turn receives only a prefix and cannot reproduce the committed lines, silently losing actionable data. Preserve the complete carry or make truncation replayable; this contradicts the verbatim/no-loss contract in docs/configuration.md:246-247.bin/fm-wake-autoack.sh:83- The auto-ack classifier scans every queue row and computes max_seq globally, but it runs as the main actor while fm-wake-drain.sh excludes live branch-granted rows. For example, a main working turn-end at seq 1 plus a branch-granted working turn-end at seq 2 makes the main drain return ack-through 1; the global max of 2 forces a needless model wake. A non-ack-class branch row similarly blocks main auto-ack. Restrict classification and cutoff calculation to the main actor's unreserved rows.🔧 Fix applied.
5 issues (2 errors, 3 warnings) still open:
bin/fm-task-inbox-lib.sh:444- A fresh new oldest record is returned as quiet before the new-oldest transition is detected at lines 454-461. For example, after record 1 has rung and gained an in-flight sighting, moving it to handled and writing fresh record 2 causes the next watcher poll to wait one grace interval instead of ringing immediately, violating the state-transition requirement. The added test backdates record 2 at tests/fm-task-inbox-inflight.test.sh:91, so it does not cover this real sequence. Detect and reset the transition before the age gate and ring immediately for it.bin/fm-task-inbox-lib.sh:510- Round 2's in-flight marker write suppresses all failures and returns success. If the state directory is temporarily unwritable after a ring, a busy handling turn leaves no.inflight; the next poll can re-ring or escalate while that turn is still active. The callers also ignore this result at bin/fm-watch.sh:552 and bin/fm-watch.sh:582. Propagate the write failure through the existing watcher error path.bin/fm-task-inbox-lib.sh:451- The Round 3 transition fix only detects a changed.ring-state. If record A was escalated for a dead/missing pane,.escalatedis written without.ring-state; after A is handled and fresh record B arrives before the next poll, lines 451-468 retain the old marker and age-gate B, delaying its first ring by the grace period instead of ringing on the state transition. Treat a marker naming a former oldest record as a transition and clear it before the age gate; the added test at tests/fm-task-inbox-inflight.test.sh:87 covers only the.ring-statepath.bin/fm-watch.sh:529- The Round 3 fix introduced an unwritable-.inflightstale path that queues a wake but never records.escalated. With the concrete failure fixture used at tests/fm-task-inbox-inflight.test.sh:164, every subsequent busy poll repeats the stale wake and grows the durable queue. The same missing deduplication remains in the sibling.ring-statefailure path at bin/fm-watch.sh:611-614. After queuing the bookkeeping failure, mark the current record escalated through the shared marker-owning path so it surfaces once.bin/fm-wake-autoack.sh:83- The auto-ack classifier scans every queue row and computes max_seq globally, but it runs as the main actor while fm-wake-drain.sh excludes live branch-granted rows. For example, a main working turn-end at seq 1 plus a branch-granted working turn-end at seq 2 makes the main drain return ack-through 1; the global max of 2 forces a needless model wake. A non-ack-class branch row similarly blocks main auto-ack. Restrict classification and cutoff calculation to the main actor's unreserved rows.bash tests/fm-wake-autoack.test.shbash tests/fm-claude-stop-autoarm.test.shLive private tmux-lab qualifying wake acknowledgmentLive private tmux-lab fail-closed actionable wake check✅ **Document** - passed
✅ No issues found.
🔧 **Lint** - 1 issue found → auto-fixed ✅
🔧 Fix applied.
✅ Re-checked - no issues remain.
✅ **Push** - passed
✅ No issues found.