@levalicious/server-memory 0.0.10 → 0.0.12

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,574 +177,764 @@ 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
- bclCtr = 0;
135
- bclTerm = "";
136
- 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;
137
186
  constructor(memoryFilePath = DEFAULT_MEMORY_FILE_PATH) {
138
- 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
+ }
139
201
  }
140
- async withLock(fn) {
141
- // 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();
142
210
  try {
143
- await fs.access(this.memoryFilePath);
211
+ this.gf.refresh();
212
+ this.st.refresh();
213
+ this.maybeRebuildNameIndex();
214
+ return fn();
144
215
  }
145
- catch {
146
- await fs.writeFile(this.memoryFilePath, "");
216
+ finally {
217
+ this.gf.unlock();
147
218
  }
148
- 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();
149
226
  try {
150
- 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;
151
236
  }
152
237
  finally {
153
- await release();
238
+ this.gf.unlock();
154
239
  }
155
240
  }
156
- async loadGraph() {
157
- try {
158
- const data = await fs.readFile(this.memoryFilePath, "utf-8");
159
- const lines = data.split("\n").filter(line => line.trim() !== "");
160
- return lines.reduce((graph, line) => {
161
- const item = JSON.parse(line);
162
- if (item.type === "entity")
163
- graph.entities.push(item);
164
- if (item.type === "relation")
165
- graph.relations.push(item);
166
- return graph;
167
- }, { 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);
168
273
  }
169
- catch (error) {
170
- if (error?.code === "ENOENT") {
171
- 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);
172
341
  }
173
- throw error;
174
342
  }
343
+ return relations;
175
344
  }
176
- async saveGraph(graph) {
177
- const lines = [
178
- ...graph.entities.map(e => JSON.stringify({ type: "entity", ...e })),
179
- ...graph.relations.map(r => JSON.stringify({ type: "relation", ...r })),
180
- ];
181
- const content = lines.join("\n") + (lines.length > 0 ? "\n" : "");
182
- 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
+ };
183
351
  }
184
352
  async createEntities(entities) {
185
- return this.withLock(async () => {
186
- const graph = await this.loadGraph();
187
- // Validate observation limits
188
- for (const entity of entities) {
189
- if (entity.observations.length > 2) {
190
- 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)}..."`);
191
361
  }
192
- for (const obs of entity.observations) {
193
- if (obs.length > 140) {
194
- throw new Error(`Observation in entity "${entity.name}" exceeds 140 characters (${obs.length} chars): "${obs.substring(0, 50)}..."`);
195
- }
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);
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);
196
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);
197
396
  }
198
- const now = Date.now();
199
- const newEntities = entities
200
- .filter(e => !graph.entities.some(existingEntity => existingEntity.name === e.name))
201
- .map(e => ({ ...e, mtime: now, obsMtime: e.observations.length > 0 ? now : undefined }));
202
- graph.entities.push(...newEntities);
203
- await this.saveGraph(graph);
204
397
  return newEntities;
205
398
  });
206
399
  }
207
400
  async createRelations(relations) {
208
- return this.withLock(async () => {
209
- const graph = await this.loadGraph();
210
- const now = Date.now();
211
- // Update mtime on 'from' entities when relations are added
212
- const fromEntityNames = new Set(relations.map(r => r.from));
213
- graph.entities.forEach(e => {
214
- if (fromEntityNames.has(e.name)) {
215
- e.mtime = now;
216
- }
217
- });
218
- const newRelations = relations
219
- .filter(r => !graph.relations.some(existingRelation => existingRelation.from === r.from &&
220
- existingRelation.to === r.to &&
221
- existingRelation.relationType === r.relationType))
222
- .map(r => ({ ...r, mtime: now }));
223
- graph.relations.push(...newRelations);
224
- 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
+ }
225
441
  return newRelations;
226
442
  });
227
443
  }
