openclaw-cortex-memory 0.1.0-Alpha.8 → 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 (106) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +347 -299
  3. package/SIGNATURE.md +7 -0
  4. package/SKILL.md +96 -350
  5. package/dist/index.d.ts +93 -23
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +1234 -1318
  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 +6 -1
  14. package/dist/src/engine/memory_engine.d.ts.map +1 -1
  15. package/dist/src/engine/ts_engine.d.ts +208 -0
  16. package/dist/src/engine/ts_engine.d.ts.map +1 -1
  17. package/dist/src/engine/ts_engine.js +1353 -84
  18. package/dist/src/engine/ts_engine.js.map +1 -1
  19. package/dist/src/engine/types.d.ts +27 -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 +31 -214
  42. package/dist/src/session/session_end.js.map +1 -1
  43. package/dist/src/store/archive_store.d.ts +52 -7
  44. package/dist/src/store/archive_store.d.ts.map +1 -1
  45. package/dist/src/store/archive_store.js +526 -96
  46. package/dist/src/store/archive_store.js.map +1 -1
  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 +115 -0
  52. package/dist/src/store/graph_memory_store.d.ts.map +1 -0
  53. package/dist/src/store/graph_memory_store.js +1061 -0
  54. package/dist/src/store/graph_memory_store.js.map +1 -0
  55. package/dist/src/store/read_store.d.ts +95 -0
  56. package/dist/src/store/read_store.d.ts.map +1 -1
  57. package/dist/src/store/read_store.js +2108 -268
  58. package/dist/src/store/read_store.js.map +1 -1
  59. package/dist/src/store/vector_store.d.ts +15 -0
  60. package/dist/src/store/vector_store.d.ts.map +1 -1
  61. package/dist/src/store/vector_store.js +75 -1
  62. package/dist/src/store/vector_store.js.map +1 -1
  63. package/dist/src/store/write_store.d.ts +46 -0
  64. package/dist/src/store/write_store.d.ts.map +1 -1
  65. package/dist/src/store/write_store.js +399 -50
  66. package/dist/src/store/write_store.js.map +1 -1
  67. package/dist/src/sync/session_sync.d.ts +115 -2
  68. package/dist/src/sync/session_sync.d.ts.map +1 -1
  69. package/dist/src/sync/session_sync.js +2497 -44
  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/dist/src/wiki/wiki_linter.d.ts +26 -0
  76. package/dist/src/wiki/wiki_linter.d.ts.map +1 -0
  77. package/dist/src/wiki/wiki_linter.js +339 -0
  78. package/dist/src/wiki/wiki_linter.js.map +1 -0
  79. package/dist/src/wiki/wiki_logger.d.ts +10 -0
  80. package/dist/src/wiki/wiki_logger.d.ts.map +1 -0
  81. package/dist/src/wiki/wiki_logger.js +78 -0
  82. package/dist/src/wiki/wiki_logger.js.map +1 -0
  83. package/dist/src/wiki/wiki_maintainer.d.ts +39 -0
  84. package/dist/src/wiki/wiki_maintainer.d.ts.map +1 -0
  85. package/dist/src/wiki/wiki_maintainer.js +38 -0
  86. package/dist/src/wiki/wiki_maintainer.js.map +1 -0
  87. package/dist/src/wiki/wiki_projector.d.ts +35 -0
  88. package/dist/src/wiki/wiki_projector.d.ts.map +1 -0
  89. package/dist/src/wiki/wiki_projector.js +1151 -0
  90. package/dist/src/wiki/wiki_projector.js.map +1 -0
  91. package/dist/src/wiki/wiki_queue.d.ts +29 -0
  92. package/dist/src/wiki/wiki_queue.d.ts.map +1 -0
  93. package/dist/src/wiki/wiki_queue.js +137 -0
  94. package/dist/src/wiki/wiki_queue.js.map +1 -0
  95. package/openclaw.plugin.json +377 -18
  96. package/package.json +52 -5
  97. package/schema/graph.schema.yaml +330 -0
  98. package/scripts/cli.js +80 -26
  99. package/scripts/repair-memory.js +321 -0
  100. package/scripts/uninstall.js +7 -1
  101. package/skills/cortex-memory/SKILL.md +83 -0
  102. package/skills/cortex-memory/references/agent-manual.md +127 -0
  103. package/skills/cortex-memory/references/configuration.md +109 -0
  104. package/skills/cortex-memory/references/publish-checklist.md +45 -0
  105. package/skills/cortex-memory/references/system-prompt-template.md +27 -0
  106. package/skills/cortex-memory/references/tools.md +191 -0
@@ -37,6 +37,15 @@ 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");
44
+ const PROMPT_VERSIONS = {
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",
48
+ };
40
49
  function createTsEngine(deps) {
41
50
  const graphSchema = (0, ontology_1.loadGraphSchema)(deps.projectRoot);
42
51
  const sessionMessageBuffer = new Map();
@@ -64,6 +73,27 @@ function createTsEngine(deps) {
64
73
  }
65
74
  }
