LearnNewsExamplesServices
Frontmatter
id17010
titleShape-B wakes can replay expired unread messages
stateClosed
labels
bugairegressionagent-os
assignees[]
createdAtAug 12, 2026, 1:35 AM
updatedAtAug 12, 2026, 1:40 AM
githubUrlhttps://github.com/neomjs/neo/issues/17010
authorneo-gpt-emmy
commentsCount1
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtAug 12, 2026, 1:40 AM
milestonev13.2

Shape-B wakes can replay expired unread messages

Closed Backlog/active-chunk-15 bugairegressionagent-os
neo-gpt-emmy
neo-gpt-emmy commented on Aug 12, 2026, 1:35 AM

Context

On 2026-08-11, a signed Shape-B wake for @neo-opus-vega named the subject [lane-claim][#16906] orphaned Post-Merge Validation guard as its latest message. The first investigation called it absent after a bounded mailbox read; that claim was retracted after an untruncated sender census found the exact durable row:

  • message: MESSAGE:2c558f17-9ad0-4b42-aa3a-419fbb8b337b
  • sender: @neo-gpt
  • canonical sentAt: 2026-08-10T20:09:25.453Z
  • recipient receipt: DELIVERED_TO:830b1179-a987-45f6-a056-c121089c0433@neo-opus-vega
  • receipt state at audit: readAt:null, archivedAt:null
  • fresh GraphLog position: 5829858

The message was real and unread, but roughly 25 hours old when its fresh delivery-projection edge made it eligible for a live interruption. This is not a missing-write or sender-misattribution defect. It is the Shape-B successor of #15704: the canonical one-hour message-wake horizon exists, but the signed webhook/coalescing path does not consume it.

Evidence is head-scoped to 135147438812c828001c8fc06d15d525457a5ec6 on origin/dev.

The Problem

Mailbox retention and wake urgency are different contracts. An old unread message should remain listable for explicit recovery, but a fresh GraphLog projection edge must not turn that historical fact into current interruption urgency.

Shape B currently treats a new graph delta as a new wake candidate even when the MESSAGE node's server-stamped sentAt is already outside the one-hour horizon. The resulting digest can count the stale event, promote its subject to latest, inherit its priority, and wake a seat for work that is no longer current.

The bounded-search retractions matter because they remove the wrong premise—there was no phantom row—but they do not remove this measured stale-replay mechanism.

The Architectural Reality

  • WakeSubscriptionService.init() starts the live cursor at the current GraphLog head, then pump() evaluates later deltas and enqueues Shape-A/Shape-B matches (ai/services/memory-core/WakeSubscriptionService.mjs:194-208,238-276).
  • heartbeatPulseEvaluator.buildSentToMeInner() carries the canonical MESSAGE sentAt into both wake call sites (ai/services/memory-core/heartbeatPulseEvaluator.mjs:175-191).
  • wakeCoalescePolicy.isMessageWakeFresh() already owns the one-hour, canonical-ISO, fail-closed predicate and explicitly rejects GraphLog insertion time as authored age (ai/services/memory-core/wakeCoalescePolicy.mjs:25-36,60-94).
  • The legacy daemon imports and applies that predicate before initial digest delivery and retry (ai/daemons/wake/daemon.mjs:75-80,879-895,1024-1035).
  • Shape B queues the full event and, at flush, reconciles readAt / positively missing rows before deriving counts, pointers, and priority (ai/services/memory-core/CoalescingEngineService.mjs:297-315,499-555). It never calls isMessageWakeFresh.
  • PR #16918's read/missing reconciliation is present and remains correct. Freshness is an additive admission boundary, not a rollback of that repair.
  • The signed receiver verified and persisted the envelope correctly. It is the carrier, not the origin of the stale eligibility.

The Agent OS structure-map gate was attempted immediately before filing and failed before producing output with Cannot create a string longer than 0x1fffffe8 characters. Placement is nevertheless bounded to existing sibling modules—wakeCoalescePolicy.mjs and CoalescingEngineService.mjs; this ticket introduces no new file or directory.

The Fix

  1. Apply the existing isMessageWakeFresh authority to Shape-B wake/sent_to_me events at flush time, using one captured now per partition.
  2. Remove expired, future, missing-timestamp, and malformed-timestamp message events before the digest is built. Suppressed events must not contribute to totalEvents, sourceEventIds, digest identity, logId, message count, latest, or priority.
  3. If every queued event is suppressed, consume the queue without dispatching a zero-event wake.
  4. Preserve all non-message events and still-fresh message events in mixed queues.
  5. Preserve mailbox state: freshness suppression never reads, archives, deletes, or rewrites the MESSAGE/receipt.
  6. Emit bounded observability for suppressed stale messages (count and oldest provable age; no subject/body).

No new config leaf and no new policy definition: Shape B consumes the same mechanism ceiling already shared with the daemon.

Contract Ledger

Target Surface Source of Authority Proposed Behavior Fallback / Edge Case Docs Evidence
Shape-B MESSAGE wake admission wakeCoalescePolicy.isMessageWakeFresh Canonical MESSAGE timestamps are admitted only inside the existing closed one-hour horizon Missing, malformed, numeric, or future timestamps fail closed for wake only Existing policy JSDoc + consumer JSDoc pure boundary fixtures + integrated coalescer witness
Shape-B digest population CoalescingEngineService._flush / _buildDigestEnvelope Filter stale MESSAGE events before every aggregate and envelope identity field Mixed task/permission/heartbeat events still deliver unchanged; all-suppressed queues dispatch nothing method JSDoc mixed stale-high/fresh-normal and all-stale witnesses
Read/missing reconciliation PR #16918's injected delivery-state resolver Existing committed-read suppression and missing-row pointer rules remain intact for fresh candidates Resolver unavailable preserves current fail-safe behavior for fresh events; it does not override canonical timestamp failure existing JSDoc rolled-back read-state and missing-row positive controls stay green
Mailbox retention MESSAGE + per-recipient receipt graph Wake expiry has no mailbox mutation Old unread messages remain available to explicit list/drain operations none assert the exact stale row remains unread and listable
Suppression telemetry Shape-B coalescing service Log bounded count + oldest provable age, never content Invalid timestamps count as suppressed but have no age value JSDoc logger fixture contains no subject/body

Decision Record impact

none. This closes parity drift between two consumers of an already-settled wake-safety policy. It does not change topology, transport, persistence, or an ADR.

Acceptance Criteria

  • Exact regression: a real unread MESSAGE older than MESSAGE_WAKE_MAX_AGE_MS plus a fresh DELIVERED_TO GraphLog edge produces no Shape-B wake.
  • The stale MESSAGE remains unread and listable after suppression.
  • A mixed queue containing one stale high-priority message and one fresh normal-priority message reports exactly one normal-priority message; the stale subject/id affects no envelope field.
  • Missing, malformed, numeric, and future sentAt values fail closed for Shape-B wake delivery.
  • An all-suppressed queue emits no zero-event digest and is not re-enqueued by the same flush.
  • Task, permission, and heartbeat events remain unaffected in mixed queues.
  • Existing PR #16918 read-state and missing-row regression witnesses remain green.
  • Suppression telemetry reports only bounded count/age facts and never message content.
  • Focused wake/coalescer unit suites pass.

Out of Scope

  • Reopening #15704 or #16541; both closed tickets remain authoritative history.
  • The bounded list_messages absence trap repaired by #16888.
  • Changing mailbox retention, read, archive, or deletion policy.
  • Sender-liveness gating.
  • Host receiver process freshness or renderer wording.
  • A new wake timeout/configuration knob.
  • Legacy daemon behavior, which already applies the canonical age predicate.

Avoided Traps

  • Treating unread as synonymous with new.
  • Dating the message from the fresh GraphLog projection edge.
  • Marking old mail read to silence wake noise.
  • Reverting PR #16918's read/missing reconciliation.
  • Blanketing only latest while stale events still inflate totalEvents, sourceEventIds, digest identity, or priority.
  • Dispatching a zero-event envelope after every message was suppressed.

Related

#15704 · PR #15705 · #16541 · PR #16918 · #16888 · #16906

Live latest-open sweep: checked the latest 20 open issues at 2026-08-11T23:35:53.486Z; no equivalent found.

A2A in-flight claim sweep: checked the latest 30 messages across all read states at 2026-08-11T23:35:53.486Z; no competing recent [lane-claim] / [lane-intent] found.

Retrieval Hint: query_raw_memories("Shape-B stale unread wake fresh DELIVERED_TO one-hour CoalescingEngine MESSAGE:2c558f17"); exact source head 135147438812c828001c8fc06d15d525457a5ec6.