228
444
  async addObservations(observations) {
229
- return this.withLock(async () => {
230
- const graph = await this.loadGraph();
231
- const results = observations.map(o => {
232
- const entity = graph.entities.find(e => e.name === o.entityName);
233
- 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) {
234
450
  throw new Error(`Entity with name ${o.entityName} not found`);
235
451
  }
236
- // Validate observation character limits
237
452
  for (const obs of o.contents) {
238
453
  if (obs.length > 140) {
239
454
  throw new Error(`Observation for "${o.entityName}" exceeds 140 characters (${obs.length} chars): "${obs.substring(0, 50)}..."`);
240
455
  }
241
456
  }
242
- const newObservations = o.contents.filter(content => !entity.observations.includes(content));
243
- // Validate total observation count
244
- if (entity.observations.length + newObservations.length > 2) {
245
- 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}).`);
246
466
  }
247
- entity.observations.push(...newObservations);
248
- if (newObservations.length > 0) {
249
- const now = Date.now();
250
- entity.mtime = now;
251
- entity.obsMtime = now;
467
+ const now = BigInt(Date.now());
468
+ for (const obs of newObservations) {
469
+ this.gf.addObservation(offset, obs, now);
252
470
  }
253
- return { entityName: o.entityName, addedObservations: newObservations };
254
- });
255
- await this.saveGraph(graph);
471
+ results.push({ entityName: o.entityName, addedObservations: newObservations });
472
+ }
256
473
  return results;
257
474
  });
258
475
  }
259
476
  async deleteEntities(entityNames) {
260
- return this.withLock(async () => {
261
- const graph = await this.loadGraph();
262
- graph.entities = graph.entities.filter(e => !entityNames.includes(e.name));
263
- graph.relations = graph.relations.filter(r => !entityNames.includes(r.from) && !entityNames.includes(r.to));
264
- 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
+ }
265
491
  });
266
492
  }
267
493
  async deleteObservations(deletions) {
268
- return this.withLock(async () => {
269
- const graph = await this.loadGraph();
270
- const now = Date.now();
271
- deletions.forEach(d => {
272
- const entity = graph.entities.find(e => e.name === d.entityName);
273
- if (entity) {
274
- const originalLen = entity.observations.length;
275
- entity.observations = entity.observations.filter(o => !d.observations.includes(o));
276
- if (entity.observations.length !== originalLen) {
277
- entity.mtime = now;
278
- entity.obsMtime = now;
279
- }
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);
280
502
  }
281
- });
282
- await this.saveGraph(graph);
503
+ }
283
504
  });
284
505
  }
285
506
  async deleteRelations(relations) {
286
- return this.withLock(async () => {
287
- const graph = await this.loadGraph();
288
- graph.relations = graph.relations.filter(r => !relations.some(delRelation => r.from === delRelation.from &&
289
- r.to === delRelation.to &&
290
- r.relationType === delRelation.relationType));
291
- 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
+ }
292
523
  });
293
524
  }
294
525
  // Regex-based search function
295
- async searchNodes(query, sortBy, sortDir) {
296
- const graph = await this.loadGraph();
526
+ async searchNodes(query, sortBy, sortDir, direction = 'forward') {
297
527
  let regex;
298
528
  try {
299
- regex = new RegExp(query, 'i'); // case-insensitive
529
+ regex = new RegExp(query, 'i');
300
530
  }
301
531
  catch (e) {
302
532
  throw new Error(`Invalid regex pattern: ${query}`);
303
533
  }
304
- // Filter entities
305
- const filteredEntities = graph.entities.filter(e => regex.test(e.name) ||
306
- regex.test(e.entityType) ||
307
- e.observations.some(o => regex.test(o)));
308
- // Create a Set of filtered entity names for quick lookup
309
- const filteredEntityNames = new Set(filteredEntities.map(e => e.name));
310
- // Filter relations to only include those between filtered entities
311
- const filteredRelations = graph.relations.filter(r => filteredEntityNames.has(r.from) && filteredEntityNames.has(r.to));
312
- const filteredGraph = {
313
- entities: sortEntities(filteredEntities, sortBy, sortDir),
314
- relations: filteredRelations,
315
- };
316
- return filteredGraph;
317
- }
318
- async openNodesFiltered(names) {
319
- const graph = await this.loadGraph();
320
- // Filter entities
321
- const filteredEntities = graph.entities.filter(e => names.includes(e.name));
322
- // Create a Set of filtered entity names for quick lookup
323
- const filteredEntityNames = new Set(filteredEntities.map(e => e.name));
324
- // Filter relations to only include those between filtered entities
325
- const filteredRelations = graph.relations.filter(r => filteredEntityNames.has(r.from) && filteredEntityNames.has(r.to));
326
- const filteredGraph = {
327
- entities: filteredEntities,
328
- relations: filteredRelations,
329
- };
330
- return filteredGraph;
331
- }
332
- async openNodes(names) {
333
- const graph = await this.loadGraph();
334
- // Filter entities
335
- const filteredEntities = graph.entities.filter(e => names.includes(e.name));
336
- // Create a Set of filtered entity names for quick lookup
337
- const filteredEntityNames = new Set(filteredEntities.map(e => e.name));
338
- // Filter relations to only include those between filtered entities
339
- const filteredRelations = graph.relations.filter(r => filteredEntityNames.has(r.from));
340
- const filteredGraph = {
341
- entities: filteredEntities,
342
- relations: filteredRelations,
343
- };
344
- return filteredGraph;
345
- }
346
- async getNeighbors(entityName, depth = 1, sortBy, sortDir) {
347
- const graph = await this.loadGraph();
348
- const visited = new Set();
349
- const neighborNames = new Set();
350
- const traverse = (currentName, currentDepth) => {
351
- if (currentDepth > depth || visited.has(currentName))
352
- return;
353
- visited.add(currentName);
354
- // Find all relations involving this entity
355
- const connectedRelations = graph.relations.filter(r => r.from === currentName || r.to === currentName);
356
- // Collect neighbor names
357
- connectedRelations.forEach(r => {
358
- const neighborName = r.from === currentName ? r.to : r.from;
359
- 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);
360
547
  });
361
- if (currentDepth < depth) {
362
- // Traverse to connected entities
363
- connectedRelations.forEach(r => {
364
- const nextEntity = r.from === currentName ? r.to : r.from;
365
- traverse(nextEntity, currentDepth + 1);
366
- });
367
- }
368
- };
369
- traverse(entityName, 0);
370
- // Remove the starting entity from neighbors (it's not its own neighbor)
371
- neighborNames.delete(entityName);
372
- // Build neighbor objects with timestamps
373
- const entityMap = new Map(graph.entities.map(e => [e.name, e]));
374
- const neighbors = Array.from(neighborNames).map(name => {
375
- const entity = entityMap.get(name);
548
+ const rankMaps = this.getRankMapsUnlocked();
376
549
  return {
377
- name,
378
- mtime: entity?.mtime,
379
- obsMtime: entity?.obsMtime,
550
+ entities: sortEntities(filteredEntities, sortBy, sortDir, rankMaps),
551
+ relations: filteredRelations,
380
552
  };
381
553
  });
382
- return sortNeighbors(neighbors, sortBy, sortDir);
383
554
  }
384
- async findPath(fromEntity, toEntity, maxDepth = 5) {
385
- const graph = await this.loadGraph();
386
- const visited = new Set();
387
- const dfs = (current, target, path, depth) => {
388
- if (depth > maxDepth || visited.has(current))
389
- return null;
390
- if (current === target)
391
- return path;
392
- visited.add(current);
393
- const outgoingRelations = graph.relations.filter(r => r.from === current);
394
- for (const relation of outgoingRelations) {
395
- const result = dfs(relation.to, target, [...path, relation], depth + 1);
396
- if (result)
397
- 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)));
398
563
  }
399
- visited.delete(current);
400
- return null;
401
- };
402
- 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
+ });
403
689
  }
404
690
  async getEntitiesByType(entityType, sortBy, sortDir) {
405
- const graph = await this.loadGraph();
406
- const filtered = graph.entities.filter(e => e.entityType === entityType);
407
- 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
+ });
408
696
  }
409
697
  async getEntityTypes() {
410
- const graph = await this.loadGraph();
411
- const types = new Set(graph.entities.map(e => e.entityType));
412
- 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
+ });
413
702
  }
414
703
  async getRelationTypes() {
415
- const graph = await this.loadGraph();
416
- const types = new Set(graph.relations.map(r => r.relationType));
417
- 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
+ });
418
708
  }
419
709
  async getStats() {
420
- const graph = await this.loadGraph();
421
- const entityTypes = new Set(graph.entities.map(e => e.entityType));
422
- const relationTypes = new Set(graph.relations.map(r => r.relationType));
423
- return {
424
- entityCount: graph.entities.length,
425
- relationCount: graph.relations.length,
426
- entityTypes: entityTypes.size,
427
- relationTypes: relationTypes.size
428
- };
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
+ });
429
722
  }
430
723
  async getOrphanedEntities(strict = false, sortBy, sortDir) {
431
- const graph = await this.loadGraph();
432
- if (!strict) {
433
- // Simple mode: entities with no relations at all
434
- const connectedEntityNames = new Set();
435
- graph.relations.forEach(r => {
436
- connectedEntityNames.add(r.from);
437
- 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);
438
743
  });
439
- const orphans = graph.entities.filter(e => !connectedEntityNames.has(e.name));
440
- return sortEntities(orphans, sortBy, sortDir);
441
- }
442
- // Strict mode: entities not connected to "Self" (directly or indirectly)
443
- // Build adjacency list (bidirectional)
444
- const neighbors = new Map();
445
- graph.entities.forEach(e => neighbors.set(e.name, new Set()));
446
- graph.relations.forEach(r => {
447
- neighbors.get(r.from)?.add(r.to);
448
- neighbors.get(r.to)?.add(r.from);
449
- });
450
- // BFS from Self to find all connected entities
451
- const connectedToSelf = new Set();
452
- const queue = ['Self'];
453
- while (queue.length > 0) {
454
- const current = queue.shift();
455
- if (connectedToSelf.has(current))
456
- continue;
457
- connectedToSelf.add(current);
458
- const currentNeighbors = neighbors.get(current);
459
- if (currentNeighbors) {
460
- for (const neighbor of currentNeighbors) {
461
- if (!connectedToSelf.has(neighbor)) {
462
- 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
+ }
463
757
  }
464
758
  }
465
759
  }
466
- }
467
- // Return entities not connected to Self (excluding Self itself if it exists)
468
- const orphans = graph.entities.filter(e => !connectedToSelf.has(e.name));
469
- 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
+ });
470
764
  }
471
765
  async validateGraph() {
472
- const graph = await this.loadGraph();
473
- const entityNames = new Set(graph.entities.map(e => e.name));
474
- const missingEntities = new Set();
475
- const observationViolations = [];
476
- // Check for missing entities in relations
477
- graph.relations.forEach(r => {
478
- if (!entityNames.has(r.from)) {
479
- missingEntities.add(r.from);
480
- }
481
- if (!entityNames.has(r.to)) {
482
- missingEntities.add(r.to);
483
- }
484
- });
485
- // Check for observation limit violations
486
- graph.entities.forEach(e => {
487
- const oversizedObservations = [];
488
- e.observations.forEach((obs, idx) => {
489
- if (obs.length > 140) {
490
- oversizedObservations.push(idx);
491
- }
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);
492
777
  });
493
- if (e.observations.length > 2 || oversizedObservations.length > 0) {
494
- observationViolations.push({
495
- entity: e.name,
496
- count: e.observations.length,
497
- oversizedObservations
778
+ entities.forEach(e => {
779
+ const oversizedObservations = [];
780
+ e.observations.forEach((obs, idx) => {
781
+ if (obs.length > 140)
782
+ oversizedObservations.push(idx);
498
783
  });
499
- }
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 };
500
793
  });
501
- return {
502
- missingEntities: Array.from(missingEntities),
503
- observationViolations
504
- };
505
794
  }
506
- // BCL (Binary Combinatory Logic) evaluator
507
- async evaluateBCL(program, maxSteps) {
508
- let stepCount = 0;
509
- let max_size = program.length;
510
- let mode = 0;
511
- let ctr = 1;
512
- let t0 = program;
513
- let t1 = '';
514
- let t2 = '';
515
- let t3 = '';
516
- let t4 = '';
517
- while (stepCount < maxSteps) {
518
- if (t0.length == 0)
519
- break;
520
- let b = t0[0];
521
- t0 = t0.slice(1);
522
- if (mode === 0) {
523
- t1 += b;
524
- let size = t1.length + t0.length;
525
- if (size > max_size)
526
- max_size = size;
527
- if (t1.slice(-4) === '1100') {
528
- mode = 1;
529
- t1 = t1.slice(0, -4);
530
- }
531
- else if (t1.slice(-5) === '11101') {
532
- mode = 3;
533
- t1 = t1.slice(0, -5);
534
- }
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}`);
535
800
  }
536
- else if (mode === 1) {
537
- t2 += b;
538
- if (b == '1') {
539
- ctr += 1;
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;
540
809
  }
541
- else if (b == '0') {
542
- ctr -= 1;
543
- t2 += t0[0];
544
- t0 = t0.slice(1);
810
+ else {
811
+ return randomBytes(4).readUInt32BE() / 0xFFFFFFFF;
545
812
  }
546
- if (ctr === 0) {
547
- mode = 2;
548
- ctr = 1;
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);
549
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);
550
838
  }
551
- else if (mode === 2) {
552
- if (b == '1') {
553
- ctr += 1;
554
- }
555
- else if (b == '0') {
556
- ctr -= 1;
557
- t0 = t0.slice(1);
558
- }
559
- if (ctr === 0) {
560
- t0 = t2 + t0;
561
- t2 = '';
562
- mode = 0;
563
- ctr = 1;
564
- stepCount += 1;
565
- }
839
+ return { entity: current, path: pathNames };
840
+ });
841
+ }
842
+ hashSeed(seed) {
843
+ let hash = 0;
844
+ for (let i = 0; i < seed.length; i++) {
845
+ const char = seed.charCodeAt(i);
846
+ hash = ((hash << 5) - hash) + char;
847
+ hash = hash & hash;
848
+ }
849
+ return hash || 1;
850
+ }
851
+ decodeTimestamp(timestamp, relative = false) {
852
+ const ts = timestamp ?? Date.now();
853
+ const date = new Date(ts);
854
+ const result = {
855
+ timestamp: ts,
856
+ iso8601: date.toISOString(),
857
+ formatted: date.toUTCString(),
858
+ };
859
+ if (relative) {
860
+ const now = Date.now();
861
+ const diffMs = now - ts;
862
+ const diffSec = Math.abs(diffMs) / 1000;
863
+ const diffMin = diffSec / 60;
864
+ const diffHour = diffMin / 60;
865
+ const diffDay = diffHour / 24;
866
+ let relStr;
867
+ if (diffSec < 60) {
868
+ relStr = `${Math.floor(diffSec)} seconds`;
566
869
  }
567
- else if (mode === 3) {
568
- t2 += b;
569
- if (b == '1') {
570
- ctr += 1;
571
- }
572
- else if (b == '0') {
573
- ctr -= 1;
574
- t2 += t0[0];
575
- t0 = t0.slice(1);
576
- }
577
- if (ctr === 0) {
578
- mode = 4;
579
- ctr = 1;
580
- }
870
+ else if (diffMin < 60) {
871
+ relStr = `${Math.floor(diffMin)} minutes`;
581
872
  }
582
- else if (mode === 4) {
583
- t3 += b;
584
- if (b == '1') {
585
- ctr += 1;
586
- }
587
- else if (b == '0') {
588
- ctr -= 1;
589
- t3 += t0[0];
590
- t0 = t0.slice(1);
591
- }
592
- if (ctr === 0) {
593
- mode = 5;
594
- ctr = 1;
595
- }
873
+ else if (diffHour < 24) {
874
+ relStr = `${Math.floor(diffHour)} hours`;
596
875
  }
597
- else if (mode === 5) {
598
- t4 += b;
599
- if (b == '1') {
600
- ctr += 1;
601
- }
602
- else if (b == '0') {
603
- ctr -= 1;
604
- t4 += t0[0];
605
- t0 = t0.slice(1);
606
- }
607
- if (ctr === 0) {
608
- t0 = '11' + t2 + t4 + '1' + t3 + t4 + t0;
609
- t2 = '';
610
- t3 = '';
611
- t4 = '';
612
- mode = 0;
613
- ctr = 1;
614
- stepCount += 1;
615
- }
876
+ else if (diffDay < 30) {
877
+ relStr = `${Math.floor(diffDay)} days`;
616
878
  }
879
+ else if (diffDay < 365) {
880
+ relStr = `${Math.floor(diffDay / 30)} months`;
881
+ }
882
+ else {
883
+ relStr = `${Math.floor(diffDay / 365)} years`;
884
+ }
885
+ result.relative = diffMs >= 0 ? `${relStr} ago` : `in ${relStr}`;
617
886
  }
618
- const halted = stepCount < maxSteps;
619
- return {
620
- result: t1,
621
- info: `${stepCount} steps, max size ${max_size}`,
622
- halted,
623
- errored: halted && mode != 0,
624
- };
625
- }
626
- async addBCLTerm(term) {
627
- const termset = ["1", "00", "01"];
628
- if (!term || term.trim() === "") {
629
- throw new Error("BCL term cannot be empty");
630
- }
631
- // Term can be 1, 00, 01, or K, S, App (application)
632
- const validTerms = ["1", "App", "00", "K", "01", "S"];
633
- if (!validTerms.includes(term)) {
634
- throw new Error(`Invalid BCL term: ${term}\nExpected one of: ${validTerms.join(", ")}`);
635
- }
636
- let processedTerm = 0;
637
- if (term === "00" || term === "K")
638
- processedTerm = 1;
639
- else if (term === "01" || term === "S")
640
- processedTerm = 2;
641
- this.bclTerm += termset[processedTerm];
642
- if (processedTerm === 0) {
643
- if (this.bclCtr === 0)
644
- this.bclCtr += 1;
645
- this.bclCtr += 1;
646
- }
647
- else {
648
- this.bclCtr -= 1;
649
- }
650
- if (this.bclCtr <= 0) {
651
- const constructedProgram = this.bclTerm;
652
- this.bclCtr = 0;
653
- this.bclTerm = "";
654
- return `Constructed Program: ${constructedProgram}`;
655
- }
656
- else {
657
- return `Need ${this.bclCtr} more term(s) to complete the program.`;
658
- }
659
- }
660
- async clearBCLTerm() {
661
- this.bclCtr = 0;
662
- this.bclTerm = "";
887
+ return result;
663
888
  }
664
889
  async addThought(observations, previousCtxId) {
665
- return this.withLock(async () => {
666
- const graph = await this.loadGraph();
667
- // Validate observations
668
- if (observations.length > 2) {
669
- 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)}..."`);
670
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);
671
904
  for (const obs of observations) {
672
- if (obs.length > 140) {
673
- throw new Error(`Observation exceeds 140 characters (${obs.length} chars): "${obs.substring(0, 50)}..."`);
674
- }
905
+ this.gf.addObservation(rec.offset, obs, now);
675
906
  }
676
- // Generate new context ID (24-char hex)
677
- const now = Date.now();
678
- const ctxId = randomBytes(12).toString('hex');
679
- // Create thought entity
680
- const thoughtEntity = {
681
- name: ctxId,
682
- entityType: "Thought",
683
- observations,
684
- mtime: now,
685
- obsMtime: observations.length > 0 ? now : undefined,
686
- };
687
- graph.entities.push(thoughtEntity);
688
- // 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);
689
914
  if (previousCtxId) {
690
- const prevEntity = graph.entities.find(e => e.name === previousCtxId);
691
- if (prevEntity) {
692
- // Update mtime on previous entity since we're adding a relation from it
693
- prevEntity.mtime = now;
694
- // Bidirectional chain: previous -> new (follows) and new -> previous (preceded_by)
695
- 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 });
696
928
  }
697
929
  }
698
- await this.saveGraph(graph);
699
930
  return { ctxId };
700
931
  });
