---
id: 10260
title: syncCache must invalidate vicinity of both endpoints when invalidating edges (#10258 follow-up)
state: CLOSED
labels:
  - bug
  - ai
  - architecture
  - core
assignees:
  - neo-gemini-pro
createdAt: '2026-04-23T21:03:43Z'
updatedAt: '2026-06-07T17:20:50Z'
githubUrl: 'https://github.com/neomjs/neo/issues/10260'
author: neo-opus-ada
commentsCount: 0
parentIssue: null
subIssues: []
subIssuesCompleted: 0
subIssuesTotal: 0
blockedBy: []
blocking: []
closedAt: '2026-04-23T21:28:51Z'
---
# syncCache must invalidate vicinity of both endpoints when invalidating edges (#10258 follow-up)

# Context

Empirically discovered this session: after PR #10258 merged (and both harnesses restarted), a follow-up round-trip revealed the cross-process coherence gap is not fully closed. Peer-written messages addressed directly to me still do not surface in my `list_messages` cache until a fresh MCP spawn.

Specific case: Gemini sent `MESSAGE:d559987d-...` from `@neo-gemini-pro` → `@neo-opus-ada` at 20:57:09Z. SQLite has it. Her Antigravity harness wrote it cleanly (correct `SENT_BY` + `SENT_TO` edges per raw query). But my long-running Claude Code MCP process (uptime ~10min) cannot see it via `list_messages` even though #10258's vicinity-reload fix IS live in the running process.

Gemini saw my messages correctly on her side because she restarted post-#10258. I restarted too and saw her earlier broadcasts — but her LATER DM (sent while my process was still running) doesn't propagate to my cache.

# The Problem

`Database.syncCache()` at `ai/graph/Database.mjs:~78-103` invalidates cached entries based on `GraphLog` WAL delta. Current logic:

```js
// Conceptual — actual code iterates delta entries
this.nodes.remove(delta.invalidNodes);
delta.invalidNodes.forEach(id => {
    this.vicinityLoadedNodes.delete(id);
    this.lastAccessMap.delete(id);
});
// similar for delta.invalidEdges — removes edges but does NOT invalidate vicinity of endpoints
```

**The gap**: when a peer process adds a NEW edge that terminates at an existing node in my cache, `syncCache` invalidates the new edge from cache but does NOT invalidate the `vicinityLoadedNodes` entry for the endpoint node. So when `#10258`'s `getAdjacentNodes(endpoint, direction)` fires, it sees `vicinityLoadedNodes.has(endpoint) === true` (from an earlier load), skips `loadNodeVicinitySync`, and the new edge never re-hydrates into cache.

Concretely for my case:
1. Gemini writes `MESSAGE:d559987d` with `SENT_TO → @neo-opus-ada` edge
2. GraphLog gets 2 new entries (node + edge)
3. My process's `syncCache` reads delta, invalidates `d559987d` (new node) + the edge ID
4. `vicinityLoadedNodes` has `@neo-opus-ada` from a prior load (from earlier in this session when I seeded the vicinity)
5. `syncCache` does NOT clear `@neo-opus-ada` from `vicinityLoadedNodes` (because `@neo-opus-ada` is not in `delta.invalidNodes` — it's an existing unchanged node)
6. My `list_messages` calls `getAdjacentNodes('@neo-opus-ada', 'inbound')` → skips lazy-reload because `vicinityLoadedNodes.has(target)` is true
7. Iteration over `edges.items` misses the new edge
8. `list_messages` returns stale cache

# The Architectural Reality

- `ai/graph/Database.mjs` — `syncCache()` implementation + `vicinityLoadedNodes` management
- `ai/graph/storage/SQLite.mjs` — `load(lastSyncId)` returns delta with `invalidNodes` + `invalidEdges` arrays
- For a new edge `<source, target, type>`, the current delta-processing invalidates the edge but leaves both `source` and `target` endpoints' vicinity-loaded state untouched
- `loadNodeVicinitySync(nodeId)` is the authoritative re-hydration primitive; it re-loads all edges where `nodeId` is source OR target

# The Fix

Extend `syncCache` to invalidate `vicinityLoadedNodes` for BOTH endpoints of every invalidated edge:

```js
// At the delta-processing site in Database.syncCache
delta.invalidEdges.forEach(edgeId => {
    const edge = this.edges.get(edgeId); // may already be null if not cached
    if (edge) {
        // Invalidate vicinity for both endpoints so the next getAdjacentNodes
        // call against either one triggers loadNodeVicinitySync, re-hydrating
        // the new/changed edge from SQLite.
        this.vicinityLoadedNodes.delete(edge.source);
        this.vicinityLoadedNodes.delete(edge.target);
    } else {
        // Edge not in local cache — we can't know its endpoints without a
        // SQLite lookup. For new peer-written edges, read the endpoint IDs
        // directly from storage (the delta load should already carry them).
        // Alternative: have storage.load() surface edgeEndpoints in the delta
        // payload so syncCache has the info without an extra SELECT.
    }
    this.edges.remove(edgeId);
});
```

Preferred direction: modify `SQLite.load(lastSyncId)` to return `delta.invalidEdges` as `[{id, source, target}]` objects instead of just IDs, so `syncCache` has the endpoint info for free. Backward-compat: check the shape, handle both.

# Acceptance Criteria

- [ ] `Database.syncCache()` invalidates `vicinityLoadedNodes` for both endpoints of each invalidated edge
- [ ] `SQLite.load()` surfaces endpoint metadata in `delta.invalidEdges` (object shape, not just ID array)
- [ ] Regression test in `test/playwright/unit/ai/graph/Database.spec.mjs`: two `Database` instances against the same file; instance A writes an edge, instance B's `syncCache` invalidates its endpoints, next `getAdjacentNodes` on either endpoint surfaces the new edge without explicit vicinity-clear
- [ ] Empirical validation: two MCP processes (Claude Code + Antigravity), peer A sends a DM to peer B while B's process is running; B's `list_messages` surfaces the DM without harness restart

# Out of Scope

- Proactive vicinity pre-fetching or subscription-based cache warming — defer until empirical demand
- Rewriting the GraphLog schema for edge-endpoint denormalization — current SQLite trigger records log entity-id only; if we need endpoint info without schema change, fetch from `Edges` table at delta-load time
- Extending the fix to ChromaDB metadata coherence — orthogonal substrate

# Avoided Traps

- **"Always re-run `loadNodeVicinitySync` on every `getAdjacentNodes` call"** — rejected. Defeats the point of caching; every read becomes a SQLite roundtrip.
- **"Poll `vicinityLoadedNodes` TTL-based invalidation"** — rejected. Timer-based invalidation introduces latency floor; delta-driven invalidation is strictly better.
- **"Fix this at the MailboxService layer by always calling `loadNodeVicinitySync` before iteration"** — rejected. Wrong layer. The bug is in `Database.syncCache`'s incomplete delta-processing, not in mailbox-specific logic. Fixing at mailbox would leave every other edge-type-scan consumer vulnerable to the same gap.

# Related

- **#10258** — cross-process coherence for mailbox read paths. This ticket is the deeper substrate fix that closes the remaining gap #10258's MailboxService-layer fix couldn't address alone.
- **#10257** — origin ticket for the mailbox-layer coherence fix.
- **#10190** / **PR #10221** — foundational `syncCache` substrate fix. This ticket extends the invalidation semantics.
- **#10139** — A2A mailbox primitive. Empirical "A2A works without restart" fully satisfied only once this lands.

# Origin Session ID

Origin Session ID: `29f490c0-41ed-4846-a767-287552d5c3c4`

# Handoff Retrieval Hints

Retrieval Hint: `"syncCache vicinityLoadedNodes edge endpoint invalidation gap"`
Retrieval Hint: `"cross-process mailbox coherence #10258 follow-up"`
Commit-range anchor: dev HEAD `96ae5f866` at ticket filing

## Timeline

- 2026-04-23T21:03:45Z @neo-opus-ada added the `bug` label
- 2026-04-23T21:03:45Z @neo-opus-ada added the `ai` label
- 2026-04-23T21:03:45Z @neo-opus-ada added the `architecture` label
- 2026-04-23T21:03:46Z @neo-opus-ada added the `core` label
- 2026-04-23T21:08:32Z @neo-gemini-pro assigned to @neo-gemini-pro
- 2026-04-23T21:11:01Z @neo-gemini-pro cross-referenced by PR #10261
- 2026-04-23T21:28:51Z @tobiu referenced in commit `4ecdac0` - "fix(ai): invalidate endpoint vicinity caches on edge updates (#10260) (#10261)

Co-authored-by: tobiu <tobiasuhlig78@gmail.com>"
- 2026-04-23T21:28:51Z @tobiu closed this issue
- 2026-04-23T22:22:29Z @neo-opus-ada cross-referenced by #9999
- 2026-04-27T06:17:59Z @neo-opus-ada cross-referenced by #10402
- 2026-04-27T12:09:08Z @neo-opus-ada cross-referenced by PR #10423

