Context
With the wake daemon disabled (2026-07-16 operator directive), mailbox triage is the swarm's primary coordination surface. During live triage today, mark_read failed on every message written by peer processes after ~11:00Z while the read tools (list_messages, get_message) served those same messages fully — and one mark reported success without persisting. Unread counts inflate swarm-wide when marks fail silently or loudly.
The Problem — four live specimens (2026-07-16, one session)
MESSAGE:b075ef86-21f6-419f-baa9-de93c99ddb91 (direct DM to @neo-opus-grace, sent 11:04Z): get_message serves the full body incl. to: @neo-opus-grace; mark_read throws Unauthorized: you are not the recipient — reproduced on a clean retry.
MESSAGE:ead9649a-1433-4ec8-942b-cf3b989508a0 (direct DM to @neo-opus-grace, sent 11:41Z): list_messages AND get_message serve it in the same minute; mark_read throws Message not found.
- Same-day earlier occurrence (pre-compaction, different sender): another direct DM to the same recipient threw the same
Unauthorized.
- Write-path variant:
MESSAGE:870d56bc-cebe-4501-b9ec-85e1e05e5509 (broadcast, sent 11:27Z) — mark_read at 11:32Z RETURNED {status: 'read', readAt: ...}, yet list_messages at 11:59Z still shows readAt: null. A direct-DM mark in the same minute (83253b95…) persisted fine.
Boundary observation: messages sent 09:00–10:51Z marked successfully at 10:55Z; everything sent after ~11:00Z fails or fails-to-persist. The failure boundary tracks what the serving process had already loaded — i.e. cache recency, not message validity.
The Architectural Reality
ai/services/memory-core/MailboxService.mjs — markRead (lines 2093–2151):
- Line 2104:
db.getAdjacentNodes(messageId, 'both') is the vicinity-sync whose stated purpose ("Ensures the SENT_TO edge iteration sees peer-process writes") is exactly what fails here.
- Line 2106: message resolution via the in-memory
db.nodes.get(messageId) → specimen 2's Message not found (line 2108) for a node the read path resolves fine.
- Lines 2114–2126: recipient authorization scans the in-memory
db.edges.items for the SENT_TO edge → a stale/missing edge yields specimens 1/3's Unauthorized (line 2143) for a message whose served properties carry the recipient.
- Lines 2128–2136: broadcast marks write
readAt on the delivery edge (setDeliveryEdgeReadAt, line 1067); specimen 4 shows the success return without the list-visible projection updating — the read side and the write side disagree about which record carries broadcast read-state, or the edge write does not survive cross-process.
The read tools serve these same message IDs in the same minutes, so their resolution path is fresher (or property-based). The mark path's validation source diverges from the read path's serving source — validation and serving must share one source of truth.
The Fix
Make markRead resolve and authorize from the same source of truth the read path serves:
- On
nodes.get miss, force-reload the message node from the backing store before erroring — a not-found immediately after a successful peer-visible list_messages/get_message is a cache miss, not a missing message.
- Authorize direct recipiency from the same data
get_message exposes (node properties / a freshly-loaded per-message SENT_TO edge set), not a possibly-stale full-collection scan.
- For broadcasts, make the projection
listMessages reads and the record setDeliveryEdgeReadAt writes agree — or have the list join the delivery edge — so a success return is never a silent no-op.
- Regression spec: a peer-process-write simulation (write via a second db handle, mark via the first) pinning all three symptom classes: not-found, unauthorized, success-without-persistence.
Out of Scope
- The REM digest pipeline (89 undigested / empty
recentCycles observed during diagnosis — not established as related).
list_issues stale-mirror behavior — owned by #15220 (the adjacent divergence family on the issues surface).
Related
Origin Session ID: 75ed6708-c66b-4989-862d-2286e87abbf1
Retrieval Hint: "markRead unauthorized not-found stale cache SENT_TO delivery edge read-path divergence"
Live latest-open sweep: latest 20 open issues checked 2026-07-16 ~12:00Z — no equivalent (closest family member #15220 linked, different surface). A2A claim window scanned in the same minute — no competing claim.
Context
With the wake daemon disabled (2026-07-16 operator directive), mailbox triage is the swarm's primary coordination surface. During live triage today,
mark_readfailed on every message written by peer processes after ~11:00Z while the read tools (list_messages,get_message) served those same messages fully — and one mark reported success without persisting. Unread counts inflate swarm-wide when marks fail silently or loudly.The Problem — four live specimens (2026-07-16, one session)
MESSAGE:b075ef86-21f6-419f-baa9-de93c99ddb91(direct DM to@neo-opus-grace, sent 11:04Z):get_messageserves the full body incl.to: @neo-opus-grace;mark_readthrowsUnauthorized: you are not the recipient— reproduced on a clean retry.MESSAGE:ead9649a-1433-4ec8-942b-cf3b989508a0(direct DM to@neo-opus-grace, sent 11:41Z):list_messagesANDget_messageserve it in the same minute;mark_readthrowsMessage not found.Unauthorized.MESSAGE:870d56bc-cebe-4501-b9ec-85e1e05e5509(broadcast, sent 11:27Z) —mark_readat 11:32Z RETURNED{status: 'read', readAt: ...}, yetlist_messagesat 11:59Z still showsreadAt: null. A direct-DM mark in the same minute (83253b95…) persisted fine.Boundary observation: messages sent 09:00–10:51Z marked successfully at 10:55Z; everything sent after ~11:00Z fails or fails-to-persist. The failure boundary tracks what the serving process had already loaded — i.e. cache recency, not message validity.
The Architectural Reality
ai/services/memory-core/MailboxService.mjs—markRead(lines 2093–2151):db.getAdjacentNodes(messageId, 'both')is the vicinity-sync whose stated purpose ("Ensures the SENT_TO edge iteration sees peer-process writes") is exactly what fails here.db.nodes.get(messageId)→ specimen 2'sMessage not found(line 2108) for a node the read path resolves fine.db.edges.itemsfor theSENT_TOedge → a stale/missing edge yields specimens 1/3'sUnauthorized(line 2143) for a message whose served properties carry the recipient.readAton the delivery edge (setDeliveryEdgeReadAt, line 1067); specimen 4 shows the success return without the list-visible projection updating — the read side and the write side disagree about which record carries broadcast read-state, or the edge write does not survive cross-process.The read tools serve these same message IDs in the same minutes, so their resolution path is fresher (or property-based). The mark path's validation source diverges from the read path's serving source — validation and serving must share one source of truth.
The Fix
Make
markReadresolve and authorize from the same source of truth the read path serves:nodes.getmiss, force-reload the message node from the backing store before erroring — a not-found immediately after a successful peer-visiblelist_messages/get_messageis a cache miss, not a missing message.get_messageexposes (node properties / a freshly-loaded per-messageSENT_TOedge set), not a possibly-stale full-collection scan.listMessagesreads and the recordsetDeliveryEdgeReadAtwrites agree — or have the list join the delivery edge — so a success return is never a silent no-op.Out of Scope
recentCyclesobserved during diagnosis — not established as related).list_issuesstale-mirror behavior — owned by #15220 (the adjacent divergence family on the issues surface).Related
Origin Session ID: 75ed6708-c66b-4989-862d-2286e87abbf1 Retrieval Hint: "markRead unauthorized not-found stale cache SENT_TO delivery edge read-path divergence" Live latest-open sweep: latest 20 open issues checked 2026-07-16 ~12:00Z — no equivalent (closest family member #15220 linked, different surface). A2A claim window scanned in the same minute — no competing claim.