701
932
  }
933
+ /** Close the underlying binary store files */
934
+ close() {
935
+ this.gf.close();
936
+ this.st.close();
937
+ }
702
938
  }
703
939
  /**
704
940
  * Creates a configured MCP server instance with all tools registered.
@@ -714,12 +950,16 @@ export function createServer(memoryFilePath) {
714
950
  sizes: ["any"]
715
951
  }
716
952
  ],
717
- version: "0.0.10",
953
+ version: "0.0.12",
718
954
  }, {
719
955
  capabilities: {
720
956
  tools: {},
721
957
  },
722
958
  });
959
+ // Close binary store on server close
960
+ server.onclose = () => {
961
+ knowledgeGraphManager.close();
962
+ };
723
963
  server.setRequestHandler(ListToolsRequestSchema, async () => {
724
964
  return {
725
965
  tools: [
@@ -867,7 +1107,8 @@ export function createServer(memoryFilePath) {
867
1107
  type: "object",
868
1108
  properties: {
869
1109
  query: { type: "string", description: "Regex pattern to match against entity names, types, and observations." },
870
- 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." },
871
1112
  sortDir: { type: "string", enum: ["asc", "desc"], description: "Sort direction. Default: desc for timestamps, asc for name." },
872
1113
  entityCursor: { type: "number", description: "Cursor for entity pagination (from previous response's nextCursor)" },
873
1114
  relationCursor: { type: "number", description: "Cursor for relation pagination" },
@@ -875,23 +1116,6 @@ export function createServer(memoryFilePath) {
875
1116
  required: ["query"],
876
1117
  },
877
1118
  },
878
- {
879
- name: "open_nodes_filtered",
880
- 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).",
881
- inputSchema: {
882
- type: "object",
883
- properties: {
884
- names: {
885
- type: "array",
886
- items: { type: "string" },
887
- description: "An array of entity names to retrieve",
888
- },
889
- entityCursor: { type: "number", description: "Cursor for entity pagination" },
890
- relationCursor: { type: "number", description: "Cursor for relation pagination" },
891
- },
892
- required: ["names"],
893
- },
894
- },
895
1119
  {
896
1120
  name: "open_nodes",
897
1121
  description: "Open specific nodes in the knowledge graph by their names. Results are paginated (max 512 chars).",
@@ -903,6 +1127,7 @@ export function createServer(memoryFilePath) {
903
1127
  items: { type: "string" },
904
1128
  description: "An array of entity names to retrieve",
905
1129
  },
1130
+ direction: { type: "string", enum: ["forward", "backward", "any"], description: "Edge direction filter for returned relations. Default: forward" },
906
1131
  entityCursor: { type: "number", description: "Cursor for entity pagination" },
907
1132
  relationCursor: { type: "number", description: "Cursor for relation pagination" },
908
1133
  },
@@ -917,7 +1142,8 @@ export function createServer(memoryFilePath) {
917
1142
  properties: {
918
1143
  entityName: { type: "string", description: "The name of the entity to find neighbors for" },
919
1144
  depth: { type: "number", description: "Maximum depth to traverse (default: 1)", default: 1 },
920
- 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." },
921
1147
  sortDir: { type: "string", enum: ["asc", "desc"], description: "Sort direction. Default: desc for timestamps, asc for name." },
922
1148
  cursor: { type: "number", description: "Cursor for pagination" },
923
1149
  },
@@ -933,6 +1159,7 @@ export function createServer(memoryFilePath) {
933
1159
  fromEntity: { type: "string", description: "The name of the starting entity" },
934
1160
  toEntity: { type: "string", description: "The name of the target entity" },
935
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" },
936
1163
  cursor: { type: "number", description: "Cursor for pagination" },
937
1164
  },
938
1165
  required: ["fromEntity", "toEntity"],
@@ -945,7 +1172,7 @@ export function createServer(memoryFilePath) {
945
1172
  type: "object",
946
1173
  properties: {
947
1174
  entityType: { type: "string", description: "The type of entities to retrieve" },
948
- 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." },
949
1176
  sortDir: { type: "string", enum: ["asc", "desc"], description: "Sort direction. Default: desc for timestamps, asc for name." },
950
1177
  cursor: { type: "number", description: "Cursor for pagination" },
951
1178
  },
@@ -983,7 +1210,7 @@ export function createServer(memoryFilePath) {
983
1210
  type: "object",
984
1211
  properties: {
985
1212
  strict: { type: "boolean", description: "If true, returns entities not connected to 'Self' (directly or indirectly). Default: false" },
986
- 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." },
987
1214
  sortDir: { type: "string", enum: ["asc", "desc"], description: "Sort direction. Default: desc for timestamps, asc for name." },
988
1215
  cursor: { type: "number", description: "Cursor for pagination" },
989
1216
  },
@@ -998,37 +1225,28 @@ export function createServer(memoryFilePath) {
998
1225
  },
999
1226
  },
1000
1227
  {
1001
- name: "evaluate_bcl",
1002
- description: "Evaluate a Binary Combinatory Logic (BCL) program",
1228
+ name: "decode_timestamp",
1229
+ description: "Decode a millisecond timestamp to human-readable UTC format. If no timestamp provided, returns the current time. Use this to interpret mtime/obsMtime values from entities.",
1003
1230
  inputSchema: {
1004
1231
  type: "object",
1005
1232
  properties: {
1006
- program: { type: "string", description: "The BCL program as a binary string (syntax: T:=00|01|1TT) 00=K, 01=S, 1=application." },
1007
- maxSteps: { type: "number", description: "Maximum number of reduction steps to perform (default: 1000000)", default: 1000000 },
1233
+ timestamp: { type: "number", description: "Millisecond timestamp to decode. If omitted, returns current time." },
1234
+ relative: { type: "boolean", description: "If true, include relative time (e.g., '3 days ago'). Default: false" },
1008
1235
  },
1009
- required: ["program"],
1010
1236
  },
1011
1237
  },
1012
1238
  {
1013
- name: "add_bcl_term",
1014
- description: "Add a BCL term to the constructor, maintaining valid syntax. Returns completion status.",
1239
+ name: "random_walk",
1240
+ description: "Perform a random walk from a starting entity, following random relations. Returns the terminal entity name and the path taken. Useful for serendipitous exploration of the knowledge graph.",
1015
1241
  inputSchema: {
1016
1242
  type: "object",
1017
1243
  properties: {
1018
- term: {
1019
- type: "string",
1020
- description: "BCL term to add. Valid values: '1' or 'App' (application), '00' or 'K' (K combinator), '01' or 'S' (S combinator)"
1021
- },
1244
+ start: { type: "string", description: "Name of the entity to start the walk from." },
1245
+ depth: { type: "number", description: "Number of steps to take. Default: 3" },
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" },
1022
1248
  },
1023
- required: ["term"],
1024
- },
1025
- },
1026
- {
1027
- name: "clear_bcl_term",
1028
- description: "Clear the current BCL term being constructed and reset the constructor state",
1029
- inputSchema: {
1030
- type: "object",
1031
- properties: {},
1249
+ required: ["start"],
1032
1250
  },
1033
1251
  },
1034
1252
  {
@@ -1062,39 +1280,49 @@ Use this to build chains of reasoning that persist in the graph. Each thought ca
1062
1280
  throw new Error(`No arguments provided for tool: ${name}`);
1063
1281
  }
1064
1282
  switch (name) {
1065
- case "create_entities":
1066
- return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.createEntities(args.entities), null, 2) }] };
1067
- case "create_relations":
1068
- 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
+ }
1069
1293
  case "add_observations":
1070
1294
  return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.addObservations(args.observations), null, 2) }] };
1071
1295
  case "delete_entities":
1072
1296
  await knowledgeGraphManager.deleteEntities(args.entityNames);
1297
+ knowledgeGraphManager.resample(); // Re-run structural sampling after graph mutation
1073
1298
  return { content: [{ type: "text", text: "Entities deleted successfully" }] };
1074
1299
  case "delete_observations":
1075
1300
  await knowledgeGraphManager.deleteObservations(args.deletions);
1076
1301
  return { content: [{ type: "text", text: "Observations deleted successfully" }] };
1077
1302
  case "delete_relations":
1078
1303
  await knowledgeGraphManager.deleteRelations(args.relations);
1304
+ knowledgeGraphManager.resample(); // Re-run structural sampling after graph mutation
1079
1305
  return { content: [{ type: "text", text: "Relations deleted successfully" }] };
1080
1306
  case "search_nodes": {
1081
- const graph = await knowledgeGraphManager.searchNodes(args.query, args.sortBy, args.sortDir);
1082
- return { content: [{ type: "text", text: JSON.stringify(paginateGraph(graph, args.entityCursor ?? 0, args.relationCursor ?? 0)) }] };
1083
- }
1084
- case "open_nodes_filtered": {
1085
- 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));
1086
1310
  return { content: [{ type: "text", text: JSON.stringify(paginateGraph(graph, args.entityCursor ?? 0, args.relationCursor ?? 0)) }] };
1087
1311
  }
1088
1312
  case "open_nodes": {
1089
- 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));
1090
1316
  return { content: [{ type: "text", text: JSON.stringify(paginateGraph(graph, args.entityCursor ?? 0, args.relationCursor ?? 0)) }] };
1091
1317
  }
1092
1318
  case "get_neighbors": {
1093
- 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));
1094
1322
  return { content: [{ type: "text", text: JSON.stringify(paginateItems(neighbors, args.cursor ?? 0)) }] };
1095
1323
  }
1096
1324
  case "find_path": {
1097
- 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');
1098
1326
  return { content: [{ type: "text", text: JSON.stringify(paginateItems(path, args.cursor ?? 0)) }] };
1099
1327
  }
1100
1328
  case "get_entities_by_type": {
@@ -1113,13 +1341,12 @@ Use this to build chains of reasoning that persist in the graph. Each thought ca
1113
1341
  }
1114
1342
  case "validate_graph":
1115
1343
  return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.validateGraph(), null, 2) }] };
1116
- case "evaluate_bcl":
1117
- return { content: [{ type: "text", text: JSON.stringify(await knowledgeGraphManager.evaluateBCL(args.program, args.maxSteps), null, 2) }] };
1118
- case "add_bcl_term":
1119
- return { content: [{ type: "text", text: await knowledgeGraphManager.addBCLTerm(args.term) }] };
1120
- case "clear_bcl_term":
1121
- await knowledgeGraphManager.clearBCLTerm();
1122
- return { content: [{ type: "text", text: "BCL term constructor cleared successfully" }] };
1344
+ case "decode_timestamp":
1345
+ return { content: [{ type: "text", text: JSON.stringify(knowledgeGraphManager.decodeTimestamp(args.timestamp, args.relative ?? false)) }] };
1346
+ case "random_walk": {
1347
+ const result = await knowledgeGraphManager.randomWalk(args.start, args.depth ?? 3, args.seed, args.direction ?? 'forward');
1348
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
1349
+ }
1123
1350
  case "sequentialthinking": {
1124
1351
  const result = await knowledgeGraphManager.addThought(args.observations, args.previousCtxId);
1125
1352
  return { content: [{ type: "text", text: JSON.stringify(result) }] };