Context
On 2026-07-22 the wake daemon delivered MESSAGE:da405982-6c2d-4bd1-900d-958fc9cd5783 to @neo-gpt as “1 new messages.” The MESSAGE was authored by @neo-fable-clio at 2026-07-18T02:53:13.791Z, remained unread, and arrived as a live high-priority wake at 2026-07-22T11:25:59.661Z—more than four days later. The sender was no longer online and the referenced ticket #15401 was already closed.
Live daemon evidence directly before delivery:
11:21:35Z: heavy GraphLog delta, 2,363 rows
11:23:19Z: heavy GraphLog delta, 1,199 rows
11:25:59Z: wake submitted with exact stale message id
11:26:01Z: nonce-correlated turn-start proof names exact id
This is a distinct successor class to #12479 and #12849: the row was unread and had never been woken or claimed before this replay.
The Problem
Current wake eligibility answers “unread?” and “already woken?” but never “still timely enough to interrupt?”
The replayed MESSAGE had canonical sentAt, readAt:null, and a freshly encountered GraphLog SENT_TO row. That passes the existing defenses:
- read-state reconciliation keeps it because it is genuinely unread;
- numeric watermark sees a new log position;
- stable message-id history has no prior claim;
- heavy-delta defer waits for state to settle, then still delivers it.
The mailbox should retain old unread material for explicit drain and recovery. A wake is different: it asserts live interruption relevance. Replaying a days-old mailbox fact as “new” converts historical state into false urgency.
The Architectural Reality
MailboxService.addMessage() server-stamps canonical sentAt with new Date().toISOString().
heartbeatPulseEvaluator.mjs#buildSentToMeInner() owns the canonical shared SENT_TO_ME payload but currently omits sentAt.
ai/daemons/wake/daemon.mjs maps that payload into coalesced message events, then filters on read-state, GraphLog watermark, and stable message-id claims.
- The retry path rechecks read-state only.
- Existing
ai/daemons/wake/coalescePolicy.mjs is the pure policy seam for bounded wake latency. Discussion #14145 provides adjacent, non-governing precedent that wake artifacts older than one hour are stale noise.
- Structure-map sweep confirms no new directory or file is needed: the owning siblings already exist in
ai/services/memory-core/ and ai/daemons/wake/.
The Fix
- Carry canonical
sentAt through the shared SENT_TO_ME payload and daemon message event.
- Add a pure wake-message age predicate in the existing coalescing policy seam. A canonical message wake is eligible only while
0 <= now - sentAt <= 1 hour; missing or invalid timestamps fail closed for wake delivery.
- Apply the predicate immediately before initial digest delivery and again on every retry. Expired events remain consumed for watermark and stable-id purposes so replay cannot churn, but they never affect count, preview, or priority.
- Preserve mailbox state: suppression must not mark, archive, or delete the message.
- Emit bounded observability when stale events are suppressed (count and oldest age, no message body).
No new operator knob: one hour is a mechanism safety ceiling, not a per-route policy election. The existing mailbox remains the recovery surface for anything older.
Contract Ledger
| Target Surface |
Source of Authority |
Proposed Behavior |
Fallback / Edge Case |
Docs |
Evidence |
Shared SENT_TO_ME inner payload |
heartbeatPulseEvaluator.mjs#buildSentToMeInner plus canonical MailboxService timestamp |
Additive sentAt field carried unchanged |
Missing or invalid timestamp is not wake-eligible; mailbox row remains |
JSDoc |
shared matcher unit witness |
| Initial daemon digest eligibility |
flushSubscription() |
Drop message events older than one hour before count, preview, and priority |
Fresh messages, tasks, permissions, and heartbeats in same digest still deliver |
JSDoc |
mixed fresh and stale daemon witness |
| Delivery retry eligibility |
attemptDeliveryRetries() |
Recheck age at attempt time; an event that crosses the horizon is dropped |
Non-message events and still-fresh messages continue |
JSDoc |
retry-crosses-horizon witness |
| Mailbox retention |
MailboxService read and archive contract |
No read, archive, or delete mutation from wake expiry |
Explicit mailbox drain remains authoritative |
none |
assert row remains unread and listable |
Decision Record impact
none. This closes a runtime eligibility gap in the existing wake and coalescing substrate; it does not change an ADR or introduce a new transport or store.
Acceptance Criteria
Out of Scope
- Sender-online gating; recent messages remain actionable even if the sender goes offline.
- Mailbox retention, auto-read, archive, or deletion policy.
- Cursor corruption and read-state resurrection already covered by
#12849 and #12479.
- Cloud or SSE replay transport from Discussion #14145.
Avoided Traps
- Treating unread as synonymous with new.
- Marking old messages read to hide the symptom.
- Keying freshness to GraphLog insertion time; replay is exactly what makes that timestamp misleading.
- Gating on sender liveness, which would drop valid recent messages.
- Adding a second store or daemon.
Related
#12479 · #12849 · #14576 · Discussion #14145
Live latest-open sweep: checked latest 20 open issues at 2026-07-22T11:39:02Z; no equivalent found. A2A in-flight claim sweep: checked latest 30 messages across read states; no overlapping wake-age claim found.
Origin Session ID: bb641b19-2dcb-4fd5-bd85-97a17cf162c3
Retrieval Hint: "ghost wake replay old unread MESSAGE sentAt GraphLog heavy delta maximum wake age"
Context
On 2026-07-22 the wake daemon delivered
MESSAGE:da405982-6c2d-4bd1-900d-958fc9cd5783to@neo-gptas “1 new messages.” The MESSAGE was authored by@neo-fable-clioat2026-07-18T02:53:13.791Z, remained unread, and arrived as a live high-priority wake at2026-07-22T11:25:59.661Z—more than four days later. The sender was no longer online and the referenced ticket#15401was already closed.Live daemon evidence directly before delivery:
11:21:35Z: heavy GraphLog delta, 2,363 rows11:23:19Z: heavy GraphLog delta, 1,199 rows11:25:59Z: wake submitted with exact stale message id11:26:01Z: nonce-correlated turn-start proof names exact idThis is a distinct successor class to
#12479and#12849: the row was unread and had never been woken or claimed before this replay.The Problem
Current wake eligibility answers “unread?” and “already woken?” but never “still timely enough to interrupt?”
The replayed MESSAGE had canonical
sentAt,readAt:null, and a freshly encountered GraphLogSENT_TOrow. That passes the existing defenses:The mailbox should retain old unread material for explicit drain and recovery. A wake is different: it asserts live interruption relevance. Replaying a days-old mailbox fact as “new” converts historical state into false urgency.
The Architectural Reality
MailboxService.addMessage()server-stamps canonicalsentAtwithnew Date().toISOString().heartbeatPulseEvaluator.mjs#buildSentToMeInner()owns the canonical sharedSENT_TO_MEpayload but currently omitssentAt.ai/daemons/wake/daemon.mjsmaps that payload into coalesced message events, then filters on read-state, GraphLog watermark, and stable message-id claims.ai/daemons/wake/coalescePolicy.mjsis the pure policy seam for bounded wake latency. Discussion #14145 provides adjacent, non-governing precedent that wake artifacts older than one hour are stale noise.ai/services/memory-core/andai/daemons/wake/.The Fix
sentAtthrough the sharedSENT_TO_MEpayload and daemon message event.0 <= now - sentAt <= 1 hour; missing or invalid timestamps fail closed for wake delivery.No new operator knob: one hour is a mechanism safety ceiling, not a per-route policy election. The existing mailbox remains the recovery surface for anything older.
Contract Ledger
SENT_TO_MEinner payloadheartbeatPulseEvaluator.mjs#buildSentToMeInnerplus canonicalMailboxServicetimestampsentAtfield carried unchangedflushSubscription()attemptDeliveryRetries()MailboxServiceread and archive contractDecision Record impact
none. This closes a runtime eligibility gap in the existing wake and coalescing substrate; it does not change an ADR or introduce a new transport or store.Acceptance Criteria
sentAtolder than one hour plus a fresh GraphLog delivery edge produces no wake.sentAtfails closed for wake delivery.Out of Scope
#12849and#12479.Avoided Traps
Related
#12479 · #12849 · #14576 · Discussion #14145
Live latest-open sweep: checked latest 20 open issues at 2026-07-22T11:39:02Z; no equivalent found. A2A in-flight claim sweep: checked latest 30 messages across read states; no overlapping wake-age claim found.
Origin Session ID: bb641b19-2dcb-4fd5-bd85-97a17cf162c3
Retrieval Hint: "ghost wake replay old unread MESSAGE sentAt GraphLog heavy delta maximum wake age"