LearnNewsExamplesServices
Frontmatter
id17658
titleRead-before-flush messages dispatch an empty wake digest
stateClosed
labels
bugairegressionmodel-experienceagent-os
assigneesneo-gpt-emmy
createdAtAug 24, 2026, 1:00 AM
updatedAtAug 24, 2026, 9:02 AM
githubUrlhttps://github.com/neomjs/neo/issues/17658
authorneo-gpt-emmy
commentsCount1
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtAug 24, 2026, 1:44 AM

Read-before-flush messages dispatch an empty wake digest

Closed Backlog/active-chunk-19 bugairegressionmodel-experienceagent-os
neo-gpt-emmy
neo-gpt-emmy commented on Aug 24, 2026, 1:00 AM

Context

Twice on the live Shape-B wake path, @neo-gpt-emmy received a header-only prompt:

[WAKE][priority:normal] 1 events for @neo-gpt-emmy:

Both receipts resolve to a high-priority direct message that was queued correctly, then read before the 150-second coalescing window flushed:

digest source message committed read digest emitted envelope
wake-digest:a15636d99233… MESSAGE:3ad59467-9876-4960-b859-4cc1868aee62 2026-08-23T22:47:15.647Z 2026-08-23T22:49:07.141Z totalEvents: 1; every breakdown count 0
wake-digest:9373a8d3f3f9… MESSAGE:3ae53c01-e87c-4068-ac96-f4def6c59128 2026-08-23T22:53:02.704Z 2026-08-23T22:53:58.476Z totalEvents: 1; every breakdown count 0

The signed host delivery records preserve both inconsistent envelopes, so this is not an inferred renderer symptom. The renderer did exactly what the payload asked: it printed the outer total and had no bucket detail to render.

This is a successor to #16541 / PR #16918, not a reopening. That repair added read-state reconciliation and correctly suppresses a committed-read message from the sent_to_me bucket. It did not make the outer envelope consume the same surviving set.

The Problem

CoalescingEngineService._buildDigestEnvelope() currently applies committed-read suppression inside the bucket-population loop:

if (state?.readAt) {
    continue
}

The later envelope fields still derive from the original events array:

  • totalEvents: events.length
  • sourceEventIds = events.map(...)
  • digest identity from those source ids
  • logId from the original tail

The result has two incompatible truths: the breakdown says there is nothing to wake for, while the outer envelope says one event survived. formatLocalWakeDigest() consumes those two fields independently, producing a wake header with no detail.

This is more than presentation drift. The inconsistent event remains part of retry/dedupe identity and can arm delivery/refractory bookkeeping even though the user has already consumed it. The cost is a real harness interruption whose only possible action is proving that nothing remains.

The Architectural Reality

  • ai/services/memory-core/CoalescingEngineService.mjs owns read-state reconciliation and structured digest construction.
  • _partitionExpiredMessageWakes() already demonstrates the correct ordering for the adjacent authored-age gate: filter before totalEvents, source ids, digest identity, logId, counts, latest, and priority; an all-suppressed queue dispatches nothing.
  • _buildDigestEnvelope() applies read-state reconciliation later, after the flush has decided that the queue is non-empty, and only to bucket population.
  • ai/daemons/wake/localWakeAdapters.mjs#formatLocalWakeDigest prints payload.totalEvents in the header and prints detail lines from payload.breakdown. A 1 / zero-sum envelope therefore becomes the exact header-only prompt above.
  • MailboxService.readBackgroundDeliveryState remains the canonical background reader for the direct-message and broadcast storage shapes.
  • The prior missing-row contract remains distinct: a positively missing row may count while being ineligible as latest; an UNKNOWN resolver result must remain fail-safe and render.

Agent OS structure-map gate: attempted immediately before filing; it failed before producing a map with Cannot create a string longer than 0x1fffffe8 characters. Placement is nevertheless bounded to the existing coalescer/formatter seams; no new file or directory is proposed.

The Fix

Derive one admitted event set and use it for every structured-envelope field.

  1. Reconcile committed read-state before final digest construction, not only while incrementing breakdown buckets.
  2. Build totalEvents, sourceEventIds, digest identity, tail logId, every bucket count/latest, and highest priority from the same admitted set.
  3. If read-state reconciliation removes every queued event, consume the queue without dispatching a digest, without arming a false refractory window, and without enqueueing a retry.
  4. Preserve mixed queues: committed-read messages disappear, while unread messages and non-message events continue normally.
  5. Preserve the fail-safe distinctions from PR #16918: resolver absent/throw/{} stays UNKNOWN and renders; a positively missing row keeps its existing count-but-no-latest behavior.
  6. Add bounded observability for all-read suppression (count and recipient/subscription identity, never message content).
  7. Pin the wire invariant: totalEvents equals the sum of rendered breakdown counts for every dispatched digest. An inconsistent zero-sum envelope must never reach a harness prompt.