66
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
+ }
67
97
  function asRecord(value) {
68
98
  if (typeof value === "object" && value !== null) {
69
99
  return value;
@@ -98,6 +128,286 @@ function createTsEngine(deps) {
98
128
  archivePath: path.join(deps.memoryRoot, "sessions", "archive", "sessions.jsonl"),
99
129
  };
100
130
  }
131
+ function parseJsonFile(filePath) {
132
+ try {
133
+ if (!fs.existsSync(filePath)) {
134
+ return null;
135
+ }
136
+ const raw = fs.readFileSync(filePath, "utf-8").trim();
137
+ if (!raw) {
138
+ return null;
139
+ }
140
+ return JSON.parse(raw);
141
+ }
142
+ catch {
143
+ return null;
144
+ }
145
+ }
146
+ function embeddingStats(records) {
147
+ let ok = 0;
148
+ let failed = 0;
149
+ let pending = 0;
150
+ for (const record of records) {
151
+ const explicit = typeof record.embedding_status === "string" ? record.embedding_status.trim() : "";
152
+ const hasEmbedding = Array.isArray(record.embedding) && record.embedding.length > 0;
153
+ if (explicit === "ok" || hasEmbedding) {
154
+ ok += 1;
155
+ }
156
+ else if (explicit === "failed") {
157
+ failed += 1;
158
+ }
159
+ else {
160
+ pending += 1;
161
+ }
162
+ }
163
+ const total = records.length;
164
+ const coverage = total > 0 ? Number((ok / total).toFixed(4)) : 0;
165
+ return { total, ok, failed, pending, coverage };
166
+ }
167
+ function normalizeBaseUrl(value) {
168
+ if (!value)
169
+ return "";
170
+ return value.endsWith("/") ? value.slice(0, -1) : value;
171
+ }
172
+ function estimateTokenCount(text) {
173
+ const parts = text
174
+ .split(/[\s,.;:!?,。;:!?、()()[\]{}"'`~]+/)
175
+ .map(part => part.trim())
176
+ .filter(Boolean);
177
+ return parts.length;
178
+ }
179
+ function buildVectorSourceText(record, layer) {
180
+ if (layer === "active") {
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;
197
+ }
198
+ const summary = typeof record.summary === "string" ? record.summary.trim() : "";
199
+ return summary;
200
+ }
201
+ function splitTextChunks(text, chunkSize, chunkOverlap) {
202
+ const normalizedSize = Number.isFinite(chunkSize) && chunkSize >= 200 ? Math.floor(chunkSize) : 600;
203
+ const normalizedOverlap = Number.isFinite(chunkOverlap) && chunkOverlap >= 0
204
+ ? Math.floor(chunkOverlap)
205
+ : 100;
206
+ const overlap = Math.min(normalizedOverlap, Math.max(0, normalizedSize - 50));
207
+ const output = [];
208
+ let cursor = 0;
209
+ let index = 0;
210
+ const punctuationSet = new Set(["。", "!", "?", ".", "!", "?", "\n", ";", ";"]);
211
+ while (cursor < text.length) {
212
+ const rawEnd = Math.min(text.length, cursor + normalizedSize);
213
+ let end = rawEnd;
214
+ if (rawEnd < text.length) {
215
+ const backwardStart = Math.max(cursor + Math.floor(normalizedSize * 0.45), cursor + 1);
216
+ let found = -1;
217
+ for (let i = rawEnd - 1; i >= backwardStart; i -= 1) {
218
+ if (punctuationSet.has(text[i])) {
219
+ found = i + 1;
220
+ break;
221
+ }
222
+ }
223
+ if (found < 0) {
224
+ const forwardEnd = Math.min(text.length, rawEnd + Math.floor(normalizedSize * 0.2));
225
+ for (let i = rawEnd; i < forwardEnd; i += 1) {
226
+ if (punctuationSet.has(text[i])) {
227
+ found = i + 1;
228
+ break;
229
+ }
230
+ }
231
+ }
232
+ if (found > cursor) {
233
+ end = found;
234
+ }
235
+ }
236
+ if (end <= cursor) {
237
+ end = Math.min(text.length, cursor + normalizedSize);
238
+ }
239
+ const chunkText = text.slice(cursor, end).trim();
240
+ if (chunkText) {
241
+ output.push({ index, start: cursor, end, text: chunkText });
242
+ index += 1;
243
+ }
244
+ if (end >= text.length) {
245
+ break;
246
+ }
247
+ const nextCursor = Math.max(cursor + 1, end - overlap);
248
+ cursor = nextCursor <= cursor ? end : nextCursor;
249
+ }
250
+ return output;
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
+ }
278
+ function upsertJsonFile(filePath, patch) {
279
+ const current = parseJsonFile(filePath) || {};
280
+ const next = { ...current, ...patch };
281
+ const dir = path.dirname(filePath);
282
+ if (!fs.existsSync(dir)) {
283
+ fs.mkdirSync(dir, { recursive: true });
284
+ }
285
+ fs.writeFileSync(filePath, JSON.stringify(next, null, 2), "utf-8");
286
+ }
287
+ async function probeModelConnection(args) {
288
+ const defaultTimeoutMs = args.kind === "llm" ? 30000 : 15000;
289
+ const timeoutMs = typeof args.timeoutMs === "number" && Number.isFinite(args.timeoutMs) && args.timeoutMs >= 1000
290
+ ? Math.floor(args.timeoutMs)
291
+ : defaultTimeoutMs;
292
+ if (!args.model || !args.apiKey || !args.baseUrl) {
293
+ return {
294
+ configured: false,
295
+ connected: false,
296
+ model: args.model || "",
297
+ base_url: args.baseUrl || "",
298
+ error: "not_configured",
299
+ };
300
+ }
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" },
338
+ });
339
+ if (response.ok) {
340
+ return {
341
+ configured: true,
342
+ connected: true,
343
+ model: args.model,
344
+ base_url: args.baseUrl,
345
+ error: "",
346
+ };
347
+ }
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
+ }
358
+ }
359
+ return {
360
+ configured: true,
361
+ connected: false,
362
+ model: args.model,
363
+ base_url: args.baseUrl,
364
+ error: lastError,
365
+ };
366
+ }
367
+ async function requestEmbedding(args) {
368
+ const endpoint = args.baseUrl.endsWith("/embeddings") ? args.baseUrl : `${args.baseUrl}/embeddings`;
369
+ const body = {
370
+ input: args.text,
371
+ model: args.model,
372
+ };
373
+ if (typeof args.dimensions === "number" && Number.isFinite(args.dimensions) && args.dimensions > 0) {
374
+ body.dimensions = args.dimensions;
375
+ }
376
+ const timeoutMs = typeof args.timeoutMs === "number" && Number.isFinite(args.timeoutMs) && args.timeoutMs >= 1000
377
+ ? Math.floor(args.timeoutMs)
378
+ : 20000;
379
+ const maxRetries = typeof args.maxRetries === "number" && Number.isFinite(args.maxRetries) && args.maxRetries >= 1
380
+ ? Math.min(8, Math.floor(args.maxRetries))
381
+ : 4;
382
+ let lastError = null;
383
+ for (let attempt = 0; attempt < maxRetries; attempt += 1) {
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
+ }
394
+ try {
395
+ const json = (response.json || {});
396
+ const embedding = json?.data?.[0]?.embedding;
397
+ if (Array.isArray(embedding) && embedding.length > 0) {
398
+ return embedding.filter(item => Number.isFinite(item));
399
+ }
400
+ lastError = new Error("embedding_empty");
401
+ }
402
+ catch (error) {
403
+ lastError = error;
404
+ }
405
+ if (attempt < maxRetries - 1) {
406
+ await new Promise(resolve => setTimeout(resolve, 300 * Math.pow(2, attempt)));
407
+ }
408
+ }
409
+ throw lastError instanceof Error ? lastError : new Error(String(lastError || "embedding_failed"));
410
+ }
101
411
  async function storeEvent(args, _context) {
102
412
  try {
103
413
  const rawArgs = args;
@@ -131,6 +441,22 @@ function createTsEngine(deps) {
131
441
  return "";
132
442
  }).filter(Boolean)
133
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
+ }
134
460
  const relationInput = Array.isArray(rawArgs.relations)
