Skip to content

fix(notifications): make pipeline.flush() drain in-flight dispatches#41

Merged
lesquel merged 1 commit into
mainfrom
fix/pipeline-flush-inflight
May 17, 2026
Merged

fix(notifications): make pipeline.flush() drain in-flight dispatches#41
lesquel merged 1 commit into
mainfrom
fix/pipeline-flush-inflight

Conversation

@lesquel

@lesquel lesquel commented May 17, 2026

Copy link
Copy Markdown
Owner

Summary

Fixes the 3rd and final latent bug from the #25 comment (surfaced by #28's characterization tests).

pipeline.flush() was an empty no-op. The genuinely fire-and-forget dispatches (telegram/push/channels fan-out) were never tracked, so a notification in flight when the process shut down was silently dropped — exactly the AGENTS.md §"No silent failures" anti-pattern.

Fix

  • Track each already-.catch()-chained fire-and-forget promise in an inFlight Set via a void track() helper (.finally removes it). 5 sites: telegram + push + channels in notifyPermissionPending; channels fan-out in notifySessionIdle / notifySessionError. The two await-ed telegram calls in idle/error are not fire-and-forget → untouched.
  • flush() now awaits the in-flight set with a bounded timeout (FLUSH_TIMEOUT_MS = 5_000) via Promise.race. On timeout it audits notifications.flush_timeout with the pending count (observable, never silent), always clearTimeouts (no leaked handle), fast-paths when nothing is in flight, and never throws/hangs beyond the bound.

Guarantees (fresh-review verified)

  • Hot path unchanged: track() is void; no new await added to any notify function; notifyPermissionPending returns the same boolean at the same point → the permission hook's blocking decision timing is identical to main.
  • No unhandled-rejection vector: only post-.catch() (never-rejecting) promises are tracked.
  • Shutdown wiring intact: server/index.ts still await notifications.flush() in the same position; now the bound is meaningful.
  • Test seam: optional createNotificationService(deps, { flushTimeoutMs }) — production caller unchanged, defaults to 5s.

Testing

  • The test(integrations,notifications): cover native hooks + pipeline #28 no-op characterization test was replaced with 4 tests that genuinely fail on main's no-op flush (fresh review verified them discriminating, not tautological); timeout test uses an injected 50ms bound (no real 5s wait, deterministic).
  • bun test 627/0 · bunx tsc --noEmit clean · scope = pipeline.ts + pipeline.test.ts only
  • Independent fresh-context adversarial review: APPROVE, no blocking issues (all 7 checks pass; one cosmetic nit — stale header comment — fixed).

Independent off main. No overlap with #33 wave-2 PRs or #40.

Refs #25completes all 3 latent items (with #40). #25's remaining lint/arch items were shipped earlier (#27/#30-32).

…spatches

Third latent bug (issue #25): flush() was an empty no-op, so the
fire-and-forget telegram/push/channel dispatches were silently dropped
on shutdown — a notification could be lost the moment the process exits.

- Track each already-`.catch()`-chained fire-and-forget promise in an
  inFlight Set via a void `track()` helper (.finally removes it). The
  5 sites: telegram + push + channels fan-out in notifyPermissionPending;
  channels fan-out in notifySessionIdle and notifySessionError. The two
  awaited telegram calls in idle/error are NOT fire-and-forget, untouched.
- flush() now awaits the in-flight set with a BOUNDED timeout
  (FLUSH_TIMEOUT_MS = 5_000) via Promise.race; on timeout it audits
  `notifications.flush_timeout` with the pending count (observable per
  AGENTS.md, never silent), always clearTimeout (no leaked handle),
  fast-path returns immediately when nothing is in flight, never throws.

Hot path unchanged: track() is void, no new await added to the notify
functions; notifyPermissionPending returns the same boolean at the same
point. Test seam: optional `createNotificationService(deps, { flushTimeoutMs })`
— production caller (server/index.ts) unchanged, defaults to 5s.

The #28 no-op characterization test was replaced with 4 tests that
genuinely fail on main's no-op flush (verified discriminating). bun test
627/0, tsc clean. Independent fresh-context review: APPROVE, no blocking
issues (hot path + flush bound + non-tautological tests all verified).

Refs #25
@lesquel lesquel merged commit 45f0d5d into main May 17, 2026
2 checks passed
@lesquel lesquel deleted the fix/pipeline-flush-inflight branch May 17, 2026 20:14
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