LearnNewsExamplesServices
Frontmatter
id15414
titleWake dispatch fires per-message at swarm cadence: widen + roll the coalescing window
stateClosed
labels
enhancementai
assigneesneo-fable-clio
createdAtJul 18, 2026, 5:44 AM
updatedAtJul 18, 2026, 4:15 PM
githubUrlhttps://github.com/neomjs/neo/issues/15414
authorneo-fable-clio
commentsCount2
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtJul 18, 2026, 4:15 PM

Wake dispatch fires per-message at swarm cadence: widen + roll the coalescing window

Closed Backlog/active-chunk-7 enhancementai
neo-fable-clio
neo-fable-clio commented on Jul 18, 2026, 5:44 AM

Context

Operator observation on the re-enabled wake daemon's first hours (2026-07-18): "the wake daemon sends too many messages in little time, not bundled. this eats turns and tokens for noise." Source-confirmed the same hour. Companion concern to #15405 (which owns presence-aware policy / heartbeat floor / dead-man — the WHO and WHEN); this ticket owns the RATE AND SHAPE of event-driven dispatch.

The Problem — evidence from one seat's first 30 wake-enabled minutes

@neo-fable-clio, 03:09–03:41: four wake dispatches, three of them single-event, plus one delivery carrying TWO separate [WAKE] blocks (two flushes queued while the seat was mid-turn, delivered together but un-merged). Every wake costs a full harness turn — system prompt + full context reload — so a single-event wake spends tens of thousands of tokens to deliver one message header.

Root cause (source-anchored): ai/daemons/wake/daemon.mjs:93DEFAULT_COALESCE_WINDOW_MS = 30000, a hardcoded 30-second FIXED window (:663-681: first event arms the timer, flush at window end; per-subscription override via harnessTargetMetadata.coalesceWindow, clamp 0–300s). The 30s default bundles INTRA-burst messages (one peer sending three messages in seconds) but the dominant swarm cadence is INTER-turn: lifecycle messages land 1–5 minutes apart during an active evening, so each catches its own window → wake-per-message. The coalescing machinery itself is sound (per-subscription queues, tuple dedupe, watermarks — :615-681); only the window policy is mis-tuned for the real distribution.

The Architectural Reality

  • The queue/dedupe/watermark mechanics stay untouched; the changes are policy-layer.
  • The 300s digest cap already exists as the design ceiling (config comment configBase.mjs:780); swarmWakeCooldownSeconds (600s) is precedent for refractory semantics, but only for the swarm-idle wake — no per-subscription analog exists for event wakes.
  • The constant is hardcoded — per ADR-0019 it belongs as an AiConfig leaf (the daemon is an entrypoint; importing AiConfig is the sanctioned shape).

The Fix

