@levalicious/server-memory 0.0.11 → 0.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.js CHANGED
@@ -1,11 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
3
  import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
4
- import { promises as fs } from 'fs';
5
4
  import { randomBytes } from 'crypto';
6
5
  import path from 'path';
7
6
  import { fileURLToPath } from 'url';
8
- import lockfile from 'proper-lockfile';
7
+ import { GraphFile, DIR_FORWARD, DIR_BACKWARD } from './src/graphfile.js';
8
+ import { StringTable } from './src/stringtable.js';
9
+ import { structuralSample } from './src/pagerank.js';
9
10
  // Define memory file path using environment variable with fallback
10
11
  const defaultMemoryPath = path.join(path.dirname(fileURLToPath(import.meta.url)), 'memory.json');
11
12
  // If MEMORY_FILE_PATH is just a filename, put it in the same directory as the script
@@ -17,17 +18,43 @@ const DEFAULT_MEMORY_FILE_PATH = process.env.MEMORY_FILE_PATH
17
18
  /**
18
19
  * Sort entities by the specified field and direction.
19
20
  * Returns a new array (does not mutate input).
20
- * If sortBy is undefined, returns the original array (no sorting - preserves insertion order).
21
+ * Defaults to 'llmrank' when sortBy is undefined.
22
+ *
23
+ * For 'pagerank' sort: uses structural rank (desc by default).
24
+ * For 'llmrank' sort: uses walker rank, falls back to structural rank on tie, then random.
25
+ * Both rank sorts require rankMaps parameter.
21
26
  */
