Skip to content

fix: redeliver suppressed Discord requests and remind when overdue - #291

Merged
dnth merged 8 commits into
mainfrom
fm/fm-ext-request-redeliver
Nov 7, 2026
Merged

dnth merged 8 commits into
mainfrom
fm/fm-ext-request-redeliver

Conversation

@dnth

@dnth dnth commented Nov 7, 2026 •

Copy link
Copy Markdown
Owner

Summary

A request's check: close now survives the main handling episode that coalesced it, and a request nobody presents re-surfaces on a bounded cadence.

 OMP main-fallback episode (bin/fm-primary-watch-core.ts)
   close arrives mid-episode
-    suppressed → episodeCoalesced → turn_end: covered, discarded
+    suppressed → episodeCoalesced (+ suppressedAtMs)
+    turn_end closes the episode:
+      any unanswered, unpresented request? → released → ordinary delivery
+      otherwise                            → covered (unchanged)
+    the successor grant prefers that release over the newest one-shot close

 which requests a close covers (bin/fm-ext-lib.sh owns the decision)
-    the ids that survive a possibly truncated wake payload
+    state/ext-inbox is the sink: an unanswered record the drain has not
+    presented since the suppression (state/.ext-presented/<slug>) releases,
+    whatever the payload managed to carry; the payload only has to hold a real
+    ext-request token, so a check path that merely contains the words
+    "ext-request" keeps the ordinary covered path

 unanswered request (bin/fm-ext-overdue-lib.sh, every watcher cycle)
   age >= FM_EXT_OVERDUE_SECS           60 s floor, default 300
     cadence window open                FM_EXT_OVERDUE_REMIND_SECS, 60 s floor, default 600
       linked task reconciles to working | parked | paused → quiet
       otherwise                      → check: ext-request-overdue <slug> age=<m>m

The reminder claims its cadence window before asking the current-state reconciliation, so a live task is re-checked once per window and a crashed one surfaces within it. The marker disappears with the record.

Evidence

  • Before: the same scratch home on the pre-change checkout reproduces the 22:45 sequence and times out — request close suppressed behind the open episode, its queue row acknowledged away unread, never re-delivered.
    After: the final-head smoke prints released gen=1 close=... by=queue-drained and delivers check: ext-request <slug> (step 4 of smoke-final.log).
  • Tests: tests/fm-omp-primary.test.sh and tests/fm-ext-bridge.test.sh pass on the final head, covering the truncated three-request batch, the multiline slow-poll result, presented/answered negatives, the path-containing check that must keep the covered path, reminder cadence, a crashed worker, a missing status log, and zero/negative thresholds.
  • CI: PR 291, 18/18 checks green, lint clean (bin/fm-lint.sh, ShellCheck 0.11.0).

Merge Danger

Door: two-way

The change is additive: a released close goes through the existing delivery path, and only closes carrying a real request token can change behaviour.

Blast Radius: supervision wake delivery only

A mis-mapped token could re-deliver one close that was already presented - a redundant notification, never a lost one - and a below-minimum threshold is clamped to 60 seconds rather than looping.

Pipeline

Updates from git push no-mistakes

✅ **Intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 2 issues found → auto-fixed (5) ✅
  • 🚨 .omp/extensions/fm-primary-omp.ts:90 - Required: a suppressed actionable request check "must be delivered once that episode closes" unless presented or removed. The new predicate rejects actual watcher payloads: bin/fm-watch.sh:911 batches ext-request A; ext-request B, which bin/fm-ext-lib.sh:2442 treats as one invalid slug; bin/fm-watch.sh:2202 prefixes poll results with the check-script path, which this regex rejects outright. If these closes are suppressed and a later ack-through empties the queue without presentation, bin/fm-primary-watch-core.ts:1958 still covers and discards them. The same mismatch defeats successor preference at bin/fm-primary-watch-core.ts:1910. Normalize both existing producer formats at the adapter/library boundary and evaluate every referenced request, preserving unrelated check behavior.
  • 🚨 bin/fm-ext-overdue-lib.sh:36 - Required: "with no live task it must be reminded." The added status_is_terminal_verb &#34;$last&#34; || return 0 equates a nonterminal historical status with current liveness. A linked worker can write working:, then crash while its acknowledged inbox request remains; its metadata and status survive, so every overdue scan suppresses that request indefinitely. A missing status also counts as live. The exemption consumer is bin/fm-ext-overdue-lib.sh:72, and docs/configuration.md:878 repeats this incorrect definition. Use the existing current-task reconciliation boundary, which distinguishes active runs from dead endpoints, before granting the exemption.

