openclaw-cortex-memory 0.1.0-Alpha.3 → 0.1.0-Alpha.30

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.
Files changed (82) hide show
  1. package/README.md +263 -204
  2. package/SKILL.md +77 -268
  3. package/dist/index.d.ts +92 -22
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +1062 -1207
  6. package/dist/index.js.map +1 -1
  7. package/dist/openclaw.plugin.json +384 -15
  8. package/dist/src/dedup/three_stage_deduplicator.d.ts +25 -0
  9. package/dist/src/dedup/three_stage_deduplicator.d.ts.map +1 -0
  10. package/dist/src/dedup/three_stage_deduplicator.js +224 -0
  11. package/dist/src/dedup/three_stage_deduplicator.js.map +1 -0
  12. package/dist/src/engine/memory_engine.d.ts +2 -1
  13. package/dist/src/engine/memory_engine.d.ts.map +1 -1
  14. package/dist/src/engine/ts_engine.d.ts +126 -0
  15. package/dist/src/engine/ts_engine.d.ts.map +1 -1
  16. package/dist/src/engine/ts_engine.js +1145 -44
  17. package/dist/src/engine/ts_engine.js.map +1 -1
  18. package/dist/src/engine/types.d.ts +12 -0
  19. package/dist/src/engine/types.d.ts.map +1 -1
  20. package/dist/src/graph/ontology.d.ts +103 -0
  21. package/dist/src/graph/ontology.d.ts.map +1 -0
  22. package/dist/src/graph/ontology.js +564 -0
  23. package/dist/src/graph/ontology.js.map +1 -0
  24. package/dist/src/net/http_post.d.ts +17 -0
  25. package/dist/src/net/http_post.d.ts.map +1 -0
  26. package/dist/src/net/http_post.js +56 -0
  27. package/dist/src/net/http_post.js.map +1 -0
  28. package/dist/src/quality/llm_output_validator.d.ts +48 -0
  29. package/dist/src/quality/llm_output_validator.d.ts.map +1 -0
  30. package/dist/src/quality/llm_output_validator.js +404 -0
  31. package/dist/src/quality/llm_output_validator.js.map +1 -0
  32. package/dist/src/reflect/reflector.d.ts +7 -0
  33. package/dist/src/reflect/reflector.d.ts.map +1 -1
  34. package/dist/src/reflect/reflector.js +352 -8
  35. package/dist/src/reflect/reflector.js.map +1 -1
  36. package/dist/src/rules/rule_store.d.ts.map +1 -1
  37. package/dist/src/rules/rule_store.js +75 -16
  38. package/dist/src/rules/rule_store.js.map +1 -1
  39. package/dist/src/session/session_end.d.ts +33 -0
  40. package/dist/src/session/session_end.d.ts.map +1 -1
  41. package/dist/src/session/session_end.js +67 -64
  42. package/dist/src/session/session_end.js.map +1 -1
  43. package/dist/src/store/archive_store.d.ts +128 -0
  44. package/dist/src/store/archive_store.d.ts.map +1 -0
  45. package/dist/src/store/archive_store.js +475 -0
  46. package/dist/src/store/archive_store.js.map +1 -0
  47. package/dist/src/store/embedding_utils.d.ts +32 -0
  48. package/dist/src/store/embedding_utils.d.ts.map +1 -0
  49. package/dist/src/store/embedding_utils.js +173 -0
  50. package/dist/src/store/embedding_utils.js.map +1 -0
  51. package/dist/src/store/graph_memory_store.d.ts +44 -0
  52. package/dist/src/store/graph_memory_store.d.ts.map +1 -0
  53. package/dist/src/store/graph_memory_store.js +168 -0
  54. package/dist/src/store/graph_memory_store.js.map +1 -0
  55. package/dist/src/store/read_store.d.ts +86 -0
  56. package/dist/src/store/read_store.d.ts.map +1 -1
  57. package/dist/src/store/read_store.js +1661 -25
  58. package/dist/src/store/read_store.js.map +1 -1
  59. package/dist/src/store/vector_store.d.ts +44 -0
  60. package/dist/src/store/vector_store.d.ts.map +1 -0
  61. package/dist/src/store/vector_store.js +201 -0
  62. package/dist/src/store/vector_store.js.map +1 -0
  63. package/dist/src/store/write_store.d.ts +52 -0
  64. package/dist/src/store/write_store.d.ts.map +1 -1
  65. package/dist/src/store/write_store.js +239 -3
  66. package/dist/src/store/write_store.js.map +1 -1
  67. package/dist/src/sync/session_sync.d.ts +100 -2
  68. package/dist/src/sync/session_sync.d.ts.map +1 -1
  69. package/dist/src/sync/session_sync.js +725 -28
  70. package/dist/src/sync/session_sync.js.map +1 -1
  71. package/dist/src/utils/runtime_env.d.ts +4 -0
  72. package/dist/src/utils/runtime_env.d.ts.map +1 -0
  73. package/dist/src/utils/runtime_env.js +51 -0
  74. package/dist/src/utils/runtime_env.js.map +1 -0
  75. package/openclaw.plugin.json +384 -15
  76. package/package.json +53 -7
  77. package/schema/graph.schema.yaml +175 -0
  78. package/scripts/cli.js +19 -14
  79. package/scripts/repair-memory.js +321 -0
  80. package/scripts/uninstall.js +22 -5
  81. package/index.ts +0 -2142
  82. package/scripts/install.js +0 -27
@@ -36,7 +36,41 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.createTsEngine = createTsEngine;
37
37
  const fs = __importStar(require("fs"));
38
38
  const path = __importStar(require("path"));