135
461
  ? rawArgs.relations
136
462
  : Array.isArray(rawArgs.input?.relations)
@@ -159,6 +485,8 @@ function createTsEngine(deps) {
159
485
  source: relation.source.trim(),
160
486
  target: relation.target.trim(),
161
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,
162
490
  };
163
491
  })
164
492
  .filter((item) => Boolean(item))
@@ -170,24 +498,98 @@ function createTsEngine(deps) {
170
498
  : typeof rawArgs.event?.outcome === "string"
171
499
  ? String(rawArgs.event.outcome)
172
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
+ }
173
540
  const result = await deps.archiveStore.storeEvents([
174
541
  {
175
542
  event_type: "manual_event",
176
543
  summary: normalizedSummary,
544
+ cause: causeValue.trim() || normalizedSummary,
545
+ process: processValue.trim() || normalizedSummary,
546
+ result: resultValue.trim() || outcomeValue.trim() || normalizedSummary,
177
547
  entities,
178
548
  relations,
549
+ entity_types: entityTypes,
179
550
  outcome: outcomeValue,
180
551
  session_id: "manual",
181
552
  source_file: "ts_store_event",
182
553
  confidence: 1,
183
- source_event_id: "",
554
+ source_event_id: `manual:${Date.now().toString(36)}`,
184
555
  actor: "manual_tool",
185
556
  },
186
557
  ]);
187
558
  if (result.stored.length === 0) {
188
559
  return { success: false, error: result.skipped[0]?.reason || "store_event_skipped" };
189
560
  }
190
- 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 } };
191
593
  }