22
- function sortEntities(entities, sortBy, sortDir) {
23
- if (!sortBy)
24
- return entities; // No sorting - preserve current behavior
27
+ function sortEntities(entities, sortBy = "llmrank", sortDir, rankMaps) {
25
28
  const dir = sortDir ?? (sortBy === "name" ? "asc" : "desc");
26
29
  const mult = dir === "asc" ? 1 : -1;
27
30
  return [...entities].sort((a, b) => {
28
31
  if (sortBy === "name") {
29
32
  return mult * a.name.localeCompare(b.name);
30
33
  }
34
+ if (sortBy === "pagerank") {
35
+ const aRank = rankMaps?.structural.get(a.name) ?? 0;
36
+ const bRank = rankMaps?.structural.get(b.name) ?? 0;
37
+ const diff = aRank - bRank;
38
+ if (diff !== 0)
39
+ return mult * diff;
40
+ return Math.random() - 0.5; // random tiebreak
41
+ }
42
+ if (sortBy === "llmrank") {
43
+ // Primary: walker rank
44
+ const aWalker = rankMaps?.walker.get(a.name) ?? 0;
45
+ const bWalker = rankMaps?.walker.get(b.name) ?? 0;
46
+ const walkerDiff = aWalker - bWalker;
47
+ if (walkerDiff !== 0)
48
+ return mult * walkerDiff;
49
+ // Fallback: structural rank
50
+ const aStruct = rankMaps?.structural.get(a.name) ?? 0;
51
+ const bStruct = rankMaps?.structural.get(b.name) ?? 0;
52
+ const structDiff = aStruct - bStruct;
53
+ if (structDiff !== 0)
54
+ return mult * structDiff;
55
+ // Final: random tiebreak
56
+ return Math.random() - 0.5;
57
+ }
31
58
  // For timestamps, treat undefined as 0 (oldest)
32
59
  const aVal = a[sortBy] ?? 0;
33
60
  const bVal = b[sortBy] ?? 0;
@@ -36,17 +63,36 @@ function sortEntities(entities, sortBy, sortDir) {
36
63
  }
37
64
  /**
38
65
  * Sort neighbors by the specified field and direction.
39
- * If sortBy is undefined, returns the original array (no sorting).
66
+ * Defaults to 'llmrank' when sortBy is undefined.
40
67
  */
41
- function sortNeighbors(neighbors, sortBy, sortDir) {
42
- if (!sortBy)
43
- return neighbors;
68
+ function sortNeighbors(neighbors, sortBy = "llmrank", sortDir, rankMaps) {
44
69
  const dir = sortDir ?? (sortBy === "name" ? "asc" : "desc");
45
70
  const mult = dir === "asc" ? 1 : -1;
46
71
  return [...neighbors].sort((a, b) => {
47
72
  if (sortBy === "name") {
48
73
  return mult * a.name.localeCompare(b.name);
49
74
  }
75
+ if (sortBy === "pagerank") {
76
+ const aRank = rankMaps?.structural.get(a.name) ?? 0;
77
+ const bRank = rankMaps?.structural.get(b.name) ?? 0;
78
+ const diff = aRank - bRank;
79
+ if (diff !== 0)
80
+ return mult * diff;
81
+ return Math.random() - 0.5;
82
+ }
83
+ if (sortBy === "llmrank") {
84
+ const aWalker = rankMaps?.walker.get(a.name) ?? 0;
85
+ const bWalker = rankMaps?.walker.get(b.name) ?? 0;
86
+ const walkerDiff = aWalker - bWalker;
87
+ if (walkerDiff !== 0)
88
+ return mult * walkerDiff;
89
+ const aStruct = rankMaps?.structural.get(a.name) ?? 0;
90
+ const bStruct = rankMaps?.structural.get(b.name) ?? 0;
91
+ const structDiff = aStruct - bStruct;
92
+ if (structDiff !== 0)
93
+ return mult * structDiff;
94
+ return Math.random() - 0.5;
95
+ }
50
96
  const aVal = a[sortBy] ?? 0;
51
97
  const bVal = b[sortBy] ?? 0;
52
98
  return mult * (aVal - bVal);
@@ -131,428 +177,676 @@ function paginateGraph(graph, entityCursor = 0, relationCursor = 0) {
131
177
  }
132
178
  // The KnowledgeGraphManager class contains all operations to interact with the knowledge graph
133
179
  export class KnowledgeGraphManager {
134
- memoryFilePath;
180
+ gf;
181
+ st;
182
+ /** In-memory name→offset index, rebuilt from node log on open */
183
+ nameIndex;
184
+ /** Generation counter from last rebuildNameIndex — avoids redundant rebuilds */
185
+ cachedEntityCount = -1;
135
186
  constructor(memoryFilePath = DEFAULT_MEMORY_FILE_PATH) {
136
- this.memoryFilePath = memoryFilePath;
187
+ // Derive binary file paths from the base path
188
+ const dir = path.dirname(memoryFilePath);
189
+ const base = path.basename(memoryFilePath, path.extname(memoryFilePath));
190
+ const graphPath = path.join(dir, `${base}.graph`);
191
+ const strPath = path.join(dir, `${base}.strings`);
192
+ this.st = new StringTable(strPath);
193
+ this.gf = new GraphFile(graphPath, this.st);
194
+ this.nameIndex = new Map();
195
+ this.rebuildNameIndex();
196
+ // Run initial structural sampling if graph is non-empty
197
+ if (this.nameIndex.size > 0) {
198
+ structuralSample(this.gf, 1, 0.85);
199
+ this.gf.sync();
200
+ }
137
201
  }
138
- async withLock(fn) {
139
- // Ensure file exists for locking
202
+ // --- Locking helpers ---
203
+ /**
204
+ * Acquire a shared (read) lock, refresh mappings (in case another process
205
+ * grew the files), rebuild the name index if the entity count changed,
206
+ * run the callback, then release the lock.
207
+ */
208
+ withReadLock(fn) {
209
+ this.gf.lockShared();
140
210
  try {
141
- await fs.access(this.memoryFilePath);
211
+ this.gf.refresh();
212
+ this.st.refresh();
213
+ this.maybeRebuildNameIndex();
214
+ return fn();
142
215
  }
143
- catch {
144
- await fs.writeFile(this.memoryFilePath, "");
216
+ finally {
217
+ this.gf.unlock();
145
218
  }
146
- const release = await lockfile.lock(this.memoryFilePath, { retries: { retries: 5, minTimeout: 100 } });
219
+ }
220
+ /**
221
+ * Acquire an exclusive (write) lock, refresh mappings, rebuild name index,
222
+ * run the callback, sync both files, then release the lock.
223
+ */
224
+ withWriteLock(fn) {
225
+ this.gf.lockExclusive();
147
226
  try {
148
- return await fn();
227
+ this.gf.refresh();
228
+ this.st.refresh();
229
+ this.maybeRebuildNameIndex();
230
+ const result = fn();
231
+ this.gf.sync();
232
+ this.st.sync();
233
+ // Update cached count after write
234
+ this.cachedEntityCount = this.gf.getEntityCount();
235
+ return result;
149
236
  }
150
237
  finally {
151
- await release();
238
+ this.gf.unlock();
152
239
  }
153
240
  }
154
- async loadGraph() {
155
- try {
156
- const data = await fs.readFile(this.memoryFilePath, "utf-8");
157
- const lines = data.split("\n").filter(line => line.trim() !== "");
158
- return lines.reduce((graph, line) => {
159
- const item = JSON.parse(line);
160
- if (item.type === "entity")
161
- graph.entities.push(item);
162
- if (item.type === "relation")
163
- graph.relations.push(item);
164
- return graph;
165
- }, { entities: [], relations: [] });
241
+ /** Rebuild the in-memory name→offset index from the node log */
242
+ rebuildNameIndex() {
243
+ this.nameIndex.clear();
244
+ const offsets = this.gf.getAllEntityOffsets();
245
+ for (const offset of offsets) {
246
+ const rec = this.gf.readEntity(offset);
247
+ const name = this.st.get(BigInt(rec.nameId));
248
+ this.nameIndex.set(name, offset);
249
+ }
250
+ this.cachedEntityCount = this.gf.getEntityCount();
251
+ }
252
+ /**
253
+ * Rebuild nameIndex only if the entity count changed (another process wrote).
254
+ * This is cheap to check (single u32 read) and avoids a full scan on every lock.
255
+ */
256
+ maybeRebuildNameIndex() {
257
+ const currentCount = this.gf.getEntityCount();
258
+ if (currentCount !== this.cachedEntityCount) {
259
+ this.rebuildNameIndex();
260
+ }
261
+ }
262
+ /** Build rank maps from the binary store for pagerank/llmrank sorting.
263
+ * NOTE: Must be called inside a lock (read or write). */
264
+ getRankMapsUnlocked() {
265
+ const structural = new Map();
266
+ const walker = new Map();
267
+ const structTotal = this.gf.getStructuralTotal();
268
+ const walkerTotal = this.gf.getWalkerTotal();
269
+ for (const [name, offset] of this.nameIndex) {
270
+ const rec = this.gf.readEntity(offset);
271
+ structural.set(name, structTotal > 0n ? Number(rec.structuralVisits) / Number(structTotal) : 0);
272
+ walker.set(name, walkerTotal > 0n ? Number(rec.walkerVisits) / Number(walkerTotal) : 0);
166
273
  }
167
- catch (error) {
168
- if (error?.code === "ENOENT") {
169
- return { entities: [], relations: [] };
274
+ return { structural, walker };
275
+ }
276
+ /** Build rank maps (acquires read lock). */
277
+ getRankMaps() {
278
+ return this.withReadLock(() => this.getRankMapsUnlocked());
279
+ }
280
+ /** Increment walker visit count for a list of entity names */
281
+ recordWalkerVisits(names) {
282
+ this.withWriteLock(() => {
283
+ for (const name of names) {
284
+ const offset = this.nameIndex.get(name);
285
+ if (offset !== undefined) {
286
+ this.gf.incrementWalkerVisit(offset);
287
+ }
288
+ }
289
+ });
290
+ }
291
+ /** Re-run structural sampling (call after graph mutations) */
292
+ resample() {
293
+ this.withWriteLock(() => {
294
+ if (this.nameIndex.size > 0) {
295
+ structuralSample(this.gf, 1, 0.85);
296
+ }
297
+ });
298
+ }
299
+ /** Convert an EntityRecord to the public Entity interface */
300
+ recordToEntity(rec) {
301
+ const name = this.st.get(BigInt(rec.nameId));
302
+ const entityType = this.st.get(BigInt(rec.typeId));
303
+ const observations = [];
304
+ if (rec.obs0Id !== 0)
305
+ observations.push(this.st.get(BigInt(rec.obs0Id)));
306
+ if (rec.obs1Id !== 0)
307
+ observations.push(this.st.get(BigInt(rec.obs1Id)));
308
+ const entity = { name, entityType, observations };
309
+ const mtime = Number(rec.mtime);
310
+ const obsMtime = Number(rec.obsMtime);
311
+ if (mtime > 0)
312
+ entity.mtime = mtime;
313
+ if (obsMtime > 0)
314
+ entity.obsMtime = obsMtime;
315
+ return entity;
316
+ }
317
+ /** Get all entities as Entity objects (preserves node log order = insertion order) */
318
+ getAllEntities() {
319
+ const offsets = this.gf.getAllEntityOffsets();
320
+ return offsets.map(o => this.recordToEntity(this.gf.readEntity(o)));
321
+ }
322
+ /** Get all relations by scanning adjacency lists (forward edges only to avoid duplication) */
323
+ getAllRelations() {
324
+ const relations = [];
325
+ const offsets = this.gf.getAllEntityOffsets();
326
+ for (const offset of offsets) {
327
+ const rec = this.gf.readEntity(offset);
328
+ const fromName = this.st.get(BigInt(rec.nameId));
329
+ const edges = this.gf.getEdges(offset);
330
+ for (const edge of edges) {
331
+ if (edge.direction !== DIR_FORWARD)
332
+ continue;
333
+ const targetRec = this.gf.readEntity(edge.targetOffset);
334
+ const toName = this.st.get(BigInt(targetRec.nameId));
335
+ const relationType = this.st.get(BigInt(edge.relTypeId));
336
+ const r = { from: fromName, to: toName, relationType };
337
+ const mtime = Number(edge.mtime);
338
+ if (mtime > 0)
339
+ r.mtime = mtime;
340
+ relations.push(r);
170
341
  }
171
- throw error;
172
342
  }
343
+ return relations;
173
344
  }
174
- async saveGraph(graph) {
175
- const lines = [
176
- ...graph.entities.map(e => JSON.stringify({ type: "entity", ...e })),
177
- ...graph.relations.map(r => JSON.stringify({ type: "relation", ...r })),
178
- ];
179
- const content = lines.join("\n") + (lines.length > 0 ? "\n" : "");
180
- await fs.writeFile(this.memoryFilePath, content);
345
+ /** Load the full graph (entities + relations) */
346
+ loadGraph() {
347
+ return {
348
+ entities: this.getAllEntities(),
349
+ relations: this.getAllRelations(),
350
+ };
181
351
  }
182
352
  async createEntities(entities) {
183
- return this.withLock(async () => {
184
- const graph = await this.loadGraph();
185
- // Validate observation limits
186
- for (const entity of entities) {
187
- if (entity.observations.length > 2) {
188
- throw new Error(`Entity "${entity.name}" has ${entity.observations.length} observations. Maximum allowed is 2.`);
353
+ // Validate observation limits (can do outside lock)
354
+ for (const entity of entities) {
355
+ if (entity.observations.length > 2) {
356
+ throw new Error(`Entity "${entity.name}" has ${entity.observations.length} observations. Maximum allowed is 2.`);
357
+ }
358
+ for (const obs of entity.observations) {
359
+ if (obs.length > 140) {
360
+ throw new Error(`Observation in entity "${entity.name}" exceeds 140 characters (${obs.length} chars): "${obs.substring(0, 50)}..."`);
189
361
  }
190
- for (const obs of entity.observations) {
191
- if (obs.length > 140) {
192
- throw new Error(`Observation in entity "${entity.name}" exceeds 140 characters (${obs.length} chars): "${obs.substring(0, 50)}..."`);
193
- }
362
+ }
363
+ }
364
+ return this.withWriteLock(() => {
365
+ const now = BigInt(Date.now());
366
+ const newEntities = [];
367
+ for (const e of entities) {
368
+ const existingOffset = this.nameIndex.get(e.name);
369
+ if (existingOffset !== undefined) {
370
+ const existing = this.recordToEntity(this.gf.readEntity(existingOffset));
371
+ const sameType = existing.entityType === e.entityType;
372
+ const sameObs = existing.observations.length === e.observations.length &&
373
+ existing.observations.every((o, i) => o === e.observations[i]);
374
+ if (sameType && sameObs)
375
+ continue;
376
+ throw new Error(`Entity "${e.name}" already exists with different data (type: "${existing.entityType}" vs "${e.entityType}", observations: ${existing.observations.length} vs ${e.observations.length})`);
377
+ }
378
+ const obsMtime = e.observations.length > 0 ? now : 0n;
379
+ const rec = this.gf.createEntity(e.name, e.entityType, now, obsMtime);
380
+ for (const obs of e.observations) {
381
+ this.gf.addObservation(rec.offset, obs, now);
194
382
  }
383
+ if (e.observations.length > 0) {
384
+ const updated = this.gf.readEntity(rec.offset);
385
+ updated.mtime = now;
386
+ updated.obsMtime = now;
387
+ this.gf.updateEntity(updated);
388
+ }
389
+ this.nameIndex.set(e.name, rec.offset);
390
+ const newEntity = {
391
+ ...e,
392
+ mtime: Number(now),
393
+ obsMtime: e.observations.length > 0 ? Number(now) : undefined,
394
+ };
395
+ newEntities.push(newEntity);
195
396
  }
196
- const now = Date.now();
197
- const newEntities = entities
198
- .filter(e => !graph.entities.some(existingEntity => existingEntity.name === e.name))
199
- .map(e => ({ ...e, mtime: now, obsMtime: e.observations.length > 0 ? now : undefined }));
200
- graph.entities.push(...newEntities);
201
- await this.saveGraph(graph);
202
397
  return newEntities;
203
398
  });
204
399
  }
205
400
  async createRelations(relations) {
206
- return this.withLock(async () => {
207
- const graph = await this.loadGraph();
208
- const now = Date.now();
209
- // Update mtime on 'from' entities when relations are added
210
- const fromEntityNames = new Set(relations.map(r => r.from));
211
- graph.entities.forEach(e => {
212
- if (fromEntityNames.has(e.name)) {
213
- e.mtime = now;
214
- }
215
- });
216
- const newRelations = relations
217
- .filter(r => !graph.relations.some(existingRelation => existingRelation.from === r.from &&
218
- existingRelation.to === r.to &&
219
- existingRelation.relationType === r.relationType))
220
- .map(r => ({ ...r, mtime: now }));
221
- graph.relations.push(...newRelations);
222
- await this.saveGraph(graph);
401
+ return this.withWriteLock(() => {
402
+ const now = BigInt(Date.now());
403
+ const newRelations = [];
404
+ const fromOffsets = new Set();
405
+ for (const r of relations) {
406
+ const fromOffset = this.nameIndex.get(r.from);
407
+ const toOffset = this.nameIndex.get(r.to);
408
+ if (fromOffset === undefined || toOffset === undefined)
409
+ continue;
410
+ const existingEdges = this.gf.getEdges(fromOffset);
411
+ const relTypeId = Number(this.st.find(r.relationType) ?? -1n);
412
+ const isDuplicate = existingEdges.some(e => e.direction === DIR_FORWARD &&
413
+ e.targetOffset === toOffset &&
414
+ e.relTypeId === relTypeId);
415
+ if (isDuplicate)
416
+ continue;
417
+ const rTypeId = Number(this.st.intern(r.relationType));
418
+ const forwardEntry = {
419
+ targetOffset: toOffset,
420
+ direction: DIR_FORWARD,
421
+ relTypeId: rTypeId,
422
+ mtime: now,
423
+ };
424
+ this.gf.addEdge(fromOffset, forwardEntry);
425
+ const rTypeId2 = Number(this.st.intern(r.relationType));
426
+ const backwardEntry = {
427
+ targetOffset: fromOffset,
428
+ direction: DIR_BACKWARD,
429
+ relTypeId: rTypeId2,
430
+ mtime: now,
431
+ };
432
+ this.gf.addEdge(toOffset, backwardEntry);
433
+ fromOffsets.add(fromOffset);
434
+ newRelations.push({ ...r, mtime: Number(now) });
435
+ }
436
+ for (const offset of fromOffsets) {
437
+ const rec = this.gf.readEntity(offset);
438
+ rec.mtime = now;
439
+ this.gf.updateEntity(rec);
440
+ }
223
441
  return newRelations;
224
442
  });
225
443
  }
226
444
  async addObservations(observations) {
227
- return this.withLock(async () => {
228
- const graph = await this.loadGraph();
229
- const results = observations.map(o => {
230
- const entity = graph.entities.find(e => e.name === o.entityName);
231
- if (!entity) {
445
+ return this.withWriteLock(() => {
446
+ const results = [];
447
+ for (const o of observations) {
448
+ const offset = this.nameIndex.get(o.entityName);
449
+ if (offset === undefined) {
232
450
  throw new Error(`Entity with name ${o.entityName} not found`);
233
451
  }
234
- // Validate observation character limits
235
452
  for (const obs of o.contents) {
236
453
  if (obs.length > 140) {
237
454
  throw new Error(`Observation for "${o.entityName}" exceeds 140 characters (${obs.length} chars): "${obs.substring(0, 50)}..."`);
238
455
  }
239
456
  }
240
- const newObservations = o.contents.filter(content => !entity.observations.includes(content));
241
- // Validate total observation count
242
- if (entity.observations.length + newObservations.length > 2) {
243
- throw new Error(`Adding ${newObservations.length} observations to "${o.entityName}" would exceed limit of 2 (currently has ${entity.observations.length}).`);
457
+ const rec = this.gf.readEntity(offset);
458
+ const existingObs = [];
459
+ if (rec.obs0Id !== 0)
460
+ existingObs.push(this.st.get(BigInt(rec.obs0Id)));
461
+ if (rec.obs1Id !== 0)
462
+ existingObs.push(this.st.get(BigInt(rec.obs1Id)));
463
+ const newObservations = o.contents.filter(content => !existingObs.includes(content));
464
+ if (existingObs.length + newObservations.length > 2) {
465
+ throw new Error(`Adding ${newObservations.length} observations to "${o.entityName}" would exceed limit of 2 (currently has ${existingObs.length}).`);
244
466
  }
245
- entity.observations.push(...newObservations);
246
- if (newObservations.length > 0) {
247
- const now = Date.now();
248
- entity.mtime = now;
249
- entity.obsMtime = now;
467
+ const now = BigInt(Date.now());
468
+ for (const obs of newObservations) {
469
+ this.gf.addObservation(offset, obs, now);
250
470
  }
251
- return { entityName: o.entityName, addedObservations: newObservations };
252
- });
253
- await this.saveGraph(graph);
471
+ results.push({ entityName: o.entityName, addedObservations: newObservations });
472
+ }
254
473
  return results;
255
474
  });
256
475
  }
257
476
  async deleteEntities(entityNames) {
258
- return this.withLock(async () => {
259
- const graph = await this.loadGraph();
260
- graph.entities = graph.entities.filter(e => !entityNames.includes(e.name));
261
- graph.relations = graph.relations.filter(r => !entityNames.includes(r.from) && !entityNames.includes(r.to));
262
- await this.saveGraph(graph);
477
+ this.withWriteLock(() => {
478
+ for (const name of entityNames) {
479
+ const offset = this.nameIndex.get(name);
480
+ if (offset === undefined)
481
+ continue;
482
+ const edges = this.gf.getEdges(offset);
483
+ for (const edge of edges) {
484
+ const reverseDir = edge.direction === DIR_FORWARD ? DIR_BACKWARD : DIR_FORWARD;
485
+ this.gf.removeEdge(edge.targetOffset, offset, edge.relTypeId, reverseDir);
486
+ this.st.release(BigInt(edge.relTypeId));
487
+ }
488
+ this.gf.deleteEntity(offset);
489
+ this.nameIndex.delete(name);
490
+ }
263
491
  });
264
492
  }
265
493
  async deleteObservations(deletions) {
266
- return this.withLock(async () => {
267
- const graph = await this.loadGraph();
268
- const now = Date.now();
269
- deletions.forEach(d => {
270
- const entity = graph.entities.find(e => e.name === d.entityName);
271
- if (entity) {
272
- const originalLen = entity.observations.length;
273
- entity.observations = entity.observations.filter(o => !d.observations.includes(o));
274
- if (entity.observations.length !== originalLen) {
275
- entity.mtime = now;
276
- entity.obsMtime = now;
277
- }
494
+ this.withWriteLock(() => {
495
+ const now = BigInt(Date.now());
496
+ for (const d of deletions) {
497
+ const offset = this.nameIndex.get(d.entityName);
498
+ if (offset === undefined)
499
+ continue;
500
+ for (const obs of d.observations) {
501
+ this.gf.removeObservation(offset, obs, now);
278
502
  }
279
- });
280
- await this.saveGraph(graph);
503
+ }
281
504
  });
282
505
  }
283
506
  async deleteRelations(relations) {
284
- return this.withLock(async () => {
285
- const graph = await this.loadGraph();
286
- graph.relations = graph.relations.filter(r => !relations.some(delRelation => r.from === delRelation.from &&
287
- r.to === delRelation.to &&
288
- r.relationType === delRelation.relationType));
289
- await this.saveGraph(graph);
507
+ this.withWriteLock(() => {
508
+ for (const r of relations) {
509
+ const fromOffset = this.nameIndex.get(r.from);
510
+ const toOffset = this.nameIndex.get(r.to);
511
+ if (fromOffset === undefined || toOffset === undefined)
512
+ continue;
513
+ const relTypeId = this.st.find(r.relationType);
514
+ if (relTypeId === null)
515
+ continue;
516
+ const removedForward = this.gf.removeEdge(fromOffset, toOffset, Number(relTypeId), DIR_FORWARD);
517
+ if (removedForward)
518
+ this.st.release(relTypeId);
519
+ const removedBackward = this.gf.removeEdge(toOffset, fromOffset, Number(relTypeId), DIR_BACKWARD);
520
+ if (removedBackward)
521
+ this.st.release(relTypeId);
522
+ }
290
523
  });
291
524
  }
292
525
  // Regex-based search function
293
- async searchNodes(query, sortBy, sortDir) {
294
- const graph = await this.loadGraph();
526
+ async searchNodes(query, sortBy, sortDir, direction = 'forward') {
295
527
  let regex;
296
528
  try {
297
- regex = new RegExp(query, 'i'); // case-insensitive
529
+ regex = new RegExp(query, 'i');
298
530
  }
299
531
  catch (e) {
300
532
  throw new Error(`Invalid regex pattern: ${query}`);
301
533
  }
302
- // Filter entities
303
- const filteredEntities = graph.entities.filter(e => regex.test(e.name) ||
304
- regex.test(e.entityType) ||
305
- e.observations.some(o => regex.test(o)));
306
- // Create a Set of filtered entity names for quick lookup
307
- const filteredEntityNames = new Set(filteredEntities.map(e => e.name));
308
- // Filter relations to only include those between filtered entities
309
- const filteredRelations = graph.relations.filter(r => filteredEntityNames.has(r.from) && filteredEntityNames.has(r.to));
310
- const filteredGraph = {
311
- entities: sortEntities(filteredEntities, sortBy, sortDir),
312
- relations: filteredRelations,
313
- };
314
- return filteredGraph;
315
- }
316
- async openNodesFiltered(names) {
317
- const graph = await this.loadGraph();
318
- // Filter entities
319
- const filteredEntities = graph.entities.filter(e => names.includes(e.name));
320
- // Create a Set of filtered entity names for quick lookup
321
- const filteredEntityNames = new Set(filteredEntities.map(e => e.name));
322
- // Filter relations to only include those between filtered entities
323
- const filteredRelations = graph.relations.filter(r => filteredEntityNames.has(r.from) && filteredEntityNames.has(r.to));
324
- const filteredGraph = {
325
- entities: filteredEntities,
326
- relations: filteredRelations,
327
- };
328
- return filteredGraph;
329
- }
330
- async openNodes(names) {
331
- const graph = await this.loadGraph();
332
- // Filter entities
333
- const filteredEntities = graph.entities.filter(e => names.includes(e.name));
334
- // Create a Set of filtered entity names for quick lookup
335
- const filteredEntityNames = new Set(filteredEntities.map(e => e.name));
336
- // Filter relations to only include those between filtered entities
337
- const filteredRelations = graph.relations.filter(r => filteredEntityNames.has(r.from));
338
- const filteredGraph = {
339
- entities: filteredEntities,
340
- relations: filteredRelations,
341
- };
342
- return filteredGraph;
343
- }
344
- async getNeighbors(entityName, depth = 1, sortBy, sortDir) {
345
- const graph = await this.loadGraph();
346
- const visited = new Set();
347
- const neighborNames = new Set();
348
- const traverse = (currentName, currentDepth) => {
349
- if (currentDepth > depth || visited.has(currentName))
350
- return;
351
- visited.add(currentName);
352
- // Find all relations involving this entity
353
- const connectedRelations = graph.relations.filter(r => r.from === currentName || r.to === currentName);
354
- // Collect neighbor names
355
- connectedRelations.forEach(r => {
356
- const neighborName = r.from === currentName ? r.to : r.from;
357
- neighborNames.add(neighborName);
534
+ return this.withReadLock(() => {
535
+ const allEntities = this.getAllEntities();
536
+ const filteredEntities = allEntities.filter(e => regex.test(e.name) ||
537
+ regex.test(e.entityType) ||
538
+ e.observations.some(o => regex.test(o)));
539
+ const filteredEntityNames = new Set(filteredEntities.map(e => e.name));
540
+ const allRelations = this.getAllRelations();
541
+ const filteredRelations = allRelations.filter(r => {
542
+ if (direction === 'forward')
543
+ return filteredEntityNames.has(r.from);
544
+ if (direction === 'backward')
545
+ return filteredEntityNames.has(r.to);
546
+ return filteredEntityNames.has(r.from) && filteredEntityNames.has(r.to);
358
547
  });
359
- if (currentDepth < depth) {
360
- // Traverse to connected entities
361
- connectedRelations.forEach(r => {
362
- const nextEntity = r.from === currentName ? r.to : r.from;
363
- traverse(nextEntity, currentDepth + 1);
364
- });
365
- }
366
- };
367
- traverse(entityName, 0);
368
- // Remove the starting entity from neighbors (it's not its own neighbor)
369
- neighborNames.delete(entityName);
370
- // Build neighbor objects with timestamps
371
- const entityMap = new Map(graph.entities.map(e => [e.name, e]));
372
- const neighbors = Array.from(neighborNames).map(name => {
373
- const entity = entityMap.get(name);
548
+ const rankMaps = this.getRankMapsUnlocked();
374
549
  return {
375
- name,
376
- mtime: entity?.mtime,
377
- obsMtime: entity?.obsMtime,
550
+ entities: sortEntities(filteredEntities, sortBy, sortDir, rankMaps),
551
+ relations: filteredRelations,
378
552
  };
379
553
  });
380
- return sortNeighbors(neighbors, sortBy, sortDir);
381
554
  }
382
- async findPath(fromEntity, toEntity, maxDepth = 5) {
383
- const graph = await this.loadGraph();
384
- const visited = new Set();
385
- const dfs = (current, target, path, depth) => {
386
- if (depth > maxDepth || visited.has(current))
387
- return null;
388
- if (current === target)
389
- return path;
390
- visited.add(current);
391
- const outgoingRelations = graph.relations.filter(r => r.from === current);
392
- for (const relation of outgoingRelations) {
393
- const result = dfs(relation.to, target, [...path, relation], depth + 1);
394
- if (result)
395
- return result;
555
+ async openNodes(names, direction = 'forward') {
556
+ return this.withReadLock(() => {
557
+ const filteredEntities = [];
558
+ for (const name of names) {
559
+ const offset = this.nameIndex.get(name);
560
+ if (offset === undefined)
561
+ continue;
562
+ filteredEntities.push(this.recordToEntity(this.gf.readEntity(offset)));
396
563
  }
397
- visited.delete(current);
398
- return null;
399
- };
400
- return dfs(fromEntity, toEntity, [], 0) || [];
564
+ const filteredEntityNames = new Set(filteredEntities.map(e => e.name));
565
+ const filteredRelations = [];
566
+ for (const name of filteredEntityNames) {
567
+ const offset = this.nameIndex.get(name);
568
+ const edges = this.gf.getEdges(offset);
569
+ for (const edge of edges) {
570
+ if (edge.direction !== DIR_FORWARD && edge.direction !== DIR_BACKWARD)
571
+ continue;
572
+ const targetRec = this.gf.readEntity(edge.targetOffset);
573
+ const targetName = this.st.get(BigInt(targetRec.nameId));
574
+ const relationType = this.st.get(BigInt(edge.relTypeId));
575
+ const mtime = Number(edge.mtime);
576
+ if (edge.direction === DIR_FORWARD) {
577
+ if (direction === 'backward')
578
+ continue;
579
+ const r = { from: name, to: targetName, relationType };
580
+ if (mtime > 0)
581
+ r.mtime = mtime;
582
+ filteredRelations.push(r);
583
+ }
584
+ else {
585
+ if (direction === 'forward')
586
+ continue;
587
+ if (direction === 'any' && !filteredEntityNames.has(targetName))
588
+ continue;
589
+ const r = { from: targetName, to: name, relationType };
590
+ if (mtime > 0)
591
+ r.mtime = mtime;
592
+ filteredRelations.push(r);
593
+ }
594
+ }
595
+ }
596
+ return { entities: filteredEntities, relations: filteredRelations };
597
+ });
598
+ }
599
+ async getNeighbors(entityName, depth = 1, sortBy, sortDir, direction = 'forward') {
600
+ return this.withReadLock(() => {
601
+ const startOffset = this.nameIndex.get(entityName);
602
+ if (startOffset === undefined)
603
+ return [];
604
+ const visited = new Set();
605
+ const neighborNames = new Set();
606
+ const traverse = (currentName, currentDepth) => {
607
+ if (currentDepth > depth || visited.has(currentName))
608
+ return;
609
+ visited.add(currentName);
610
+ const offset = this.nameIndex.get(currentName);
611
+ if (offset === undefined)
612
+ return;
613
+ const edges = this.gf.getEdges(offset);
614
+ for (const edge of edges) {
615
+ if (direction === 'forward' && edge.direction !== DIR_FORWARD)
616
+ continue;
617
+ if (direction === 'backward' && edge.direction !== DIR_BACKWARD)
618
+ continue;
619
+ const targetRec = this.gf.readEntity(edge.targetOffset);
620
+ const neighborName = this.st.get(BigInt(targetRec.nameId));
621
+ neighborNames.add(neighborName);
622
+ if (currentDepth < depth) {
623
+ traverse(neighborName, currentDepth + 1);
624
+ }
625
+ }
626
+ };
627
+ traverse(entityName, 0);
628
+ neighborNames.delete(entityName);
629
+ const neighbors = Array.from(neighborNames).map(name => {
630
+ const offset = this.nameIndex.get(name);
631
+ if (!offset)
632
+ return { name };
633
+ const rec = this.gf.readEntity(offset);
634
+ const mtime = Number(rec.mtime);
635
+ const obsMtime = Number(rec.obsMtime);
636
+ const n = { name };
637
+ if (mtime > 0)
638
+ n.mtime = mtime;
639
+ if (obsMtime > 0)
640
+ n.obsMtime = obsMtime;
641
+ return n;
642
+ });
643
+ const rankMaps = this.getRankMapsUnlocked();
644
+ return sortNeighbors(neighbors, sortBy, sortDir, rankMaps);
645
+ });
646
+ }
647
+ async findPath(fromEntity, toEntity, maxDepth = 5, direction = 'forward') {
648
+ return this.withReadLock(() => {
649
+ const visited = new Set();
650
+ const dfs = (current, target, pathSoFar, depth) => {
651
+ if (depth > maxDepth || visited.has(current))
652
+ return null;
653
+ if (current === target)
654
+ return pathSoFar;
655
+ visited.add(current);
656
+ const offset = this.nameIndex.get(current);
657
+ if (offset === undefined) {
658
+ visited.delete(current);
659
+ return null;
660
+ }
661
+ const edges = this.gf.getEdges(offset);
662
+ for (const edge of edges) {
663
+ if (direction === 'forward' && edge.direction !== DIR_FORWARD)
664
+ continue;
665
+ if (direction === 'backward' && edge.direction !== DIR_BACKWARD)
666
+ continue;
667
+ const targetRec = this.gf.readEntity(edge.targetOffset);
668
+ const nextName = this.st.get(BigInt(targetRec.nameId));
669
+ const relationType = this.st.get(BigInt(edge.relTypeId));
670
+ const mtime = Number(edge.mtime);
671
+ let rel;
672
+ if (edge.direction === DIR_FORWARD) {
673
+ rel = { from: current, to: nextName, relationType };
674
+ }
675
+ else {
676
+ rel = { from: nextName, to: current, relationType };
677
+ }
678
+ if (mtime > 0)
679
+ rel.mtime = mtime;
680
+ const result = dfs(nextName, target, [...pathSoFar, rel], depth + 1);
681
+ if (result)
682
+ return result;
683
+ }
684
+ visited.delete(current);
685
+ return null;
686
+ };
687
+ return dfs(fromEntity, toEntity, [], 0) || [];
688
+ });
401
689
  }
402
690
  async getEntitiesByType(entityType, sortBy, sortDir) {
403
- const graph = await this.loadGraph();
404
- const filtered = graph.entities.filter(e => e.entityType === entityType);
405
- return sortEntities(filtered, sortBy, sortDir);
691
+ return this.withReadLock(() => {
692
+ const filtered = this.getAllEntities().filter(e => e.entityType === entityType);
693
+ const rankMaps = this.getRankMapsUnlocked();
694
+ return sortEntities(filtered, sortBy, sortDir, rankMaps);
695
+ });
406
696
  }
407
697
  async getEntityTypes() {
408
- const graph = await this.loadGraph();
409
- const types = new Set(graph.entities.map(e => e.entityType));
410
- return Array.from(types).sort();
698
+ return this.withReadLock(() => {
699
+ const types = new Set(this.getAllEntities().map(e => e.entityType));
700
+ return Array.from(types).sort();
701
+ });
411
702
  }
412
703
  async getRelationTypes() {
413
- const graph = await this.loadGraph();
414
- const types = new Set(graph.relations.map(r => r.relationType));
415
- return Array.from(types).sort();
704
+ return this.withReadLock(() => {
705
+ const types = new Set(this.getAllRelations().map(r => r.relationType));
706
+ return Array.from(types).sort();
707
+ });
416
708
  }
417
709
  async getStats() {
418
- const graph = await this.loadGraph();
419
- const entityTypes = new Set(graph.entities.map(e => e.entityType));
420
- const relationTypes = new Set(graph.relations.map(r => r.relationType));
421
- return {
422
- entityCount: graph.entities.length,
423
- relationCount: graph.relations.length,
424
- entityTypes: entityTypes.size,
425
- relationTypes: relationTypes.size
426
- };
710
+ return this.withReadLock(() => {
711
+ const entities = this.getAllEntities();
712
+ const relations = this.getAllRelations();
713
+ const entityTypes = new Set(entities.map(e => e.entityType));
714
+ const relationTypes = new Set(relations.map(r => r.relationType));
715
+ return {
716
+ entityCount: entities.length,
717
+ relationCount: relations.length,
718
+ entityTypes: entityTypes.size,
719
+ relationTypes: relationTypes.size,
720
+ };
721
+ });
427
722
  }
428
723
  async getOrphanedEntities(strict = false, sortBy, sortDir) {
429
- const graph = await this.loadGraph();
430
- if (!strict) {
431
- // Simple mode: entities with no relations at all
432
- const connectedEntityNames = new Set();
433
- graph.relations.forEach(r => {
434
- connectedEntityNames.add(r.from);
435
- connectedEntityNames.add(r.to);
724
+ return this.withReadLock(() => {
725
+ const entities = this.getAllEntities();
726
+ if (!strict) {
727
+ const connectedEntityNames = new Set();
728
+ const relations = this.getAllRelations();
729
+ relations.forEach(r => {
730
+ connectedEntityNames.add(r.from);
731
+ connectedEntityNames.add(r.to);
732
+ });
733
+ const orphans = entities.filter(e => !connectedEntityNames.has(e.name));
734
+ const rankMaps = this.getRankMapsUnlocked();
735
+ return sortEntities(orphans, sortBy, sortDir, rankMaps);
736
+ }
737
+ const neighbors = new Map();
738
+ entities.forEach(e => neighbors.set(e.name, new Set()));
739
+ const relations = this.getAllRelations();
740
+ relations.forEach(r => {
741
+ neighbors.get(r.from)?.add(r.to);
742
+ neighbors.get(r.to)?.add(r.from);
436
743
  });
437
- const orphans = graph.entities.filter(e => !connectedEntityNames.has(e.name));
438
- return sortEntities(orphans, sortBy, sortDir);
439
- }
440
- // Strict mode: entities not connected to "Self" (directly or indirectly)
441
- // Build adjacency list (bidirectional)
442
- const neighbors = new Map();
443
- graph.entities.forEach(e => neighbors.set(e.name, new Set()));
444
- graph.relations.forEach(r => {
445
- neighbors.get(r.from)?.add(r.to);
446
- neighbors.get(r.to)?.add(r.from);
447
- });
448
- // BFS from Self to find all connected entities
449
- const connectedToSelf = new Set();
450
- const queue = ['Self'];
451
- while (queue.length > 0) {
452
- const current = queue.shift();
453
- if (connectedToSelf.has(current))
454
- continue;
455
- connectedToSelf.add(current);
456
- const currentNeighbors = neighbors.get(current);
457
- if (currentNeighbors) {
458
- for (const neighbor of currentNeighbors) {
459
- if (!connectedToSelf.has(neighbor)) {
460
- queue.push(neighbor);
744
+ const connectedToSelf = new Set();
745
+ const queue = ['Self'];
746
+ while (queue.length > 0) {
747
+ const current = queue.shift();
748
+ if (connectedToSelf.has(current))
749
+ continue;
750
+ connectedToSelf.add(current);
751
+ const currentNeighbors = neighbors.get(current);
752
+ if (currentNeighbors) {
753
+ for (const neighbor of currentNeighbors) {
754
+ if (!connectedToSelf.has(neighbor)) {
755
+ queue.push(neighbor);
756
+ }
461
757
  }
462
758
  }
463
759
  }
464
- }
465
- // Return entities not connected to Self (excluding Self itself if it exists)
466
- const orphans = graph.entities.filter(e => !connectedToSelf.has(e.name));
467
- return sortEntities(orphans, sortBy, sortDir);
760
+ const orphans = entities.filter(e => !connectedToSelf.has(e.name));
761
+ const rankMaps = this.getRankMapsUnlocked();
762
+ return sortEntities(orphans, sortBy, sortDir, rankMaps);
763
+ });
468
764
  }
469
765
  async validateGraph() {
470
- const graph = await this.loadGraph();
471
- const entityNames = new Set(graph.entities.map(e => e.name));
472
- const missingEntities = new Set();
473
- const observationViolations = [];
474
- // Check for missing entities in relations
475
- graph.relations.forEach(r => {
476
- if (!entityNames.has(r.from)) {
477
- missingEntities.add(r.from);
478
- }
479
- if (!entityNames.has(r.to)) {
480
- missingEntities.add(r.to);
481
- }
482
- });
483
- // Check for observation limit violations
484
- graph.entities.forEach(e => {
485
- const oversizedObservations = [];
486
- e.observations.forEach((obs, idx) => {
487
- if (obs.length > 140) {
488
- oversizedObservations.push(idx);
489
- }
766
+ return this.withReadLock(() => {
767
+ const entities = this.getAllEntities();
768
+ const relations = this.getAllRelations();
769
+ const entityNames = new Set(entities.map(e => e.name));
770
+ const missingEntities = new Set();
771
+ const observationViolations = [];
772
+ relations.forEach(r => {
773
+ if (!entityNames.has(r.from))
774
+ missingEntities.add(r.from);
775
+ if (!entityNames.has(r.to))
776
+ missingEntities.add(r.to);
490
777
  });
491
- if (e.observations.length > 2 || oversizedObservations.length > 0) {
492
- observationViolations.push({
493
- entity: e.name,
494
- count: e.observations.length,
495
- oversizedObservations
778
+ entities.forEach(e => {
779
+ const oversizedObservations = [];
780
+ e.observations.forEach((obs, idx) => {
781
+ if (obs.length > 140)
782
+ oversizedObservations.push(idx);
496
783
  });
497
- }
784
+ if (e.observations.length > 2 || oversizedObservations.length > 0) {
785
+ observationViolations.push({
786
+ entity: e.name,
787
+ count: e.observations.length,
788
+ oversizedObservations,
789
+ });
790
+ }
791
+ });
792
+ return { missingEntities: Array.from(missingEntities), observationViolations };
498
793
  });
499
- return {
500
- missingEntities: Array.from(missingEntities),
501
- observationViolations
502
- };
503
794
  }
504
- async randomWalk(start, depth = 3, seed) {
505
- const graph = await this.loadGraph();
506
- // Verify start entity exists
507
- const startEntity = graph.entities.find(e => e.name === start);
508
- if (!startEntity) {
509
- throw new Error(`Start entity not found: ${start}`);
510
- }
511
- // Create seeded RNG if seed provided, otherwise use crypto.randomBytes
512
- let rngState = seed ? this.hashSeed(seed) : null;
513
- const random = () => {
514
- if (rngState !== null) {
515
- // Simple seeded PRNG (xorshift32)
516
- rngState ^= rngState << 13;
517
- rngState ^= rngState >>> 17;
518
- rngState ^= rngState << 5;
519
- return (rngState >>> 0) / 0xFFFFFFFF;
795
+ async randomWalk(start, depth = 3, seed, direction = 'forward') {
796
+ return this.withReadLock(() => {
797
+ const startOffset = this.nameIndex.get(start);
798
+ if (!startOffset) {
799
+ throw new Error(`Start entity not found: ${start}`);
520
800
  }
521
- else {
522
- return randomBytes(4).readUInt32BE() / 0xFFFFFFFF;
523
- }
524
- };
525
- const path = [start];
526
- let current = start;
527
- for (let i = 0; i < depth; i++) {
528
- // Get unique neighbors (both directions)
529
- const neighbors = new Set();
530
- for (const rel of graph.relations) {
531
- if (rel.from === current && rel.to !== current)
532
- neighbors.add(rel.to);
533
- if (rel.to === current && rel.from !== current)
534
- neighbors.add(rel.from);
801
+ // Create seeded RNG if seed provided
802
+ let rngState = seed ? this.hashSeed(seed) : null;
803
+ const random = () => {
804
+ if (rngState !== null) {
805
+ rngState ^= rngState << 13;
806
+ rngState ^= rngState >>> 17;
807
+ rngState ^= rngState << 5;
808
+ return (rngState >>> 0) / 0xFFFFFFFF;
809
+ }
810
+ else {
811
+ return randomBytes(4).readUInt32BE() / 0xFFFFFFFF;
812
+ }
813
+ };
814
+ const pathNames = [start];
815
+ let current = start;
816
+ for (let i = 0; i < depth; i++) {
817
+ const offset = this.nameIndex.get(current);
818
+ if (!offset)
819
+ break;
820
+ const edges = this.gf.getEdges(offset);
821
+ const validNeighbors = new Set();
822
+ for (const edge of edges) {
823
+ if (direction === 'forward' && edge.direction !== DIR_FORWARD)
824
+ continue;
825
+ if (direction === 'backward' && edge.direction !== DIR_BACKWARD)
826
+ continue;
827
+ const targetRec = this.gf.readEntity(edge.targetOffset);
828
+ const neighborName = this.st.get(BigInt(targetRec.nameId));
829
+ if (neighborName !== current)
830
+ validNeighbors.add(neighborName);
831
+ }
832
+ const neighborArr = Array.from(validNeighbors).filter(n => this.nameIndex.has(n));
833
+ if (neighborArr.length === 0)
834
+ break;
835
+ const idx = Math.floor(random() * neighborArr.length);
836
+ current = neighborArr[idx];
837
+ pathNames.push(current);
535
838
  }
536
- // Filter to only existing entities
537
- const validNeighbors = Array.from(neighbors).filter(n => graph.entities.some(e => e.name === n));
538
- if (validNeighbors.length === 0)
539
- break; // Dead end
540
- // Pick random neighbor (uniform over entities)
541
- const idx = Math.floor(random() * validNeighbors.length);
542
- current = validNeighbors[idx];
543
- path.push(current);
544
- }
545
- return { entity: current, path };
839
+ return { entity: current, path: pathNames };
840
+ });
546
841
  }
547
842
  hashSeed(seed) {
548
- // Simple string hash to 32-bit integer
549
843
  let hash = 0;
550
844
  for (let i = 0; i < seed.length; i++) {
551
845
  const char = seed.charCodeAt(i);
552
846
  hash = ((hash << 5) - hash) + char;
553
- hash = hash & hash; // Convert to 32-bit integer
847
+ hash = hash & hash;
554
848
  }
555
- return hash || 1; // Ensure non-zero for xorshift
849
+ return hash || 1;
556
850
  }
557
851
  decodeTimestamp(timestamp, relative = false) {
558
852
  const ts = timestamp ?? Date.now();
@@ -593,43 +887,54 @@ export class KnowledgeGraphManager {
593
887
  return result;
594
888
  }
595
889
  async addThought(observations, previousCtxId) {
596
- return this.withLock(async () => {
597
- const graph = await this.loadGraph();
598
- // Validate observations
599
- if (observations.length > 2) {
600
- throw new Error(`Thought has ${observations.length} observations. Maximum allowed is 2.`);
890
+ // Validate observations (can do outside lock)
891
+ if (observations.length > 2) {
892
+ throw new Error(`Thought has ${observations.length} observations. Maximum allowed is 2.`);
893
+ }
894
+ for (const obs of observations) {
895
+ if (obs.length > 140) {
896
+ throw new Error(`Observation exceeds 140 characters (${obs.length} chars): "${obs.substring(0, 50)}..."`);
601
897
  }
898
+ }
899
+ return this.withWriteLock(() => {
900
+ const now = BigInt(Date.now());
901
+ const ctxId = randomBytes(12).toString('hex');
902
+ const obsMtime = observations.length > 0 ? now : 0n;
903
+ const rec = this.gf.createEntity(ctxId, 'Thought', now, obsMtime);
602
904
  for (const obs of observations) {
603
- if (obs.length > 140) {
604
- throw new Error(`Observation exceeds 140 characters (${obs.length} chars): "${obs.substring(0, 50)}..."`);
605
- }
905
+ this.gf.addObservation(rec.offset, obs, now);
606
906
  }
607
- // Generate new context ID (24-char hex)
608
- const now = Date.now();
609
- const ctxId = randomBytes(12).toString('hex');
610
- // Create thought entity
611
- const thoughtEntity = {
612
- name: ctxId,
613
- entityType: "Thought",
614
- observations,
615
- mtime: now,
616
- obsMtime: observations.length > 0 ? now : undefined,
617
- };
618
- graph.entities.push(thoughtEntity);
619
- // Link to previous thought if it exists
907
+ if (observations.length > 0) {
908
+ const updated = this.gf.readEntity(rec.offset);
909
+ updated.mtime = now;
910
+ updated.obsMtime = now;
911
+ this.gf.updateEntity(updated);
912
+ }
913
+ this.nameIndex.set(ctxId, rec.offset);
620
914
  if (previousCtxId) {
621
- const prevEntity = graph.entities.find(e => e.name === previousCtxId);
622
- if (prevEntity) {
623
- // Update mtime on previous entity since we're adding a relation from it
624
- prevEntity.mtime = now;
625
- // Bidirectional chain: previous -> new (follows) and new -> previous (preceded_by)
626
- graph.relations.push({ from: previousCtxId, to: ctxId, relationType: "follows", mtime: now }, { from: ctxId, to: previousCtxId, relationType: "preceded_by", mtime: now });
915
+ const prevOffset = this.nameIndex.get(previousCtxId);
916
+ if (prevOffset !== undefined) {
917
+ const prevRec = this.gf.readEntity(prevOffset);
918
+ prevRec.mtime = now;
919
+ this.gf.updateEntity(prevRec);
920
+ const followsTypeId = Number(this.st.intern('follows'));
921
+ this.gf.addEdge(prevOffset, { targetOffset: rec.offset, direction: DIR_FORWARD, relTypeId: followsTypeId, mtime: now });
922
+ const followsTypeId2 = Number(this.st.intern('follows'));
923
+ this.gf.addEdge(rec.offset, { targetOffset: prevOffset, direction: DIR_BACKWARD, relTypeId: followsTypeId2, mtime: now });
924
+ const precededByTypeId = Number(this.st.intern('preceded_by'));
925
+ this.gf.addEdge(rec.offset, { targetOffset: prevOffset, direction: DIR_FORWARD, relTypeId: precededByTypeId, mtime: now });
926
+ const precededByTypeId2 = Number(this.st.intern('preceded_by'));
927
+ this.gf.addEdge(prevOffset, { targetOffset: rec.offset, direction: DIR_BACKWARD, relTypeId: precededByTypeId2, mtime: now });
627
928
  }
628
929
  }
629
- await this.saveGraph(graph);
630
930
  return { ctxId };
631
931
  });
632
932
  }
933
+ /** Close the underlying binary store files */
934
+ close() {
935
+ this.gf.close();
936
+ this.st.close();
937
+ }
633
938
  }
634
939
  /**
635
940
  * Creates a configured MCP server instance with all tools registered.
@@ -645,12 +950,16 @@ export function createServer(memoryFilePath) {
645
950
  sizes: ["any"]
646
951
  }
647
952
  ],
648
- version: "0.0.11",
953
+ version: "0.0.13",
649
954
  }, {
650
955
  capabilities: {
651
956
  tools: {},
652
957
  },
653
958
  });
959
+ // Close binary store on server close
960
+ server.onclose = () => {
961
+ knowledgeGraphManager.close();
962
+ };
654
963
  server.setRequestHandler(ListToolsRequestSchema, async () => {
655
964
  return {
656
965
  tools: [
@@ -798,7 +1107,8 @@ export function createServer(memoryFilePath) {
798
1107
  type: "object",
799
1108
  properties: {
800
1109
  query: { type: "string", description: "Regex pattern to match against entity names, types, and observations." },
801
- sortBy: { type: "string", enum: ["mtime", "obsMtime", "name"], description: "Sort field for entities. Omit for insertion order." },
1110
+ direction: { type: "string", enum: ["forward", "backward", "any"], description: "Edge direction filter for returned relations. Default: forward" },
1111
+ sortBy: { type: "string", enum: ["mtime", "obsMtime", "name", "pagerank", "llmrank"], description: "Sort field for entities. Omit for insertion order." },
802
1112
  sortDir: { type: "string", enum: ["asc", "desc"], description: "Sort direction. Default: desc for timestamps, asc for name." },
803
1113
  entityCursor: { type: "number", description: "Cursor for entity pagination (from previous response's nextCursor)" },
804
1114
  relationCursor: { type: "number", description: "Cursor for relation pagination" },
@@ -806,23 +1116,6 @@ export function createServer(memoryFilePath) {
806
1116
  required: ["query"],
807
1117
  },
808
1118
  },
809
- {
810
- name: "open_nodes_filtered",
811
- description: "Open specific nodes in the knowledge graph by their names, filtering relations to only those between the opened nodes. Results are paginated (max 512 chars).",
812
- inputSchema: {
813
- type: "object",
814
- properties: {
815
- names: {
816
- type: "array",
817
- items: { type: "string" },
818
- description: "An array of entity names to retrieve",
819
- },
820
- entityCursor: { type: "number", description: "Cursor for entity pagination" },
821
- relationCursor: { type: "number", description: "Cursor for relation pagination" },
822
- },
823
- required: ["names"],
824
- },
825
- },
826
1119
  {
827
1120
  name: "open_nodes",
828
1121
  description: "Open specific nodes in the knowledge graph by their names. Results are paginated (max 512 chars).",
@@ -834,6 +1127,7 @@ export function createServer(memoryFilePath) {
834
1127
  items: { type: "string" },
835
1128
  description: "An array of entity names to retrieve",
836
1129
  },
1130
+ direction: { type: "string", enum: ["forward", "backward", "any"], description: "Edge direction filter for returned relations. Default: forward" },
837
1131
  entityCursor: { type: "number", description: "Cursor for entity pagination" },
838
1132
  relationCursor: { type: "number", description: "Cursor for relation pagination" },
839
1133
  },
@@ -848,7 +1142,8 @@ export function createServer(memoryFilePath) {
848
1142
  properties: {
849
1143
  entityName: { type: "string", description: "The name of the entity to find neighbors for" },
850
1144
  depth: { type: "number", description: "Maximum depth to traverse (default: 1)", default: 1 },
851
- sortBy: { type: "string", enum: ["mtime", "obsMtime", "name"], description: "Sort field for neighbors. Omit for arbitrary order." },
1145
+ direction: { type: "string", enum: ["forward", "backward", "any"], description: "Edge direction to follow. Default: forward" },
1146
+ sortBy: { type: "string", enum: ["mtime", "obsMtime", "name", "pagerank", "llmrank"], description: "Sort field for neighbors. Omit for arbitrary order." },
852
1147
  sortDir: { type: "string", enum: ["asc", "desc"], description: "Sort direction. Default: desc for timestamps, asc for name." },
853
1148
  cursor: { type: "number", description: "Cursor for pagination" },
854
1149
  },
@@ -864,6 +1159,7 @@ export function createServer(memoryFilePath) {
864
1159
  fromEntity: { type: "string", description: "The name of the starting entity" },
865
1160
  toEntity: { type: "string", description: "The name of the target entity" },
866
1161
  maxDepth: { type: "number", description: "Maximum depth to search (default: 5)", default: 5 },
1162
+ direction: { type: "string", enum: ["forward", "backward", "any"], description: "Edge direction to follow. Default: forward" },
867
1163
  cursor: { type: "number", description: "Cursor for pagination" },
868
1164
  },
869
1165
  required: ["fromEntity", "toEntity"],
@@ -876,7 +1172,7 @@ export function createServer(memoryFilePath) {
876
1172
  type: "object",
877
1173
  properties: {
878
1174
  entityType: { type: "string", description: "The type of entities to retrieve" },
879
- sortBy: { type: "string", enum: ["mtime", "obsMtime", "name"], description: "Sort field for entities. Omit for insertion order." },
1175
+ sortBy: { type: "string", enum: ["mtime", "obsMtime", "name", "pagerank", "llmrank"], description: "Sort field for entities. Omit for insertion order." },
880
1176
  sortDir: { type: "string", enum: ["asc", "desc"], description: "Sort direction. Default: desc for timestamps, asc for name." },
881
1177
  cursor: { type: "number", description: "Cursor for pagination" },
882
1178
  },
@@ -914,7 +1210,7 @@ export function createServer(memoryFilePath) {
914
1210
  type: "object",
915
1211
  properties: {
916
1212
  strict: { type: "boolean", description: "If true, returns entities not connected to 'Self' (directly or indirectly). Default: false" },
917
- sortBy: { type: "string", enum: ["mtime", "obsMtime", "name"], description: "Sort field for entities. Omit for insertion order." },
1213
+ sortBy: { type: "string", enum: ["mtime", "obsMtime", "name", "pagerank", "llmrank"], description: "Sort field for entities. Omit for insertion order." },
918
1214
  sortDir: { type: "string", enum: ["asc", "desc"], description: "Sort direction. Default: desc for timestamps, asc for name." },
919
1215
  cursor: { type: "number", description: "Cursor for pagination" },
920
1216
  },
@@ -948,6 +1244,7 @@ export function createServer(memoryFilePath) {
948
1244
  start: { type: "string", description: "Name of the entity to start the walk from." },
949
1245
  depth: { type: "number", description: "Number of steps to take. Default: 3" },
950
1246
  seed: { type: "string", description: "Optional seed for reproducible walks." },
1247
+ direction: { type: "string", enum: ["forward", "backward", "any"], description: "Edge direction to follow. Default: forward" },
951
1248
  },
952
1249
  required: ["start"],
953
1250
  },
@@ -983,39 +1280,49 @@ Use this to build chains of reasoning that persist in the graph. Each thought ca
983
1280
  throw new Error(`No arguments provided for tool: ${name}`);
984
1281
  }
985
1282
  switch (name) {
986
- case "create_entities":
987
- return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.createEntities(args.entities), null, 2) }] };
988
- case "create_relations":
989
- return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.createRelations(args.relations), null, 2) }] };
1283
+ case "create_entities": {
1284
+ const result = await knowledgeGraphManager.createEntities(args.entities);
1285
+ knowledgeGraphManager.resample(); // Re-run structural sampling after graph mutation
1286
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
1287
+ }
1288
+ case "create_relations": {
1289
+ const result = await knowledgeGraphManager.createRelations(args.relations);
1290
+ knowledgeGraphManager.resample(); // Re-run structural sampling after graph mutation
1291
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
1292
+ }
990
1293
  case "add_observations":
991
1294
  return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.addObservations(args.observations), null, 2) }] };
992
1295
  case "delete_entities":
993
1296
  await knowledgeGraphManager.deleteEntities(args.entityNames);
1297
+ knowledgeGraphManager.resample(); // Re-run structural sampling after graph mutation
994
1298
  return { content: [{ type: "text", text: "Entities deleted successfully" }] };
995
1299
  case "delete_observations":
996
1300
  await knowledgeGraphManager.deleteObservations(args.deletions);
997
1301
  return { content: [{ type: "text", text: "Observations deleted successfully" }] };
998
1302
  case "delete_relations":
999
1303
  await knowledgeGraphManager.deleteRelations(args.relations);
1304
+ knowledgeGraphManager.resample(); // Re-run structural sampling after graph mutation
1000
1305
  return { content: [{ type: "text", text: "Relations deleted successfully" }] };
1001
1306
  case "search_nodes": {
1002
- const graph = await knowledgeGraphManager.searchNodes(args.query, args.sortBy, args.sortDir);
1003
- return { content: [{ type: "text", text: JSON.stringify(paginateGraph(graph, args.entityCursor ?? 0, args.relationCursor ?? 0)) }] };
1004
- }
1005
- case "open_nodes_filtered": {
1006
- const graph = await knowledgeGraphManager.openNodesFiltered(args.names);
1307
+ const graph = await knowledgeGraphManager.searchNodes(args.query, args.sortBy, args.sortDir, args.direction ?? 'forward');
1308
+ // Record walker visits for entities that will be returned to the LLM
1309
+ knowledgeGraphManager.recordWalkerVisits(graph.entities.map(e => e.name));
1007
1310
  return { content: [{ type: "text", text: JSON.stringify(paginateGraph(graph, args.entityCursor ?? 0, args.relationCursor ?? 0)) }] };
1008
1311
  }
1009
1312
  case "open_nodes": {
1010
- const graph = await knowledgeGraphManager.openNodes(args.names);
1313
+ const graph = await knowledgeGraphManager.openNodes(args.names, args.direction ?? 'forward');
1314
+ // Record walker visits for opened nodes
1315
+ knowledgeGraphManager.recordWalkerVisits(graph.entities.map(e => e.name));
1011
1316
  return { content: [{ type: "text", text: JSON.stringify(paginateGraph(graph, args.entityCursor ?? 0, args.relationCursor ?? 0)) }] };
1012
1317
  }
1013
1318
  case "get_neighbors": {
1014
- const neighbors = await knowledgeGraphManager.getNeighbors(args.entityName, args.depth ?? 1, args.sortBy, args.sortDir);
1319
+ const neighbors = await knowledgeGraphManager.getNeighbors(args.entityName, args.depth ?? 1, args.sortBy, args.sortDir, args.direction ?? 'forward');
1320
+ // Record walker visits for returned neighbors
1321
+ knowledgeGraphManager.recordWalkerVisits(neighbors.map(n => n.name));
1015
1322
  return { content: [{ type: "text", text: JSON.stringify(paginateItems(neighbors, args.cursor ?? 0)) }] };
1016
1323
  }
1017
1324
  case "find_path": {
1018
- const path = await knowledgeGraphManager.findPath(args.fromEntity, args.toEntity, args.maxDepth);
1325
+ const path = await knowledgeGraphManager.findPath(args.fromEntity, args.toEntity, args.maxDepth, args.direction ?? 'forward');
1019
1326
  return { content: [{ type: "text", text: JSON.stringify(paginateItems(path, args.cursor ?? 0)) }] };
1020
1327
  }
1021
1328
  case "get_entities_by_type": {
@@ -1037,7 +1344,7 @@ Use this to build chains of reasoning that persist in the graph. Each thought ca
1037
1344
  case "decode_timestamp":
1038
1345
  return { content: [{ type: "text", text: JSON.stringify(knowledgeGraphManager.decodeTimestamp(args.timestamp, args.relative ?? false)) }] };
1039
1346
  case "random_walk": {
1040
- const result = await knowledgeGraphManager.randomWalk(args.start, args.depth ?? 3, args.seed);
1347
+ const result = await knowledgeGraphManager.randomWalk(args.start, args.depth ?? 3, args.seed, args.direction ?? 'forward');
1041
1348
  return { content: [{ type: "text", text: JSON.stringify(result) }] };
1042
1349
  }
1043
1350
  case "sequentialthinking": {