openclaw-cortex-memory 0.1.0-Alpha.9 → 0.1.0

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 (97) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +347 -290
  3. package/SIGNATURE.md +7 -0
  4. package/SKILL.md +96 -345
  5. package/dist/index.d.ts +69 -23
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +1130 -1330
  8. package/dist/index.js.map +1 -1
  9. package/dist/openclaw.plugin.json +377 -18
  10. package/dist/src/dedup/three_stage_deduplicator.d.ts.map +1 -1
  11. package/dist/src/dedup/three_stage_deduplicator.js +13 -3
  12. package/dist/src/dedup/three_stage_deduplicator.js.map +1 -1
  13. package/dist/src/engine/memory_engine.d.ts +5 -1
  14. package/dist/src/engine/memory_engine.d.ts.map +1 -1
  15. package/dist/src/engine/ts_engine.d.ts +149 -0
  16. package/dist/src/engine/ts_engine.d.ts.map +1 -1
  17. package/dist/src/engine/ts_engine.js +863 -203
  18. package/dist/src/engine/ts_engine.js.map +1 -1
  19. package/dist/src/engine/types.d.ts +20 -0
  20. package/dist/src/engine/types.d.ts.map +1 -1
  21. package/dist/src/graph/ontology.d.ts +87 -15
  22. package/dist/src/graph/ontology.d.ts.map +1 -1
  23. package/dist/src/graph/ontology.js +999 -12
  24. package/dist/src/graph/ontology.js.map +1 -1
  25. package/dist/src/net/http_post.d.ts +17 -0
  26. package/dist/src/net/http_post.d.ts.map +1 -0
  27. package/dist/src/net/http_post.js +56 -0
  28. package/dist/src/net/http_post.js.map +1 -0
  29. package/dist/src/quality/llm_output_validator.d.ts +65 -0
  30. package/dist/src/quality/llm_output_validator.d.ts.map +1 -0
  31. package/dist/src/quality/llm_output_validator.js +635 -0
  32. package/dist/src/quality/llm_output_validator.js.map +1 -0
  33. package/dist/src/reflect/reflector.d.ts.map +1 -1
  34. package/dist/src/reflect/reflector.js +296 -26
  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 +20 -42
  40. package/dist/src/session/session_end.d.ts.map +1 -1
  41. package/dist/src/session/session_end.js +21 -218
  42. package/dist/src/session/session_end.js.map +1 -1
  43. package/dist/src/store/archive_store.d.ts +28 -7
  44. package/dist/src/store/archive_store.d.ts.map +1 -1
  45. package/dist/src/store/archive_store.js +367 -130
  46. package/dist/src/store/archive_store.js.map +1 -1
  47. package/dist/src/store/graph_memory_store.d.ts +115 -0
  48. package/dist/src/store/graph_memory_store.d.ts.map +1 -0
  49. package/dist/src/store/graph_memory_store.js +1061 -0
  50. package/dist/src/store/graph_memory_store.js.map +1 -0
  51. package/dist/src/store/read_store.d.ts +75 -0
  52. package/dist/src/store/read_store.d.ts.map +1 -1
  53. package/dist/src/store/read_store.js +1837 -312
  54. package/dist/src/store/read_store.js.map +1 -1
  55. package/dist/src/store/vector_store.d.ts +2 -0
  56. package/dist/src/store/vector_store.d.ts.map +1 -1
  57. package/dist/src/store/vector_store.js +19 -3
  58. package/dist/src/store/vector_store.js.map +1 -1
  59. package/dist/src/store/write_store.d.ts +11 -0
  60. package/dist/src/store/write_store.d.ts.map +1 -1
  61. package/dist/src/store/write_store.js +242 -42
  62. package/dist/src/store/write_store.js.map +1 -1
  63. package/dist/src/sync/session_sync.d.ts +72 -1
  64. package/dist/src/sync/session_sync.d.ts.map +1 -1
  65. package/dist/src/sync/session_sync.js +2246 -126
  66. package/dist/src/sync/session_sync.js.map +1 -1
  67. package/dist/src/wiki/wiki_linter.d.ts +26 -0
  68. package/dist/src/wiki/wiki_linter.d.ts.map +1 -0
  69. package/dist/src/wiki/wiki_linter.js +339 -0
  70. package/dist/src/wiki/wiki_linter.js.map +1 -0
  71. package/dist/src/wiki/wiki_logger.d.ts +10 -0
  72. package/dist/src/wiki/wiki_logger.d.ts.map +1 -0
  73. package/dist/src/wiki/wiki_logger.js +78 -0
  74. package/dist/src/wiki/wiki_logger.js.map +1 -0
  75. package/dist/src/wiki/wiki_maintainer.d.ts +39 -0
  76. package/dist/src/wiki/wiki_maintainer.d.ts.map +1 -0
  77. package/dist/src/wiki/wiki_maintainer.js +38 -0
  78. package/dist/src/wiki/wiki_maintainer.js.map +1 -0
  79. package/dist/src/wiki/wiki_projector.d.ts +35 -0
  80. package/dist/src/wiki/wiki_projector.d.ts.map +1 -0
  81. package/dist/src/wiki/wiki_projector.js +1151 -0
  82. package/dist/src/wiki/wiki_projector.js.map +1 -0
  83. package/dist/src/wiki/wiki_queue.d.ts +29 -0
  84. package/dist/src/wiki/wiki_queue.d.ts.map +1 -0
  85. package/dist/src/wiki/wiki_queue.js +137 -0
  86. package/dist/src/wiki/wiki_queue.js.map +1 -0
  87. package/openclaw.plugin.json +377 -18
  88. package/package.json +52 -6
  89. package/schema/graph.schema.yaml +330 -0
  90. package/scripts/cli.js +67 -13
  91. package/scripts/repair-memory.js +321 -0
  92. package/skills/cortex-memory/SKILL.md +83 -0
  93. package/skills/cortex-memory/references/agent-manual.md +127 -0
  94. package/skills/cortex-memory/references/configuration.md +109 -0
  95. package/skills/cortex-memory/references/publish-checklist.md +45 -0
  96. package/skills/cortex-memory/references/system-prompt-template.md +27 -0
  97. package/skills/cortex-memory/references/tools.md +191 -0
@@ -37,10 +37,14 @@ exports.createTsEngine = createTsEngine;
37
37
  const fs = __importStar(require("fs"));
38
38
  const path = __importStar(require("path"));
39
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 wiki_projector_1 = require("../wiki/wiki_projector");
43
+ const wiki_linter_1 = require("../wiki/wiki_linter");
40
44
  const PROMPT_VERSIONS = {
41
- write_gate: "write-gate.v1.1.0",
42
- session_end_write: "session-end-write.v1.1.0",
43
- read_fusion: "read-fusion.v1.1.0",
45
+ write_gate: "write-gate.v1.7.9",
46
+ session_end_write: "session-end-write.v1.2.0",
47
+ read_fusion: "read-fusion.v1.2.0",
44
48
  };
45
49
  function createTsEngine(deps) {
46
50
  const graphSchema = (0, ontology_1.loadGraphSchema)(deps.projectRoot);
@@ -69,6 +73,27 @@ function createTsEngine(deps) {
69
73
  }
70
74
  }
71
75
  }
76
+ function getRecentSessionMessages(sessionId, limit) {
77
+ const messages = sessionMessageBuffer.get(sessionId) || [];
78
+ return messages.slice(Math.max(0, messages.length - Math.max(1, limit)));
79
+ }
80
+ function isHistoricalMemoryQuery(text) {
81
+ return /(上次|之前|以前|你记得|记得|历史|上个月|去年|上个星期|昨天|前天|查一下|回忆|记忆|偏好|项目上下文|既有决策|决策|决定|修复|方案|last time|previous|previously|before|remember|history|prior|preference|decision|fix|what did we)/i.test(text);
82
+ }
83
+ function buildAutoSearchQuery(sessionId, latestUserText, historical) {
84
+ if (!historical) {
85
+ return latestUserText.trim();
86
+ }
87
+ const recent = getRecentSessionMessages(sessionId, 8)
88
+ .map(message => {
89
+ const role = typeof message.role === "string" && message.role.trim() ? message.role.trim() : "message";
90
+ const content = typeof message.content === "string" ? message.content.trim().replace(/\s+/g, " ") : "";
91
+ return content ? `${role}: ${content}` : "";
92
+ })
93
+ .filter(Boolean)
94
+ .join("\n");
95
+ return (recent || latestUserText).slice(-240).trim();
96
+ }
72
97
  function asRecord(value) {
73
98
  if (typeof value === "object" && value !== null) {
74
99
  return value;
@@ -153,44 +178,25 @@ function createTsEngine(deps) {
153
178
  }
154
179
  function buildVectorSourceText(record, layer) {
155
180
  if (layer === "active") {
156
- const content = typeof record.content === "string" && record.content.trim()
157
- ? record.content.trim()
158
- : (typeof record.text === "string" ? record.text.trim() : "");
159
- return content;
181
+ const sourceText = typeof record.source_text === "string" && record.source_text.trim()
182
+ ? record.source_text.trim()
183
+ : "";
184
+ if (sourceText) {
185
+ return sourceText;
186
+ }
187
+ const summary = typeof record.summary === "string" ? record.summary.trim() : "";
188
+ if (summary) {
189
+ return summary;
190
+ }
191
+ const text = typeof record.text === "string" ? record.text.trim() : "";
192
+ return text;
193
+ }
194
+ const sourceText = typeof record.source_text === "string" ? record.source_text.trim() : "";
195
+ if (sourceText) {
196
+ return sourceText;
160
197
  }
161
198
  const summary = typeof record.summary === "string" ? record.summary.trim() : "";
162
- const eventType = typeof record.event_type === "string" ? record.event_type.trim() : "insight";
163
- const outcome = typeof record.outcome === "string" ? record.outcome.trim() : "";
164
- const sourceFile = typeof record.source_file === "string" ? record.source_file.trim() : "";
165
- const actor = typeof record.actor === "string" ? record.actor.trim() : "";
166
- const entities = Array.isArray(record.entities)
167
- ? record.entities.filter(v => typeof v === "string").map(v => String(v).trim()).filter(Boolean)
168
- : [];
169
- const relations = Array.isArray(record.relations)
170
- ? record.relations
171
- .map(v => {
172
- if (!v || typeof v !== "object")
173
- return "";
174
- const relation = v;
175
- const source = typeof relation.source === "string" ? relation.source.trim() : "";
176
- const target = typeof relation.target === "string" ? relation.target.trim() : "";
177
- const type = typeof relation.type === "string" ? relation.type.trim() : "related_to";
178
- if (!source || !target)
179
- return "";
180
- return `${source} -[${type}]-> ${target}`;
181
- })
182
- .filter(Boolean)
183
- : [];
184
- const lines = [
185
- `event_type: ${eventType}`,
186
- `summary: ${summary}`,
187
- `outcome: ${outcome}`,
188
- `entities: ${entities.join(", ")}`,
189
- `source_file: ${sourceFile}`,
190
- `actor: ${actor}`,
191
- relations.length > 0 ? `relations: ${relations.join(" ; ")}` : "",
192
- ].filter(Boolean);
193
- return lines.join("\n").trim();
199
+ return summary;
194
200
  }
195
201
  function splitTextChunks(text, chunkSize, chunkOverlap) {
196
202
  const normalizedSize = Number.isFinite(chunkSize) && chunkSize >= 200 ? Math.floor(chunkSize) : 600;
@@ -243,6 +249,32 @@ function createTsEngine(deps) {
243
249
  }
244
250
  return output;
245
251
  }
252
+ function pickEvidenceChunks(chunks, maxCount) {
253
+ if (!chunks.length || maxCount <= 0)
254
+ return [];
255
+ if (chunks.length <= maxCount)
256
+ return chunks;
257
+ const picked = new Map();
258
+ picked.set(chunks[0].index, chunks[0]);
259
+ if (maxCount >= 2) {
260
+ const mid = chunks[Math.floor(chunks.length / 2)];
261
+ picked.set(mid.index, mid);
262
+ }
263
+ if (maxCount >= 3) {
264
+ const last = chunks[chunks.length - 1];
265
+ picked.set(last.index, last);
266
+ }
267
+ if (picked.size < maxCount) {
268
+ for (const chunk of chunks) {
269
+ if (!picked.has(chunk.index)) {
270
+ picked.set(chunk.index, chunk);
271
+ }
272
+ if (picked.size >= maxCount)
273
+ break;
274
+ }
275
+ }
276
+ return [...picked.values()].sort((a, b) => a.index - b.index).slice(0, maxCount);
277
+ }
246
278
  function upsertJsonFile(filePath, patch) {
247
279
  const current = parseJsonFile(filePath) || {};
248
280
  const next = { ...current, ...patch };
@@ -253,9 +285,10 @@ function createTsEngine(deps) {
253
285
  fs.writeFileSync(filePath, JSON.stringify(next, null, 2), "utf-8");
254
286
  }
255
287
  async function probeModelConnection(args) {
288
+ const defaultTimeoutMs = args.kind === "llm" ? 30000 : 15000;
256
289
  const timeoutMs = typeof args.timeoutMs === "number" && Number.isFinite(args.timeoutMs) && args.timeoutMs >= 1000
257
290
  ? Math.floor(args.timeoutMs)
258
- : 8000;
291
+ : defaultTimeoutMs;
259
292
  if (!args.model || !args.apiKey || !args.baseUrl) {
260
293
  return {
261
294
  configured: false,
@@ -265,73 +298,71 @@ function createTsEngine(deps) {
265
298
  error: "not_configured",
266
299
  };
267
300
  }
268
- const controller = new AbortController();
269
- const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
270
- try {
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: 1,
286
- temperature: 0,
287
- };
288
- }
289
- else {
290
- endpoint = args.baseUrl.endsWith("/rerank") ? args.baseUrl : `${args.baseUrl}/rerank`;
291
- payload = {
292
- model: args.model,
293
- query: "diagnostics",
294
- documents: ["diagnostics connectivity probe"],
295
- top_n: 1,
296
- };
297
- }
298
- const response = await fetch(endpoint, {
299
- method: "POST",
300
- headers: {
301
- "content-type": "application/json",
302
- authorization: `Bearer ${args.apiKey}`,
303
- },
304
- body: JSON.stringify(payload),
305
- signal: controller.signal,
301
+ let endpoint = args.baseUrl;
302
+ let payload = {};
303
+ if (args.kind === "embedding") {
304
+ endpoint = args.baseUrl.endsWith("/embeddings") ? args.baseUrl : `${args.baseUrl}/embeddings`;
305
+ payload = {
306
+ model: args.model,
307
+ input: "diagnostics connectivity probe",
308
+ };
309
+ }
310
+ else if (args.kind === "llm") {
311
+ endpoint = args.baseUrl.endsWith("/chat/completions") ? args.baseUrl : `${args.baseUrl}/chat/completions`;
312
+ payload = {
313
+ model: args.model,
314
+ messages: [{ role: "user", content: "ping" }],
315
+ max_tokens: 4,
316
+ temperature: 0,
317
+ stream: false,
318
+ };
319
+ }
320
+ else {
321
+ endpoint = args.baseUrl.endsWith("/rerank") ? args.baseUrl : `${args.baseUrl}/rerank`;
322
+ payload = {
323
+ model: args.model,
324
+ query: "diagnostics",
325
+ documents: ["diagnostics connectivity probe"],
326
+ top_n: 1,
327
+ };
328
+ }
329
+ let lastError = "unknown_error";
330
+ const maxAttempts = args.kind === "llm" ? 3 : 1;
331
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
332
+ const response = await (0, http_post_1.postJsonWithTimeout)({
333
+ endpoint,
334
+ apiKey: args.apiKey,
335
+ body: payload,
336
+ timeoutMs,
337
+ headers: { accept: "application/json" },
306
338
  });
307
- clearTimeout(timeoutId);
308
- if (!response.ok) {
339
+ if (response.ok) {
309
340
  return {
310
341
  configured: true,
311
- connected: false,
342
+ connected: true,
312
343
  model: args.model,
313
344
  base_url: args.baseUrl,
314
- error: `http_${response.status}`,
345
+ error: "",
315
346
  };
316
347
  }
317
- return {
318
- configured: true,
319
- connected: true,
320
- model: args.model,
321
- base_url: args.baseUrl,
322
- error: "",
323
- };
324
- }
325
- catch (error) {
326
- clearTimeout(timeoutId);
327
- return {
328
- configured: true,
329
- connected: false,
330
- model: args.model,
331
- base_url: args.baseUrl,
332
- error: error instanceof Error ? error.message : String(error),
333
- };
348
+ if (response.aborted) {
349
+ lastError = `timeout_${timeoutMs}ms`;
350
+ }
351
+ else if (response.status > 0) {
352
+ const details = (response.text || "").trim().slice(0, 180);
353
+ lastError = details ? `http_${response.status}:${details}` : `http_${response.status}`;
354
+ }
355
+ else {
356
+ lastError = response.error || "network_error";
357
+ }
334
358
  }
359
+ return {
360
+ configured: true,
361
+ connected: false,
362
+ model: args.model,
363
+ base_url: args.baseUrl,
364
+ error: lastError,
365
+ };
335
366
  }
336
367
  async function requestEmbedding(args) {
337
368
  const endpoint = args.baseUrl.endsWith("/embeddings") ? args.baseUrl : `${args.baseUrl}/embeddings`;
@@ -350,24 +381,18 @@ function createTsEngine(deps) {
350
381
  : 4;
351
382
  let lastError = null;
352
383
  for (let attempt = 0; attempt < maxRetries; attempt += 1) {
353
- const controller = new AbortController();
354
- const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
384
+ const response = await (0, http_post_1.postJsonWithTimeout)({
385
+ endpoint,
386
+ apiKey: args.apiKey,
387
+ body,
388
+ timeoutMs,
389
+ });
390
+ if (!response.ok) {
391
+ lastError = new Error(response.status > 0 ? `embedding_http_${response.status}` : (response.error || "embedding_network_error"));
392
+ continue;
393
+ }
355
394
  try {
356
- const response = await fetch(endpoint, {
357
- method: "POST",
358
- headers: {
359
- "content-type": "application/json",
360
- authorization: `Bearer ${args.apiKey}`,
361
- },
362
- body: JSON.stringify(body),
363
- signal: controller.signal,
364
- });
365
- clearTimeout(timeoutId);
366
- if (!response.ok) {
367
- lastError = new Error(`embedding_http_${response.status}`);
368
- continue;
369
- }
370
- const json = await response.json();
395
+ const json = (response.json || {});
371
396
  const embedding = json?.data?.[0]?.embedding;
372
397
  if (Array.isArray(embedding) && embedding.length > 0) {
373
398
  return embedding.filter(item => Number.isFinite(item));
@@ -375,7 +400,6 @@ function createTsEngine(deps) {
375
400
  lastError = new Error("embedding_empty");
376
401
  }
377
402
  catch (error) {
378
- clearTimeout(timeoutId);
379
403
  lastError = error;
380
404
  }
381
405
  if (attempt < maxRetries - 1) {
@@ -417,6 +441,22 @@ function createTsEngine(deps) {
417
441
  return "";
418
442
  }).filter(Boolean)
419
443
  : [];
444
+ const entityTypesFromEntities = {};
445
+ if (Array.isArray(entityInput)) {
446
+ for (const item of entityInput) {
447
+ if (!item || typeof item !== "object") {
448
+ continue;
449
+ }
450
+ const entityObj = item;
451
+ const entityName = typeof entityObj.name === "string" && entityObj.name.trim()
452
+ ? entityObj.name.trim()
453
+ : (typeof entityObj.id === "string" ? entityObj.id.trim() : "");
454
+ const entityType = typeof entityObj.type === "string" ? entityObj.type.trim() : "";
455
+ if (entityName && entityType) {
456
+ entityTypesFromEntities[entityName] = entityType;
457
+ }
458
+ }
459
+ }
420
460
  const relationInput = Array.isArray(rawArgs.relations)
421
461
  ? rawArgs.relations
422
462
  : Array.isArray(rawArgs.input?.relations)
@@ -445,6 +485,8 @@ function createTsEngine(deps) {
445
485
  source: relation.source.trim(),
446
486
  target: relation.target.trim(),
447
487
  type: (0, ontology_1.normalizeRelationType)(relation.type || "related_to", graphSchema),
488
+ evidence_span: typeof relation.evidence_span === "string" ? relation.evidence_span.trim() : undefined,
489
+ confidence: typeof relation.confidence === "number" ? Math.max(0, Math.min(1, relation.confidence)) : undefined,
448
490
  };
449
491
  })
450
492
  .filter((item) => Boolean(item))
@@ -456,24 +498,98 @@ function createTsEngine(deps) {
456
498
  : typeof rawArgs.event?.outcome === "string"
457
499
  ? String(rawArgs.event.outcome)
458
500
  : "";
501
+ const causeValue = typeof rawArgs.cause === "string"
502
+ ? rawArgs.cause
503
+ : typeof rawArgs.input?.cause === "string"
504
+ ? String(rawArgs.input.cause)
505
+ : typeof rawArgs.event?.cause === "string"
506
+ ? String(rawArgs.event.cause)
507
+ : "";
508
+ const processValue = typeof rawArgs.process === "string"
509
+ ? rawArgs.process
510
+ : typeof rawArgs.input?.process === "string"
511
+ ? String(rawArgs.input.process)
512
+ : typeof rawArgs.event?.process === "string"
513
+ ? String(rawArgs.event.process)
514
+ : "";
515
+ const resultValue = typeof rawArgs.result === "string"
516
+ ? rawArgs.result
517
+ : typeof rawArgs.input?.result === "string"
518
+ ? String(rawArgs.input.result)
519
+ : typeof rawArgs.event?.result === "string"
520
+ ? String(rawArgs.event.result)
521
+ : "";
522
+ const entityTypesInput = typeof rawArgs.entity_types === "object" && rawArgs.entity_types !== null
523
+ ? rawArgs.entity_types
524
+ : typeof rawArgs.input?.entity_types === "object"
525
+ ? rawArgs.input.entity_types
526
+ : typeof rawArgs.event?.entity_types === "object"
527
+ ? rawArgs.event.entity_types
528
+ : {};
529
+ const entityTypes = {};
530
+ for (const [key, value] of Object.entries(entityTypesInput)) {
531
+ if (typeof value === "string") {
532
+ entityTypes[key.trim()] = value.trim();
533
+ }
534
+ }
535
+ for (const [key, value] of Object.entries(entityTypesFromEntities)) {
536
+ if (!entityTypes[key] && value) {
537
+ entityTypes[key] = value;
538
+ }
539
+ }
459
540
  const result = await deps.archiveStore.storeEvents([
460
541
  {
461
542
  event_type: "manual_event",
462
543
  summary: normalizedSummary,
544
+ cause: causeValue.trim() || normalizedSummary,
545
+ process: processValue.trim() || normalizedSummary,
546
+ result: resultValue.trim() || outcomeValue.trim() || normalizedSummary,
463
547
  entities,
464
548
  relations,
549
+ entity_types: entityTypes,
465
550
  outcome: outcomeValue,
466
551
  session_id: "manual",
467
552
  source_file: "ts_store_event",
468
553
  confidence: 1,
469
- source_event_id: "",
554
+ source_event_id: `manual:${Date.now().toString(36)}`,
470
555
  actor: "manual_tool",
471
556
  },
472
557
  ]);
473
558
  if (result.stored.length === 0) {
474
559
  return { success: false, error: result.skipped[0]?.reason || "store_event_skipped" };
475
560
  }
476
- return { success: true, data: { event_id: result.stored[0].id } };
561
+ const storedId = result.stored[0].id;
562
+ if (deps.graphMemoryStore && entities.length > 0 && Object.keys(entityTypes).length > 0 && relations.length > 0) {
563
+ const graphSummary = normalizedSummary.toLowerCase().includes("entities:")
564
+ ? normalizedSummary
565
+ : `${normalizedSummary} Entities: ${entities.join(", ")}.`;
566
+ const graphResult = await deps.graphMemoryStore.append({
567
+ sourceEventId: storedId,
568
+ sourceLayer: "archive_event",
569
+ archiveEventId: storedId,
570
+ sessionId: "manual",
571
+ sourceFile: "ts_store_event",
572
+ summary: graphSummary,
573
+ source_text_nav: {
574
+ layer: "archive_event",
575
+ session_id: "manual",
576
+ source_file: "ts_store_event",
577
+ source_memory_id: storedId,
578
+ source_event_id: storedId,
579
+ },
580
+ eventType: "manual_event",
581
+ entities,
582
+ entity_types: entityTypes,
583
+ relations,
584
+ gateSource: "manual",
585
+ confidence: 1,
586
+ sourceText: normalizedSummary,
587
+ });
588
+ if (!graphResult.success) {
589
+ deps.logger.info(`store_event graph_skip_reason=${graphResult.reason} source_event_id=${storedId}`);
590
+ }
591
+ }
592
+ return { success: true, data: { event_id: storedId } };
477
593
  }
478
594
  catch (error) {
479
595
  return { success: false, error: String(error) };
@@ -492,32 +608,20 @@ function createTsEngine(deps) {
492
608
  : "both";
493
609
  const pathTo = typeof args.path_to === "string" && args.path_to.trim() ? args.path_to.trim() : "";
494
610
  const maxDepth = Math.max(2, Math.min(4, typeof args.max_depth === "number" ? Math.floor(args.max_depth) : 3));
495
- const { archivePath } = memoryFiles();
496
- const records = readJsonl(archivePath);
611
+ const graphMemoryPath = path.join(deps.memoryRoot, "graph", "memory.jsonl");
612
+ const projectionIndexPath = path.join(deps.memoryRoot, "wiki", ".projection_index.json");
497
613
  const nodes = new Map();
498
614
  const edges = [];
499
- const adjacency = new Map();
500
615
  const pathAdjacency = new Map();
501
616
  const relationTypeDistribution = new Map();
502
617
  const edgeKeySet = new Set();
503
- function pushEdge(source, target, type) {
504
- const key = `${source}|${type}|${target}`;
505
- if (edgeKeySet.has(key)) {
506
- return;
507
- }
508
- edgeKeySet.add(key);
509
- edges.push({ source, target, type });
510
- relationTypeDistribution.set(type, (relationTypeDistribution.get(type) || 0) + 1);
511
- if (!adjacency.has(source)) {
512
- adjacency.set(source, []);
513
- }
514
- adjacency.get(source)?.push({ next: target, edge: { source, target, type } });
515
- if (!adjacency.has(target)) {
516
- adjacency.set(target, []);
517
- }
518
- adjacency.get(target)?.push({ next: source, edge: { source, target, type } });
618
+ const allEdges = [];
619
+ function relationKeyOf(source, type, target) {
620
+ return `${source.trim().toLowerCase()}|${type.trim().toLowerCase()}|${target.trim().toLowerCase()}`;
519
621
  }
520
- function pushPathEdge(source, target, type) {
622
+ function pushPathEdge(edge) {
623
+ const source = edge.source;
624
+ const target = edge.target;
521
625
  if (!pathAdjacency.has(source)) {
522
626
  pathAdjacency.set(source, []);
523
627
  }
@@ -525,71 +629,173 @@ function createTsEngine(deps) {
525
629
  pathAdjacency.set(target, []);
526
630
  }
527
631
  if (direction === "incoming") {
528
- pathAdjacency.get(target)?.push({ next: source, edge: { source, target, type } });
632
+ pathAdjacency.get(target)?.push({ next: source, edge });
529
633
  }
530
634
  else if (direction === "outgoing") {
531
- pathAdjacency.get(source)?.push({ next: target, edge: { source, target, type } });
635
+ pathAdjacency.get(source)?.push({ next: target, edge });
532
636
  }
533
637
  else {
534
- pathAdjacency.get(source)?.push({ next: target, edge: { source, target, type } });
535
- pathAdjacency.get(target)?.push({ next: source, edge: { source, target, type } });
638
+ pathAdjacency.get(source)?.push({ next: target, edge });
639
+ pathAdjacency.get(target)?.push({ next: source, edge });
536
640
  }
537
641
  }
538
- for (const record of records) {
539
- const entities = Array.isArray(record.entities) ? record.entities : [];
540
- const named = entities.map(e => (typeof e === "string" ? e.trim() : "")).filter(Boolean);
541
- const relations = Array.isArray(record.relations) ? record.relations : [];
542
- let explicitMatched = false;
543
- for (const relationRaw of relations) {
544
- if (typeof relationRaw !== "object" || relationRaw === null) {
545
- continue;
546
- }
547
- const relation = relationRaw;
548
- const source = typeof relation.source === "string" ? relation.source.trim() : "";
549
- const target = typeof relation.target === "string" ? relation.target.trim() : "";
550
- const type = (0, ontology_1.normalizeRelationType)(typeof relation.type === "string" && relation.type.trim() ? relation.type.trim() : "related_to", graphSchema);
551
- if (!source || !target) {
552
- continue;
553
- }
554
- if (relFilter && type !== relFilter) {
642
+ function normalizeStatus(value) {
643
+ const token = (value || "").trim().toLowerCase();
644
+ if (token === "pending" || token === "pending_conflict")
645
+ return "pending_conflict";
646
+ if (token === "superseded")
647
+ return "superseded";
648
+ if (token === "rejected")
649
+ return "rejected";
650
+ return "active";
651
+ }
652
+ const viewData = deps.graphMemoryStore?.exportGraphView();
653
+ if (viewData && Array.isArray(viewData.edges)) {
654
+ for (const edge of viewData.edges) {
655
+ const source = typeof edge.source === "string" ? edge.source.trim() : "";
656
+ const target = typeof edge.target === "string" ? edge.target.trim() : "";
657
+ const type = (0, ontology_1.normalizeRelationType)(typeof edge.type === "string" ? edge.type : "related_to", graphSchema);
658
+ if (!source || !target)
555
659
  continue;
660
+ const relationKey = typeof edge.relation_key === "string" && edge.relation_key.trim()
661
+ ? edge.relation_key.trim().toLowerCase()
662
+ : relationKeyOf(source, type, target);
663
+ allEdges.push({
664
+ source,
665
+ target,
666
+ type,
667
+ fact_status: normalizeStatus(typeof edge.status === "string" ? edge.status : "active"),
668
+ relation_key: relationKey,
669
+ source_event_id: typeof edge.source_event_id === "string" ? edge.source_event_id : undefined,
670
+ evidence_span: typeof edge.evidence_span === "string" ? edge.evidence_span : undefined,
671
+ confidence: typeof edge.confidence === "number" ? edge.confidence : undefined,
672
+ conflict_id: typeof edge.conflict_id === "string" ? edge.conflict_id : undefined,
673
+ evidence_ids: [],
674
+ });
675
+ }
676
+ }
677
+ if (allEdges.length === 0) {
678
+ const graphRecords = deps.graphMemoryStore
679
+ ? deps.graphMemoryStore.loadAll()
680
+ : (fs.existsSync(graphMemoryPath) ? readJsonl(graphMemoryPath) : []);
681
+ for (const record of graphRecords) {
682
+ const sourceEventId = typeof record.source_event_id === "string" ? record.source_event_id.trim() : "";
683
+ const relations = Array.isArray(record.relations) ? record.relations : [];
684
+ for (const relationRaw of relations) {
685
+ if (typeof relationRaw !== "object" || relationRaw === null)
686
+ continue;
687
+ const relation = relationRaw;
688
+ const source = typeof relation.source === "string" ? relation.source.trim() : "";
689
+ const target = typeof relation.target === "string" ? relation.target.trim() : "";
690
+ const type = (0, ontology_1.normalizeRelationType)(typeof relation.type === "string" && relation.type.trim() ? relation.type.trim() : "related_to", graphSchema);
691
+ if (!source || !target)
692
+ continue;
693
+ allEdges.push({
694
+ source,
695
+ target,
696
+ type,
697
+ fact_status: "active",
698
+ relation_key: relationKeyOf(source, type, target),
699
+ source_event_id: sourceEventId || undefined,
700
+ evidence_span: typeof relation.evidence_span === "string" ? relation.evidence_span.trim() : undefined,
701
+ confidence: typeof relation.confidence === "number" ? relation.confidence : undefined,
702
+ evidence_ids: [],
703
+ });
556
704
  }
557
- pushPathEdge(source, target, type);
558
- const outgoingMatch = source === entity;
559
- const incomingMatch = target === entity;
560
- const directionMatched = direction === "both" ? (outgoingMatch || incomingMatch)
561
- : direction === "outgoing" ? outgoingMatch
562
- : incomingMatch;
563
- if (!directionMatched) {
564
- continue;
705
+ }
706
+ }
707
+ const projectionIndex = parseJsonFile(projectionIndexPath);
708
+ const entityWikiPathByName = new Map();
709
+ const topicWikiPathByType = new Map();
710
+ const projectionEntities = Array.isArray(projectionIndex?.entities) ? projectionIndex.entities : [];
711
+ const projectionTopics = Array.isArray(projectionIndex?.topics) ? projectionIndex.topics : [];
712
+ for (const item of projectionEntities) {
713
+ if (!item || typeof item !== "object")
714
+ continue;
715
+ const row = item;
716
+ const name = typeof row.name === "string" ? row.name.trim() : "";
717
+ const wikiPath = typeof row.path === "string" ? row.path.trim() : "";
718
+ if (!name || !wikiPath)
719
+ continue;
720
+ entityWikiPathByName.set(name.toLowerCase(), wikiPath);
721
+ }
722
+ for (const item of projectionTopics) {
723
+ if (!item || typeof item !== "object")
724
+ continue;
725
+ const row = item;
726
+ const topic = typeof row.type === "string" ? row.type.trim() : "";
727
+ const wikiPath = typeof row.path === "string" ? row.path.trim() : "";
728
+ if (!topic || !wikiPath)
729
+ continue;
730
+ topicWikiPathByType.set(topic.toLowerCase(), wikiPath);
731
+ }
732
+ function statusAnchor(status) {
733
+ if (status === "active")
734
+ return "current-facts";
735
+ if (status === "pending_conflict")
736
+ return "disputed-facts";
737
+ return "history";
738
+ }
739
+ function buildEvidenceIdsForEdge(edge) {
740
+ const ids = [
741
+ `graph:relation:${edge.relation_key}`,
742
+ edge.source_event_id ? `graph:event:${edge.source_event_id}` : "",
743
+ edge.evidence_span ? `graph:evidence:${edge.source_event_id || edge.relation_key}` : "",
744
+ edge.conflict_id ? `graph:conflict:${edge.conflict_id}` : "",
745
+ ];
746
+ const anchor = statusAnchor(edge.fact_status);
747
+ for (const name of [edge.source, edge.target]) {
748
+ const wikiPath = entityWikiPathByName.get(name.toLowerCase());
749
+ if (wikiPath) {
750
+ ids.push(`wiki:${wikiPath}#${anchor}`);
565
751
  }
566
- explicitMatched = true;
567
- if (!nodes.has(source))
568
- nodes.set(source, { id: source, type: "entity" });
569
- if (!nodes.has(target))
570
- nodes.set(target, { id: target, type: "entity" });
571
- pushEdge(source, target, type);
572
752
  }
573
- if (explicitMatched) {
753
+ const topicPath = topicWikiPathByType.get(edge.type.toLowerCase());
754
+ if (topicPath) {
755
+ ids.push(`wiki:${topicPath}#relations`);
756
+ }
757
+ return [...new Set(ids.filter(Boolean))];
758
+ }
759
+ let explicitMatched = false;
760
+ for (const edge of allEdges) {
761
+ if (relFilter && edge.type !== relFilter) {
574
762
  continue;
575
763
  }
576
- if (!named.includes(entity)) {
764
+ const pathEligible = edge.fact_status === "active" || edge.fact_status === "pending_conflict";
765
+ if (pathEligible) {
766
+ pushPathEdge(edge);
767
+ }
768
+ const outgoingMatch = edge.source === entity;
769
+ const incomingMatch = edge.target === entity;
770
+ const directionMatched = direction === "both" ? (outgoingMatch || incomingMatch)
771
+ : direction === "outgoing" ? outgoingMatch
772
+ : incomingMatch;
773
+ if (!directionMatched) {
577
774
  continue;
578
775
  }
579
- for (const name of named) {
580
- if (!nodes.has(name)) {
581
- nodes.set(name, { id: name, type: "entity" });
582
- }
776
+ explicitMatched = true;
777
+ const edgeKey = `${edge.relation_key}|${edge.fact_status}|${edge.conflict_id || ""}`;
778
+ if (edgeKeySet.has(edgeKey)) {
779
+ continue;
583
780
  }
584
- for (const name of named) {
585
- if (name !== entity) {
586
- if (!relFilter || relFilter === "co_occurrence") {
587
- pushEdge(entity, name, "co_occurrence");
588
- }
589
- }
781
+ edgeKeySet.add(edgeKey);
782
+ const enrichedEdge = {
783
+ ...edge,
784
+ evidence_ids: buildEvidenceIdsForEdge(edge),
785
+ };
786
+ edges.push(enrichedEdge);
787
+ relationTypeDistribution.set(enrichedEdge.type, (relationTypeDistribution.get(enrichedEdge.type) || 0) + 1);
788
+ if (!nodes.has(enrichedEdge.source))
789
+ nodes.set(enrichedEdge.source, { id: enrichedEdge.source, type: "entity" });
790
+ if (!nodes.has(enrichedEdge.target))
791
+ nodes.set(enrichedEdge.target, { id: enrichedEdge.target, type: "entity" });
792
+ }
793
+ if (!explicitMatched) {
794
+ if (!nodes.has(entity)) {
795
+ nodes.set(entity, { id: entity, type: "entity" });
590
796
  }
591
797
  }
592
- let path = [];
798
+ let graphPath = [];
593
799
  if (pathTo) {
594
800
  const visited = new Set();
595
801
  const queue = [
@@ -600,7 +806,7 @@ function createTsEngine(deps) {
600
806
  if (!current)
601
807
  break;
602
808
  if (current.node === pathTo) {
603
- path = current.pathEdges;
809
+ graphPath = current.pathEdges;
604
810
  break;
605
811
  }
606
812
  if (current.depth >= maxDepth) {
@@ -620,6 +826,34 @@ function createTsEngine(deps) {
620
826
  }
621
827
  }
622
828
  }
829
+ const statusCounts = {
830
+ active: 0,
831
+ pending_conflict: 0,
832
+ superseded: 0,
833
+ rejected: 0,
834
+ };
835
+ const wikiRefSet = new Set();
836
+ if (entityWikiPathByName.has(entity.toLowerCase())) {
837
+ wikiRefSet.add(`wiki/${entityWikiPathByName.get(entity.toLowerCase())}`);
838
+ }
839
+ for (const edge of edges) {
840
+ statusCounts[edge.fact_status] += 1;
841
+ for (const name of [edge.source, edge.target]) {
842
+ const wikiPath = entityWikiPathByName.get(name.toLowerCase());
843
+ if (wikiPath) {
844
+ wikiRefSet.add(`wiki/${wikiPath}`);
845
+ }
846
+ }
847
+ const topicPath = topicWikiPathByType.get(edge.type.toLowerCase());
848
+ if (topicPath) {
849
+ wikiRefSet.add(`wiki/${topicPath}`);
850
+ }
851
+ }
852
+ const evidenceIds = [...new Set(edges.flatMap(edge => edge.evidence_ids).filter(Boolean))];
853
+ const conflictEdges = edges.filter(edge => edge.fact_status === "pending_conflict" || edge.fact_status === "rejected");
854
+ const pendingCount = conflictEdges.filter(edge => edge.fact_status === "pending_conflict").length;
855
+ const rejectedCount = conflictEdges.filter(edge => edge.fact_status === "rejected").length;
856
+ const conflictIds = [...new Set(conflictEdges.map(edge => edge.conflict_id).filter((item) => !!item))];
623
857
  return {
624
858
  success: true,
625
859
  data: {
@@ -628,13 +862,61 @@ function createTsEngine(deps) {
628
862
  dir: direction,
629
863
  nodes: [...nodes.values()],
630
864
  edges,
865
+ status_counts: statusCounts,
631
866
  path_to: pathTo || "",
632
867
  max_depth: maxDepth,
633
- path,
868
+ path: graphPath,
869
+ wiki_refs: [...wikiRefSet],
870
+ evidence_ids: evidenceIds,
871
+ conflict_hint: conflictEdges.length > 0
872
+ ? {
873
+ pending_count: pendingCount,
874
+ rejected_count: rejectedCount,
875
+ conflict_ids: conflictIds,
876
+ suggestion: pendingCount > 0
877
+ ? "Pending graph conflicts found. Call list_graph_conflicts first, then resolve_graph_conflict(accept/reject)."
878
+ : "Rejected candidate facts exist. Submit stronger evidence if you want to update the graph.",
879
+ }
880
+ : undefined,
634
881
  relation_type_distribution: [...relationTypeDistribution.entries()].map(([type, count]) => ({ type, count })),
635
882
  },
636
883
  };
637
884
  }
885
+ async function exportGraphView(args, _context) {
886
+ if (!deps.graphMemoryStore) {
887
+ return { success: false, error: "Graph memory store is not available." };
888
+ }
889
+ const writeSnapshot = args.write_snapshot !== false;
890
+ const view = deps.graphMemoryStore.exportGraphView();
891
+ const projection = writeSnapshot
892
+ ? (0, wiki_projector_1.writeGraphViewProjection)({
893
+ memoryRoot: deps.memoryRoot,
894
+ view,
895
+ })
896
+ : null;
897
+ return {
898
+ success: true,
899
+ data: {
900
+ ...view,
901
+ snapshot_written: writeSnapshot,
902
+ projection,
903
+ },
904
+ };
905
+ }
906
+ async function lintMemoryWiki(_args, _context) {
907
+ if (!deps.graphMemoryStore) {
908
+ return { success: false, error: "Graph memory store is not available." };
909
+ }
910
+ const graphView = deps.graphMemoryStore.exportGraphView();
911
+ const report = (0, wiki_linter_1.lintMemoryWiki)({
912
+ memoryRoot: deps.memoryRoot,
913
+ graphView,
914
+ });
915
+ return {
916
+ success: true,
917
+ data: report,
918
+ };
919
+ }
638
920
  async function deleteMemory(args, _context) {
639
921
  const targetId = args.memory_id?.trim();
640
922
  if (!targetId) {
@@ -674,11 +956,14 @@ function createTsEngine(deps) {
674
956
  continue;
675
957
  }
676
958
  if (typeof args.text === "string") {
677
- if (typeof record.content === "string") {
678
- record.content = args.text;
959
+ const nextText = args.text.trim();
960
+ const layer = typeof record.layer === "string" ? record.layer.trim() : "";
961
+ if (layer === "active") {
962
+ record.summary = nextText;
963
+ record.source_text = nextText;
679
964
  }
680
965
  else {
681
- record.summary = args.text;
966
+ record.summary = nextText;
682
967
  }
683
968
  }
684
969
  if (typeof args.type === "string") {
@@ -763,6 +1048,16 @@ function createTsEngine(deps) {
763
1048
  if (layer === "all" || layer === "archive") {
764
1049
  targetFiles.push({ layer: "archive", filePath: archivePath });
765
1050
  }
1051
+ const vectorJsonlPath = path.join(deps.memoryRoot, "vector", "lancedb_events.jsonl");
1052
+ const vectorJsonlRecords = readJsonl(vectorJsonlPath);
1053
+ const vectorSourceIndex = new Set();
1054
+ for (const row of vectorJsonlRecords) {
1055
+ const rowLayer = row.layer === "active" || row.layer === "archive" ? row.layer : "";
1056
+ const sourceMemoryId = typeof row.source_memory_id === "string" ? row.source_memory_id.trim() : "";
1057
+ if (!rowLayer || !sourceMemoryId)
1058
+ continue;
1059
+ vectorSourceIndex.add(`${rowLayer}|${sourceMemoryId}`);
1060
+ }
766
1061
  const queue = [];
767
1062
  const recordsByFile = new Map();
768
1063
  for (const target of targetFiles) {
@@ -776,6 +1071,7 @@ function createTsEngine(deps) {
776
1071
  }
777
1072
  const status = typeof record.embedding_status === "string" ? record.embedding_status.trim() : "";
778
1073
  const hasEmbedding = Array.isArray(record.embedding) && record.embedding.length > 0;
1074
+ const hasVectorRows = vectorSourceIndex.has(`${target.layer}|${id}`);
779
1075
  if (forceRebuild) {
780
1076
  queue.push({ layer: target.layer, filePath: target.filePath, index: i });
781
1077
  continue;
@@ -785,7 +1081,7 @@ function createTsEngine(deps) {
785
1081
  continue;
786
1082
  }
787
1083
  }
788
- else if (status === "ok" || hasEmbedding) {
1084
+ else if ((status === "ok" || hasEmbedding) && hasVectorRows) {
789
1085
  continue;
790
1086
  }
791
1087
  const failCountRaw = failureCountState[id];
@@ -831,7 +1127,48 @@ function createTsEngine(deps) {
831
1127
  }
832
1128
  const chunkSize = deps.vectorChunking?.chunkSize ?? 600;
833
1129
  const chunkOverlap = deps.vectorChunking?.chunkOverlap ?? 100;
834
- const chunks = splitTextChunks(text, chunkSize, chunkOverlap);
1130
+ const evidenceMaxChunks = typeof deps.vectorChunking?.evidenceMaxChunks === "number"
1131
+ ? Math.max(0, Math.min(8, Math.floor(deps.vectorChunking.evidenceMaxChunks)))
1132
+ : 2;
1133
+ let chunks = [];
1134
+ if (item.layer === "archive") {
1135
+ const summaryText = typeof record.summary === "string" ? record.summary.trim() : "";
1136
+ const sourceText = typeof record.source_text === "string" ? record.source_text.trim() : "";
1137
+ const summaryChunk = summaryText
1138
+ ? [{
1139
+ index: 0,
1140
+ start: 0,
1141
+ end: summaryText.length,
1142
+ text: summaryText,
1143
+ source_field: "summary",
1144
+ }]
1145
+ : [];
1146
+ const evidenceChunks = sourceText
1147
+ ? pickEvidenceChunks(splitTextChunks(sourceText, chunkSize, chunkOverlap), evidenceMaxChunks)
1148
+ : [];
1149
+ chunks = [
1150
+ ...summaryChunk,
1151
+ ...evidenceChunks.map((chunk, idx) => ({
1152
+ index: idx + summaryChunk.length,
1153
+ start: chunk.start,
1154
+ end: chunk.end,
1155
+ text: chunk.text,
1156
+ source_field: "evidence",
1157
+ })),
1158
+ ];
1159
+ if (chunks.length === 0 && text) {
1160
+ chunks = splitTextChunks(text, chunkSize, chunkOverlap).map(chunk => ({
1161
+ ...chunk,
1162
+ source_field: "summary",
1163
+ }));
1164
+ }
1165
+ }
1166
+ else {
1167
+ chunks = splitTextChunks(text, chunkSize, chunkOverlap).map(chunk => ({
1168
+ ...chunk,
1169
+ source_field: "summary",
1170
+ }));
1171
+ }
835
1172
  if (chunks.length === 0) {
836
1173
  record.embedding_status = "failed";
837
1174
  failed += 1;
@@ -869,6 +1206,8 @@ function createTsEngine(deps) {
869
1206
  layer: item.layer,
870
1207
  source_memory_id: id,
871
1208
  source_memory_canonical_id: typeof record.canonical_id === "string" ? record.canonical_id : id,
1209
+ source_event_id: typeof record.source_event_id === "string" ? record.source_event_id : id,
1210
+ source_field: chunk.source_field,
872
1211
  outcome: typeof record.outcome === "string" ? record.outcome : "",
873
1212
  entities: Array.isArray(record.entities) ? record.entities.filter(v => typeof v === "string") : [],
874
1213
  relations: Array.isArray(record.relations)
@@ -972,6 +1311,20 @@ function createTsEngine(deps) {
972
1311
  const vectorJsonlRecords = readJsonl(vectorJsonlPath);
973
1312
  const activeVectorRecords = vectorJsonlRecords.filter(record => (record.layer === "active"));
974
1313
  const archiveVectorRecords = vectorJsonlRecords.filter(record => (record.layer === "archive"));
1314
+ const lancedbDir = path.join(deps.memoryRoot, "vector", "lancedb");
1315
+ const lancedbExists = fs.existsSync(lancedbDir);
1316
+ let lancedbRecordCount = 0;
1317
+ if (lancedbExists) {
1318
+ try {
1319
+ const lancedbFiles = fs.readdirSync(lancedbDir).filter(f => f.endsWith(".lance") || f.endsWith(".manifest"));
1320
+ lancedbRecordCount = lancedbFiles.length > 0 ? -1 : 0;
1321
+ }
1322
+ catch {
1323
+ lancedbRecordCount = 0;
1324
+ }
1325
+ }
1326
+ const totalVectorRecords = vectorJsonlRecords.length > 0 ? vectorJsonlRecords.length : (lancedbRecordCount === -1 ? -1 : 0);
1327
+ const vectorStorageType = lancedbExists && lancedbRecordCount === -1 ? "lancedb" : (vectorJsonlRecords.length > 0 ? "jsonl" : "none");
975
1328
  const syncState = parseJsonFile(path.join(deps.memoryRoot, ".sync_state.json"));
976
1329
  const backfillState = parseJsonFile(path.join(deps.memoryRoot, ".vector_backfill_state.json"));
977
1330
  const failureCounts = backfillState && typeof backfillState.failureCounts === "object" && backfillState.failureCounts !== null
@@ -1011,6 +1364,166 @@ function createTsEngine(deps) {
1011
1364
  { name: "LLM model connectivity", passed: llmConnectivity.connected, message: llmConnectivity.error || "ok" },
1012
1365
  { name: "Reranker model connectivity", passed: rerankerConnectivity.connected, message: rerankerConnectivity.error || "ok" },
1013
1366
  ];
1367
+ const qualityCheck = {
1368
+ active: { total: 0, valid: 0, invalid: 0, issues: [] },
1369
+ archive: { total: 0, valid: 0, invalid: 0, issues: [] },
1370
+ };
1371
+ if (fs.existsSync(activePath)) {
1372
+ const content = fs.readFileSync(activePath, "utf-8");
1373
+ const lines = content.split(/\r?\n/).filter(l => l.trim());
1374
+ qualityCheck.active.total = lines.length;
1375
+ for (let i = 0; i < lines.length; i++) {
1376
+ const validation = (0, llm_output_validator_1.validateJsonlLine)(lines[i]);
1377
+ if (validation.valid) {
1378
+ qualityCheck.active.valid++;
1379
+ }
1380
+ else {
1381
+ qualityCheck.active.invalid++;
1382
+ if (qualityCheck.active.issues.length < 5) {
1383
+ qualityCheck.active.issues.push({ line: i + 1, errors: validation.errors });
1384
+ }
1385
+ }
1386
+ }
1387
+ }
1388
+ if (fs.existsSync(archivePath)) {
1389
+ const content = fs.readFileSync(archivePath, "utf-8");
1390
+ const lines = content.split(/\r?\n/).filter(l => l.trim());
1391
+ qualityCheck.archive.total = lines.length;
1392
+ for (let i = 0; i < lines.length; i++) {
1393
+ const validation = (0, llm_output_validator_1.validateJsonlLine)(lines[i]);
1394
+ if (validation.valid) {
1395
+ qualityCheck.archive.valid++;
1396
+ }
1397
+ else {
1398
+ qualityCheck.archive.invalid++;
1399
+ if (qualityCheck.archive.issues.length < 5) {
1400
+ qualityCheck.archive.issues.push({ line: i + 1, errors: validation.errors });
1401
+ }
1402
+ }
1403
+ }
1404
+ }
1405
+ const totalInvalid = qualityCheck.active.invalid + qualityCheck.archive.invalid;
1406
+ if (totalInvalid > 0) {
1407
+ checks.push({ name: "Data integrity", passed: false, message: `${totalInvalid} invalid records found` });
1408
+ }
1409
+ else {
1410
+ checks.push({ name: "Data integrity", passed: true, message: "All records valid" });
1411
+ }
1412
+ const graphMemoryPath = path.join(deps.memoryRoot, "graph", "memory.jsonl");
1413
+ const graphRecords = readJsonl(graphMemoryPath);
1414
+ function stringField(record, key) {
1415
+ const value = record[key];
1416
+ return typeof value === "string" ? value.trim() : "";
1417
+ }
1418
+ const activeFieldIssues = {
1419
+ missing_id: 0,
1420
+ missing_timestamp: 0,
1421
+ missing_layer: 0,
1422
+ missing_text_payload: 0,
1423
+ };
1424
+ for (const record of activeRecords) {
1425
+ if (!stringField(record, "id"))
1426
+ activeFieldIssues.missing_id += 1;
1427
+ if (!stringField(record, "timestamp"))
1428
+ activeFieldIssues.missing_timestamp += 1;
1429
+ if (stringField(record, "layer") !== "active")
1430
+ activeFieldIssues.missing_layer += 1;
1431
+ const hasPayload = [
1432
+ stringField(record, "summary"),
1433
+ stringField(record, "source_text"),
1434
+ stringField(record, "text"),
1435
+ stringField(record, "message"),
1436
+ ].some(Boolean);
1437
+ if (!hasPayload)
1438
+ activeFieldIssues.missing_text_payload += 1;
1439
+ }
1440
+ const archiveFieldIssues = {
1441
+ missing_id: 0,
1442
+ missing_timestamp: 0,
1443
+ missing_layer: 0,
1444
+ missing_summary: 0,
1445
+ missing_source_memory_id: 0,
1446
+ };
1447
+ for (const record of archiveRecords) {
1448
+ if (!stringField(record, "id"))
1449
+ archiveFieldIssues.missing_id += 1;
1450
+ if (!stringField(record, "timestamp"))
1451
+ archiveFieldIssues.missing_timestamp += 1;
1452
+ if (stringField(record, "layer") !== "archive")
1453
+ archiveFieldIssues.missing_layer += 1;
1454
+ if (!stringField(record, "summary"))
1455
+ archiveFieldIssues.missing_summary += 1;
1456
+ if (!stringField(record, "source_memory_id") && !stringField(record, "canonical_id")) {
1457
+ archiveFieldIssues.missing_source_memory_id += 1;
1458
+ }
1459
+ }
1460
+ const vectorFieldIssues = {
1461
+ missing_id: 0,
1462
+ missing_layer: 0,
1463
+ missing_summary: 0,
1464
+ missing_source_memory_id: 0,
1465
+ missing_embedding: 0,
1466
+ };
1467
+ for (const record of vectorJsonlRecords) {
1468
+ if (!stringField(record, "id"))
1469
+ vectorFieldIssues.missing_id += 1;
1470
+ const layer = stringField(record, "layer");
1471
+ if (layer !== "active" && layer !== "archive")
1472
+ vectorFieldIssues.missing_layer += 1;
1473
+ if (!stringField(record, "summary"))
1474
+ vectorFieldIssues.missing_summary += 1;
1475
+ if (!stringField(record, "source_memory_id"))
1476
+ vectorFieldIssues.missing_source_memory_id += 1;
1477
+ const embedding = record.embedding;
1478
+ const vector = record.vector;
1479
+ const hasEmbedding = Array.isArray(embedding) ? embedding.length > 0 : (Array.isArray(vector) ? vector.length > 0 : false);
1480
+ if (!hasEmbedding)
1481
+ vectorFieldIssues.missing_embedding += 1;
1482
+ }
1483
+ const graphFieldIssues = {
1484
+ missing_id: 0,
1485
+ missing_event_ref: 0,
1486
+ missing_layer: 0,
1487
+ malformed_relations: 0,
1488
+ };
1489
+ for (const record of graphRecords) {
1490
+ if (!stringField(record, "id"))
1491
+ graphFieldIssues.missing_id += 1;
1492
+ if (!stringField(record, "source_event_id") && !stringField(record, "archive_event_id")) {
1493
+ graphFieldIssues.missing_event_ref += 1;
1494
+ }
1495
+ const layer = stringField(record, "source_layer");
1496
+ if (layer !== "archive_event" && layer !== "active_only")
1497
+ graphFieldIssues.missing_layer += 1;
1498
+ if (!Array.isArray(record.relations))
1499
+ graphFieldIssues.malformed_relations += 1;
1500
+ }
1501
+ const archiveIdSet = new Set(archiveRecords
1502
+ .map(record => stringField(record, "id"))
1503
+ .filter(Boolean));
1504
+ const vectorLinkedToArchive = vectorJsonlRecords.filter(record => {
1505
+ const sourceMemoryId = stringField(record, "source_memory_id");
1506
+ return !!sourceMemoryId && archiveIdSet.has(sourceMemoryId);
1507
+ }).length;
1508
+ const graphLinkedToArchive = graphRecords.filter(record => {
1509
+ const sourceLayer = stringField(record, "source_layer");
1510
+ const refId = stringField(record, "source_event_id") || stringField(record, "archive_event_id");
1511
+ return sourceLayer === "archive_event" && !!refId && archiveIdSet.has(refId);
1512
+ }).length;
1513
+ const graphConflictStats = deps.graphMemoryStore
1514
+ ? deps.graphMemoryStore.getConflictStats()
1515
+ : { pending: 0, accepted: 0, rejected: 0 };
1516
+ const schemaIssueTotal = Object.values(activeFieldIssues).reduce((sum, n) => sum + n, 0)
1517
+ + Object.values(archiveFieldIssues).reduce((sum, n) => sum + n, 0)
1518
+ + Object.values(vectorFieldIssues).reduce((sum, n) => sum + n, 0)
1519
+ + Object.values(graphFieldIssues).reduce((sum, n) => sum + n, 0);
1520
+ checks.push({
1521
+ name: "Field mapping alignment",
1522
+ passed: schemaIssueTotal === 0,
1523
+ message: schemaIssueTotal === 0
1524
+ ? "active/archive/vector/graph field mapping aligned with read path"
1525
+ : `${schemaIssueTotal} field mapping issues detected across four memory stores`,
1526
+ });
1014
1527
  return {
1015
1528
  success: true,
1016
1529
  data: {
@@ -1027,6 +1540,8 @@ function createTsEngine(deps) {
1027
1540
  path: archivePath,
1028
1541
  },
1029
1542
  vector: {
1543
+ storage_type: vectorStorageType,
1544
+ lancedb_exists: lancedbExists,
1030
1545
  active_coverage: activeVector.coverage,
1031
1546
  archive_coverage: archiveVector.coverage,
1032
1547
  active_unembedded: activeVector.pending + activeVector.failed,
@@ -1040,6 +1555,7 @@ function createTsEngine(deps) {
1040
1555
  active: activeVectorRecords.length,
1041
1556
  archive: archiveVectorRecords.length,
1042
1557
  },
1558
+ total_vector_records: totalVectorRecords,
1043
1559
  last_backfill_summary: lastVectorBackfill,
1044
1560
  backfill_state: {
1045
1561
  pending_retry_records: pendingRetry,
@@ -1048,6 +1564,7 @@ function createTsEngine(deps) {
1048
1564
  },
1049
1565
  graph_rules: {
1050
1566
  graph_mutation_log_exists: fs.existsSync(path.join(deps.memoryRoot, "graph", "mutation_log.jsonl")),
1567
+ graph_conflicts: graphConflictStats,
1051
1568
  rules_exists: fs.existsSync(path.join(deps.memoryRoot, "CORTEX_RULES.md")),
1052
1569
  },
1053
1570
  },
@@ -1056,22 +1573,100 @@ function createTsEngine(deps) {
1056
1573
  llm: llmConnectivity,
1057
1574
  reranker: rerankerConnectivity,
1058
1575
  },
1059
- recommendations: [],
1576
+ quality_check: qualityCheck,
1577
+ schema_alignment: {
1578
+ active: {
1579
+ records: activeRecords.length,
1580
+ issues: activeFieldIssues,
1581
+ },
1582
+ archive: {
1583
+ records: archiveRecords.length,
1584
+ issues: archiveFieldIssues,
1585
+ },
1586
+ vector: {
1587
+ records: vectorJsonlRecords.length,
1588
+ issues: vectorFieldIssues,
1589
+ linked_to_archive: vectorLinkedToArchive,
1590
+ },
1591
+ graph: {
1592
+ records: graphRecords.length,
1593
+ issues: graphFieldIssues,
1594
+ linked_archive_events: graphLinkedToArchive,
1595
+ },
1596
+ cross_layer_links: {
1597
+ archive_records: archiveIdSet.size,
1598
+ vector_archive_link_coverage: archiveIdSet.size > 0
1599
+ ? Number((vectorLinkedToArchive / archiveIdSet.size).toFixed(4))
1600
+ : 0,
1601
+ graph_archive_link_coverage: archiveIdSet.size > 0
1602
+ ? Number((graphLinkedToArchive / archiveIdSet.size).toFixed(4))
1603
+ : 0,
1604
+ },
1605
+ },
1606
+ recommendations: [
1607
+ ...(totalInvalid > 0 ? ["Run repair-memory --fix to clean invalid records"] : []),
1608
+ ...(schemaIssueTotal > 0 ? ["Run diagnostics output schema_alignment and repair missing cross-layer fields"] : []),
1609
+ ],
1060
1610
  },
1061
1611
  };
1062
1612
  }
1063
1613
  async function searchMemory(args, context) {
1064
- if (!args || !args.query) {
1614
+ const argsRecord = asRecord(args) || {};
1615
+ const argsInput = asRecord(argsRecord.input);
1616
+ const queryCandidate = [
1617
+ typeof args.query === "string" ? args.query : "",
1618
+ typeof argsRecord.query === "string" ? String(argsRecord.query) : "",
1619
+ typeof argsRecord.q === "string" ? String(argsRecord.q) : "",
1620
+ typeof argsRecord.keyword === "string" ? String(argsRecord.keyword) : "",
1621
+ typeof argsInput?.query === "string" ? String(argsInput.query) : "",
1622
+ typeof argsInput?.q === "string" ? String(argsInput.q) : "",
1623
+ ].find(item => item.trim());
1624
+ const query = queryCandidate ? queryCandidate.trim() : "";
1625
+ if (!query) {
1065
1626
  return {
1066
1627
  success: false,
1067
1628
  error: "Invalid input provided. Missing 'query' parameter.",
1068
1629
  };
1069
1630
  }
1631
+ const topKRaw = [
1632
+ typeof args.top_k === "number" ? args.top_k : undefined,
1633
+ typeof argsRecord.top_k === "number" ? Number(argsRecord.top_k) : undefined,
1634
+ typeof argsRecord.topK === "number" ? Number(argsRecord.topK) : undefined,
1635
+ typeof argsInput?.top_k === "number" ? Number(argsInput.top_k) : undefined,
1636
+ typeof argsInput?.topK === "number" ? Number(argsInput.topK) : undefined,
1637
+ ].find(value => typeof value === "number" && Number.isFinite(value));
1638
+ const fusionModeRaw = [
1639
+ typeof args.fusion_mode === "string" ? args.fusion_mode : "",
1640
+ typeof argsRecord.fusion_mode === "string" ? String(argsRecord.fusion_mode) : "",
1641
+ typeof argsRecord.fusionMode === "string" ? String(argsRecord.fusionMode) : "",
1642
+ typeof argsInput?.fusion_mode === "string" ? String(argsInput.fusion_mode) : "",
1643
+ typeof argsInput?.fusionMode === "string" ? String(argsInput.fusionMode) : "",
1644
+ ].find(value => value === "auto" || value === "authoritative" || value === "candidates" || value === "off");
1645
+ const trackHitsRaw = [
1646
+ typeof args.track_hits === "boolean" ? args.track_hits : undefined,
1647
+ typeof argsRecord.track_hits === "boolean" ? Boolean(argsRecord.track_hits) : undefined,
1648
+ typeof argsRecord.trackHits === "boolean" ? Boolean(argsRecord.trackHits) : undefined,
1649
+ typeof argsInput?.track_hits === "boolean" ? Boolean(argsInput.track_hits) : undefined,
1650
+ typeof argsInput?.trackHits === "boolean" ? Boolean(argsInput.trackHits) : undefined,
1651
+ ].find(value => typeof value === "boolean");
1070
1652
  const result = await deps.readStore.searchMemory({
1071
- query: args.query,
1072
- topK: typeof args.top_k === "number" && args.top_k > 0 ? Math.floor(args.top_k) : 3,
1653
+ query,
1654
+ topK: typeof topKRaw === "number" && topKRaw > 0 ? Math.floor(topKRaw) : 3,
1655
+ fusionMode: fusionModeRaw || "auto",
1656
+ trackHits: trackHitsRaw !== false,
1073
1657
  });
1074
- return { success: true, data: result.results };
1658
+ return {
1659
+ success: true,
1660
+ data: {
1661
+ results: result.results,
1662
+ vector_semantic_results: result.semantic_results,
1663
+ vector_keyword_results: result.keyword_results,
1664
+ channel_results: result.channel_results,
1665
+ vector_search_strategy: result.strategy,
1666
+ timing_ms: result.timing_ms,
1667
+ debug: result.debug,
1668
+ },
1669
+ };
1075
1670
  }
1076
1671
  async function getHotContext(args, _context) {
1077
1672
  const limit = typeof args.limit === "number" && args.limit > 0 ? Math.floor(args.limit) : 20;
@@ -1079,12 +1674,22 @@ function createTsEngine(deps) {
1079
1674
  return { success: true, data: result.context };
1080
1675
  }
1081
1676
  async function getAutoContext(args, context) {
1082
- const sessionId = deps.resolveSessionId(context);
1677
+ const argsRecord = asRecord(args) || {};
1678
+ const argsInput = asRecord(argsRecord.input);
1679
+ const includeHotRaw = [
1680
+ typeof args.include_hot === "boolean" ? args.include_hot : undefined,
1681
+ typeof argsRecord.include_hot === "boolean" ? Boolean(argsRecord.include_hot) : undefined,
1682
+ typeof argsRecord.includeHot === "boolean" ? Boolean(argsRecord.includeHot) : undefined,
1683
+ typeof argsInput?.include_hot === "boolean" ? Boolean(argsInput.include_hot) : undefined,
1684
+ typeof argsInput?.includeHot === "boolean" ? Boolean(argsInput.includeHot) : undefined,
1685
+ ].find(value => typeof value === "boolean");
1686
+ const sessionId = deps.resolveSessionId((context || {}));
1083
1687
  const cached = deps.getCachedAutoSearch(sessionId);
1084
1688
  const result = await deps.readStore.getAutoContext({
1085
- includeHot: args.include_hot !== false,
1689
+ includeHot: includeHotRaw !== false,
1086
1690
  sessionId,
1087
1691
  cachedAutoSearch: cached ?? undefined,
1692
+ recentMessages: getRecentSessionMessages(sessionId, 8),
1088
1693
  });
1089
1694
  if (!result.auto_search && !result.hot_context) {
1090
1695
  return {
@@ -1097,6 +1702,49 @@ function createTsEngine(deps) {
1097
1702
  }
1098
1703
  return { success: true, data: result };
1099
1704
  }
1705
+ async function listGraphConflicts(args, _context) {
1706
+ if (!deps.graphMemoryStore) {
1707
+ return { success: false, error: "Graph memory store is not available." };
1708
+ }
1709
+ const status = args.status === "pending" || args.status === "accepted" || args.status === "rejected" || args.status === "all"
1710
+ ? args.status
1711
+ : "pending";
1712
+ const limit = typeof args.limit === "number" && Number.isFinite(args.limit) && args.limit > 0
1713
+ ? Math.min(500, Math.floor(args.limit))
1714
+ : 50;
1715
+ const items = deps.graphMemoryStore.listConflicts({ status, limit });
1716
+ return { success: true, data: { status, count: items.length, items } };
1717
+ }
1718
+ async function resolveGraphConflict(args, _context) {
1719
+ if (!deps.graphMemoryStore) {
1720
+ return { success: false, error: "Graph memory store is not available." };
1721
+ }
1722
+ const conflictId = (args.conflict_id || "").trim();
1723
+ const action = args.action === "accept" || args.action === "reject" ? args.action : null;
1724
+ if (!conflictId) {
1725
+ return { success: false, error: "Invalid input provided. Missing 'conflict_id' parameter." };
1726
+ }
1727
+ if (!action) {
1728
+ return { success: false, error: "Invalid input provided. 'action' must be accept or reject." };
1729
+ }
1730
+ const note = typeof args.note === "string" ? args.note.trim() : undefined;
1731
+ const result = await deps.graphMemoryStore.resolveConflict({
1732
+ conflictId,
1733
+ action,
1734
+ note,
1735
+ });
1736
+ if (!result.success) {
1737
+ return { success: false, error: result.reason || "resolve_graph_conflict_failed" };
1738
+ }
1739
+ return {
1740
+ success: true,
1741
+ data: {
1742
+ conflict_id: conflictId,
1743
+ action,
1744
+ applied_record_id: result.appliedRecordId,
1745
+ },
1746
+ };
1747
+ }
1100
1748
  async function syncMemory(_args, _context) {
1101
1749
  try {
1102
1750
  const result = await deps.sessionSync.syncMemory();
@@ -1157,9 +1805,17 @@ function createTsEngine(deps) {
1157
1805
  deps.logger.debug(`TS buffered ${role} message for session ${sessionId} source=${source}`);
1158
1806
  if (role === "user" && text.length > 5) {
1159
1807
  try {
1160
- const searchResult = await deps.readStore.searchMemory({ query: text, topK: 3 });
1808
+ const historical = isHistoricalMemoryQuery(text);
1809
+ const query = buildAutoSearchQuery(sessionId, text, historical);
1810
+ const searchResult = await deps.readStore.searchMemory({
1811
+ query,
1812
+ topK: 3,
1813
+ mode: historical ? "auto" : "lightweight",
1814
+ fusionMode: "off",
1815
+ trackHits: false,
1816
+ });
1161
1817
  if (searchResult.results.length > 0) {
1162
- deps.setSessionAutoSearchCache(sessionId, text, searchResult.results);
1818
+ deps.setSessionAutoSearchCache(sessionId, query, searchResult.results);
1163
1819
  deps.logger.info(`TS auto-search cached ${searchResult.results.length} results for context`);
1164
1820
  }
1165
1821
  }
@@ -1190,6 +1846,10 @@ function createTsEngine(deps) {
1190
1846
  getAutoContext,
1191
1847
  storeEvent,
1192
1848
  queryGraph,
1849
+ exportGraphView,
1850
+ lintMemoryWiki,
1851
+ listGraphConflicts,
1852
+ resolveGraphConflict,
1193
1853
  reflectMemory,
1194
1854
  syncMemory,
1195
1855
  promoteMemory,