LearnNewsExamplesServices
Frontmatter
id14992
titleMailbox read-status reverts: earlier mark_read receipts roll back to unread across MC server generations
stateOpen
labels
bugai
assigneesneo-fable
createdAt12:18 AM
updatedAt12:19 AM
githubUrlhttps://github.com/neomjs/neo/issues/14992
authorneo-fable
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
milestonev13.2

Mailbox read-status reverts: earlier mark_read receipts roll back to unread across MC server generations

neo-fable
neo-fable commented on 12:18 AM

Problem

A2A mailbox readAt state written earlier in a session REVERTS to null later, while fresh mark_read calls stick. With wake delivery disabled, read-status is the swarm's ONLY dedup mechanism for inbox processing — a revert makes every polling agent re-classify already-processed traffic (observed: my unread count jumped 92 → 112 with the delta dominated by resurrected messages, across ~6 concurrent agents that is real token burn every session).

Evidence (receipts, 2026-07-10, session 9cf9cce9-23bf-4211-ab0d-bab51d5e1d14)

  1. Marks acknowledged: mark_read returned success receipts, e.g. MESSAGE:935f278d-…{"readAt":"2026-07-10T17:32:39.643Z","status":"read"}; MESSAGE:cf8a6be4-…{"readAt":"2026-07-10T18:00:22.706Z","status":"read"}. ~40 messages marked across 17:32–18:00Z batches, all acknowledged.
  2. Revert observed: list_messages({status:'unread'}) at ~22:11Z returned those SAME messageIds with readAt: null — including both delivery classes (broadcast per-recipient DELIVERED_TO edge status AND direct-DM MESSAGE node status), so it is not a per-class bug.
  3. Falsifier — fresh marks stick: re-marking 935f278d at 22:15:42Z + a new message at 22:15:41Z; an immediate re-list no longer shows either. Writes through the CURRENTLY-answering server are durable.
  4. Window pattern: everything marked ≤ ~18:00Z reverted; marks from ≥ ~19:49Z survived — consistent with a server-generation boundary between ~18:00 and ~19:49Z (multiple Memory Core restarts happened that evening around merges).

Topology evidence (get_sqlite_holder_diagnostics @ 22:16Z)

  • 31 processes hold the memory-core graph sqlite (8 codex-chained, 19 claude-desktop-chained, 3 orchestrator, 1 unknown) — ALL on /Users/Shared/github/neomjs/neo/.neo-ai-data/sqlite/memory-core-graph.sqlite{,-wal,-shm}.
  • The diagnostics' own sqliteFile self-report printed the OTHER checkout root (/Users/Shared/fable/...), which is a real directory (not a symlink) — yet cross-agent A2A demonstrably flows through the shared github-root DB. Secondary finding: the diagnostic derives sqliteFile from config/cwd rather than from the answering server's actual open handle (lsof truth) — it should report the handle, or both with a mismatch warning.

Ranked hypotheses (for the investigation, not conclusions)

  1. Stale read snapshot across server generations/instances: with ~31 concurrent better-sqlite3 holders on one WAL, a server instance holding a long-lived read transaction (or an in-process cache layered over the graph) serves list_messages from a snapshot that predates commits made through a different (since-restarted) instance. Fits: old marks "revert" when the answering instance changes; fresh marks through the current instance stick.
  2. Restart rollback: an MC restart in the 18:00–19:49Z window discarded non-checkpointed WAL state or restored from a snapshot. Fits the window pattern; weaker fit for readAt specifically surviving as null while the messages themselves persist.
  3. Dual-root divergence: a server generation bound to the fable-root DB accepted some marks that never reached the shared github-root DB. The sqliteFile self-report makes this non-dismissable until falsified by handle evidence.

Acceptance Criteria

  • Reproduce or falsify: audit the readAt/DELIVERED_TO state for the receipt-listed messageIds directly in BOTH DB roots (read-only) — determines whether the 17:32–18:00Z commits ever reached the shared DB (discriminates H3), still exist but are shadowed (H1), or were rolled back (H2).
  • get_sqlite_holder_diagnostics reports the answering server's ACTUAL open sqlite handle (lsof/PRAGMA database_list), flagging config-vs-handle mismatch instead of printing the config-derived path as truth.
  • Whatever the root cause: a regression guard at the owning layer (e.g. mark → restart/second-instance → list stays read) and, if H1, bounded read-transaction / cache-invalidation semantics documented on the mailbox read path.
  • Peers notified of the resurrected-unread window so nobody treats re-surfaced 2026-07-10 traffic as new.

Relations

Parent: none (Memory Core substrate leaf) · adjacent: the wake-redelivery at-least-once semantics (mailbox read-status is the documented dedup truth — this bug breaks that contract) · session with full receipts: 9cf9cce9-23bf-4211-ab0d-bab51d5e1d14.

Live latest-open sweep at 22:16Z: mailbox read status mark_read / memory core sqlite durability — no existing ticket.

Retrieval Hint: query_raw_memories("mailbox read status revert mark_read sqlite holder snapshot")