@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/README.md +41 -27
- package/dist/index.js +4 -0
- package/dist/scripts/migrate-jsonl.js +169 -0
- package/dist/scripts/verify-migration.js +39 -0
- package/dist/server.js +763 -536
- package/dist/src/graphfile.js +560 -0
- package/dist/src/memoryfile.js +121 -0
- package/dist/src/pagerank.js +78 -0
- package/dist/src/stringtable.js +373 -0
- package/dist/tests/concurrency.test.js +189 -0
- package/dist/tests/memory-server.test.js +225 -53
- package/package.json +6 -4
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
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
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
|
-
|
|
141
|
-
|
|
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
|
-
|
|
211
|
+
this.gf.refresh();
|
|
212
|
+
this.st.refresh();
|
|
213
|
+
this.maybeRebuildNameIndex();
|
|
214
|
+
return fn();
|
|
144
215
|
}
|
|
145
|
-
|
|
146
|
-
|
|
216
|
+
finally {
|
|
217
|
+
this.gf.unlock();
|
|
147
218
|
}
|
|
148
|
-
|
|
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
|
-
|
|
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
|
-
|
|
238
|
+
this.gf.unlock();
|
|
154
239
|
}
|
|
155
240
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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.
|
|
209
|
-
const
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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.
|
|
230
|
-
const
|
|
231
|
-
const
|
|
232
|
-
const
|
|
233
|
-
if (
|
|
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
|
|
243
|
-
|
|
244
|
-
if (
|
|
245
|
-
|
|
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
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
|
|
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
|
-
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
-
|
|
269
|
-
const
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
-
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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');
|
|
529
|
+
regex = new RegExp(query, 'i');
|
|
300
530
|
}
|
|
301
531
|
catch (e) {
|
|
302
532
|
throw new Error(`Invalid regex pattern: ${query}`);
|
|
303
533
|
}
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
regex.test(e.
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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
|
-
|
|
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
|
-
|
|
378
|
-
|
|
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
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
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
|
-
|
|
406
|
-
|
|
407
|
-
|
|
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
|
-
|
|
411
|
-
|
|
412
|
-
|
|
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
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
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
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
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
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
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
|
-
|
|
468
|
-
|
|
469
|
-
|
|
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
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
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
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
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
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
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
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
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
|
|
542
|
-
|
|
543
|
-
t2 += t0[0];
|
|
544
|
-
t0 = t0.slice(1);
|
|
810
|
+
else {
|
|
811
|
+
return randomBytes(4).readUInt32BE() / 0xFFFFFFFF;
|
|
545
812
|
}
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
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
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
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 (
|
|
568
|
-
|
|
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 (
|
|
583
|
-
|
|
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 (
|
|
598
|
-
|
|
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
|
-
|
|
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
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
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
|
-
|
|
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
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
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
|
|
691
|
-
if (
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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: "
|
|
1002
|
-
description: "
|
|
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
|
-
|
|
1007
|
-
|
|
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: "
|
|
1014
|
-
description: "
|
|
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
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
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: ["
|
|
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
|
-
|
|
1067
|
-
|
|
1068
|
-
return { content: [{ type: "text", text: JSON.stringify(
|
|
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
|
-
|
|
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 "
|
|
1117
|
-
return { content: [{ type: "text", text: JSON.stringify(
|
|
1118
|
-
case "
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
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) }] };
|