39
+ const ontology_1 = require("../graph/ontology");
40
+ const http_post_1 = require("../net/http_post");
41
+ const llm_output_validator_1 = require("../quality/llm_output_validator");
42
+ const PROMPT_VERSIONS = {
43
+ write_gate: "write-gate.v1.2.0",
44
+ session_end_write: "session-end-write.v1.2.0",
45
+ read_fusion: "read-fusion.v1.2.0",
46
+ };
39
47
  function createTsEngine(deps) {
48
+ const graphSchema = (0, ontology_1.loadGraphSchema)(deps.projectRoot);
49
+ const sessionMessageBuffer = new Map();
50
+ const maxMessagesPerSession = 500;
51
+ const maxBufferedSessions = 500;
52
+ function pushSessionMessage(sessionId, message) {
53
+ const current = sessionMessageBuffer.get(sessionId) || [];
54
+ current.push({
55
+ id: `msg_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`,
56
+ session_id: sessionId,
57
+ role: message.role,
58
+ content: message.text,
59
+ timestamp: new Date().toISOString(),
60
+ });
61
+ if (current.length > maxMessagesPerSession) {
62
+ sessionMessageBuffer.set(sessionId, current.slice(current.length - maxMessagesPerSession));
63
+ }
64
+ else {
65
+ sessionMessageBuffer.set(sessionId, current);
66
+ }
67
+ if (sessionMessageBuffer.size > maxBufferedSessions) {
68
+ const first = sessionMessageBuffer.keys().next().value;
69
+ if (first) {
70
+ sessionMessageBuffer.delete(first);
71
+ }
72
+ }
73
+ }
40
74
  function asRecord(value) {
41
75
  if (typeof value === "object" && value !== null) {
42
76
  return value;
@@ -71,25 +105,426 @@ function createTsEngine(deps) {
71
105
  archivePath: path.join(deps.memoryRoot, "sessions", "archive", "sessions.jsonl"),
72
106
  };
73
107
  }
108
+ function parseJsonFile(filePath) {
109
+ try {
110
+ if (!fs.existsSync(filePath)) {
111
+ return null;
112
+ }
113
+ const raw = fs.readFileSync(filePath, "utf-8").trim();
114
+ if (!raw) {
115
+ return null;
116
+ }
117
+ return JSON.parse(raw);
118
+ }
119
+ catch {
120
+ return null;
121
+ }
122
+ }
123
+ function embeddingStats(records) {
124
+ let ok = 0;
125
+ let failed = 0;
126
+ let pending = 0;
127
+ for (const record of records) {
128
+ const explicit = typeof record.embedding_status === "string" ? record.embedding_status.trim() : "";
129
+ const hasEmbedding = Array.isArray(record.embedding) && record.embedding.length > 0;
130
+ if (explicit === "ok" || hasEmbedding) {
131
+ ok += 1;
132
+ }
133
+ else if (explicit === "failed") {
134
+ failed += 1;
135
+ }
136
+ else {
137
+ pending += 1;
138
+ }
139
+ }
140
+ const total = records.length;
141
+ const coverage = total > 0 ? Number((ok / total).toFixed(4)) : 0;
142
+ return { total, ok, failed, pending, coverage };
143
+ }
144
+ function normalizeBaseUrl(value) {
145
+ if (!value)
146
+ return "";
147
+ return value.endsWith("/") ? value.slice(0, -1) : value;
148
+ }
149
+ function estimateTokenCount(text) {
150
+ const parts = text
151
+ .split(/[\s,.;:!?,。;:!?、()()[\]{}"'`~]+/)
152
+ .map(part => part.trim())
153
+ .filter(Boolean);
154
+ return parts.length;
155
+ }
156
+ function buildVectorSourceText(record, layer) {
157
+ if (layer === "active") {
158
+ const content = typeof record.content === "string" && record.content.trim()
159
+ ? record.content.trim()
160
+ : (typeof record.text === "string" ? record.text.trim() : "");
161
+ return content;
162
+ }
163
+ const summary = typeof record.summary === "string" ? record.summary.trim() : "";
164
+ const eventType = typeof record.event_type === "string" ? record.event_type.trim() : "insight";
165
+ const outcome = typeof record.outcome === "string" ? record.outcome.trim() : "";
166
+ const sourceFile = typeof record.source_file === "string" ? record.source_file.trim() : "";
167
+ const actor = typeof record.actor === "string" ? record.actor.trim() : "";
168
+ const entities = Array.isArray(record.entities)
169
+ ? record.entities.filter(v => typeof v === "string").map(v => String(v).trim()).filter(Boolean)
170
+ : [];
171
+ const relations = Array.isArray(record.relations)
172
+ ? record.relations
173
+ .map(v => {
174
+ if (!v || typeof v !== "object")
175
+ return "";
176
+ const relation = v;
177
+ const source = typeof relation.source === "string" ? relation.source.trim() : "";
178
+ const target = typeof relation.target === "string" ? relation.target.trim() : "";
179
+ const type = typeof relation.type === "string" ? relation.type.trim() : "related_to";
180
+ if (!source || !target)
181
+ return "";
182
+ return `${source} -[${type}]-> ${target}`;
183
+ })
184
+ .filter(Boolean)
185
+ : [];
186
+ const lines = [
187
+ `event_type: ${eventType}`,
188
+ `summary: ${summary}`,
189
+ `outcome: ${outcome}`,
190
+ `entities: ${entities.join(", ")}`,
191
+ `source_file: ${sourceFile}`,
192
+ `actor: ${actor}`,
193
+ relations.length > 0 ? `relations: ${relations.join(" ; ")}` : "",
194
+ ].filter(Boolean);
195
+ return lines.join("\n").trim();
196
+ }
197
+ function splitTextChunks(text, chunkSize, chunkOverlap) {
198
+ const normalizedSize = Number.isFinite(chunkSize) && chunkSize >= 200 ? Math.floor(chunkSize) : 600;
199
+ const normalizedOverlap = Number.isFinite(chunkOverlap) && chunkOverlap >= 0
200
+ ? Math.floor(chunkOverlap)
201
+ : 100;
202
+ const overlap = Math.min(normalizedOverlap, Math.max(0, normalizedSize - 50));
203
+ const output = [];
204
+ let cursor = 0;
205
+ let index = 0;
206
+ const punctuationSet = new Set(["。", "!", "?", ".", "!", "?", "\n", ";", ";"]);
207
+ while (cursor < text.length) {
208
+ const rawEnd = Math.min(text.length, cursor + normalizedSize);
209
+ let end = rawEnd;
210
+ if (rawEnd < text.length) {
211
+ const backwardStart = Math.max(cursor + Math.floor(normalizedSize * 0.45), cursor + 1);
212
+ let found = -1;
213
+ for (let i = rawEnd - 1; i >= backwardStart; i -= 1) {
214
+ if (punctuationSet.has(text[i])) {
215
+ found = i + 1;
216
+ break;
217
+ }
218
+ }
219
+ if (found < 0) {
220
+ const forwardEnd = Math.min(text.length, rawEnd + Math.floor(normalizedSize * 0.2));
221
+ for (let i = rawEnd; i < forwardEnd; i += 1) {
222
+ if (punctuationSet.has(text[i])) {
223
+ found = i + 1;
224
+ break;
225
+ }
226
+ }
227
+ }
228
+ if (found > cursor) {
229
+ end = found;
230
+ }
231
+ }
232
+ if (end <= cursor) {
233
+ end = Math.min(text.length, cursor + normalizedSize);
234
+ }
235
+ const chunkText = text.slice(cursor, end).trim();
236
+ if (chunkText) {
237
+ output.push({ index, start: cursor, end, text: chunkText });
238
+ index += 1;
239
+ }
240
+ if (end >= text.length) {
241
+ break;
242
+ }
243
+ const nextCursor = Math.max(cursor + 1, end - overlap);
244
+ cursor = nextCursor <= cursor ? end : nextCursor;
245
+ }
246
+ return output;
247
+ }
248
+ function upsertJsonFile(filePath, patch) {
249
+ const current = parseJsonFile(filePath) || {};
250
+ const next = { ...current, ...patch };
251
+ const dir = path.dirname(filePath);
252
+ if (!fs.existsSync(dir)) {
253
+ fs.mkdirSync(dir, { recursive: true });
254
+ }
255
+ fs.writeFileSync(filePath, JSON.stringify(next, null, 2), "utf-8");
256
+ }
257
+ async function probeModelConnection(args) {
258
+ const defaultTimeoutMs = args.kind === "llm" ? 30000 : 15000;
259
+ const timeoutMs = typeof args.timeoutMs === "number" && Number.isFinite(args.timeoutMs) && args.timeoutMs >= 1000
260
+ ? Math.floor(args.timeoutMs)
261
+ : defaultTimeoutMs;
262
+ if (!args.model || !args.apiKey || !args.baseUrl) {
263
+ return {
264
+ configured: false,
265
+ connected: false,
266
+ model: args.model || "",
267
+ base_url: args.baseUrl || "",
268
+ error: "not_configured",
269
+ };
270
+ }
271
+ let endpoint = args.baseUrl;
272
+ let payload = {};
273
+ if (args.kind === "embedding") {
274
+ endpoint = args.baseUrl.endsWith("/embeddings") ? args.baseUrl : `${args.baseUrl}/embeddings`;
275
+ payload = {
276
+ model: args.model,
277
+ input: "diagnostics connectivity probe",
278
+ };
279
+ }
280
+ else if (args.kind === "llm") {
281
+ endpoint = args.baseUrl.endsWith("/chat/completions") ? args.baseUrl : `${args.baseUrl}/chat/completions`;
282
+ payload = {
283
+ model: args.model,
284
+ messages: [{ role: "user", content: "ping" }],
285
+ max_tokens: 4,
286
+ temperature: 0,
287
+ stream: false,
288
+ };
289
+ }
290
+ else {
291
+ endpoint = args.baseUrl.endsWith("/rerank") ? args.baseUrl : `${args.baseUrl}/rerank`;
292
+ payload = {
293
+ model: args.model,
294
+ query: "diagnostics",
295
+ documents: ["diagnostics connectivity probe"],
296
+ top_n: 1,
297
+ };
298
+ }
299
+ let lastError = "unknown_error";
300
+ const maxAttempts = args.kind === "llm" ? 3 : 1;
301
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
302
+ const response = await (0, http_post_1.postJsonWithTimeout)({
303
+ endpoint,
304
+ apiKey: args.apiKey,
305
+ body: payload,
306
+ timeoutMs,
307
+ headers: { accept: "application/json" },
308
+ });
309
+ if (response.ok) {
310
+ return {
311
+ configured: true,
312
+ connected: true,
313
+ model: args.model,
314
+ base_url: args.baseUrl,
315
+ error: "",
316
+ };
317
+ }
318
+ if (response.aborted) {
319
+ lastError = `timeout_${timeoutMs}ms`;
320
+ }
321
+ else if (response.status > 0) {
322
+ const details = (response.text || "").trim().slice(0, 180);
323
+ lastError = details ? `http_${response.status}:${details}` : `http_${response.status}`;
324
+ }
325
+ else {
326
+ lastError = response.error || "network_error";
327
+ }
328
+ }
329
+ return {
330
+ configured: true,
331
+ connected: false,
332
+ model: args.model,
333
+ base_url: args.baseUrl,
334
+ error: lastError,
335
+ };
336
+ }
337
+ async function requestEmbedding(args) {
338
+ const endpoint = args.baseUrl.endsWith("/embeddings") ? args.baseUrl : `${args.baseUrl}/embeddings`;
339
+ const body = {
340
+ input: args.text,
341
+ model: args.model,
342
+ };
343
+ if (typeof args.dimensions === "number" && Number.isFinite(args.dimensions) && args.dimensions > 0) {
344
+ body.dimensions = args.dimensions;
345
+ }
346
+ const timeoutMs = typeof args.timeoutMs === "number" && Number.isFinite(args.timeoutMs) && args.timeoutMs >= 1000
347
+ ? Math.floor(args.timeoutMs)
348
+ : 20000;
349
+ const maxRetries = typeof args.maxRetries === "number" && Number.isFinite(args.maxRetries) && args.maxRetries >= 1
350
+ ? Math.min(8, Math.floor(args.maxRetries))
351
+ : 4;
352
+ let lastError = null;
353
+ for (let attempt = 0; attempt < maxRetries; attempt += 1) {
354
+ const response = await (0, http_post_1.postJsonWithTimeout)({
355
+ endpoint,
356
+ apiKey: args.apiKey,
357
+ body,
358
+ timeoutMs,
359
+ });
360
+ if (!response.ok) {
361
+ lastError = new Error(response.status > 0 ? `embedding_http_${response.status}` : (response.error || "embedding_network_error"));
362
+ continue;
363
+ }
364
+ try {
365
+ const json = (response.json || {});
366
+ const embedding = json?.data?.[0]?.embedding;
367
+ if (Array.isArray(embedding) && embedding.length > 0) {
368
+ return embedding.filter(item => Number.isFinite(item));
369
+ }
370
+ lastError = new Error("embedding_empty");
371
+ }
372
+ catch (error) {
373
+ lastError = error;
374
+ }
375
+ if (attempt < maxRetries - 1) {
376
+ await new Promise(resolve => setTimeout(resolve, 300 * Math.pow(2, attempt)));
377
+ }
378
+ }
379
+ throw lastError instanceof Error ? lastError : new Error(String(lastError || "embedding_failed"));
380
+ }
74
381
  async function storeEvent(args, _context) {
75
382
  try {
76
- if (!args.summary?.trim()) {
383
+ const rawArgs = args;
384
+ const summaryCandidate = typeof rawArgs?.summary === "string"
385
+ ? rawArgs.summary
386
+ : typeof rawArgs?.input?.summary === "string"
387
+ ? String(rawArgs.input.summary)
388
+ : typeof rawArgs?.event?.summary === "string"
389
+ ? String(rawArgs.event.summary)
390
+ : "";
391
+ const normalizedSummary = summaryCandidate.trim();
392
+ if (!normalizedSummary) {
77
393
  return { success: false, error: "Invalid input provided. Missing 'summary' parameter." };
78
394
  }
79
- const { archivePath } = memoryFiles();
80
- const records = readJsonl(archivePath);
81
- const id = `evt_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
82
- records.push({
83
- id,
84
- timestamp: new Date().toISOString(),
85
- summary: args.summary.trim(),
86
- entities: args.entities ?? [],
87
- outcome: args.outcome ?? "",
88
- relations: args.relations ?? [],
89
- source_file: "ts_store_event",
90
- });
91
- writeJsonl(archivePath, records);
92
- return { success: true, data: { event_id: id } };
395
+ const entityInput = Array.isArray(rawArgs.entities)
396
+ ? rawArgs.entities
397
+ : Array.isArray(rawArgs.input?.entities)
398
+ ? rawArgs.input.entities
399
+ : Array.isArray(rawArgs.event?.entities)
400
+ ? rawArgs.event.entities
401
+ : [];
402
+ const entities = Array.isArray(entityInput)
403
+ ? entityInput.map(item => {
404
+ if (typeof item === "string") {
405
+ return item.trim();
406
+ }
407
+ if (item && typeof item === "object") {
408
+ const value = (item.name || item.id || "");
409
+ return typeof value === "string" ? value.trim() : "";
410
+ }
411
+ return "";
412
+ }).filter(Boolean)
413
+ : [];
414
+ const entityTypesFromEntities = {};
415
+ if (Array.isArray(entityInput)) {
416
+ for (const item of entityInput) {
417
+ if (!item || typeof item !== "object") {
418
+ continue;
419
+ }
420
+ const entityObj = item;
421
+ const entityName = typeof entityObj.name === "string" && entityObj.name.trim()
422
+ ? entityObj.name.trim()
423
+ : (typeof entityObj.id === "string" ? entityObj.id.trim() : "");
424
+ const entityType = typeof entityObj.type === "string" ? entityObj.type.trim() : "";
425
+ if (entityName && entityType) {
426
+ entityTypesFromEntities[entityName] = entityType;
427
+ }
428
+ }
429
+ }
430
+ const relationInput = Array.isArray(rawArgs.relations)
431
+ ? rawArgs.relations
432
+ : Array.isArray(rawArgs.input?.relations)
433
+ ? rawArgs.input.relations
434
+ : Array.isArray(rawArgs.event?.relations)
435
+ ? rawArgs.event.relations
436
+ : [];
437
+ const relations = Array.isArray(relationInput)
438
+ ? relationInput
439
+ .map(item => {
440
+ if (typeof item === "string") {
441
+ const [sourceRaw, typeRaw, targetRaw] = item.split("|");
442
+ const source = (sourceRaw || "").trim();
443
+ const target = (targetRaw || "").trim();
444
+ const type = (0, ontology_1.normalizeRelationType)((typeRaw || "related_to").trim(), graphSchema);
445
+ if (!source || !target)
446
+ return null;
447
+ return { source, target, type };
448
+ }
449
+ if (!item || typeof item !== "object")
450
+ return null;
451
+ const relation = item;
452
+ if (!relation.source || !relation.target)
453
+ return null;
454
+ return {
455
+ source: relation.source.trim(),
456
+ target: relation.target.trim(),
457
+ type: (0, ontology_1.normalizeRelationType)(relation.type || "related_to", graphSchema),
458
+ evidence_span: typeof relation.evidence_span === "string" ? relation.evidence_span.trim() : undefined,
459
+ confidence: typeof relation.confidence === "number" ? Math.max(0, Math.min(1, relation.confidence)) : undefined,
460
+ };
461
+ })
462
+ .filter((item) => Boolean(item))
463
+ : [];
464
+ const outcomeValue = typeof rawArgs.outcome === "string"
465
+ ? rawArgs.outcome
466
+ : typeof rawArgs.input?.outcome === "string"
467
+ ? String(rawArgs.input.outcome)
468
+ : typeof rawArgs.event?.outcome === "string"
469
+ ? String(rawArgs.event.outcome)
470
+ : "";
471
+ const entityTypesInput = typeof rawArgs.entity_types === "object" && rawArgs.entity_types !== null
472
+ ? rawArgs.entity_types
473
+ : typeof rawArgs.input?.entity_types === "object"
474
+ ? rawArgs.input.entity_types
475
+ : typeof rawArgs.event?.entity_types === "object"
476
+ ? rawArgs.event.entity_types
477
+ : {};
478
+ const entityTypes = {};
479
+ for (const [key, value] of Object.entries(entityTypesInput)) {
480
+ if (typeof value === "string") {
481
+ entityTypes[key.trim()] = value.trim();
482
+ }
483
+ }
484
+ for (const [key, value] of Object.entries(entityTypesFromEntities)) {
485
+ if (!entityTypes[key] && value) {
486
+ entityTypes[key] = value;
487
+ }
488
+ }
489
+ const result = await deps.archiveStore.storeEvents([
490
+ {
491
+ event_type: "manual_event",
492
+ summary: normalizedSummary,
493
+ entities,
494
+ relations,
495
+ entity_types: entityTypes,
496
+ outcome: outcomeValue,
497
+ session_id: "manual",
498
+ source_file: "ts_store_event",
499
+ confidence: 1,
500
+ source_event_id: `manual:${Date.now().toString(36)}`,
501
+ actor: "manual_tool",
502
+ },
503
+ ]);
504
+ if (result.stored.length === 0) {
505
+ return { success: false, error: result.skipped[0]?.reason || "store_event_skipped" };
506
+ }
507
+ const storedId = result.stored[0].id;
508
+ if (deps.graphMemoryStore && entities.length > 0 && Object.keys(entityTypes).length > 0 && relations.length > 0) {
509
+ const graphResult = await deps.graphMemoryStore.append({
510
+ sourceEventId: storedId,
511
+ sourceLayer: "archive_event",
512
+ archiveEventId: storedId,
513
+ sessionId: "manual",
514
+ sourceFile: "ts_store_event",
515
+ eventType: "manual_event",
516
+ entities,
517
+ entity_types: entityTypes,
518
+ relations,
519
+ gateSource: "manual",
520
+ confidence: 1,
521
+ sourceText: normalizedSummary,
522
+ });
523
+ if (!graphResult.success) {
524
+ deps.logger.info(`store_event graph_skip_reason=${graphResult.reason} source_event_id=${storedId}`);
525
+ }
526
+ }
527
+ return { success: true, data: { event_id: storedId } };
93
528
  }
94
529
  catch (error) {
95
530
  return { success: false, error: String(error) };
@@ -100,13 +535,95 @@ function createTsEngine(deps) {
100
535
  if (!entity) {
101
536
  return { success: false, error: "Invalid input provided. Missing 'entity' parameter." };
102
537
  }
103
- const { archivePath } = memoryFiles();
104
- const records = readJsonl(archivePath);
538
+ const relFilter = typeof args.rel === "string" && args.rel.trim()
539
+ ? (0, ontology_1.normalizeRelationType)(args.rel, graphSchema)
540
+ : "";
541
+ const direction = args.dir === "incoming" || args.dir === "outgoing" || args.dir === "both"
542
+ ? args.dir
543
+ : "both";
544
+ const pathTo = typeof args.path_to === "string" && args.path_to.trim() ? args.path_to.trim() : "";
545
+ const maxDepth = Math.max(2, Math.min(4, typeof args.max_depth === "number" ? Math.floor(args.max_depth) : 3));
546
+ const graphMemoryPath = path.join(deps.memoryRoot, "graph", "memory.jsonl");
105
547
  const nodes = new Map();
106
548
  const edges = [];
107
- for (const record of records) {
549
+ const adjacency = new Map();
550
+ const pathAdjacency = new Map();
551
+ const relationTypeDistribution = new Map();
552
+ const edgeKeySet = new Set();
553
+ function pushEdge(source, target, type) {
554
+ const key = `${source}|${type}|${target}`;
555
+ if (edgeKeySet.has(key)) {
556
+ return;
557
+ }
558
+ edgeKeySet.add(key);
559
+ edges.push({ source, target, type });
560
+ relationTypeDistribution.set(type, (relationTypeDistribution.get(type) || 0) + 1);
561
+ if (!adjacency.has(source)) {
562
+ adjacency.set(source, []);
563
+ }
564
+ adjacency.get(source)?.push({ next: target, edge: { source, target, type } });
565
+ if (!adjacency.has(target)) {
566
+ adjacency.set(target, []);
567
+ }
568
+ adjacency.get(target)?.push({ next: source, edge: { source, target, type } });
569
+ }
570
+ function pushPathEdge(source, target, type) {
571
+ if (!pathAdjacency.has(source)) {
572
+ pathAdjacency.set(source, []);
573
+ }
574
+ if (!pathAdjacency.has(target)) {
575
+ pathAdjacency.set(target, []);
576
+ }
577
+ if (direction === "incoming") {
578
+ pathAdjacency.get(target)?.push({ next: source, edge: { source, target, type } });
579
+ }
580
+ else if (direction === "outgoing") {
581
+ pathAdjacency.get(source)?.push({ next: target, edge: { source, target, type } });
582
+ }
583
+ else {
584
+ pathAdjacency.get(source)?.push({ next: target, edge: { source, target, type } });
585
+ pathAdjacency.get(target)?.push({ next: source, edge: { source, target, type } });
586
+ }
587
+ }
588
+ const graphRecords = fs.existsSync(graphMemoryPath) ? readJsonl(graphMemoryPath) : [];
589
+ for (const record of graphRecords) {
108
590
  const entities = Array.isArray(record.entities) ? record.entities : [];
109
- const named = entities.map(e => (typeof e === "string" ? e.trim() : "")).filter(Boolean);
591
+ const named = entities.map((e) => (typeof e === "string" ? e.trim() : "")).filter(Boolean);
592
+ const relations = Array.isArray(record.relations) ? record.relations : [];
593
+ let explicitMatched = false;
594
+ for (const relationRaw of relations) {
595
+ if (typeof relationRaw !== "object" || relationRaw === null) {
596
+ continue;
597
+ }
598
+ const relation = relationRaw;
599
+ const source = typeof relation.source === "string" ? relation.source.trim() : "";
600
+ const target = typeof relation.target === "string" ? relation.target.trim() : "";
601
+ const type = (0, ontology_1.normalizeRelationType)(typeof relation.type === "string" && relation.type.trim() ? relation.type.trim() : "related_to", graphSchema);
602
+ if (!source || !target) {
603
+ continue;
604
+ }
605
+ if (relFilter && type !== relFilter) {
606
+ continue;
607
+ }
608
+ pushPathEdge(source, target, type);
609
+ const outgoingMatch = source === entity;
610
+ const incomingMatch = target === entity;
611
+ const directionMatched = direction === "both" ? (outgoingMatch || incomingMatch)
612
+ : direction === "outgoing" ? outgoingMatch
613
+ : incomingMatch;
614
+ if (!directionMatched) {
615
+ continue;
616
+ }
617
+ explicitMatched = true;
618
+ if (!nodes.has(source))
619
+ nodes.set(source, { id: source, type: "entity" });
620
+ if (!nodes.has(target))
621
+ nodes.set(target, { id: target, type: "entity" });
622
+ pushEdge(source, target, type);
623
+ }
624
+ if (explicitMatched) {
625
+ continue;
626
+ }
110
627
  if (!named.includes(entity)) {
111
628
  continue;
112
629
  }
@@ -117,7 +634,40 @@ function createTsEngine(deps) {
117
634
  }
118
635
  for (const name of named) {
119
636
  if (name !== entity) {
120
- edges.push({ source: entity, target: name, type: "co_occurrence" });
637
+ if (!relFilter || relFilter === "co_occurrence") {
638
+ pushEdge(entity, name, "co_occurrence");
639
+ }
640
+ }
641
+ }
642
+ }
643
+ let graphPath = [];
644
+ if (pathTo) {
645
+ const visited = new Set();
646
+ const queue = [
647
+ { node: entity, depth: 0, pathEdges: [] },
648
+ ];
649
+ while (queue.length > 0) {
650
+ const current = queue.shift();
651
+ if (!current)
652
+ break;
653
+ if (current.node === pathTo) {
654
+ graphPath = current.pathEdges;
655
+ break;
656
+ }
657
+ if (current.depth >= maxDepth) {
658
+ continue;
659
+ }
660
+ const visitKey = `${current.node}:${current.depth}`;
661
+ if (visited.has(visitKey)) {
662
+ continue;
663
+ }
664
+ visited.add(visitKey);
665
+ for (const next of pathAdjacency.get(current.node) || []) {
666
+ queue.push({
667
+ node: next.next,
668
+ depth: current.depth + 1,
669
+ pathEdges: [...current.pathEdges, next.edge],
670
+ });
121
671
  }
122
672
  }
123
673
  }
@@ -125,8 +675,14 @@ function createTsEngine(deps) {
125
675
  success: true,
126
676
  data: {
127
677
  entity,
678
+ rel: relFilter || "",
679
+ dir: direction,
128
680
  nodes: [...nodes.values()],
129
681
  edges,
682
+ path_to: pathTo || "",
683
+ max_depth: maxDepth,
684
+ path: graphPath,
685
+ relation_type_distribution: [...relationTypeDistribution.entries()].map(([type, count]) => ({ type, count })),
130
686
  },
131
687
  };
132
688
  }
@@ -216,33 +772,580 @@ function createTsEngine(deps) {
216
772
  }
217
773
  return { success: true, data: { deletedCount } };
218
774
  }
775
+ async function backfillEmbeddings(args, _context) {
776
+ const layer = args.layer === "active" || args.layer === "archive" || args.layer === "all" ? args.layer : "all";
777
+ const rebuildMode = args.rebuild_mode === "vector_only" || args.rebuild_mode === "full"
778
+ ? args.rebuild_mode
779
+ : "incremental";
780
+ const batchSize = typeof args.batch_size === "number" && Number.isFinite(args.batch_size) && args.batch_size > 0
781
+ ? Math.min(500, Math.floor(args.batch_size))
782
+ : 100;
783
+ const maxRetries = typeof args.max_retries === "number" && Number.isFinite(args.max_retries) && args.max_retries >= 1
784
+ ? Math.min(10, Math.floor(args.max_retries))
785
+ : 3;
786
+ const retryFailedOnly = args.retry_failed_only === true;
787
+ const forceRebuild = rebuildMode === "vector_only" || rebuildMode === "full";
788
+ const model = deps.embedding?.model || "";
789
+ const apiKey = deps.embedding?.apiKey || "";
790
+ const baseUrl = normalizeBaseUrl(deps.embedding?.baseURL || deps.embedding?.baseUrl);
791
+ if (!model || !apiKey || !baseUrl) {
792
+ return { success: false, error: "Embedding config missing for backfill tool." };
793
+ }
794
+ const statePath = path.join(deps.memoryRoot, ".vector_backfill_state.json");
795
+ const syncStatePath = path.join(deps.memoryRoot, ".sync_state.json");
796
+ const previousState = parseJsonFile(statePath) || {};
797
+ const failureCountState = (typeof previousState.failureCounts === "object" && previousState.failureCounts !== null)
798
+ ? previousState.failureCounts
799
+ : {};
800
+ let fullSyncResult = null;
801
+ if (rebuildMode === "full") {
802
+ try {
803
+ fullSyncResult = await deps.sessionSync.syncMemory();
804
+ }
805
+ catch (error) {
806
+ deps.logger.warn(`backfill_full_rebuild_sync_failed error=${error}`);
807
+ }
808
+ }
809
+ const { activePath, archivePath } = memoryFiles();
810
+ const targetFiles = [];
811
+ if (layer === "all" || layer === "active") {
812
+ targetFiles.push({ layer: "active", filePath: activePath });
813
+ }
814
+ if (layer === "all" || layer === "archive") {
815
+ targetFiles.push({ layer: "archive", filePath: archivePath });
816
+ }
817
+ const queue = [];
818
+ const recordsByFile = new Map();
819
+ for (const target of targetFiles) {
820
+ const records = readJsonl(target.filePath);
821
+ recordsByFile.set(target.filePath, records);
822
+ for (let i = 0; i < records.length; i += 1) {
823
+ const record = records[i];
824
+ const id = typeof record.id === "string" ? record.id : "";
825
+ if (!id) {
826
+ continue;
827
+ }
828
+ const status = typeof record.embedding_status === "string" ? record.embedding_status.trim() : "";
829
+ const hasEmbedding = Array.isArray(record.embedding) && record.embedding.length > 0;
830
+ if (forceRebuild) {
831
+ queue.push({ layer: target.layer, filePath: target.filePath, index: i });
832
+ continue;
833
+ }
834
+ if (retryFailedOnly) {
835
+ if (status !== "failed") {
836
+ continue;
837
+ }
838
+ }
839
+ else if (status === "ok" || hasEmbedding) {
840
+ continue;
841
+ }
842
+ const failCountRaw = failureCountState[id];
843
+ const failCount = typeof failCountRaw === "number" ? failCountRaw : 0;
844
+ if (failCount >= maxRetries && status === "failed") {
845
+ continue;
846
+ }
847
+ queue.push({ layer: target.layer, filePath: target.filePath, index: i });
848
+ }
849
+ }
850
+ const totalCandidates = queue.length;
851
+ let success = 0;
852
+ let failed = 0;
853
+ let skipped = 0;
854
+ let processed = 0;
855
+ const failureCounts = {};
856
+ for (const [key, value] of Object.entries(failureCountState)) {
857
+ if (typeof value === "number" && Number.isFinite(value)) {
858
+ failureCounts[key] = value;
859
+ }
860
+ }
861
+ for (let start = 0; start < queue.length; start += batchSize) {
862
+ const batch = queue.slice(start, start + batchSize);
863
+ for (const item of batch) {
864
+ processed += 1;
865
+ const records = recordsByFile.get(item.filePath) || [];
866
+ const record = records[item.index];
867
+ if (!record) {
868
+ skipped += 1;
869
+ continue;
870
+ }
871
+ const id = typeof record.id === "string" ? record.id : "";
872
+ if (!id) {
873
+ skipped += 1;
874
+ continue;
875
+ }
876
+ const text = buildVectorSourceText(record, item.layer);
877
+ if (!text) {
878
+ record.embedding_status = "failed";
879
+ failed += 1;
880
+ failureCounts[id] = (failureCounts[id] || 0) + 1;
881
+ continue;
882
+ }
883
+ const chunkSize = deps.vectorChunking?.chunkSize ?? 600;
884
+ const chunkOverlap = deps.vectorChunking?.chunkOverlap ?? 100;
885
+ const chunks = splitTextChunks(text, chunkSize, chunkOverlap);
886
+ if (chunks.length === 0) {
887
+ record.embedding_status = "failed";
888
+ failed += 1;
889
+ failureCounts[id] = (failureCounts[id] || 0) + 1;
890
+ continue;
891
+ }
892
+ try {
893
+ if (forceRebuild) {
894
+ record.embedding_status = "pending";
895
+ }
896
+ await deps.vectorStore.deleteBySourceMemory({ layer: item.layer, sourceMemoryId: id });
897
+ let chunkOk = 0;
898
+ for (const chunk of chunks) {
899
+ const embedding = await requestEmbedding({
900
+ text: chunk.text,
901
+ model,
902
+ apiKey,
903
+ baseUrl,
904
+ dimensions: deps.embedding?.dimensions,
905
+ timeoutMs: deps.embedding?.timeoutMs,
906
+ maxRetries: deps.embedding?.maxRetries,
907
+ });
908
+ if (!embedding || embedding.length === 0) {
909
+ continue;
910
+ }
911
+ if (!record.embedding) {
912
+ record.embedding = embedding;
913
+ }
914
+ await deps.vectorStore.upsert({
915
+ id: `${id}_c${chunk.index}`,
916
+ session_id: typeof record.session_id === "string" ? record.session_id : "unknown",
917
+ event_type: typeof record.event_type === "string" ? record.event_type : (item.layer === "active" ? "message" : "insight"),
918
+ summary: chunk.text,
919
+ timestamp: typeof record.timestamp === "string" ? record.timestamp : new Date().toISOString(),
920
+ layer: item.layer,
921
+ source_memory_id: id,
922
+ source_memory_canonical_id: typeof record.canonical_id === "string" ? record.canonical_id : id,
923
+ outcome: typeof record.outcome === "string" ? record.outcome : "",
924
+ entities: Array.isArray(record.entities) ? record.entities.filter(v => typeof v === "string") : [],
925
+ relations: Array.isArray(record.relations)
926
+ ? record.relations
927
+ .map(v => {
928
+ if (!v || typeof v !== "object")
929
+ return null;
930
+ const relation = v;
931
+ const source = typeof relation.source === "string" ? relation.source : "";
932
+ const target = typeof relation.target === "string" ? relation.target : "";
933
+ const type = typeof relation.type === "string" ? relation.type : "related_to";
934
+ if (!source || !target)
935
+ return null;
936
+ return { source, target, type };
937
+ })
938
+ .filter((v) => Boolean(v))
939
+ : [],
940
+ embedding,
941
+ quality_score: typeof record.quality_score === "number" ? record.quality_score : 0.5,
942
+ char_count: chunk.text.length,
943
+ token_count: estimateTokenCount(chunk.text),
944
+ chunk_index: chunk.index,
945
+ chunk_total: chunks.length,
946
+ chunk_start: chunk.start,
947
+ chunk_end: chunk.end,
948
+ });
949
+ chunkOk += 1;
950
+ }
951
+ record.vector_chunks_total = chunks.length;
952
+ record.vector_chunks_ok = chunkOk;
953
+ record.embedding_status = chunkOk === chunks.length ? "ok" : "failed";
954
+ if (!record.layer) {
955
+ record.layer = item.layer;
956
+ }
957
+ if (typeof record.char_count !== "number") {
958
+ record.char_count = text.length;
959
+ }
960
+ if (typeof record.token_count !== "number") {
961
+ record.token_count = estimateTokenCount(text);
962
+ }
963
+ if (chunkOk === chunks.length) {
964
+ success += 1;
965
+ failureCounts[id] = 0;
966
+ }
967
+ else {
968
+ failed += 1;
969
+ failureCounts[id] = (failureCounts[id] || 0) + 1;
970
+ }
971
+ }
972
+ catch (error) {
973
+ record.embedding_status = "failed";
974
+ failed += 1;
975
+ failureCounts[id] = (failureCounts[id] || 0) + 1;
976
+ deps.logger.warn(`backfill_embedding_failed id=${id} layer=${item.layer} error=${error}`);
977
+ }
978
+ }
979
+ deps.logger.info(`backfill_progress processed=${processed}/${totalCandidates} success=${success} failed=${failed} skipped=${skipped}`);
980
+ }
981
+ for (const target of targetFiles) {
982
+ const records = recordsByFile.get(target.filePath);
983
+ if (records) {
984
+ writeJsonl(target.filePath, records);
985
+ }
986
+ }
987
+ const summary = {
988
+ runAt: new Date().toISOString(),
989
+ layer,
990
+ rebuild_mode: rebuildMode,
991
+ candidates: totalCandidates,
992
+ success,
993
+ failed,
994
+ skipped,
995
+ batch_size: batchSize,
996
+ max_retries: maxRetries,
997
+ retry_failed_only: retryFailedOnly,
998
+ full_sync_result: fullSyncResult,
999
+ };
1000
+ upsertJsonFile(statePath, {
1001
+ version: "1",
1002
+ lastRun: summary,
1003
+ failureCounts,
1004
+ });
1005
+ upsertJsonFile(syncStatePath, {
1006
+ version: "2",
1007
+ lastVectorBackfill: {
1008
+ runAt: summary.runAt,
1009
+ success,
1010
+ failed,
1011
+ skipped,
1012
+ },
1013
+ });
1014
+ return { success: true, data: summary };
1015
+ }
219
1016
  async function runDiagnostics(_args, _context) {
220
1017
  const { activePath, archivePath } = memoryFiles();
1018
+ const activeRecords = readJsonl(activePath);
1019
+ const archiveRecords = readJsonl(archivePath);
1020
+ const activeVector = embeddingStats(activeRecords);
1021
+ const archiveVector = embeddingStats(archiveRecords);
1022
+ const vectorJsonlPath = path.join(deps.memoryRoot, "vector", "lancedb_events.jsonl");
1023
+ const vectorJsonlRecords = readJsonl(vectorJsonlPath);
1024
+ const activeVectorRecords = vectorJsonlRecords.filter(record => (record.layer === "active"));
1025
+ const archiveVectorRecords = vectorJsonlRecords.filter(record => (record.layer === "archive"));
1026
+ const lancedbDir = path.join(deps.memoryRoot, "vector", "lancedb");
1027
+ const lancedbExists = fs.existsSync(lancedbDir);
1028
+ let lancedbRecordCount = 0;
1029
+ if (lancedbExists) {
1030
+ try {
1031
+ const lancedbFiles = fs.readdirSync(lancedbDir).filter(f => f.endsWith(".lance") || f.endsWith(".manifest"));
1032
+ lancedbRecordCount = lancedbFiles.length > 0 ? -1 : 0;
1033
+ }
1034
+ catch {
1035
+ lancedbRecordCount = 0;
1036
+ }
1037
+ }
1038
+ const totalVectorRecords = vectorJsonlRecords.length > 0 ? vectorJsonlRecords.length : (lancedbRecordCount === -1 ? -1 : 0);
1039
+ const vectorStorageType = lancedbExists && lancedbRecordCount === -1 ? "lancedb" : (vectorJsonlRecords.length > 0 ? "jsonl" : "none");
1040
+ const syncState = parseJsonFile(path.join(deps.memoryRoot, ".sync_state.json"));
1041
+ const backfillState = parseJsonFile(path.join(deps.memoryRoot, ".vector_backfill_state.json"));
1042
+ const failureCounts = backfillState && typeof backfillState.failureCounts === "object" && backfillState.failureCounts !== null
1043
+ ? backfillState.failureCounts
1044
+ : {};
1045
+ const pendingRetry = Object.values(failureCounts).filter(value => typeof value === "number" && Number.isFinite(value) && value > 0).length;
1046
+ const lastVectorBackfill = syncState && typeof syncState.lastVectorBackfill === "object" && syncState.lastVectorBackfill !== null
1047
+ ? syncState.lastVectorBackfill
1048
+ : null;
1049
+ const embeddingConnectivity = await probeModelConnection({
1050
+ kind: "embedding",
1051
+ model: deps.embedding?.model || "",
1052
+ apiKey: deps.embedding?.apiKey || "",
1053
+ baseUrl: normalizeBaseUrl(deps.embedding?.baseURL || deps.embedding?.baseUrl),
1054
+ timeoutMs: deps.embedding?.timeoutMs,
1055
+ });
1056
+ const llmConnectivity = await probeModelConnection({
1057
+ kind: "llm",
1058
+ model: deps.llm?.model || "",
1059
+ apiKey: deps.llm?.apiKey || "",
1060
+ baseUrl: normalizeBaseUrl(deps.llm?.baseURL || deps.llm?.baseUrl),
1061
+ timeoutMs: 8000,
1062
+ });
1063
+ const rerankerConnectivity = await probeModelConnection({
1064
+ kind: "reranker",
1065
+ model: deps.reranker?.model || "",
1066
+ apiKey: deps.reranker?.apiKey || "",
1067
+ baseUrl: normalizeBaseUrl(deps.reranker?.baseURL || deps.reranker?.baseUrl),
1068
+ timeoutMs: 8000,
1069
+ });
221
1070
  const checks = [
222
1071
  { name: "Engine mode", passed: true, message: "TS engine active" },
223
1072
  { name: "Active sessions store", passed: fs.existsSync(activePath), message: activePath },
224
1073
  { name: "Archive sessions store", passed: fs.existsSync(archivePath), message: archivePath },
225
1074
  { name: "Core rules store", passed: fs.existsSync(path.join(deps.memoryRoot, "CORTEX_RULES.md")), message: "CORTEX_RULES.md checked" },
1075
+ { name: "Embedding model connectivity", passed: embeddingConnectivity.connected, message: embeddingConnectivity.error || "ok" },
1076
+ { name: "LLM model connectivity", passed: llmConnectivity.connected, message: llmConnectivity.error || "ok" },
1077
+ { name: "Reranker model connectivity", passed: rerankerConnectivity.connected, message: rerankerConnectivity.error || "ok" },
226
1078
  ];
1079
+ const qualityCheck = {
1080
+ active: { total: 0, valid: 0, invalid: 0, issues: [] },
1081
+ archive: { total: 0, valid: 0, invalid: 0, issues: [] },
1082
+ };
1083
+ if (fs.existsSync(activePath)) {
1084
+ const content = fs.readFileSync(activePath, "utf-8");
1085
+ const lines = content.split(/\r?\n/).filter(l => l.trim());
1086
+ qualityCheck.active.total = lines.length;
1087
+ for (let i = 0; i < lines.length; i++) {
1088
+ const validation = (0, llm_output_validator_1.validateJsonlLine)(lines[i]);
1089
+ if (validation.valid) {
1090
+ qualityCheck.active.valid++;
1091
+ }
1092
+ else {
1093
+ qualityCheck.active.invalid++;
1094
+ if (qualityCheck.active.issues.length < 5) {
1095
+ qualityCheck.active.issues.push({ line: i + 1, errors: validation.errors });
1096
+ }
1097
+ }
1098
+ }
1099
+ }
1100
+ if (fs.existsSync(archivePath)) {
1101
+ const content = fs.readFileSync(archivePath, "utf-8");
1102
+ const lines = content.split(/\r?\n/).filter(l => l.trim());
1103
+ qualityCheck.archive.total = lines.length;
1104
+ for (let i = 0; i < lines.length; i++) {
1105
+ const validation = (0, llm_output_validator_1.validateJsonlLine)(lines[i]);
1106
+ if (validation.valid) {
1107
+ qualityCheck.archive.valid++;
1108
+ }
1109
+ else {
1110
+ qualityCheck.archive.invalid++;
1111
+ if (qualityCheck.archive.issues.length < 5) {
1112
+ qualityCheck.archive.issues.push({ line: i + 1, errors: validation.errors });
1113
+ }
1114
+ }
1115
+ }
1116
+ }
1117
+ const totalInvalid = qualityCheck.active.invalid + qualityCheck.archive.invalid;
1118
+ if (totalInvalid > 0) {
1119
+ checks.push({ name: "Data integrity", passed: false, message: `${totalInvalid} invalid records found` });
1120
+ }
1121
+ else {
1122
+ checks.push({ name: "Data integrity", passed: true, message: "All records valid" });
1123
+ }
1124
+ const graphMemoryPath = path.join(deps.memoryRoot, "graph", "memory.jsonl");
1125
+ const graphRecords = readJsonl(graphMemoryPath);
1126
+ function stringField(record, key) {
1127
+ const value = record[key];
1128
+ return typeof value === "string" ? value.trim() : "";
1129
+ }
1130
+ const activeFieldIssues = {
1131
+ missing_id: 0,
1132
+ missing_timestamp: 0,
1133
+ missing_layer: 0,
1134
+ missing_text_payload: 0,
1135
+ };
1136
+ for (const record of activeRecords) {
1137
+ if (!stringField(record, "id"))
1138
+ activeFieldIssues.missing_id += 1;
1139
+ if (!stringField(record, "timestamp"))
1140
+ activeFieldIssues.missing_timestamp += 1;
1141
+ if (stringField(record, "layer") !== "active")
1142
+ activeFieldIssues.missing_layer += 1;
1143
+ const hasPayload = [
1144
+ stringField(record, "content"),
1145
+ stringField(record, "summary"),
1146
+ stringField(record, "text"),
1147
+ stringField(record, "message"),
1148
+ ].some(Boolean);
1149
+ if (!hasPayload)
1150
+ activeFieldIssues.missing_text_payload += 1;
1151
+ }
1152
+ const archiveFieldIssues = {
1153
+ missing_id: 0,
1154
+ missing_timestamp: 0,
1155
+ missing_layer: 0,
1156
+ missing_summary: 0,
1157
+ missing_source_memory_id: 0,
1158
+ };
1159
+ for (const record of archiveRecords) {
1160
+ if (!stringField(record, "id"))
1161
+ archiveFieldIssues.missing_id += 1;
1162
+ if (!stringField(record, "timestamp"))
1163
+ archiveFieldIssues.missing_timestamp += 1;
1164
+ if (stringField(record, "layer") !== "archive")
1165
+ archiveFieldIssues.missing_layer += 1;
1166
+ if (!stringField(record, "summary"))
1167
+ archiveFieldIssues.missing_summary += 1;
1168
+ if (!stringField(record, "source_memory_id") && !stringField(record, "canonical_id")) {
1169
+ archiveFieldIssues.missing_source_memory_id += 1;
1170
+ }
1171
+ }
1172
+ const vectorFieldIssues = {
1173
+ missing_id: 0,
1174
+ missing_layer: 0,
1175
+ missing_summary: 0,
1176
+ missing_source_memory_id: 0,
1177
+ missing_embedding: 0,
1178
+ };
1179
+ for (const record of vectorJsonlRecords) {
1180
+ if (!stringField(record, "id"))
1181
+ vectorFieldIssues.missing_id += 1;
1182
+ const layer = stringField(record, "layer");
1183
+ if (layer !== "active" && layer !== "archive")
1184
+ vectorFieldIssues.missing_layer += 1;
1185
+ if (!stringField(record, "summary"))
1186
+ vectorFieldIssues.missing_summary += 1;
1187
+ if (!stringField(record, "source_memory_id"))
1188
+ vectorFieldIssues.missing_source_memory_id += 1;
1189
+ const embedding = record.embedding;
1190
+ const vector = record.vector;
1191
+ const hasEmbedding = Array.isArray(embedding) ? embedding.length > 0 : (Array.isArray(vector) ? vector.length > 0 : false);
1192
+ if (!hasEmbedding)
1193
+ vectorFieldIssues.missing_embedding += 1;
1194
+ }
1195
+ const graphFieldIssues = {
1196
+ missing_id: 0,
1197
+ missing_event_ref: 0,
1198
+ missing_layer: 0,
1199
+ malformed_relations: 0,
1200
+ };
1201
+ for (const record of graphRecords) {
1202
+ if (!stringField(record, "id"))
1203
+ graphFieldIssues.missing_id += 1;
1204
+ if (!stringField(record, "source_event_id") && !stringField(record, "archive_event_id")) {
1205
+ graphFieldIssues.missing_event_ref += 1;
1206
+ }
1207
+ const layer = stringField(record, "source_layer");
1208
+ if (layer !== "archive_event" && layer !== "active_only")
1209
+ graphFieldIssues.missing_layer += 1;
1210
+ if (!Array.isArray(record.relations))
1211
+ graphFieldIssues.malformed_relations += 1;
1212
+ }
1213
+ const archiveIdSet = new Set(archiveRecords
1214
+ .map(record => stringField(record, "id"))
1215
+ .filter(Boolean));
1216
+ const vectorLinkedToArchive = vectorJsonlRecords.filter(record => {
1217
+ const sourceMemoryId = stringField(record, "source_memory_id");
1218
+ return !!sourceMemoryId && archiveIdSet.has(sourceMemoryId);
1219
+ }).length;
1220
+ const graphLinkedToArchive = graphRecords.filter(record => {
1221
+ const sourceLayer = stringField(record, "source_layer");
1222
+ const refId = stringField(record, "source_event_id") || stringField(record, "archive_event_id");
1223
+ return sourceLayer === "archive_event" && !!refId && archiveIdSet.has(refId);
1224
+ }).length;
1225
+ const schemaIssueTotal = Object.values(activeFieldIssues).reduce((sum, n) => sum + n, 0)
1226
+ + Object.values(archiveFieldIssues).reduce((sum, n) => sum + n, 0)
1227
+ + Object.values(vectorFieldIssues).reduce((sum, n) => sum + n, 0)
1228
+ + Object.values(graphFieldIssues).reduce((sum, n) => sum + n, 0);
1229
+ checks.push({
1230
+ name: "Field mapping alignment",
1231
+ passed: schemaIssueTotal === 0,
1232
+ message: schemaIssueTotal === 0
1233
+ ? "active/archive/vector/graph field mapping aligned with read path"
1234
+ : `${schemaIssueTotal} field mapping issues detected across four memory stores`,
1235
+ });
227
1236
  return {
228
1237
  success: true,
229
1238
  data: {
230
1239
  status: "ok",
1240
+ prompt_versions: PROMPT_VERSIONS,
231
1241
  checks,
232
- recommendations: [],
1242
+ layers: {
1243
+ active: {
1244
+ records: activeRecords.length,
1245
+ path: activePath,
1246
+ },
1247
+ archive: {
1248
+ records: archiveRecords.length,
1249
+ path: archivePath,
1250
+ },
1251
+ vector: {
1252
+ storage_type: vectorStorageType,
1253
+ lancedb_exists: lancedbExists,
1254
+ active_coverage: activeVector.coverage,
1255
+ archive_coverage: archiveVector.coverage,
1256
+ active_unembedded: activeVector.pending + activeVector.failed,
1257
+ archive_unembedded: archiveVector.pending + archiveVector.failed,
1258
+ chunking: {
1259
+ chunk_size: deps.vectorChunking?.chunkSize ?? 600,
1260
+ chunk_overlap: deps.vectorChunking?.chunkOverlap ?? 100,
1261
+ },
1262
+ vector_jsonl_records: vectorJsonlRecords.length,
1263
+ vector_jsonl_by_layer: {
1264
+ active: activeVectorRecords.length,
1265
+ archive: archiveVectorRecords.length,
1266
+ },
1267
+ total_vector_records: totalVectorRecords,
1268
+ last_backfill_summary: lastVectorBackfill,
1269
+ backfill_state: {
1270
+ pending_retry_records: pendingRetry,
1271
+ has_state_file: fs.existsSync(path.join(deps.memoryRoot, ".vector_backfill_state.json")),
1272
+ },
1273
+ },
1274
+ graph_rules: {
1275
+ graph_mutation_log_exists: fs.existsSync(path.join(deps.memoryRoot, "graph", "mutation_log.jsonl")),
1276
+ rules_exists: fs.existsSync(path.join(deps.memoryRoot, "CORTEX_RULES.md")),
1277
+ },
1278
+ },
1279
+ model_connectivity: {
1280
+ embedding: embeddingConnectivity,
1281
+ llm: llmConnectivity,
1282
+ reranker: rerankerConnectivity,
1283
+ },
1284
+ quality_check: qualityCheck,
1285
+ schema_alignment: {
1286
+ active: {
1287
+ records: activeRecords.length,
1288
+ issues: activeFieldIssues,
1289
+ },
1290
+ archive: {
1291
+ records: archiveRecords.length,
1292
+ issues: archiveFieldIssues,
1293
+ },
1294
+ vector: {
1295
+ records: vectorJsonlRecords.length,
1296
+ issues: vectorFieldIssues,
1297
+ linked_to_archive: vectorLinkedToArchive,
1298
+ },
1299
+ graph: {
1300
+ records: graphRecords.length,
1301
+ issues: graphFieldIssues,
1302
+ linked_archive_events: graphLinkedToArchive,
1303
+ },
1304
+ cross_layer_links: {
1305
+ archive_records: archiveIdSet.size,
1306
+ vector_archive_link_coverage: archiveIdSet.size > 0
1307
+ ? Number((vectorLinkedToArchive / archiveIdSet.size).toFixed(4))
1308
+ : 0,
1309
+ graph_archive_link_coverage: archiveIdSet.size > 0
1310
+ ? Number((graphLinkedToArchive / archiveIdSet.size).toFixed(4))
1311
+ : 0,
1312
+ },
1313
+ },
1314
+ recommendations: [
1315
+ ...(totalInvalid > 0 ? ["Run repair-memory --fix to clean invalid records"] : []),
1316
+ ...(schemaIssueTotal > 0 ? ["Run diagnostics output schema_alignment and repair missing cross-layer fields"] : []),
1317
+ ],
233
1318
  },
234
1319
  };
235
1320
  }
236
1321
  async function searchMemory(args, context) {
237
- if (!args || !args.query) {
1322
+ const argsRecord = asRecord(args) || {};
1323
+ const argsInput = asRecord(argsRecord.input);
1324
+ const queryCandidate = [
1325
+ typeof args.query === "string" ? args.query : "",
1326
+ typeof argsRecord.query === "string" ? String(argsRecord.query) : "",
1327
+ typeof argsRecord.q === "string" ? String(argsRecord.q) : "",
1328
+ typeof argsRecord.keyword === "string" ? String(argsRecord.keyword) : "",
1329
+ typeof argsInput?.query === "string" ? String(argsInput.query) : "",
1330
+ typeof argsInput?.q === "string" ? String(argsInput.q) : "",
1331
+ ].find(item => item.trim());
1332
+ const query = queryCandidate ? queryCandidate.trim() : "";
1333
+ if (!query) {
238
1334
  return {
239
1335
  success: false,
240
1336
  error: "Invalid input provided. Missing 'query' parameter.",
241
1337
  };
242
1338
  }
1339
+ const topKRaw = [
1340
+ typeof args.top_k === "number" ? args.top_k : undefined,
1341
+ typeof argsRecord.top_k === "number" ? Number(argsRecord.top_k) : undefined,
1342
+ typeof argsRecord.topK === "number" ? Number(argsRecord.topK) : undefined,
1343
+ typeof argsInput?.top_k === "number" ? Number(argsInput.top_k) : undefined,
1344
+ typeof argsInput?.topK === "number" ? Number(argsInput.topK) : undefined,
1345
+ ].find(value => typeof value === "number" && Number.isFinite(value));
243
1346
  const result = await deps.readStore.searchMemory({
244
- query: args.query,
245
- topK: typeof args.top_k === "number" && args.top_k > 0 ? Math.floor(args.top_k) : 3,
1347
+ query,
1348
+ topK: typeof topKRaw === "number" && topKRaw > 0 ? Math.floor(topKRaw) : 3,
246
1349
  });
247
1350
  return { success: true, data: result.results };
248
1351
  }
@@ -252,10 +1355,19 @@ function createTsEngine(deps) {
252
1355
  return { success: true, data: result.context };
253
1356
  }
254
1357
  async function getAutoContext(args, context) {
255
- const sessionId = deps.resolveSessionId(context);
1358
+ const argsRecord = asRecord(args) || {};
1359
+ const argsInput = asRecord(argsRecord.input);
1360
+ const includeHotRaw = [
1361
+ typeof args.include_hot === "boolean" ? args.include_hot : undefined,
1362
+ typeof argsRecord.include_hot === "boolean" ? Boolean(argsRecord.include_hot) : undefined,
1363
+ typeof argsRecord.includeHot === "boolean" ? Boolean(argsRecord.includeHot) : undefined,
1364
+ typeof argsInput?.include_hot === "boolean" ? Boolean(argsInput.include_hot) : undefined,
1365
+ typeof argsInput?.includeHot === "boolean" ? Boolean(argsInput.includeHot) : undefined,
1366
+ ].find(value => typeof value === "boolean");
1367
+ const sessionId = deps.resolveSessionId((context || {}));
256
1368
  const cached = deps.getCachedAutoSearch(sessionId);
257
1369
  const result = await deps.readStore.getAutoContext({
258
- includeHot: args.include_hot !== false,
1370
+ includeHot: includeHotRaw !== false,
259
1371
  sessionId,
260
1372
  cachedAutoSearch: cached ?? undefined,
261
1373
  });
@@ -305,12 +1417,15 @@ function createTsEngine(deps) {
305
1417
  const sessionId = deps.resolveSessionId(context, payload);
306
1418
  const syncRecordsRaw = payloadObj?.sync_records;
307
1419
  const syncRecords = typeof syncRecordsRaw === "boolean" ? syncRecordsRaw : deps.defaultAutoSync;
1420
+ const bufferedMessages = sessionMessageBuffer.get(sessionId) || [];
308
1421
  try {
309
1422
  const result = await deps.sessionEnd.onSessionEnd({
310
1423
  sessionId,
311
1424
  syncRecords,
1425
+ messages: bufferedMessages,
312
1426
  });
313
1427
  deps.logger.info(`TS session_end completed for ${sessionId}, events=${result.events_generated}`);
1428
+ sessionMessageBuffer.delete(sessionId);
314
1429
  }
315
1430
  catch (error) {
316
1431
  deps.logger.warn(`TS session_end failed for ${sessionId}: ${error}`);
@@ -323,26 +1438,11 @@ function createTsEngine(deps) {
323
1438
  }
324
1439
  const { text, role, source } = normalized;
325
1440
  const sessionId = deps.resolveSessionId(context, payload);
326
- try {
327
- const writeResult = await deps.writeStore.writeMemory({
328
- text,
329
- role,
330
- source,
331
- sessionId,
332
- });
333
- if (writeResult.status === "ok") {
334
- deps.logger.info(`TS write stored ${role} message for session ${sessionId}`);
335
- }
336
- else {
337
- deps.logger.debug(`TS write skipped for session ${sessionId}: ${writeResult.reason || "unknown"}`);
338
- }
339
- }
340
- catch (error) {
341
- deps.logger.warn(`TS write failed for session ${sessionId}: ${error}`);
342
- }
1441
+ pushSessionMessage(sessionId, { role, text });
1442
+ deps.logger.debug(`TS buffered ${role} message for session ${sessionId} source=${source}`);
343
1443
  if (role === "user" && text.length > 5) {
344
1444
  try {
345
- const searchResult = await deps.readStore.searchMemory({ query: text, topK: 3 });
1445
+ const searchResult = await deps.readStore.searchMemory({ query: text, topK: 3, mode: "lightweight" });
346
1446
  if (searchResult.results.length > 0) {
347
1447
  deps.setSessionAutoSearchCache(sessionId, text, searchResult.results);
348
1448
  deps.logger.info(`TS auto-search cached ${searchResult.results.length} results for context`);
@@ -381,6 +1481,7 @@ function createTsEngine(deps) {
381
1481
  deleteMemory,
382
1482
  updateMemory,
383
1483
  cleanupMemories,
1484
+ backfillEmbeddings,
384
1485
  runDiagnostics,
385
1486
  onMessage,
386
1487
  onSessionEnd,