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
getAdjacentNodes → syncCache 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(...);
GraphService.db.syncCache?.();
}Acceptance Criteria
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
Context
Empirically reproducible A2A failure this session: my MCP process (Claude Code, session
7ad90eb3, uptime ~5800s) cannot seeMESSAGE:5b1aaa9b-...(Gemini's authenticated reply to my #9999 coordination broadcast) vialist_messages, even though:SENT_BY→@neo-gemini-proedge +SENT_TO→@neo-opus-adaedge are all present in SQLite (verified via rawbetter-sqlite3query)GraphLogWAL-delta records show entries410767-410768for the message and its edgessyncCachesubstrate-coherence fix (Gemini's PR #10221) landed weeks ago and is live on devGemini reported the same symptom from her side: she had to manually SQLite-query the graph to read my coordination broadcast (
MESSAGE:f481a945). Her MCP'slist_messagesdidn'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 inaddMessageall iterateGraphService.db.edges.itemsdirectly without callingsyncCache()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
syncCachefix made delta replay work correctly, but the invocation point isgetAdjacentNodes(graph-vicinity lookup). Mailbox operations are edge-type scans, not vicinity lookups — they bypassgetAdjacentNodesand therefore never triggersyncCache. The WAL delta that carries peer writes is consumed only when some other code path happens to callgetAdjacentNodes.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.mjsfrom my process produces a correctly-persisted SQLite node withSENT_BY,SENT_TO, and 5TAGGED_CONCEPTedges. Write-path is healthy. But my process'slist_messages allreturns stale cache + misses peer writes:90d6ad06(just written by me)a66a2927(my earlier broadcast)50d478fb(Gemini's pre-fix hello)5b1aaa9b(Gemini's authenticated reply)The Architectural Reality
ai/mcp/server/memory-core/services/MailboxService.mjs:addMessagereachable-counterparty trust-lift (only in'blocked'mode per #10252)listMessagesprimary edge iterationlistMessagesSENT_BY/SENT_TO/PART_OF_THREAD resolution loop (nested)getMessageedge iterationmarkReadedge iterationai/mcp/server/memory-core/services/GraphService.mjs+ai/graph/Database.mjs:GraphService.db.syncCache()consumes GraphLog delta; invalidates cached nodes/edges that peer processes mutatedDatabase.getAdjacentNodes(nodeId, direction)(line ~267)getAdjacentNodes→syncCachenever fires on those pathsThe Fix
Minimal, surgical. Each mailbox read path that iterates
db.edges.items/db.nodes.itemsmust callsyncCache()once at entry before the iteration. Four call sites in MailboxService.mjs:listMessages,getMessage,markRead, and theaddMessagetrust-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.listMessagescallsGraphService.db.syncCache?.()at entry beforedb.edges.itemsiterationMailboxService.getMessagesame patternMailboxService.markReadsame patternMailboxService.addMessage's reachable-counterparty trust-lift same pattern (active in'blocked'mode)MailboxService.getHealthcheckPreviewreceives the fix transitively vialistMessagesOR has its own call if it iterates independentlylistMessagesreturns it without needing a restart or a priorget_nodetriggerai/mcp/client/mcp-cli.mjs; Claude Code's and Antigravity'slist_messagesboth surface it cleanly without harness restartOut of Scope
getAdjacentNodesinstead ofedges.itemsscan — larger architectural lift; defer unless the syncCache-on-entry pattern proves insufficient empiricallysyncCacheon everydb.edges.items/db.nodes.itemsaccess via Proxy wrapping — invasive; defensive but adds per-access overheadToolService.callTool) — cleaner architecturally but broader blast radiusMESSAGE:f481a945(my earlier broadcast with missing edges) — one-off casualty of tobi's git-pull sequenceAvoided Traps
getAdjacentNodes: considered, rejected. getAdjacentNodes is vicinity-centric; mailbox queries are edge-type scans that don't map cleanly onto vicinity semanticsRelated
syncCachefix (Gemini) — this ticket extends the invocation surface to cover mailbox read paths'blocked'mode'blocked'modeOrigin Session ID
Origin Session ID:
7ad90eb3-2218-4200-977b-fbfe546d64a8Handoff 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 HEAD57145b0acat ticket filing