Skip to content

Commit 475013f

Browse files
committed
fix: Memory Core Graph desynchronization, index leakage, and singleton storage bugs. (#9721)
1 parent 0e08665 commit 475013f

6 files changed

Lines changed: 84 additions & 55 deletions

File tree

ai/graph/Database.mjs

Lines changed: 17 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@ import NodeModel from './NodeModel.mjs';
66
import StorageBase from './storage/Base.mjs';
77

88
/**
9-
* The Database class serves as the core coordinator for the Native Edge Graph Database engine.
10-
* Operating in headless MCP server environments (Sandman, memory-core), it orchestrates local
9+
* The Database class serves as the core coordinator for the Native Edge Graph Database engine.
10+
* Operating in headless MCP server environments (Sandman, memory-core), it orchestrates local
1111
* Native Node embeddings tracking alongside Semantic vectors natively inside ChromaDB (GraphRAG).
12-
*
13-
* Implements strict Multi-Worker Cache Coherence via SQLite Hardware Triggers & Delta Logs, combined
14-
* with completely Synchronous Lazy Loading architectures and automated LRU Garbage Collection
12+
*
13+
* Implements strict Multi-Worker Cache Coherence via SQLite Hardware Triggers & Delta Logs, combined
14+
* with completely Synchronous Lazy Loading architectures and automated LRU Garbage Collection
1515
* to guarantee V8 execution limits seamlessly evaluating massive Application ASTs natively.
16-
*
16+
*
1717
* It leverages Neo.data.Store for high-speed local vicinity edge traversals smoothly safely!
1818
* @class Neo.ai.graph.Database
1919
* @extends Neo.core.Base
@@ -120,7 +120,7 @@ class Database extends Base {
120120
if (me.maxGraphNodes !== null && me.lastAccessMap.size > me.maxGraphNodes) {
121121
let nodesArray = Array.from(me.lastAccessMap.entries());
122122
nodesArray.sort((a,b) => a[1] - b[1]); // Oldest timestamps first
123-
123+
124124
let deleteCount = Math.max(1, Math.floor(me.maxGraphNodes * 0.2)); // Execute 20% chunk truncation cleanly locally guaranteeing at least 1 dropped
125125
let toDelete = nodesArray.slice(0, deleteCount).map(entry => entry[0]);
126126

@@ -222,7 +222,7 @@ class Database extends Base {
222222
value = ClassSystemUtil.beforeSetInstance(value, StorageBase, {
223223
database: this
224224
});
225-
225+
226226
value.database = this;
227227
}
228228
return value;
@@ -251,19 +251,14 @@ class Database extends Base {
251251
// 2. Resolve Lazy Loading Vicinity Cache Misses seamlessly blocking via synchronous boundaries efficiently
252252
if (me.storage && !me.vicinityLoadedNodes.has(nodeId)) {
253253
let vicinity = me.storage.loadNodeVicinitySync(nodeId);
254-
254+
255255
let wasTransacting = me.isExecutingTransaction;
256256
let wasAutoSave = me.autoSave;
257257
me.isExecutingTransaction = false;
258258
me.autoSave = false;
259259

260-
261-
let newNodes = vicinity.nodes.filter(n => !me.nodes.get(n.id));
262-
let newEdges = vicinity.edges.filter(e => !me.edges.get(e.id));
263-
264-
if (newNodes.length > 0) me.nodes.add(newNodes);
265-
if (newEdges.length > 0) me.edges.add(newEdges);
266-
260+
if (vicinity.nodes.length > 0) me.nodes.add(vicinity.nodes);
261+
if (vicinity.edges.length > 0) me.edges.add(vicinity.edges);
267262

268263
me.autoSave = wasAutoSave;
269264
me.isExecutingTransaction = wasTransacting;
@@ -283,7 +278,7 @@ class Database extends Base {
283278

284279
if (direction === 'inbound' || direction === 'both') {
285280
let inboundEdges = me.edges.getByIndex('target', nodeId);
286-
281+
287282
if (direction === 'both') {
288283
inboundEdges.forEach(e => {
289284
if (e.source !== nodeId) edges.push(e);
@@ -367,7 +362,7 @@ class Database extends Base {
367362
outbound = me.edges.getByIndex('source', nodeId),
368363
inbound = me.edges.getByIndex('target', nodeId),
369364
edgesToRemove = outbound.slice();
370-
365+
371366
me.nodes.remove(nodeId);
372367
me.vicinityLoadedNodes.delete(nodeId);
373368
me.lastAccessMap.delete(nodeId);
@@ -388,7 +383,7 @@ class Database extends Base {
388383
* @protected
389384
*/
390385
rollbackTransaction(diffLog) {
391-
// Iterate backward guarantees dependencies (e.g. node deletion then edge cascade) reverse perfectly
386+
// Iterate backward guarantees dependencies (e.g. node deletion then edge cascade) reverse perfectly
392387
for (let i = diffLog.length - 1; i >= 0; i--) {
393388
let trace = diffLog[i];
394389
let store = trace.type === 'nodes' ? this.nodes : this.edges;
@@ -415,7 +410,7 @@ class Database extends Base {
415410
/**
416411
* Executes purely synchronous atomic closures securely mirroring standard database parameters effectively.
417412
* Utilizes a rollback buffer erasing local V8 mapped instances correctly if backend SQLite queries detonate cleanly.
418-
*
413+
*
419414
* @param {Function} fn Synchronous logical closure interacting via standard `Database.addNode/removeNode`.
420415
*/
421416
transaction(fn) {
@@ -428,14 +423,14 @@ class Database extends Base {
428423

429424
try {
430425
fn(); // Synchronous array splices apply isolating memory mappings instantaneously internally
431-
426+
432427
if (this.storage && this.transactionDiff.length > 0) {
433428
this.storage.executeTransaction(this.transactionDiff);
434429
}
435430
} catch (error) {
436431
// Intercept internal throw commands seamlessly rendering perfect state erasures instantly
437432
this.rollbackTransaction(this.transactionDiff);
438-
throw error;
433+
throw error;
439434
} finally {
440435
this.isExecutingTransaction = false;
441436
this.transactionDiff = [];

ai/graph/Store.mjs

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,12 +57,30 @@ class Store extends DataStore {
5757
}
5858

5959
/**
60-
* Hook bypassing natural splice flow during raw structural deletion.
61-
* Flush all Native Graph Topologies.
60+
* Removes all items and clears the map. Overridden from Base.mjs to ensure indexMaps are flushed.
61+
* @param {Boolean} [reset=true]
62+
*/
63+
clear(reset=true) {
64+
let me = this;
65+
66+
super.clear(reset);
67+
68+
if (me.indexMaps) {
69+
let value = me.indices;
70+
me.indexMaps.clear();
71+
value.forEach(indexConfig => {
72+
me.indexMaps.set(indexConfig.property, new Map());
73+
});
74+
}
75+
}
76+
77+
/**
78+
* Clears filters, allItems, and the data map softly without alerting subscribers.
6279
* @param {Boolean} [reset=true]
6380
*/
6481
clearSilent(reset=true) {
6582
let me = this;
83+
6684
super.clearSilent(reset);
6785

6886
if (me.indexMaps) {

ai/mcp/server/memory-core/managers/SQLiteVectorManager.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ class SQLiteVectorManager extends Base {
4747
async initAsync() {
4848
await super.initAsync();
4949

50+
if (this.db) return;
51+
5052
if (aiConfig.engine !== 'neo' && aiConfig.engine !== 'both') {
5153
logger.log('[SQLiteVectorManager] Engine configured to bypass local vector manager. Skipping init.');
5254
return;

ai/mcp/server/memory-core/services/GraphService.mjs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,20 @@ class GraphService extends Base {
3737
async initAsync() {
3838
await super.initAsync();
3939

40+
if (this.db) return;
41+
4042
let storage = Neo.create(SQLite, { dbPath: aiConfig.sqlitePath });
4143
await storage.initAsync();
4244

43-
this.db = Neo.create(CoreDatabase, {
44-
id: 'memory-core-graph',
45-
storage: storage
46-
});
45+
if (Neo.get('memory-core-graph')) {
46+
this.db = Neo.get('memory-core-graph');
47+
this.db.storage = storage;
48+
} else {
49+
this.db = Neo.create(CoreDatabase, {
50+
id: 'memory-core-graph',
51+
storage: storage
52+
});
53+
}
4754

4855
await storage.load();
4956

ai/services.mjs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,7 @@ import Memory_LifecycleService from './mcp/server/memory-core/services/Data
4141
import Memory_HealthService from './mcp/server/memory-core/services/HealthService.mjs';
4242
import Memory_GraphService from './mcp/server/memory-core/services/GraphService.mjs';
4343
import Memory_SummaryService from './mcp/server/memory-core/services/SummaryService.mjs';
44-
import Memory_ChromaManager from './mcp/server/memory-core/services/ChromaManager.mjs';
45-
import Memory_SQLiteVectorManager from './mcp/server/memory-core/services/SQLiteVectorManager.mjs';
44+
import Memory_SQLiteVectorManager from './mcp/server/memory-core/managers/SQLiteVectorManager.mjs';
4645
import Memory_Config from './mcp/server/memory-core/config.mjs';
4746

4847
Memory_Config.data.autoSummarize = false;
@@ -233,7 +232,6 @@ export {
233232

234233
// Memory Core
235234
Memory_Config,
236-
Memory_ChromaManager,
237235
Memory_SQLiteVectorManager,
238236
Memory_Service,
239237
Memory_SessionService,
@@ -252,4 +250,4 @@ export {
252250
NeuralLink_InstanceService,
253251
NeuralLink_InteractionService,
254252
NeuralLink_RuntimeService
255-
};
253+
};

test/playwright/unit/ai/mcp/server/memory-core/services/GraphService.spec.mjs

Lines changed: 32 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { setup } from '../../../../../../setup.mjs';
1+
import {setup} from '../../../../../../setup.mjs';
22

33
const appName = 'GraphServiceTest';
44

@@ -7,21 +7,22 @@ setup({
77
unitTestMode: true
88
},
99
appConfig: {
10-
name: appName,
11-
isMounted: () => true,
10+
name : appName,
11+
isMounted : () => true,
1212
vnodeInitialising: false
1313
}
1414
});
1515

16-
import { test, expect } from '@playwright/test';
17-
import Neo from '../../../../../../../../src/Neo.mjs';
18-
import * as core from '../../../../../../../../src/core/_export.mjs';
19-
import GraphService from '../../../../../../../../ai/mcp/server/memory-core/services/GraphService.mjs';
20-
import aiConfig from '../../../../../../../../ai/mcp/server/memory-core/config.mjs';
21-
import fs from 'fs-extra';
22-
import path from 'path';
23-
import os from 'os';
24-
import {getPaths} from '../../../../../../../../ai/graph/queries/Traversal.mjs';
16+
import {test, expect} from '@playwright/test';
17+
import Neo from '../../../../../../../../src/Neo.mjs';
18+
import * as core from '../../../../../../../../src/core/_export.mjs';
19+
import InstanceManager from '../../../../../../../../src/manager/Instance.mjs';
20+
import GraphService from '../../../../../../../../ai/mcp/server/memory-core/services/GraphService.mjs';
21+
import aiConfig from '../../../../../../../../ai/mcp/server/memory-core/config.mjs';
22+
import fs from 'fs-extra';
23+
import path from 'path';
24+
import os from 'os';
25+
import {getPaths} from '../../../../../../../../ai/graph/queries/Traversal.mjs';
2526

2627
test.describe('Neo.ai.mcp.server.memory-core.services.GraphService', () => {
2728
let service;
@@ -42,14 +43,22 @@ test.describe('Neo.ai.mcp.server.memory-core.services.GraphService', () => {
4243
GraphService.db.nodes.clear();
4344
GraphService.db.edges.clear();
4445
GraphService.db.vicinityLoadedNodes.clear();
46+
47+
if (GraphService.db.storage) {
48+
await GraphService.db.storage.clear();
49+
}
4550
}
4651
});
4752

48-
test.afterEach(() => {
53+
test.afterEach(async () => {
4954
if (GraphService.db) {
5055
GraphService.db.nodes.clear();
5156
GraphService.db.edges.clear();
5257
GraphService.db.vicinityLoadedNodes.clear();
58+
59+
if (GraphService.db.storage) {
60+
await GraphService.db.storage.clear();
61+
}
5362
}
5463
});
5564

@@ -177,12 +186,12 @@ test.describe('Neo.ai.mcp.server.memory-core.services.GraphService', () => {
177186

178187
let wasAutoSave = GraphService.db.autoSave;
179188
GraphService.db.autoSave = false;
180-
189+
181190
// Explicitly clear RAM cache WITHOUT cascading to SQLite
182191
GraphService.db.nodes.clear();
183192
GraphService.db.edges.clear();
184193
GraphService.db.vicinityLoadedNodes.clear();
185-
194+
186195
GraphService.db.autoSave = wasAutoSave;
187196

188197
// Traverse using getPaths. Depth parameter is set to 3 to hit all nodes.
@@ -194,10 +203,10 @@ test.describe('Neo.ai.mcp.server.memory-core.services.GraphService', () => {
194203
// Depth 2: Depth2
195204
// Depth 3: Depth3
196205
expect(results.length).toBe(4);
197-
206+
198207
let pathIds = results.map(n => n.id).sort();
199208
expect(pathIds).toEqual(['Depth1', 'Depth2', 'Depth3', 'Root']);
200-
209+
201210
// Verify that deeply resolved nodes were structurally hydrated into memory
202211
expect(GraphService.db.nodes.has('Depth3')).toBe(true);
203212
expect(GraphService.getNode({ id: 'Depth3' }).name).toBe('Final Hop');
@@ -209,12 +218,12 @@ test.describe('Neo.ai.mcp.server.memory-core.services.GraphService', () => {
209218

210219
GraphService.upsertNode({ id: 'N1', name: 'First' });
211220
GraphService.getNode({ id: 'N1' }); // Register to LRU Matrix natively
212-
221+
213222
// Let timestamp differential tick natively avoiding micro-millisecond collisions gracefully
214223
await new Promise(resolve => setTimeout(resolve, 5));
215224
GraphService.upsertNode({ id: 'N2', name: 'Second' });
216225
GraphService.getNode({ id: 'N2' });
217-
226+
218227
await new Promise(resolve => setTimeout(resolve, 5));
219228
GraphService.upsertNode({ id: 'N3', name: 'Third' });
220229
GraphService.getNode({ id: 'N3' });
@@ -224,20 +233,20 @@ test.describe('Neo.ai.mcp.server.memory-core.services.GraphService', () => {
224233
expect(GraphService.db.nodes.has('N1')).toBe(true);
225234

226235
await new Promise(resolve => setTimeout(resolve, 5));
227-
236+
228237
// This 4th insert will push the length over 3 when accessed.
229238
GraphService.upsertNode({ id: 'N4', name: 'Fourth' });
230239
GraphService.getNode({ id: 'N4' }); // GC fires here natively!
231240

232241
// V8 footprint must hold 3 items cleanly locally natively. Output should be N2, N3, N4
233242
expect(GraphService.db.nodes.getCount()).toBe(3);
234-
243+
235244
expect(GraphService.db.nodes.has('N1')).toBe(false); // N1 dropped out of cache natively gracefully!
236245
expect(GraphService.db.nodes.has('N2')).toBe(true);
237246
expect(GraphService.db.nodes.has('N3')).toBe(true);
238247
expect(GraphService.db.nodes.has('N4')).toBe(true);
239-
240-
// Restore maxGraphNodes constraint cleanly safely natively
248+
249+
// Restore maxGraphNodes constraint cleanly safely natively
241250
GraphService.db.maxGraphNodes = null;
242251
});
243252

0 commit comments

Comments
 (0)