192
594
  catch (error) {
193
595
  return { success: false, error: String(error) };
@@ -206,32 +608,20 @@ function createTsEngine(deps) {
206
608
  : "both";
207
609
  const pathTo = typeof args.path_to === "string" && args.path_to.trim() ? args.path_to.trim() : "";
208
610
  const maxDepth = Math.max(2, Math.min(4, typeof args.max_depth === "number" ? Math.floor(args.max_depth) : 3));
209
- const { archivePath } = memoryFiles();
210
- 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");
211
613
  const nodes = new Map();
212
614
  const edges = [];
213
- const adjacency = new Map();
214
615
  const pathAdjacency = new Map();
215
616
  const relationTypeDistribution = new Map();
216
617
  const edgeKeySet = new Set();
217
- function pushEdge(source, target, type) {
218
- const key = `${source}|${type}|${target}`;
219
- if (edgeKeySet.has(key)) {
220
- return;
221
- }
222
- edgeKeySet.add(key);
223
- edges.push({ source, target, type });
224
- relationTypeDistribution.set(type, (relationTypeDistribution.get(type) || 0) + 1);
225
- if (!adjacency.has(source)) {
226
- adjacency.set(source, []);
227
- }
228
- adjacency.get(source)?.push({ next: target, edge: { source, target, type } });
229
- if (!adjacency.has(target)) {
230
- adjacency.set(target, []);
231
- }
232
- 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()}`;
233
621
  }
234
- function pushPathEdge(source, target, type) {
622
+ function pushPathEdge(edge) {
623
+ const source = edge.source;
624
+ const target = edge.target;
235
625
  if (!pathAdjacency.has(source)) {
236
626
  pathAdjacency.set(source, []);
237
627
  }
@@ -239,71 +629,173 @@ function createTsEngine(deps) {
239
629
  pathAdjacency.set(target, []);
240
630
  }
241
631
  if (direction === "incoming") {
242
- pathAdjacency.get(target)?.push({ next: source, edge: { source, target, type } });
632
+ pathAdjacency.get(target)?.push({ next: source, edge });
243
633
  }
244
634
  else if (direction === "outgoing") {
245
- pathAdjacency.get(source)?.push({ next: target, edge: { source, target, type } });
635
+ pathAdjacency.get(source)?.push({ next: target, edge });
246
636
  }
247
637
  else {
248
- pathAdjacency.get(source)?.push({ next: target, edge: { source, target, type } });
249
- 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 });
250
640
  }
251
641
  }
252
- for (const record of records) {
253
- const entities = Array.isArray(record.entities) ? record.entities : [];
254
- const named = entities.map(e => (typeof e === "string" ? e.trim() : "")).filter(Boolean);
255
- const relations = Array.isArray(record.relations) ? record.relations : [];
256
- let explicitMatched = false;
257
- for (const relationRaw of relations) {
258
- if (typeof relationRaw !== "object" || relationRaw === null) {
259
- continue;
260
- }
261
- const relation = relationRaw;
262
- const source = typeof relation.source === "string" ? relation.source.trim() : "";
263
- const target = typeof relation.target === "string" ? relation.target.trim() : "";
264
- const type = (0, ontology_1.normalizeRelationType)(typeof relation.type === "string" && relation.type.trim() ? relation.type.trim() : "related_to", graphSchema);
265
- if (!source || !target) {
266
- continue;
267
- }
268
- 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)
269
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
+ });
270
704
  }
271
- pushPathEdge(source, target, type);
272
- const outgoingMatch = source === entity;
273
- const incomingMatch = target === entity;
274
- const directionMatched = direction === "both" ? (outgoingMatch || incomingMatch)
275
- : direction === "outgoing" ? outgoingMatch
276
- : incomingMatch;
277
- if (!directionMatched) {
278
- 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}`);
279
751
  }
280
- explicitMatched = true;
281
- if (!nodes.has(source))
282
- nodes.set(source, { id: source, type: "entity" });
283
- if (!nodes.has(target))
284
- nodes.set(target, { id: target, type: "entity" });
285
- pushEdge(source, target, type);
286
752
  }
287
- 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) {
288
762
  continue;
289
763
  }
290
- 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) {
291
774
  continue;
292
775
  }
293
- for (const name of named) {
294
- if (!nodes.has(name)) {
295
- nodes.set(name, { id: name, type: "entity" });
296
- }
776
+ explicitMatched = true;
777
+ const edgeKey = `${edge.relation_key}|${edge.fact_status}|${edge.conflict_id || ""}`;
778
+ if (edgeKeySet.has(edgeKey)) {
779
+ continue;
297
780
  }
298
- for (const name of named) {
299
- if (name !== entity) {
300
- if (!relFilter || relFilter === "co_occurrence") {
301
- pushEdge(entity, name, "co_occurrence");
302
- }
303
- }
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" });
304
796
  }
305
797
  }