Contract Ledger Matrix

Target surface Source of authority Before After Fallback Evidence
Shape-B read-state admission MailboxService.readBackgroundDeliveryState + CoalescingEngineService committed-read events leave the bucket but remain in the envelope population committed-read events leave the one admitted set used by every field UNKNOWN stays admitted; positively missing retains its prior semantics exact read-before-flush fixture
Structured digest population CoalescingEngineService._buildDigestEnvelope totalEvents/ids/identity/tail can disagree with breakdown outer fields and buckets derive from one set mixed non-message events survive field-by-field invariant assertions
All-read flush CoalescingEngineService._flush dispatches totalEvents > 0 with zero bucket detail consumes without dispatch/retry/refractory mailbox read state remains untouched dispatch spy + state assertions
Local wake prompt formatLocalWakeDigest inconsistent envelope renders a header-only wake no producer-generated inconsistent envelope reaches the formatter malformed external input fails visibly, never as an empty actionable prompt integrated formatter/coalescer control
Suppression telemetry coalescer logger none for read-before-flush all-suppressed queues bounded count + subscription/recipient only no subject/body logger negative-content assertion

Decision Record impact

none — restores the consistency contract already claimed by #16541 / PR #16918 and mirrors the settled all-suppressed behavior from #17009 / PR #17032. No topology, transport, persistence, or policy change.

Acceptance Criteria

  • Exact regression: enqueue a wake-eligible direct message, commit its read-state before the coalescing flush, and observe zero digest dispatches.
  • The all-read queue is consumed once and does not arm lastFlushAtBySub, enter the retry store, or mutate mailbox state.
  • Mixed queue control: one committed-read high-priority message plus one unread normal-priority message dispatches exactly one normal-priority message; the read id/subject affects no envelope field.
  • For every dispatched digest, totalEvents equals the sum of breakdown counts, and sourceEventIds, digest identity, and logId describe only that same admitted set.
  • Resolver absent, throwing, or returning {} remains fail-safe: the event stays visible rather than becoming a missing wake.
  • A positively missing MESSAGE row retains PR #16918’s count-but-no-latest contract.
  • Bounded suppression telemetry contains no message subject/body.
  • Focused coalescer, wake-adapter, and delivery-owner unit suites pass.
  • Post-deploy observation: a high-priority direct message read inside the coalescing window produces no later header-only prompt.

Out of Scope

  • Broadcast quiet-default policy — #17646 / PR #17657.
  • Interactive-dialog protection — #17629.
  • Authored-age admission and stale projection replay — #17009 / PR #17032.
  • Mailbox retention, read/archive semantics, or the coalescing-window duration.
  • Reopening #16541. This ticket is the successor for a newly measured residual.

Avoided Traps

  • Fixing only the renderer. Hiding the header leaves retry identity, source ids, logId, and refractory bookkeeping wrong.
  • Changing only totalEvents. Every aggregate must consume the same set; one corrected number beside stale ids is still split truth.
  • Marking messages unread again to preserve the queued wake. A committed user action is the authority; the queue must yield to it.
  • Treating the coincident suppressed broadcast as the cause. The signed receipts identify two read direct messages as the source events; PR #17657 is adjacent, not causal.

Related

#16541 · PR #16918 · #17009 · #17010 · PR #17032 · #17629 · #17646 · PR #17657

Live latest-open sweep: checked the latest 20 open issues at 2026-08-23T22:59:13Z; no equivalent found. A2A in-flight claim sweep: checked the latest 40 messages across all read states at the same instant; no overlapping claim found.

Origin Session ID: c6d0f891-97a9-4acf-8ebc-3f121a435980

Retrieval Hint: query_raw_memories("read-before-flush empty wake totalEvents one breakdown zero CoalescingEngineService"); exact live digest anchors a15636d99233 and 9373a8d3f3f9.

— Emmy (GPT-5.6 Sol Ultra, Codex)

tobiu referenced in commit 1c6c140 - "fix(wake): suppress read-before-flush empty digests (#17658) (#17659)" on Aug 24, 2026, 1:44 AM
tobiu closed this issue on Aug 24, 2026, 1:44 AM