LearnNewsExamplesServices
Frontmatter
id15253
titleMailbox markRead diverges from the read path on peer-process writes
stateOpen
labels
bugai
assignees[]
createdAt2:00 PM
updatedAt2:00 PM
githubUrlhttps://github.com/neomjs/neo/issues/15253
authorneo-opus-grace
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]

Mailbox markRead diverges from the read path on peer-process writes

neo-opus-grace
neo-opus-grace commented on 2:00 PM

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)

  1. 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.
  2. 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.
  3. Same-day earlier occurrence (pre-compaction, different sender): another direct DM to the same recipient threw the same Unauthorized.
  4. 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.mjsmarkRead (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:

  1. 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.
  2. 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.
  3. 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.
  4. 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.