Policy-layer, four parts (each independently valuable; decided-at-implementation with dogfood receipts):

  1. Config-leaf the default + widen it: DEFAULT_COALESCE_WINDOW_MS → an orchestrator leaf (env-overridable), default raised 30s → 120–180s. Direct actionable wakes still arrive minutes-fresh — peers' turns take that long anyway, and the team's graduated wake semantics (AC-7, quiet-by-default) value turns over sub-minute latency.
  2. Rolling window with the hard cap: reset the timer on each newly queued event, flush at quiet-for-window OR at the 300s cap since first event — trailing arrivals join the digest instead of arming the next wake (events at t=0/100s/170s become ONE wake, not two).
  3. Per-subscription post-flush refractory (~120s): events arriving just after a flush queue into the NEXT digest rather than immediately re-arming — kills the wake-per-message cadence at just-outside-window spacing. Mirror of the swarm-cooldown precedent, scoped per recipient.
  4. Pending-undelivered merge: at flush time, if a prior wake for this seat is still undelivered (seat mid-turn — the two-blocks-in-one-prompt observation), merge into it instead of stacking a second block. (Presence-awareness proper stays #15405's scope; this is only the merge-don't-stack rule.)

Optional tiering (design call at implementation): priority: high events may keep a shorter window (30–60s) — sender-chosen urgency composing with the delivery-class system.

Contract Ledger (T3 — the operator/peer-consumed dispatch-policy surface)

Target Surface Source of Authority Behavior Fallback Evidence
orchestrator.wakeDispatch.coalesceWindowSeconds leaf (env NEO_WAKE_COALESCE_WINDOW_SECONDS) ADR-0019 (leaf shape) + ADR-0002 §6.4.1.1 (the Shape-C divergence amendment) Shape-C default coalescing window, 150s; operator-tunable; read at the daemon use site leaf default when env unset; config parity snapshot records the path parity lint + leaf resolution specs
Per-subscription harnessTargetMetadata.coalesceWindow override ADR-0002 §6.4.1 (universal, unchanged) Override-else-default; clamp [0, 300]s; precedence: override > leaf default undefined/null → leaf default policy unit sweep
Explicit coalesceWindow: 0 ADR-0002 §6.4.1 (universal, unchanged) Immediate per-event dispatch — exempt from rolling, refractory, AND cap n/a (explicit contract) policy + daemon witnesses
Rolling window + hard cap (orchestrator.wakeDispatch.flushHardCapSeconds leaf, env NEO_WAKE_FLUSH_HARD_CAP_SECONDS, default 300) ADR-0002 §6.4.1.1 + ADR-0019 Every queued event re-arms; flush at quiet-for-window OR cap-since-first-event; heavy-delta read-state deferral composes ABOVE the cap by design (late beats corrupt; own bound) n/a policy sweep + mounted daemon cap witness
orchestrator.wakeDispatch.flushRefractorySeconds leaf (env NEO_WAKE_FLUSH_REFRACTORY_SECONDS, default 120) ADR-0002 §6.4.1.1 + ADR-0019 (leaf shape — the PR review's A1 repair) Holds the next digest at distance after a CONFIRMED delivery only; applied at ARM time and at FLUSH time no confirmed delivery recorded → no refractory policy sweep + mounted witness
Adapter outcome enum delivered / skipped / failed this ticket (RA2 of the PR #15442 review) delivered = adapter accepted; skipped = fail-closed refusal (no refractory, no count); failed = thrown dispatch (retry path); the [Wake Dispatch] counting line fires on delivered only — direct AND retry unknown/legacy undefined maps to skipped (fail-safe: never counts, never arms) mounted accounting witnesses
orchestrator.wakeDispatch.attemptTimeoutSeconds leaf (env NEO_WAKE_ATTEMPT_TIMEOUT_SECONDS, default 30) this ticket (the PR review's hung-adapter falsifier) One adapter attempt holds the per-subscription delivery owner at most this bound — a hung transport resolves as a FAILED attempt (retry path); the bound ABORTS signal-capable transports (the webhook fetch); spawn-based adapters may orphan a late attempt (outcome discarded, duplicate risk bounded by refractory + message claims) n/a mounted hung-adapter witness
Cross-shape scope ADR-0002 §6.4.1.1 Shape C only; Shapes A/B (CoalescingEngineService) keep fixed 30s n/a amendment text + untouched A/B suite

Acceptance Criteria

  • The window default is an AiConfig leaf (ADR-0019-clean), default ≥120s; the hardcoded constant is gone.
  • Rolling-with-cap semantics unit-tested: t=0/100/170s events → one flush; the 300s cap bounds worst-case latency.
  • Post-flush refractory unit-tested: an event 10s after a flush does not arm an immediate wake.
  • Merge-don't-stack: two flushes while undelivered produce ONE [WAKE] block with the union digest — no double-block deliveries.
  • Dedupe/watermark behavior byte-identical (regression-pinned).
  • Dogfood receipt after one active evening: wakes-per-seat-per-hour before/after, from the daemon's own dispatch records (add the dispatch log line if none exists — the forensic for THIS ticket found no daemon log to count from). [L3-dogfood-deferred — post-merge by design: the measurement needs the merged policy live for one active evening; the receipt lands as a comment on this ticket. The dispatch log line ships in the PR.]

Out of Scope

Presence-aware dispatch policy, heartbeat floor, dead-man (#15405) · delivery-class semantics (#15376, landed) · the stop-economics pair (#15401/#15404).

Decision Record impact

aligned-with ADR-0019 (the constant→leaf migration is mandated by it).

Related

#15405 (companion — WHO/WHEN vs this ticket's RATE/SHAPE; @neo-fable's call whether to fold or keep separate) · #15376 (the class system this composes with) · the wake re-enable operational change (2026-07-18, canonical overlay).

Live latest-open sweep: latest 6 at 2026-07-18T03:44Z; no equivalent (#15405 is the adjacent-distinct companion). A2A: no competing claim on dispatch-rate scope.

Origin Session ID: abce4d75-7dcb-4145-8afc-b0ff2cdc51e6

Retrieval Hint: query_raw_memories("wake coalesce 30s fixed window rolling refractory merge dont stack per-message")