306
- let path = [];
798
+ let graphPath = [];
307
799
  if (pathTo) {
308
800
  const visited = new Set();
309
801
  const queue = [
@@ -314,7 +806,7 @@ function createTsEngine(deps) {
314
806
  if (!current)
315
807
  break;
316
808
  if (current.node === pathTo) {
317
- path = current.pathEdges;
809
+ graphPath = current.pathEdges;
318
810
  break;
319
811
  }
320
812
  if (current.depth >= maxDepth) {
@@ -334,6 +826,34 @@ function createTsEngine(deps) {
334
826
  }
335
827
  }
336
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))];
337
857
  return {
338
858
  success: true,
339
859
  data: {
@@ -342,13 +862,61 @@ function createTsEngine(deps) {
342
862
  dir: direction,
343
863
  nodes: [...nodes.values()],
344
864
  edges,
865
+ status_counts: statusCounts,
345
866
  path_to: pathTo || "",
346
867
  max_depth: maxDepth,
347
- 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,
348
881
  relation_type_distribution: [...relationTypeDistribution.entries()].map(([type, count]) => ({ type, count })),
349
882
  },
350
883
  };
351
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
+ }
352
920
  async function deleteMemory(args, _context) {
353
921
  const targetId = args.memory_id?.trim();
354
922
  if (!targetId) {
@@ -388,11 +956,14 @@ function createTsEngine(deps) {
388
956
  continue;
389
957
  }
390
958
  if (typeof args.text === "string") {
391
- if (typeof record.content === "string") {
392
- 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;
393
964
  }
394
965
  else {
395
- record.summary = args.text;
966
+ record.summary = nextText;
396
967
  }
397
968
  }
398
969
  if (typeof args.type === "string") {
@@ -435,35 +1006,667 @@ function createTsEngine(deps) {
435
1006
  }
436
1007
  return { success: true, data: { deletedCount } };
437
1008
  }
1009
+ async function backfillEmbeddings(args, _context) {
1010
+ const layer = args.layer === "active" || args.layer === "archive" || args.layer === "all" ? args.layer : "all";
1011
+ const rebuildMode = args.rebuild_mode === "vector_only" || args.rebuild_mode === "full"
1012
+ ? args.rebuild_mode
1013
+ : "incremental";
1014
+ const batchSize = typeof args.batch_size === "number" && Number.isFinite(args.batch_size) && args.batch_size > 0
1015
+ ? Math.min(500, Math.floor(args.batch_size))
1016
+ : 100;
1017
+ const maxRetries = typeof args.max_retries === "number" && Number.isFinite(args.max_retries) && args.max_retries >= 1
1018
+ ? Math.min(10, Math.floor(args.max_retries))
1019
+ : 3;
1020
+ const retryFailedOnly = args.retry_failed_only === true;
1021
+ const forceRebuild = rebuildMode === "vector_only" || rebuildMode === "full";
1022
+ const model = deps.embedding?.model || "";
1023
+ const apiKey = deps.embedding?.apiKey || "";
1024
+ const baseUrl = normalizeBaseUrl(deps.embedding?.baseURL || deps.embedding?.baseUrl);
1025
+ if (!model || !apiKey || !baseUrl) {
1026
+ return { success: false, error: "Embedding config missing for backfill tool." };
1027
+ }
1028
+ const statePath = path.join(deps.memoryRoot, ".vector_backfill_state.json");
1029
+ const syncStatePath = path.join(deps.memoryRoot, ".sync_state.json");
1030
+ const previousState = parseJsonFile(statePath) || {};
1031
+ const failureCountState = (typeof previousState.failureCounts === "object" && previousState.failureCounts !== null)
1032
+ ? previousState.failureCounts
1033
+ : {};
1034
+ let fullSyncResult = null;
1035
+ if (rebuildMode === "full") {
1036
+ try {
1037
+ fullSyncResult = await deps.sessionSync.syncMemory();
1038
+ }
1039
+ catch (error) {
1040
+ deps.logger.warn(`backfill_full_rebuild_sync_failed error=${error}`);
1041
+ }
1042
+ }
1043
+ const { activePath, archivePath } = memoryFiles();
1044
+ const targetFiles = [];
1045
+ if (layer === "all" || layer === "active") {
1046
+ targetFiles.push({ layer: "active", filePath: activePath });
1047
+ }
1048
+ if (layer === "all" || layer === "archive") {
1049
+ targetFiles.push({ layer: "archive", filePath: archivePath });
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
+ }
1061
+ const queue = [];
1062
+ const recordsByFile = new Map();
1063
+ for (const target of targetFiles) {
1064
+ const records = readJsonl(target.filePath);
1065
+ recordsByFile.set(target.filePath, records);
1066
+ for (let i = 0; i < records.length; i += 1) {
1067
+ const record = records[i];
1068
+ const id = typeof record.id === "string" ? record.id : "";
1069
+ if (!id) {
1070
+ continue;
1071
+ }
1072
+ const status = typeof record.embedding_status === "string" ? record.embedding_status.trim() : "";
1073
+ const hasEmbedding = Array.isArray(record.embedding) && record.embedding.length > 0;
1074
+ const hasVectorRows = vectorSourceIndex.has(`${target.layer}|${id}`);
1075
+ if (forceRebuild) {
1076
+ queue.push({ layer: target.layer, filePath: target.filePath, index: i });
1077
+ continue;
1078
+ }
1079
+ if (retryFailedOnly) {
1080
+ if (status !== "failed") {
1081
+ continue;
1082
+ }
1083
+ }
1084
+ else if ((status === "ok" || hasEmbedding) && hasVectorRows) {
1085
+ continue;
1086
+ }
1087
+ const failCountRaw = failureCountState[id];
1088
+ const failCount = typeof failCountRaw === "number" ? failCountRaw : 0;
1089
+ if (failCount >= maxRetries && status === "failed") {
1090
+ continue;
1091
+ }
1092
+ queue.push({ layer: target.layer, filePath: target.filePath, index: i });
1093
+ }
1094
+ }
1095
+ const totalCandidates = queue.length;
1096
+ let success = 0;
1097
+ let failed = 0;
1098
+ let skipped = 0;
1099
+ let processed = 0;
1100
+ const failureCounts = {};
1101
+ for (const [key, value] of Object.entries(failureCountState)) {
1102
+ if (typeof value === "number" && Number.isFinite(value)) {
1103
+ failureCounts[key] = value;
1104
+ }
1105
+ }
1106
+ for (let start = 0; start < queue.length; start += batchSize) {
1107
+ const batch = queue.slice(start, start + batchSize);
1108
+ for (const item of batch) {
1109
+ processed += 1;
1110
+ const records = recordsByFile.get(item.filePath) || [];
1111
+ const record = records[item.index];
1112
+ if (!record) {
1113
+ skipped += 1;
1114
+ continue;
1115
+ }
1116
+ const id = typeof record.id === "string" ? record.id : "";
1117
+ if (!id) {
1118
+ skipped += 1;
1119
+ continue;
1120
+ }
1121
+ const text = buildVectorSourceText(record, item.layer);
1122
+ if (!text) {
1123
+ record.embedding_status = "failed";
1124
+ failed += 1;
1125
+ failureCounts[id] = (failureCounts[id] || 0) + 1;
1126
+ continue;
1127
+ }
1128
+ const chunkSize = deps.vectorChunking?.chunkSize ?? 600;
1129
+ const chunkOverlap = deps.vectorChunking?.chunkOverlap ?? 100;
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
+ }
1172
+ if (chunks.length === 0) {
1173
+ record.embedding_status = "failed";
1174
+ failed += 1;
1175
+ failureCounts[id] = (failureCounts[id] || 0) + 1;
1176
+ continue;
1177
+ }
1178
+ try {
1179
+ if (forceRebuild) {
1180
+ record.embedding_status = "pending";
1181
+ }
1182
+ await deps.vectorStore.deleteBySourceMemory({ layer: item.layer, sourceMemoryId: id });
1183
+ let chunkOk = 0;
1184
+ for (const chunk of chunks) {
1185
+ const embedding = await requestEmbedding({
1186
+ text: chunk.text,
1187
+ model,
1188
+ apiKey,
1189
+ baseUrl,
1190
+ dimensions: deps.embedding?.dimensions,
1191
+ timeoutMs: deps.embedding?.timeoutMs,
1192
+ maxRetries: deps.embedding?.maxRetries,
1193
+ });
1194
+ if (!embedding || embedding.length === 0) {
1195
+ continue;
1196
+ }
1197
+ if (!record.embedding) {
1198
+ record.embedding = embedding;
1199
+ }
1200
+ await deps.vectorStore.upsert({
1201
+ id: `${id}_c${chunk.index}`,
1202
+ session_id: typeof record.session_id === "string" ? record.session_id : "unknown",
1203
+ event_type: typeof record.event_type === "string" ? record.event_type : (item.layer === "active" ? "message" : "insight"),
1204
+ summary: chunk.text,
1205
+ timestamp: typeof record.timestamp === "string" ? record.timestamp : new Date().toISOString(),
1206
+ layer: item.layer,
1207
+ source_memory_id: id,
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,
1211
+ outcome: typeof record.outcome === "string" ? record.outcome : "",
1212
+ entities: Array.isArray(record.entities) ? record.entities.filter(v => typeof v === "string") : [],
1213
+ relations: Array.isArray(record.relations)
1214
+ ? record.relations
1215
+ .map(v => {
1216
+ if (!v || typeof v !== "object")
1217
+ return null;
1218
+ const relation = v;
1219
+ const source = typeof relation.source === "string" ? relation.source : "";
1220
+ const target = typeof relation.target === "string" ? relation.target : "";
1221
+ const type = typeof relation.type === "string" ? relation.type : "related_to";
1222
+ if (!source || !target)
1223
+ return null;
1224
+ return { source, target, type };
1225
+ })
1226
+ .filter((v) => Boolean(v))
1227
+ : [],
1228
+ embedding,
1229
+ quality_score: typeof record.quality_score === "number" ? record.quality_score : 0.5,
1230
+ char_count: chunk.text.length,
1231
+ token_count: estimateTokenCount(chunk.text),
1232
+ chunk_index: chunk.index,
1233
+ chunk_total: chunks.length,
1234
+ chunk_start: chunk.start,
1235
+ chunk_end: chunk.end,
1236
+ });
1237
+ chunkOk += 1;
1238
+ }
1239
+ record.vector_chunks_total = chunks.length;
1240
+ record.vector_chunks_ok = chunkOk;
1241
+ record.embedding_status = chunkOk === chunks.length ? "ok" : "failed";
1242
+ if (!record.layer) {
1243
+ record.layer = item.layer;
1244
+ }
1245
+ if (typeof record.char_count !== "number") {
1246
+ record.char_count = text.length;
1247
+ }
1248
+ if (typeof record.token_count !== "number") {
1249
+ record.token_count = estimateTokenCount(text);
1250
+ }
1251
+ if (chunkOk === chunks.length) {
1252
+ success += 1;
1253
+ failureCounts[id] = 0;
1254
+ }
1255
+ else {
1256
+ failed += 1;
1257
+ failureCounts[id] = (failureCounts[id] || 0) + 1;
1258
+ }
1259
+ }
1260
+ catch (error) {
1261
+ record.embedding_status = "failed";
1262
+ failed += 1;
1263
+ failureCounts[id] = (failureCounts[id] || 0) + 1;
1264
+ deps.logger.warn(`backfill_embedding_failed id=${id} layer=${item.layer} error=${error}`);
1265
+ }
1266
+ }
1267
+ deps.logger.info(`backfill_progress processed=${processed}/${totalCandidates} success=${success} failed=${failed} skipped=${skipped}`);
1268
+ }
1269
+ for (const target of targetFiles) {
1270
+ const records = recordsByFile.get(target.filePath);
1271
+ if (records) {
1272
+ writeJsonl(target.filePath, records);
1273
+ }
1274
+ }
1275
+ const summary = {
1276
+ runAt: new Date().toISOString(),
1277
+ layer,
1278
+ rebuild_mode: rebuildMode,
1279
+ candidates: totalCandidates,
1280
+ success,
1281
+ failed,
1282
+ skipped,
1283
+ batch_size: batchSize,
1284
+ max_retries: maxRetries,
1285
+ retry_failed_only: retryFailedOnly,
1286
+ full_sync_result: fullSyncResult,
1287
+ };
1288
+ upsertJsonFile(statePath, {
1289
+ version: "1",
1290
+ lastRun: summary,
1291
+ failureCounts,
1292
+ });
1293
+ upsertJsonFile(syncStatePath, {
1294
+ version: "2",
1295
+ lastVectorBackfill: {
1296
+ runAt: summary.runAt,
1297
+ success,
1298
+ failed,
1299
+ skipped,
1300
+ },
1301
+ });
1302
+ return { success: true, data: summary };
1303
+ }
438
1304
  async function runDiagnostics(_args, _context) {
439
1305
  const { activePath, archivePath } = memoryFiles();
1306
+ const activeRecords = readJsonl(activePath);
1307
+ const archiveRecords = readJsonl(archivePath);
1308
+ const activeVector = embeddingStats(activeRecords);
1309
+ const archiveVector = embeddingStats(archiveRecords);
1310
+ const vectorJsonlPath = path.join(deps.memoryRoot, "vector", "lancedb_events.jsonl");
1311
+ const vectorJsonlRecords = readJsonl(vectorJsonlPath);
1312
+ const activeVectorRecords = vectorJsonlRecords.filter(record => (record.layer === "active"));
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");
1328
+ const syncState = parseJsonFile(path.join(deps.memoryRoot, ".sync_state.json"));
1329
+ const backfillState = parseJsonFile(path.join(deps.memoryRoot, ".vector_backfill_state.json"));
1330
+ const failureCounts = backfillState && typeof backfillState.failureCounts === "object" && backfillState.failureCounts !== null
1331
+ ? backfillState.failureCounts
1332
+ : {};
1333
+ const pendingRetry = Object.values(failureCounts).filter(value => typeof value === "number" && Number.isFinite(value) && value > 0).length;
1334
+ const lastVectorBackfill = syncState && typeof syncState.lastVectorBackfill === "object" && syncState.lastVectorBackfill !== null
1335
+ ? syncState.lastVectorBackfill
1336
+ : null;
1337
+ const embeddingConnectivity = await probeModelConnection({
1338
+ kind: "embedding",
1339
+ model: deps.embedding?.model || "",
1340
+ apiKey: deps.embedding?.apiKey || "",
1341
+ baseUrl: normalizeBaseUrl(deps.embedding?.baseURL || deps.embedding?.baseUrl),
1342
+ timeoutMs: deps.embedding?.timeoutMs,
1343
+ });
1344
+ const llmConnectivity = await probeModelConnection({
1345
+ kind: "llm",
1346
+ model: deps.llm?.model || "",
1347
+ apiKey: deps.llm?.apiKey || "",
1348
+ baseUrl: normalizeBaseUrl(deps.llm?.baseURL || deps.llm?.baseUrl),
1349
+ timeoutMs: 8000,
1350
+ });
1351
+ const rerankerConnectivity = await probeModelConnection({
1352
+ kind: "reranker",
1353
+ model: deps.reranker?.model || "",
1354
+ apiKey: deps.reranker?.apiKey || "",
1355
+ baseUrl: normalizeBaseUrl(deps.reranker?.baseURL || deps.reranker?.baseUrl),
1356
+ timeoutMs: 8000,
1357
+ });
440
1358
  const checks = [
441
1359
  { name: "Engine mode", passed: true, message: "TS engine active" },
442
1360
  { name: "Active sessions store", passed: fs.existsSync(activePath), message: activePath },
443
1361
  { name: "Archive sessions store", passed: fs.existsSync(archivePath), message: archivePath },
444
1362
  { name: "Core rules store", passed: fs.existsSync(path.join(deps.memoryRoot, "CORTEX_RULES.md")), message: "CORTEX_RULES.md checked" },
1363
+ { name: "Embedding model connectivity", passed: embeddingConnectivity.connected, message: embeddingConnectivity.error || "ok" },
1364
+ { name: "LLM model connectivity", passed: llmConnectivity.connected, message: llmConnectivity.error || "ok" },
1365
+ { name: "Reranker model connectivity", passed: rerankerConnectivity.connected, message: rerankerConnectivity.error || "ok" },
445
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
+ });
446
1527
  return {
447
1528
  success: true,
448
1529
  data: {
449
1530
  status: "ok",
1531
+ prompt_versions: PROMPT_VERSIONS,
450
1532
  checks,
451
- recommendations: [],
1533
+ layers: {
1534
+ active: {
1535
+ records: activeRecords.length,
1536
+ path: activePath,
1537
+ },
1538
+ archive: {
1539
+ records: archiveRecords.length,
1540
+ path: archivePath,
1541
+ },
1542
+ vector: {
1543
+ storage_type: vectorStorageType,
1544
+ lancedb_exists: lancedbExists,
1545
+ active_coverage: activeVector.coverage,
1546
+ archive_coverage: archiveVector.coverage,
1547
+ active_unembedded: activeVector.pending + activeVector.failed,
1548
+ archive_unembedded: archiveVector.pending + archiveVector.failed,
1549
+ chunking: {
1550
+ chunk_size: deps.vectorChunking?.chunkSize ?? 600,
1551
+ chunk_overlap: deps.vectorChunking?.chunkOverlap ?? 100,
1552
+ },
1553
+ vector_jsonl_records: vectorJsonlRecords.length,
1554
+ vector_jsonl_by_layer: {
1555
+ active: activeVectorRecords.length,
1556
+ archive: archiveVectorRecords.length,
1557
+ },
1558
+ total_vector_records: totalVectorRecords,
1559
+ last_backfill_summary: lastVectorBackfill,
1560
+ backfill_state: {
1561
+ pending_retry_records: pendingRetry,
1562
+ has_state_file: fs.existsSync(path.join(deps.memoryRoot, ".vector_backfill_state.json")),
1563
+ },
1564
+ },
1565
+ graph_rules: {
1566
+ graph_mutation_log_exists: fs.existsSync(path.join(deps.memoryRoot, "graph", "mutation_log.jsonl")),
1567
+ graph_conflicts: graphConflictStats,
1568
+ rules_exists: fs.existsSync(path.join(deps.memoryRoot, "CORTEX_RULES.md")),
1569
+ },
1570
+ },
1571
+ model_connectivity: {
1572
+ embedding: embeddingConnectivity,
1573
+ llm: llmConnectivity,
1574
+ reranker: rerankerConnectivity,
1575
+ },
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
+ ],
452
1610
  },
453
1611
  };