🔧 Fix applied.
1 warning still open:

  • ⚠️ bin/fm-ext-lib.sh:2492 - Required: reminders must run on a bounded cadence, "never a tight loop." Setting FM_EXT_OVERDUE_SECS=0 and FM_EXT_OVERDUE_REMIND_SECS=0 passes both numeric checks, and the new clamp [ &#34;$raw&#34; -ge &#34;$floor&#34; ] || raw=$floor preserves zero. Consequently, bin/fm-ext-overdue-lib.sh:71 always passes the age gate and :76 always passes the cadence gate, even immediately after writing the marker. bin/fm-watch.sh:924 then repeatedly appends and rings on successive watcher launches, accumulating notifications while main is busy. Validate both settings at bin/fm-ext-lib.sh:2472 and :2485 so the effective reminder cadence remains positive.

🔧 Fix applied.
2 issues (1 error, 1 warning) still open:

  • ⚠️ bin/fm-ext-lib.sh:2492 - Required: reminders must run on a bounded cadence, "never a tight loop." Setting FM_EXT_OVERDUE_SECS=0 and FM_EXT_OVERDUE_REMIND_SECS=0 passes both numeric checks, and the new clamp [ &#34;$raw&#34; -ge &#34;$floor&#34; ] || raw=$floor preserves zero. Consequently, bin/fm-ext-overdue-lib.sh:71 always passes the age gate and :76 always passes the cadence gate, even immediately after writing the marker. bin/fm-watch.sh:924 then repeatedly appends and rings on successive watcher launches, accumulating notifications while main is busy. Validate both settings at bin/fm-ext-lib.sh:2472 and :2485 so the effective reminder cadence remains positive.
  • 🚨 bin/fm-ext-lib.sh:2466 - Round 1's parser fix leaves truncated batches behind. Required: a suppressed actionable request check “must be delivered once that episode closes” unless presented or removed. This predicate checks only IDs retained in the payload. bin/fm-watch.sh:911-913 includes three requests but marks every batch row rung; the slow-poll producer at bin/fm-watch.sh:2226 emits multiline results, while bin/fm-primary-watch-core.ts:307-309 retains only the first line. If the retained requests are answered while an omitted, ack-only request remains unpresented, a later ack-through empties the queue and bin/fm-primary-watch-core.ts:1958-1974 covers the close. The omitted request waits for the overdue backstop instead of episode redelivery. The same incomplete set affects successor preference at bin/fm-primary-watch-core.ts:1910. Preserve every represented request ID in a single close payload at the producer/close-normalization boundary before deciding that the batch is handled.

🔧 Fix applied.
3 issues (1 error, 2 warnings) still open:

  • ⚠️ bin/fm-ext-lib.sh:2492 - Required: reminders must run on a bounded cadence, "never a tight loop." Setting FM_EXT_OVERDUE_SECS=0 and FM_EXT_OVERDUE_REMIND_SECS=0 passes both numeric checks, and the new clamp [ &#34;$raw&#34; -ge &#34;$floor&#34; ] || raw=$floor preserves zero. Consequently, bin/fm-ext-overdue-lib.sh:71 always passes the age gate and :76 always passes the cadence gate, even immediately after writing the marker. bin/fm-watch.sh:924 then repeatedly appends and rings on successive watcher launches, accumulating notifications while main is busy. Validate both settings at bin/fm-ext-lib.sh:2472 and :2485 so the effective reminder cadence remains positive.
  • 🚨 bin/fm-ext-lib.sh:2466 - Round 1's parser fix leaves truncated batches behind. Required: a suppressed actionable request check “must be delivered once that episode closes” unless presented or removed. This predicate checks only IDs retained in the payload. bin/fm-watch.sh:911-913 includes three requests but marks every batch row rung; the slow-poll producer at bin/fm-watch.sh:2226 emits multiline results, while bin/fm-primary-watch-core.ts:307-309 retains only the first line. If the retained requests are answered while an omitted, ack-only request remains unpresented, a later ack-through empties the queue and bin/fm-primary-watch-core.ts:1958-1974 covers the close. The omitted request waits for the overdue backstop instead of episode redelivery. The same incomplete set affects successor preference at bin/fm-primary-watch-core.ts:1910. Preserve every represented request ID in a single close payload at the producer/close-normalization boundary before deciding that the batch is handled.
  • ⚠️ bin/fm-ext-lib.sh:2433 - Simplification: round 3 left fm_ext_check_payload_slugs unused after moving release decisions to the inbox scan. Its only callers are parser assertions at tests/fm-ext-bridge.test.sh:2508, :2514, and :2521. No current requirement needs this separate parsing path. Remove the helper and its parser-only assertions, retaining the behavioral predicate tests.

🔧 Fix applied.
4 issues (2 errors, 2 warnings) still open:

  • ⚠️ bin/fm-ext-lib.sh:2492 - Required: reminders must run on a bounded cadence, "never a tight loop." Setting FM_EXT_OVERDUE_SECS=0 and FM_EXT_OVERDUE_REMIND_SECS=0 passes both numeric checks, and the new clamp [ &#34;$raw&#34; -ge &#34;$floor&#34; ] || raw=$floor preserves zero. Consequently, bin/fm-ext-overdue-lib.sh:71 always passes the age gate and :76 always passes the cadence gate, even immediately after writing the marker. bin/fm-watch.sh:924 then repeatedly appends and rings on successive watcher launches, accumulating notifications while main is busy. Validate both settings at bin/fm-ext-lib.sh:2472 and :2485 so the effective reminder cadence remains positive.
  • 🚨 bin/fm-ext-lib.sh:2466 - Round 1's parser fix leaves truncated batches behind. Required: a suppressed actionable request check “must be delivered once that episode closes” unless presented or removed. This predicate checks only IDs retained in the payload. bin/fm-watch.sh:911-913 includes three requests but marks every batch row rung; the slow-poll producer at bin/fm-watch.sh:2226 emits multiline results, while bin/fm-primary-watch-core.ts:307-309 retains only the first line. If the retained requests are answered while an omitted, ack-only request remains unpresented, a later ack-through empties the queue and bin/fm-primary-watch-core.ts:1958-1974 covers the close. The omitted request waits for the overdue backstop instead of episode redelivery. The same incomplete set affects successor preference at bin/fm-primary-watch-core.ts:1910. Preserve every represented request ID in a single close payload at the producer/close-normalization boundary before deciding that the batch is handled.
  • ⚠️ bin/fm-ext-lib.sh:2433 - Simplification: round 3 left fm_ext_check_payload_slugs unused after moving release decisions to the inbox scan. Its only callers are parser assertions at tests/fm-ext-bridge.test.sh:2508, :2514, and :2521. No current requirement needs this separate parsing path. Remove the helper and its parser-only assertions, retaining the behavioral predicate tests.
  • 🚨 bin/fm-ext-lib.sh:2435 - Round 3's R4 fix introduced an overly broad ownership guard: *ext-request* matches script paths as well as request events. A legitimate PR check such as state/fm-ext-request-redeliver.check.sh: merged therefore returns pending whenever an unrelated inbox request remains unpresented. The closing boundary then repeats the PR notification, contradicting the required constraint “no other wake class changes behaviour.” Narrow the shared guard to an actual ext-request/ext-request-overdue event token while preserving the inbox-wide scan. Related changed sites: .omp/extensions/fm-primary-omp.ts:90 accepts the same substring; bin/fm-primary-watch-core.ts:1910 changes successor preference and :1958 releases the unrelated close.

🔧 Fix applied.
✅ Re-checked - no issues remain.

✅ **Test** - passed

✅ No issues found.

  • Live validation: ✅ go - 5 of 5 scenarios driven live against the product
Scenario Result Live Evidence
A synthetic request arriving during a real OMP turn is redelivered after the episode closes despite its queue row being consumed unread ✅ pass live Episode redelivery
Requests presented by the real drain or dismissed through the inbox CLI produce no duplicate episode wake ✅ pass live Presentation guard; Removal guard
A truncated synthetic request batch retains successor priority over a newer unrelated check without repeating that check ✅ pass live Batch successor and unrelated-check guard
The real watcher reminds aged synthetic requests on bounded cadence and stops after dismissal, including with zero or negative settings ✅ pass live Reminder cadence and endpoint liveness
An acknowledged synthetic request stays quiet with a live linked tmux endpoint and resumes reminders after endpoint loss ✅ pass live Reminder cadence and endpoint liveness
  • git diff 7c71cfb43ea01263763dae7689d1dd9085f7391f HEAD to scope acceptance scenarios.
  • python3 .validation-temp/probe.py launched real OMP RPC with production extension/watchers and a localhost model endpoint.
  • python3 .validation-temp/episode.py exercised suppressed-request redelivery after unread queue consumption.
  • python3 .validation-temp/guards.py exercised real drain presentation, inbox dismissal, and duplicate suppression.
  • python3 .validation-temp/batch-resume.py completed the actual watcher batch and successor-preference checks after correcting driver setup.
  • python3 .validation-temp/reminders.py exercised production watcher cadence, threshold clamps, dismissal, and real tmux liveness reconciliation; repeated with FM_POLL=1.
  • Awaited or stopped all owned children, verified the private tmux server stopped, removed disposable files, and confirmed clean git status --short.
✅ **Document** - passed

✅ No issues found.

🔧 **Lint** - 1 issue found → auto-fixed ✅
  • ⚠️ linter found issues (exit code 1)

🔧 Fix applied.
✅ Re-checked - no issues remain.

✅ **Push** - passed

✅ No issues found.

dnth added 8 commits November 6, 2026 23:43
…uests

A Discord request's check close is a live condition, but the OMP main-fallback
episode finished it as covered whenever the episode closed on an acknowledged
queue. When the supervisor was inside one long turn, the request's row could be
acknowledged away with nobody having presented it, so the request waited for
some unrelated wake - about 20 minutes in the 22:45 incident.

Suppressed closes now record their suppression time, and the boundary that
closes an episode releases a check whose sink still reports it pending instead
of covering it; the released close goes through the ordinary delivery path.
The adapter supplies that sink predicate, so only the local Communication
Officer path changes: the inbox record decides whether a request is answered,
and the drain's new presentation note in state/.ext-presented/ keeps an
already-presented request from being repeated.

A request can still outlive every close that reached main, so each watcher
cycle re-surfaces one request older than FM_EXT_OVERDUE_SECS (default 300) as
check: ext-request-overdue <slug> age=<m>m, at most once per
FM_EXT_OVERDUE_REMIND_SECS (default 600). The reminder stops with the record,
and an ack-only request stays quiet while a task linked to it is still live.
@dnth
dnth merged commit fce6a5c into main Nov 7, 2026
19 checks passed
@dnth
dnth deleted the fm/fm-ext-request-redeliver branch November 7, 2026 02:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant