LearnNewsExamplesServices
Frontmatter
id10257
titleMailboxService read paths must consume WAL delta before iterating in-memory edges
stateClosed
labels
bugaiarchitecturecore
assigneesneo-opus-ada
createdAtApr 23, 2026, 8:38 PM
updatedAtJun 7, 2026, 7:20 PM
githubUrlhttps://github.com/neomjs/neo/issues/10257
authorneo-opus-ada
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
blockedBy[]
blocking[]
closedAtApr 23, 2026, 10:46 PM

MailboxService read paths must consume WAL delta before iterating in-memory edges

Closed v13.0.0/archive-v13-0-0-chunk-5 bugaiarchitecturecore
neo-opus-ada
neo-opus-ada commented on Apr 23, 2026, 8:38 PM

Context

Empirically reproducible A2A failure this session: my MCP process (Claude Code, session 7ad90eb3, uptime ~5800s) cannot see MESSAGE:5b1aaa9b-... (Gemini's authenticated reply to my #9999 coordination broadcast) via list_messages, even though:

  • The message node + SENT_BY@neo-gemini-pro edge + SENT_TO@neo-opus-ada edge are all present in SQLite (verified via raw better-sqlite3 query)
  • The GraphLog WAL-delta records show entries 410767-410768 for the message and its edges
  • The #10190 syncCache substrate-coherence fix (Gemini's PR #10221) landed weeks ago and is live on dev

Gemini reported the same symptom from her side: she had to manually SQLite-query the graph to read my coordination broadcast (MESSAGE:f481a945). Her MCP's list_messages didn't surface it either. Both processes diverged from SQLite ground truth for mailbox-scope reads.

Tobi's directive: "manually querying the graph is NOT the goal. we need to get a2a finally working."

The Problem

MailboxService.listMessages, getMessage, markRead, getHealthcheckPreview, and the reachable-counterparty trust-lift path in addMessage all iterate GraphService.db.edges.items directly without calling syncCache() first. They see whatever edges were in the in-memory collection at the moment of call — which, for long-running MCP processes in a multi-harness swarm, is stale relative to SQLite.

The #10190 / PR #10221 syncCache fix made delta replay work correctly, but the invocation point is getAdjacentNodes (graph-vicinity lookup). Mailbox operations are edge-type scans, not vicinity lookups — they bypass getAdjacentNodes and therefore never trigger syncCache. The WAL delta that carries peer writes is consumed only when some other code path happens to call getAdjacentNodes.

Result: messages written by MCP process A remain invisible to process B's mailbox read tools, even though SQLite has them. The only workarounds operators have today are (a) restart the MCP process, forcing a fresh graph load, or (b) raw SQLite query — exactly what tobi flagged as "not the goal."

Empirical Reproduction

Fresh broadcast via ai/mcp/client/mcp-cli.mjs from my process produces a correctly-persisted SQLite node with SENT_BY, SENT_TO, and 5 TAGGED_CONCEPT edges. Write-path is healthy. But my process's list_messages all returns stale cache + misses peer writes:

Message My cache SQLite
90d6ad06 (just written by me)
a66a2927 (my earlier broadcast) ✗ (lost during git chaos — one-off)
50d478fb (Gemini's pre-fix hello)
5b1aaa9b (Gemini's authenticated reply) ✗ MISSING

The Architectural Reality

ai/mcp/server/memory-core/services/MailboxService.mjs:

  • Line 119-123 — addMessage reachable-counterparty trust-lift (only in 'blocked' mode per #10252)
  • Line 225 — listMessages primary edge iteration
  • Line 258 — listMessages SENT_BY/SENT_TO/PART_OF_THREAD resolution loop (nested)
  • Line 312 — getMessage edge iteration
  • Line 370 — markRead edge iteration

ai/mcp/server/memory-core/services/GraphService.mjs + ai/graph/Database.mjs:

  • GraphService.db.syncCache() consumes GraphLog delta; invalidates cached nodes/edges that peer processes mutated
  • Currently auto-fires inside Database.getAdjacentNodes(nodeId, direction) (line ~267)
  • Mailbox edge-type scans never traverse getAdjacentNodessyncCache never fires on those paths

The Fix

Minimal, surgical. Each mailbox read path that iterates db.edges.items / db.nodes.items must call syncCache() once at entry before the iteration. Four call sites in MailboxService.mjs: listMessages, getMessage, markRead, and the addMessage trust-lift ('blocked'-mode only).

async listMessages({...}) {
    const me = RequestContextService.getAgentIdentityNodeId();
    if (!me) throw new Error(...);

    // Consume WAL delta before iterating in-memory edges.items — ensures peer-process
    // writes (other harnesses, other agents) are visible to this read. Without this,
    // the iteration sees only this process's cached edges at last read time; messages
    // written by peers remain invisible until some other code path happens to call
    // getAdjacentNodes (which does trigger syncCache at Database.mjs:~267).
    GraphService.db.syncCache?.();

    // ... existing logic unchanged
}

Acceptance Criteria

  • MailboxService.listMessages calls GraphService.db.syncCache?.() at entry before db.edges.items iteration
  • MailboxService.getMessage same pattern
  • MailboxService.markRead same pattern
  • MailboxService.addMessage's reachable-counterparty trust-lift same pattern (active in 'blocked' mode)
  • MailboxService.getHealthcheckPreview receives the fix transitively via listMessages OR has its own call if it iterates independently
  • Regression test: two MCP processes against the same SQLite, process A writes a message, process B's listMessages returns it without needing a restart or a prior get_node trigger
  • Empirical validation: after merge, send a message from ai/mcp/client/mcp-cli.mjs; Claude Code's and Antigravity's list_messages both surface it cleanly without harness restart

Out of Scope

  • Refactoring MailboxService to use getAdjacentNodes instead of edges.items scan — larger architectural lift; defer unless the syncCache-on-entry pattern proves insufficient empirically
  • Auto-fire syncCache on every db.edges.items / db.nodes.items access via Proxy wrapping — invasive; defensive but adds per-access overhead
  • Per-tool dispatch-level syncCache at the MCP request layer (e.g., in ToolService.callTool) — cleaner architecturally but broader blast radius
  • Write-path propagation verification for MESSAGE:f481a945 (my earlier broadcast with missing edges) — one-off casualty of tobi's git-pull sequence

Avoided Traps

  • Polling syncCache on a timer: rejected. Introduces latency floor + wastes cycles when idle; per-read pull is strictly superior for mailbox workload
  • Kill syncCache and force SQLite-direct reads: rejected. In-memory cache is load-bearing for every other graph operation
  • Bump MailboxService to getAdjacentNodes: considered, rejected. getAdjacentNodes is vicinity-centric; mailbox queries are edge-type scans that don't map cleanly onto vicinity semantics

Related

  • #10190 / PR #10221 — substrate-level syncCache fix (Gemini) — this ticket extends the invocation surface to cover mailbox read paths
  • #10179 — reachable-counterparty trust-lift — this ticket's addMessage fix ensures trust-lift iteration sees fresh SENT_TO edges in 'blocked' mode
  • #10252 / PR #10253 — config-gated reply policy — addMessage fix only matters in 'blocked' mode
  • #10139 — A2A mailbox primitive (parent) — unblocks "A2A end-to-end works without harness restart"
  • #9999 — multi-user Memory Core (grand-parent) — multi-user deployments will exercise cross-process coherence far more aggressively

Origin Session ID

Origin Session ID: 7ad90eb3-2218-4200-977b-fbfe546d64a8

Handoff Retrieval Hints

Retrieval Hint: "MailboxService listMessages stale cache syncCache cross-process WAL delta" Retrieval Hint: "A2A not working manually querying SQLite" Retrieval Hint: "edges.items iteration without syncCache" Commit-range anchor: dev HEAD 57145b0ac at ticket filing

tobiu referenced in commit a525396 - "fix(memory-core): mailbox read paths must exercise syncCache + vicinity reload (#10257) (#10258) on Apr 23, 2026, 10:46 PM
tobiu closed this issue on Apr 23, 2026, 10:46 PM