454
1612
  }
455
1613
  async function searchMemory(args, context) {
456
- 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) {
457
1626
  return {
458
1627
  success: false,
459
1628
  error: "Invalid input provided. Missing 'query' parameter.",
460
1629
  };
461
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");
462
1652
  const result = await deps.readStore.searchMemory({
463
- query: args.query,
464
- 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,
465
1657
  });
466
- 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
+ };
467
1670
  }
468
1671
  async function getHotContext(args, _context) {
469
1672
  const limit = typeof args.limit === "number" && args.limit > 0 ? Math.floor(args.limit) : 20;
@@ -471,12 +1674,22 @@ function createTsEngine(deps) {
471
1674
  return { success: true, data: result.context };
472
1675
  }
473
1676
  async function getAutoContext(args, context) {
474
- 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 || {}));
475
1687
  const cached = deps.getCachedAutoSearch(sessionId);
476
1688
  const result = await deps.readStore.getAutoContext({
477
- includeHot: args.include_hot !== false,
1689
+ includeHot: includeHotRaw !== false,
478
1690
  sessionId,
479
1691
  cachedAutoSearch: cached ?? undefined,
1692
+ recentMessages: getRecentSessionMessages(sessionId, 8),
480
1693
  });
481
1694
  if (!result.auto_search && !result.hot_context) {
482
1695
  return {
@@ -489,6 +1702,49 @@ function createTsEngine(deps) {
489
1702
  }
490
1703
  return { success: true, data: result };
491
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
+ }
492
1748
  async function syncMemory(_args, _context) {
493
1749
  try {
494
1750
  const result = await deps.sessionSync.syncMemory();
@@ -549,9 +1805,17 @@ function createTsEngine(deps) {
549
1805
  deps.logger.debug(`TS buffered ${role} message for session ${sessionId} source=${source}`);
550
1806
  if (role === "user" && text.length > 5) {
551
1807
  try {
552
- 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
+ });
553
1817
  if (searchResult.results.length > 0) {
554
- deps.setSessionAutoSearchCache(sessionId, text, searchResult.results);
1818
+ deps.setSessionAutoSearchCache(sessionId, query, searchResult.results);
555
1819
  deps.logger.info(`TS auto-search cached ${searchResult.results.length} results for context`);
556
1820
  }
557
1821
  }
@@ -582,12 +1846,17 @@ function createTsEngine(deps) {
582
1846
  getAutoContext,
583
1847
  storeEvent,
584
1848
  queryGraph,
1849
+ exportGraphView,
1850
+ lintMemoryWiki,
1851
+ listGraphConflicts,
1852
+ resolveGraphConflict,
585
1853
  reflectMemory,
586
1854
  syncMemory,
587
1855
  promoteMemory,
588
1856
  deleteMemory,
589
1857
  updateMemory,
590
1858
  cleanupMemories,
1859
+ backfillEmbeddings,
591
1860
  runDiagnostics,
592
1861
  onMessage,
593
1862
  onSessionEnd,