openclaw-cortex-memory 0.1.0-Alpha.2 → 0.1.0-Alpha.20

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 (65) hide show
  1. package/README.md +115 -90
  2. package/SKILL.md +96 -32
  3. package/dist/index.d.ts +52 -15
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +677 -1195
  6. package/dist/index.js.map +1 -1
  7. package/dist/openclaw.plugin.json +157 -5
  8. package/dist/src/dedup/three_stage_deduplicator.d.ts +25 -0
  9. package/dist/src/dedup/three_stage_deduplicator.d.ts.map +1 -0
  10. package/dist/src/dedup/three_stage_deduplicator.js +225 -0
  11. package/dist/src/dedup/three_stage_deduplicator.js.map +1 -0
  12. package/dist/src/engine/memory_engine.d.ts +2 -1
  13. package/dist/src/engine/memory_engine.d.ts.map +1 -1
  14. package/dist/src/engine/ts_engine.d.ts +95 -0
  15. package/dist/src/engine/ts_engine.d.ts.map +1 -1
  16. package/dist/src/engine/ts_engine.js +918 -38
  17. package/dist/src/engine/ts_engine.js.map +1 -1
  18. package/dist/src/engine/types.d.ts +11 -0
  19. package/dist/src/engine/types.d.ts.map +1 -1
  20. package/dist/src/graph/ontology.d.ts +53 -0
  21. package/dist/src/graph/ontology.d.ts.map +1 -0
  22. package/dist/src/graph/ontology.js +252 -0
  23. package/dist/src/graph/ontology.js.map +1 -0
  24. package/dist/src/reflect/reflector.d.ts +7 -0
  25. package/dist/src/reflect/reflector.d.ts.map +1 -1
  26. package/dist/src/reflect/reflector.js +75 -1
  27. package/dist/src/reflect/reflector.js.map +1 -1
  28. package/dist/src/session/session_end.d.ts +56 -0
  29. package/dist/src/session/session_end.d.ts.map +1 -1
  30. package/dist/src/session/session_end.js +270 -55
  31. package/dist/src/session/session_end.js.map +1 -1
  32. package/dist/src/store/archive_store.d.ts +115 -0
  33. package/dist/src/store/archive_store.d.ts.map +1 -0
  34. package/dist/src/store/archive_store.js +446 -0
  35. package/dist/src/store/archive_store.js.map +1 -0
  36. package/dist/src/store/embedding_utils.d.ts +32 -0
  37. package/dist/src/store/embedding_utils.d.ts.map +1 -0
  38. package/dist/src/store/embedding_utils.js +173 -0
  39. package/dist/src/store/embedding_utils.js.map +1 -0
  40. package/dist/src/store/read_store.d.ts +59 -0
  41. package/dist/src/store/read_store.d.ts.map +1 -1
  42. package/dist/src/store/read_store.js +1114 -17
  43. package/dist/src/store/read_store.js.map +1 -1
  44. package/dist/src/store/vector_store.d.ts +43 -0
  45. package/dist/src/store/vector_store.d.ts.map +1 -0
  46. package/dist/src/store/vector_store.js +200 -0
  47. package/dist/src/store/vector_store.js.map +1 -0
  48. package/dist/src/store/write_store.d.ts +45 -0
  49. package/dist/src/store/write_store.d.ts.map +1 -1
  50. package/dist/src/store/write_store.js +230 -0
  51. package/dist/src/store/write_store.js.map +1 -1
  52. package/dist/src/sync/session_sync.d.ts +52 -2
  53. package/dist/src/sync/session_sync.d.ts.map +1 -1
  54. package/dist/src/sync/session_sync.js +474 -22
  55. package/dist/src/sync/session_sync.js.map +1 -1
  56. package/dist/src/utils/runtime_env.d.ts +4 -0
  57. package/dist/src/utils/runtime_env.d.ts.map +1 -0
  58. package/dist/src/utils/runtime_env.js +51 -0
  59. package/dist/src/utils/runtime_env.js.map +1 -0
  60. package/openclaw.plugin.json +157 -5
  61. package/package.json +21 -6
  62. package/scripts/cli.js +19 -14
  63. package/scripts/uninstall.js +22 -5
  64. package/index.ts +0 -2092
  65. package/scripts/install.js +0 -27
@@ -36,7 +36,39 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.createTsEngine = createTsEngine;
37
37
  const fs = __importStar(require("fs"));
38
38
  const path = __importStar(require("path"));
39
+ const ontology_1 = require("../graph/ontology");
40
+ const 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",
44
+ };
39
45
  function createTsEngine(deps) {
46
+ const graphSchema = (0, ontology_1.loadGraphSchema)(deps.projectRoot);
47
+ const sessionMessageBuffer = new Map();
48
+ const maxMessagesPerSession = 500;
49
+ const maxBufferedSessions = 500;
50
+ function pushSessionMessage(sessionId, message) {
51
+ const current = sessionMessageBuffer.get(sessionId) || [];
52
+ current.push({
53
+ id: `msg_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`,
54
+ session_id: sessionId,
55
+ role: message.role,
56
+ content: message.text,
57
+ timestamp: new Date().toISOString(),
58
+ });
59
+ if (current.length > maxMessagesPerSession) {
60
+ sessionMessageBuffer.set(sessionId, current.slice(current.length - maxMessagesPerSession));
61
+ }
62
+ else {
63
+ sessionMessageBuffer.set(sessionId, current);
64
+ }
65
+ if (sessionMessageBuffer.size > maxBufferedSessions) {
66
+ const first = sessionMessageBuffer.keys().next().value;
67
+ if (first) {
68
+ sessionMessageBuffer.delete(first);
69
+ }
70
+ }
71
+ }
40
72
  function asRecord(value) {
41
73
  if (typeof value === "object" && value !== null) {
42
74
  return value;
@@ -71,25 +103,397 @@ function createTsEngine(deps) {
71
103
  archivePath: path.join(deps.memoryRoot, "sessions", "archive", "sessions.jsonl"),
72
104
  };
73
105
  }
106
+ function parseJsonFile(filePath) {
107
+ try {
108
+ if (!fs.existsSync(filePath)) {
109
+ return null;
110
+ }
111
+ const raw = fs.readFileSync(filePath, "utf-8").trim();
112
+ if (!raw) {
113
+ return null;
114
+ }
115
+ return JSON.parse(raw);
116
+ }
117
+ catch {
118
+ return null;
119
+ }
120
+ }
121
+ function embeddingStats(records) {
122
+ let ok = 0;
123
+ let failed = 0;
124
+ let pending = 0;
125
+ for (const record of records) {
126
+ const explicit = typeof record.embedding_status === "string" ? record.embedding_status.trim() : "";
127
+ const hasEmbedding = Array.isArray(record.embedding) && record.embedding.length > 0;
128
+ if (explicit === "ok" || hasEmbedding) {
129
+ ok += 1;
130
+ }
131
+ else if (explicit === "failed") {
132
+ failed += 1;
133
+ }
134
+ else {
135
+ pending += 1;
136
+ }
137
+ }
138
+ const total = records.length;
139
+ const coverage = total > 0 ? Number((ok / total).toFixed(4)) : 0;
140
+ return { total, ok, failed, pending, coverage };
141
+ }
142
+ function normalizeBaseUrl(value) {
143
+ if (!value)
144
+ return "";
145
+ return value.endsWith("/") ? value.slice(0, -1) : value;
146
+ }
147
+ function estimateTokenCount(text) {
148
+ const parts = text
149
+ .split(/[\s,.;:!?,。;:!?、()()[\]{}"'`~]+/)
150
+ .map(part => part.trim())
151
+ .filter(Boolean);
152
+ return parts.length;
153
+ }
154
+ function buildVectorSourceText(record, layer) {
155
+ 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;
160
+ }
161
+ 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();
194
+ }
195
+ function splitTextChunks(text, chunkSize, chunkOverlap) {
196
+ const normalizedSize = Number.isFinite(chunkSize) && chunkSize >= 200 ? Math.floor(chunkSize) : 600;
197
+ const normalizedOverlap = Number.isFinite(chunkOverlap) && chunkOverlap >= 0
198
+ ? Math.floor(chunkOverlap)
199
+ : 100;
200
+ const overlap = Math.min(normalizedOverlap, Math.max(0, normalizedSize - 50));
201
+ const output = [];
202
+ let cursor = 0;
203
+ let index = 0;
204
+ const punctuationSet = new Set(["。", "!", "?", ".", "!", "?", "\n", ";", ";"]);
205
+ while (cursor < text.length) {
206
+ const rawEnd = Math.min(text.length, cursor + normalizedSize);
207
+ let end = rawEnd;
208
+ if (rawEnd < text.length) {
209
+ const backwardStart = Math.max(cursor + Math.floor(normalizedSize * 0.45), cursor + 1);
210
+ let found = -1;
211
+ for (let i = rawEnd - 1; i >= backwardStart; i -= 1) {
212
+ if (punctuationSet.has(text[i])) {
213
+ found = i + 1;
214
+ break;
215
+ }
216
+ }
217
+ if (found < 0) {
218
+ const forwardEnd = Math.min(text.length, rawEnd + Math.floor(normalizedSize * 0.2));
219
+ for (let i = rawEnd; i < forwardEnd; i += 1) {
220
+ if (punctuationSet.has(text[i])) {
221
+ found = i + 1;
222
+ break;
223
+ }
224
+ }
225
+ }
226
+ if (found > cursor) {
227
+ end = found;
228
+ }
229
+ }
230
+ if (end <= cursor) {
231
+ end = Math.min(text.length, cursor + normalizedSize);
232
+ }
233
+ const chunkText = text.slice(cursor, end).trim();
234
+ if (chunkText) {
235
+ output.push({ index, start: cursor, end, text: chunkText });
236
+ index += 1;
237
+ }
238
+ if (end >= text.length) {
239
+ break;
240
+ }
241
+ const nextCursor = Math.max(cursor + 1, end - overlap);
242
+ cursor = nextCursor <= cursor ? end : nextCursor;
243
+ }
244
+ return output;
245
+ }
246
+ function upsertJsonFile(filePath, patch) {
247
+ const current = parseJsonFile(filePath) || {};
248
+ const next = { ...current, ...patch };
249
+ const dir = path.dirname(filePath);
250
+ if (!fs.existsSync(dir)) {
251
+ fs.mkdirSync(dir, { recursive: true });
252
+ }
253
+ fs.writeFileSync(filePath, JSON.stringify(next, null, 2), "utf-8");
254
+ }
255
+ async function probeModelConnection(args) {
256
+ const defaultTimeoutMs = args.kind === "llm" ? 30000 : 15000;
257
+ const timeoutMs = typeof args.timeoutMs === "number" && Number.isFinite(args.timeoutMs) && args.timeoutMs >= 1000
258
+ ? Math.floor(args.timeoutMs)
259
+ : defaultTimeoutMs;
260
+ if (!args.model || !args.apiKey || !args.baseUrl) {
261
+ return {
262
+ configured: false,
263
+ connected: false,
264
+ model: args.model || "",
265
+ base_url: args.baseUrl || "",
266
+ error: "not_configured",
267
+ };
268
+ }
269
+ let endpoint = args.baseUrl;
270
+ let payload = {};
271
+ if (args.kind === "embedding") {
272
+ endpoint = args.baseUrl.endsWith("/embeddings") ? args.baseUrl : `${args.baseUrl}/embeddings`;
273
+ payload = {
274
+ model: args.model,
275
+ input: "diagnostics connectivity probe",
276
+ };
277
+ }
278
+ else if (args.kind === "llm") {
279
+ endpoint = args.baseUrl.endsWith("/chat/completions") ? args.baseUrl : `${args.baseUrl}/chat/completions`;
280
+ payload = {
281
+ model: args.model,
282
+ messages: [{ role: "user", content: "ping" }],
283
+ max_tokens: 4,
284
+ temperature: 0,
285
+ stream: false,
286
+ };
287
+ }
288
+ else {
289
+ endpoint = args.baseUrl.endsWith("/rerank") ? args.baseUrl : `${args.baseUrl}/rerank`;
290
+ payload = {
291
+ model: args.model,
292
+ query: "diagnostics",
293
+ documents: ["diagnostics connectivity probe"],
294
+ top_n: 1,
295
+ };
296
+ }
297
+ let lastError = "unknown_error";
298
+ const maxAttempts = args.kind === "llm" ? 3 : 1;
299
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
300
+ const controller = new AbortController();
301
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
302
+ try {
303
+ const response = await fetch(endpoint, {
304
+ method: "POST",
305
+ headers: {
306
+ accept: "application/json",
307
+ "content-type": "application/json",
308
+ authorization: `Bearer ${args.apiKey}`,
309
+ },
310
+ body: JSON.stringify(payload),
311
+ signal: controller.signal,
312
+ });
313
+ clearTimeout(timeoutId);
314
+ if (response.ok) {
315
+ return {
316
+ configured: true,
317
+ connected: true,
318
+ model: args.model,
319
+ base_url: args.baseUrl,
320
+ error: "",
321
+ };
322
+ }
323
+ let details = "";
324
+ try {
325
+ const text = (await response.text()).trim();
326
+ if (text) {
327
+ details = text.slice(0, 180);
328
+ }
329
+ }
330
+ catch {
331
+ details = "";
332
+ }
333
+ lastError = details ? `http_${response.status}:${details}` : `http_${response.status}`;
334
+ }
335
+ catch (error) {
336
+ const raw = error instanceof Error ? error.message : String(error);
337
+ if (error?.name === "AbortError" || /aborted/i.test(raw)) {
338
+ lastError = `timeout_${timeoutMs}ms`;
339
+ }
340
+ else {
341
+ lastError = raw;
342
+ }
343
+ }
344
+ finally {
345
+ clearTimeout(timeoutId);
346
+ }
347
+ }
348
+ return {
349
+ configured: true,
350
+ connected: false,
351
+ model: args.model,
352
+ base_url: args.baseUrl,
353
+ error: lastError,
354
+ };
355
+ }
356
+ async function requestEmbedding(args) {
357
+ const endpoint = args.baseUrl.endsWith("/embeddings") ? args.baseUrl : `${args.baseUrl}/embeddings`;
358
+ const body = {
359
+ input: args.text,
360
+ model: args.model,
361
+ };
362
+ if (typeof args.dimensions === "number" && Number.isFinite(args.dimensions) && args.dimensions > 0) {
363
+ body.dimensions = args.dimensions;
364
+ }
365
+ const timeoutMs = typeof args.timeoutMs === "number" && Number.isFinite(args.timeoutMs) && args.timeoutMs >= 1000
366
+ ? Math.floor(args.timeoutMs)
367
+ : 20000;
368
+ const maxRetries = typeof args.maxRetries === "number" && Number.isFinite(args.maxRetries) && args.maxRetries >= 1
369
+ ? Math.min(8, Math.floor(args.maxRetries))
370
+ : 4;
371
+ let lastError = null;
372
+ for (let attempt = 0; attempt < maxRetries; attempt += 1) {
373
+ const controller = new AbortController();
374
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
375
+ try {
376
+ const response = await fetch(endpoint, {
377
+ method: "POST",
378
+ headers: {
379
+ "content-type": "application/json",
380
+ authorization: `Bearer ${args.apiKey}`,
381
+ },
382
+ body: JSON.stringify(body),
383
+ signal: controller.signal,
384
+ });
385
+ clearTimeout(timeoutId);
386
+ if (!response.ok) {
387
+ lastError = new Error(`embedding_http_${response.status}`);
388
+ continue;
389
+ }
390
+ const json = await response.json();
391
+ const embedding = json?.data?.[0]?.embedding;
392
+ if (Array.isArray(embedding) && embedding.length > 0) {
393
+ return embedding.filter(item => Number.isFinite(item));
394
+ }
395
+ lastError = new Error("embedding_empty");
396
+ }
397
+ catch (error) {
398
+ clearTimeout(timeoutId);
399
+ lastError = error;
400
+ }
401
+ if (attempt < maxRetries - 1) {
402
+ await new Promise(resolve => setTimeout(resolve, 300 * Math.pow(2, attempt)));
403
+ }
404
+ }
405
+ throw lastError instanceof Error ? lastError : new Error(String(lastError || "embedding_failed"));
406
+ }
74
407
  async function storeEvent(args, _context) {
75
408
  try {
76
- if (!args.summary?.trim()) {
409
+ const rawArgs = args;
410
+ const summaryCandidate = typeof rawArgs?.summary === "string"
411
+ ? rawArgs.summary
412
+ : typeof rawArgs?.input?.summary === "string"
413
+ ? String(rawArgs.input.summary)
414
+ : typeof rawArgs?.event?.summary === "string"
415
+ ? String(rawArgs.event.summary)
416
+ : "";
417
+ const normalizedSummary = summaryCandidate.trim();
418
+ if (!normalizedSummary) {
77
419
  return { success: false, error: "Invalid input provided. Missing 'summary' parameter." };
78
420
  }
79
- const { archivePath } = memoryFiles();
80
- const records = readJsonl(archivePath);
81
- const id = `evt_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
82
- records.push({
83
- id,
84
- timestamp: new Date().toISOString(),
85
- summary: args.summary.trim(),
86
- entities: args.entities ?? [],
87
- outcome: args.outcome ?? "",
88
- relations: args.relations ?? [],
89
- source_file: "ts_store_event",
90
- });
91
- writeJsonl(archivePath, records);
92
- return { success: true, data: { event_id: id } };
421
+ const entityInput = Array.isArray(rawArgs.entities)
422
+ ? rawArgs.entities
423
+ : Array.isArray(rawArgs.input?.entities)
424
+ ? rawArgs.input.entities
425
+ : Array.isArray(rawArgs.event?.entities)
426
+ ? rawArgs.event.entities
427
+ : [];
428
+ const entities = Array.isArray(entityInput)
429
+ ? entityInput.map(item => {
430
+ if (typeof item === "string") {
431
+ return item.trim();
432
+ }
433
+ if (item && typeof item === "object") {
434
+ const value = (item.name || item.id || "");
435
+ return typeof value === "string" ? value.trim() : "";
436
+ }
437
+ return "";
438
+ }).filter(Boolean)
439
+ : [];
440
+ const relationInput = Array.isArray(rawArgs.relations)
441
+ ? rawArgs.relations
442
+ : Array.isArray(rawArgs.input?.relations)
443
+ ? rawArgs.input.relations
444
+ : Array.isArray(rawArgs.event?.relations)
445
+ ? rawArgs.event.relations
446
+ : [];
447
+ const relations = Array.isArray(relationInput)
448
+ ? relationInput
449
+ .map(item => {
450
+ if (typeof item === "string") {
451
+ const [sourceRaw, typeRaw, targetRaw] = item.split("|");
452
+ const source = (sourceRaw || "").trim();
453
+ const target = (targetRaw || "").trim();
454
+ const type = (0, ontology_1.normalizeRelationType)((typeRaw || "related_to").trim(), graphSchema);
455
+ if (!source || !target)
456
+ return null;
457
+ return { source, target, type };
458
+ }
459
+ if (!item || typeof item !== "object")
460
+ return null;
461
+ const relation = item;
462
+ if (!relation.source || !relation.target)
463
+ return null;
464
+ return {
465
+ source: relation.source.trim(),
466
+ target: relation.target.trim(),
467
+ type: (0, ontology_1.normalizeRelationType)(relation.type || "related_to", graphSchema),
468
+ };
469
+ })
470
+ .filter((item) => Boolean(item))
471
+ : [];
472
+ const outcomeValue = typeof rawArgs.outcome === "string"
473
+ ? rawArgs.outcome
474
+ : typeof rawArgs.input?.outcome === "string"
475
+ ? String(rawArgs.input.outcome)
476
+ : typeof rawArgs.event?.outcome === "string"
477
+ ? String(rawArgs.event.outcome)
478
+ : "";
479
+ const result = await deps.archiveStore.storeEvents([
480
+ {
481
+ event_type: "manual_event",
482
+ summary: normalizedSummary,
483
+ entities,
484
+ relations,
485
+ outcome: outcomeValue,
486
+ session_id: "manual",
487
+ source_file: "ts_store_event",
488
+ confidence: 1,
489
+ source_event_id: "",
490
+ actor: "manual_tool",
491
+ },
492
+ ]);
493
+ if (result.stored.length === 0) {
494
+ return { success: false, error: result.skipped[0]?.reason || "store_event_skipped" };
495
+ }
496
+ return { success: true, data: { event_id: result.stored[0].id } };
93
497
  }
94
498
  catch (error) {
95
499
  return { success: false, error: String(error) };
@@ -100,13 +504,95 @@ function createTsEngine(deps) {
100
504
  if (!entity) {
101
505
  return { success: false, error: "Invalid input provided. Missing 'entity' parameter." };
102
506
  }
507
+ const relFilter = typeof args.rel === "string" && args.rel.trim()
508
+ ? (0, ontology_1.normalizeRelationType)(args.rel, graphSchema)
509
+ : "";
510
+ const direction = args.dir === "incoming" || args.dir === "outgoing" || args.dir === "both"
511
+ ? args.dir
512
+ : "both";
513
+ const pathTo = typeof args.path_to === "string" && args.path_to.trim() ? args.path_to.trim() : "";
514
+ const maxDepth = Math.max(2, Math.min(4, typeof args.max_depth === "number" ? Math.floor(args.max_depth) : 3));
103
515
  const { archivePath } = memoryFiles();
104
516
  const records = readJsonl(archivePath);
105
517
  const nodes = new Map();
106
518
  const edges = [];
519
+ const adjacency = new Map();
520
+ const pathAdjacency = new Map();
521
+ const relationTypeDistribution = new Map();
522
+ const edgeKeySet = new Set();
523
+ function pushEdge(source, target, type) {
524
+ const key = `${source}|${type}|${target}`;
525
+ if (edgeKeySet.has(key)) {
526
+ return;
527
+ }
528
+ edgeKeySet.add(key);
529
+ edges.push({ source, target, type });
530
+ relationTypeDistribution.set(type, (relationTypeDistribution.get(type) || 0) + 1);
531
+ if (!adjacency.has(source)) {
532
+ adjacency.set(source, []);
533
+ }
534
+ adjacency.get(source)?.push({ next: target, edge: { source, target, type } });
535
+ if (!adjacency.has(target)) {
536
+ adjacency.set(target, []);
537
+ }
538
+ adjacency.get(target)?.push({ next: source, edge: { source, target, type } });
539
+ }
540
+ function pushPathEdge(source, target, type) {
541
+ if (!pathAdjacency.has(source)) {
542
+ pathAdjacency.set(source, []);
543
+ }
544
+ if (!pathAdjacency.has(target)) {
545
+ pathAdjacency.set(target, []);
546
+ }
547
+ if (direction === "incoming") {
548
+ pathAdjacency.get(target)?.push({ next: source, edge: { source, target, type } });
549
+ }
550
+ else if (direction === "outgoing") {
551
+ pathAdjacency.get(source)?.push({ next: target, edge: { source, target, type } });
552
+ }
553
+ else {
554
+ pathAdjacency.get(source)?.push({ next: target, edge: { source, target, type } });
555
+ pathAdjacency.get(target)?.push({ next: source, edge: { source, target, type } });
556
+ }
557
+ }
107
558
  for (const record of records) {
108
559
  const entities = Array.isArray(record.entities) ? record.entities : [];
109
560
  const named = entities.map(e => (typeof e === "string" ? e.trim() : "")).filter(Boolean);
561
+ const relations = Array.isArray(record.relations) ? record.relations : [];
562
+ let explicitMatched = false;
563
+ for (const relationRaw of relations) {
564
+ if (typeof relationRaw !== "object" || relationRaw === null) {
565
+ continue;
566
+ }
567
+ const relation = relationRaw;
568
+ const source = typeof relation.source === "string" ? relation.source.trim() : "";
569
+ const target = typeof relation.target === "string" ? relation.target.trim() : "";
570
+ const type = (0, ontology_1.normalizeRelationType)(typeof relation.type === "string" && relation.type.trim() ? relation.type.trim() : "related_to", graphSchema);
571
+ if (!source || !target) {
572
+ continue;
573
+ }
574
+ if (relFilter && type !== relFilter) {
575
+ continue;
576
+ }
577
+ pushPathEdge(source, target, type);
578
+ const outgoingMatch = source === entity;
579
+ const incomingMatch = target === entity;
580
+ const directionMatched = direction === "both" ? (outgoingMatch || incomingMatch)
581
+ : direction === "outgoing" ? outgoingMatch
582
+ : incomingMatch;
583
+ if (!directionMatched) {
584
+ continue;
585
+ }
586
+ explicitMatched = true;
587
+ if (!nodes.has(source))
588
+ nodes.set(source, { id: source, type: "entity" });
589
+ if (!nodes.has(target))
590
+ nodes.set(target, { id: target, type: "entity" });
591
+ pushEdge(source, target, type);
592
+ }
593
+ if (explicitMatched) {
594
+ continue;
595
+ }
110
596
  if (!named.includes(entity)) {
111
597
  continue;
112
598
  }
@@ -117,7 +603,40 @@ function createTsEngine(deps) {
117
603
  }
118
604
  for (const name of named) {
119
605
  if (name !== entity) {
120
- edges.push({ source: entity, target: name, type: "co_occurrence" });
606
+ if (!relFilter || relFilter === "co_occurrence") {
607
+ pushEdge(entity, name, "co_occurrence");
608
+ }
609
+ }
610
+ }
611
+ }
612
+ let path = [];
613
+ if (pathTo) {
614
+ const visited = new Set();
615
+ const queue = [
616
+ { node: entity, depth: 0, pathEdges: [] },
617
+ ];
618
+ while (queue.length > 0) {
619
+ const current = queue.shift();
620
+ if (!current)
621
+ break;
622
+ if (current.node === pathTo) {
623
+ path = current.pathEdges;
624
+ break;
625
+ }
626
+ if (current.depth >= maxDepth) {
627
+ continue;
628
+ }
629
+ const visitKey = `${current.node}:${current.depth}`;
630
+ if (visited.has(visitKey)) {
631
+ continue;
632
+ }
633
+ visited.add(visitKey);
634
+ for (const next of pathAdjacency.get(current.node) || []) {
635
+ queue.push({
636
+ node: next.next,
637
+ depth: current.depth + 1,
638
+ pathEdges: [...current.pathEdges, next.edge],
639
+ });
121
640
  }
122
641
  }
123
642
  }
@@ -125,8 +644,14 @@ function createTsEngine(deps) {
125
644
  success: true,
126
645
  data: {
127
646
  entity,
647
+ rel: relFilter || "",
648
+ dir: direction,
128
649
  nodes: [...nodes.values()],
129
650
  edges,
651
+ path_to: pathTo || "",
652
+ max_depth: maxDepth,
653
+ path,
654
+ relation_type_distribution: [...relationTypeDistribution.entries()].map(([type, count]) => ({ type, count })),
130
655
  },
131
656
  };
132
657
  }
@@ -216,33 +741,390 @@ function createTsEngine(deps) {
216
741
  }
217
742
  return { success: true, data: { deletedCount } };
218
743
  }
744
+ async function backfillEmbeddings(args, _context) {
745
+ const layer = args.layer === "active" || args.layer === "archive" || args.layer === "all" ? args.layer : "all";
746
+ const rebuildMode = args.rebuild_mode === "vector_only" || args.rebuild_mode === "full"
747
+ ? args.rebuild_mode
748
+ : "incremental";
749
+ const batchSize = typeof args.batch_size === "number" && Number.isFinite(args.batch_size) && args.batch_size > 0
750
+ ? Math.min(500, Math.floor(args.batch_size))
751
+ : 100;
752
+ const maxRetries = typeof args.max_retries === "number" && Number.isFinite(args.max_retries) && args.max_retries >= 1
753
+ ? Math.min(10, Math.floor(args.max_retries))
754
+ : 3;
755
+ const retryFailedOnly = args.retry_failed_only === true;
756
+ const forceRebuild = rebuildMode === "vector_only" || rebuildMode === "full";
757
+ const model = deps.embedding?.model || "";
758
+ const apiKey = deps.embedding?.apiKey || "";
759
+ const baseUrl = normalizeBaseUrl(deps.embedding?.baseURL || deps.embedding?.baseUrl);
760
+ if (!model || !apiKey || !baseUrl) {
761
+ return { success: false, error: "Embedding config missing for backfill tool." };
762
+ }
763
+ const statePath = path.join(deps.memoryRoot, ".vector_backfill_state.json");
764
+ const syncStatePath = path.join(deps.memoryRoot, ".sync_state.json");
765
+ const previousState = parseJsonFile(statePath) || {};
766
+ const failureCountState = (typeof previousState.failureCounts === "object" && previousState.failureCounts !== null)
767
+ ? previousState.failureCounts
768
+ : {};
769
+ let fullSyncResult = null;
770
+ if (rebuildMode === "full") {
771
+ try {
772
+ fullSyncResult = await deps.sessionSync.syncMemory();
773
+ }
774
+ catch (error) {
775
+ deps.logger.warn(`backfill_full_rebuild_sync_failed error=${error}`);
776
+ }
777
+ }
778
+ const { activePath, archivePath } = memoryFiles();
779
+ const targetFiles = [];
780
+ if (layer === "all" || layer === "active") {
781
+ targetFiles.push({ layer: "active", filePath: activePath });
782
+ }
783
+ if (layer === "all" || layer === "archive") {
784
+ targetFiles.push({ layer: "archive", filePath: archivePath });
785
+ }
786
+ const queue = [];
787
+ const recordsByFile = new Map();
788
+ for (const target of targetFiles) {
789
+ const records = readJsonl(target.filePath);
790
+ recordsByFile.set(target.filePath, records);
791
+ for (let i = 0; i < records.length; i += 1) {
792
+ const record = records[i];
793
+ const id = typeof record.id === "string" ? record.id : "";
794
+ if (!id) {
795
+ continue;
796
+ }
797
+ const status = typeof record.embedding_status === "string" ? record.embedding_status.trim() : "";
798
+ const hasEmbedding = Array.isArray(record.embedding) && record.embedding.length > 0;
799
+ if (forceRebuild) {
800
+ queue.push({ layer: target.layer, filePath: target.filePath, index: i });
801
+ continue;
802
+ }
803
+ if (retryFailedOnly) {
804
+ if (status !== "failed") {
805
+ continue;
806
+ }
807
+ }
808
+ else if (status === "ok" || hasEmbedding) {
809
+ continue;
810
+ }
811
+ const failCountRaw = failureCountState[id];
812
+ const failCount = typeof failCountRaw === "number" ? failCountRaw : 0;
813
+ if (failCount >= maxRetries && status === "failed") {
814
+ continue;
815
+ }
816
+ queue.push({ layer: target.layer, filePath: target.filePath, index: i });
817
+ }
818
+ }
819
+ const totalCandidates = queue.length;
820
+ let success = 0;
821
+ let failed = 0;
822
+ let skipped = 0;
823
+ let processed = 0;
824
+ const failureCounts = {};
825
+ for (const [key, value] of Object.entries(failureCountState)) {
826
+ if (typeof value === "number" && Number.isFinite(value)) {
827
+ failureCounts[key] = value;
828
+ }
829
+ }
830
+ for (let start = 0; start < queue.length; start += batchSize) {
831
+ const batch = queue.slice(start, start + batchSize);
832
+ for (const item of batch) {
833
+ processed += 1;
834
+ const records = recordsByFile.get(item.filePath) || [];
835
+ const record = records[item.index];
836
+ if (!record) {
837
+ skipped += 1;
838
+ continue;
839
+ }
840
+ const id = typeof record.id === "string" ? record.id : "";
841
+ if (!id) {
842
+ skipped += 1;
843
+ continue;
844
+ }
845
+ const text = buildVectorSourceText(record, item.layer);
846
+ if (!text) {
847
+ record.embedding_status = "failed";
848
+ failed += 1;
849
+ failureCounts[id] = (failureCounts[id] || 0) + 1;
850
+ continue;
851
+ }
852
+ const chunkSize = deps.vectorChunking?.chunkSize ?? 600;
853
+ const chunkOverlap = deps.vectorChunking?.chunkOverlap ?? 100;
854
+ const chunks = splitTextChunks(text, chunkSize, chunkOverlap);
855
+ if (chunks.length === 0) {
856
+ record.embedding_status = "failed";
857
+ failed += 1;
858
+ failureCounts[id] = (failureCounts[id] || 0) + 1;
859
+ continue;
860
+ }
861
+ try {
862
+ if (forceRebuild) {
863
+ record.embedding_status = "pending";
864
+ }
865
+ await deps.vectorStore.deleteBySourceMemory({ layer: item.layer, sourceMemoryId: id });
866
+ let chunkOk = 0;
867
+ for (const chunk of chunks) {
868
+ const embedding = await requestEmbedding({
869
+ text: chunk.text,
870
+ model,
871
+ apiKey,
872
+ baseUrl,
873
+ dimensions: deps.embedding?.dimensions,
874
+ timeoutMs: deps.embedding?.timeoutMs,
875
+ maxRetries: deps.embedding?.maxRetries,
876
+ });
877
+ if (!embedding || embedding.length === 0) {
878
+ continue;
879
+ }
880
+ if (!record.embedding) {
881
+ record.embedding = embedding;
882
+ }
883
+ await deps.vectorStore.upsert({
884
+ id: `${id}_c${chunk.index}`,
885
+ session_id: typeof record.session_id === "string" ? record.session_id : "unknown",
886
+ event_type: typeof record.event_type === "string" ? record.event_type : (item.layer === "active" ? "message" : "insight"),
887
+ summary: chunk.text,
888
+ timestamp: typeof record.timestamp === "string" ? record.timestamp : new Date().toISOString(),
889
+ layer: item.layer,
890
+ source_memory_id: id,
891
+ source_memory_canonical_id: typeof record.canonical_id === "string" ? record.canonical_id : id,
892
+ outcome: typeof record.outcome === "string" ? record.outcome : "",
893
+ entities: Array.isArray(record.entities) ? record.entities.filter(v => typeof v === "string") : [],
894
+ relations: Array.isArray(record.relations)
895
+ ? record.relations
896
+ .map(v => {
897
+ if (!v || typeof v !== "object")
898
+ return null;
899
+ const relation = v;
900
+ const source = typeof relation.source === "string" ? relation.source : "";
901
+ const target = typeof relation.target === "string" ? relation.target : "";
902
+ const type = typeof relation.type === "string" ? relation.type : "related_to";
903
+ if (!source || !target)
904
+ return null;
905
+ return { source, target, type };
906
+ })
907
+ .filter((v) => Boolean(v))
908
+ : [],
909
+ embedding,
910
+ quality_score: typeof record.quality_score === "number" ? record.quality_score : 0.5,
911
+ char_count: chunk.text.length,
912
+ token_count: estimateTokenCount(chunk.text),
913
+ chunk_index: chunk.index,
914
+ chunk_total: chunks.length,
915
+ chunk_start: chunk.start,
916
+ chunk_end: chunk.end,
917
+ });
918
+ chunkOk += 1;
919
+ }
920
+ record.vector_chunks_total = chunks.length;
921
+ record.vector_chunks_ok = chunkOk;
922
+ record.embedding_status = chunkOk === chunks.length ? "ok" : "failed";
923
+ if (!record.layer) {
924
+ record.layer = item.layer;
925
+ }
926
+ if (typeof record.char_count !== "number") {
927
+ record.char_count = text.length;
928
+ }
929
+ if (typeof record.token_count !== "number") {
930
+ record.token_count = estimateTokenCount(text);
931
+ }
932
+ if (chunkOk === chunks.length) {
933
+ success += 1;
934
+ failureCounts[id] = 0;
935
+ }
936
+ else {
937
+ failed += 1;
938
+ failureCounts[id] = (failureCounts[id] || 0) + 1;
939
+ }
940
+ }
941
+ catch (error) {
942
+ record.embedding_status = "failed";
943
+ failed += 1;
944
+ failureCounts[id] = (failureCounts[id] || 0) + 1;
945
+ deps.logger.warn(`backfill_embedding_failed id=${id} layer=${item.layer} error=${error}`);
946
+ }
947
+ }
948
+ deps.logger.info(`backfill_progress processed=${processed}/${totalCandidates} success=${success} failed=${failed} skipped=${skipped}`);
949
+ }
950
+ for (const target of targetFiles) {
951
+ const records = recordsByFile.get(target.filePath);
952
+ if (records) {
953
+ writeJsonl(target.filePath, records);
954
+ }
955
+ }
956
+ const summary = {
957
+ runAt: new Date().toISOString(),
958
+ layer,
959
+ rebuild_mode: rebuildMode,
960
+ candidates: totalCandidates,
961
+ success,
962
+ failed,
963
+ skipped,
964
+ batch_size: batchSize,
965
+ max_retries: maxRetries,
966
+ retry_failed_only: retryFailedOnly,
967
+ full_sync_result: fullSyncResult,
968
+ };
969
+ upsertJsonFile(statePath, {
970
+ version: "1",
971
+ lastRun: summary,
972
+ failureCounts,
973
+ });
974
+ upsertJsonFile(syncStatePath, {
975
+ version: "2",
976
+ lastVectorBackfill: {
977
+ runAt: summary.runAt,
978
+ success,
979
+ failed,
980
+ skipped,
981
+ },
982
+ });
983
+ return { success: true, data: summary };
984
+ }
219
985
  async function runDiagnostics(_args, _context) {
220
986
  const { activePath, archivePath } = memoryFiles();
987
+ const activeRecords = readJsonl(activePath);
988
+ const archiveRecords = readJsonl(archivePath);
989
+ const activeVector = embeddingStats(activeRecords);
990
+ const archiveVector = embeddingStats(archiveRecords);
991
+ const vectorJsonlPath = path.join(deps.memoryRoot, "vector", "lancedb_events.jsonl");
992
+ const vectorJsonlRecords = readJsonl(vectorJsonlPath);
993
+ const activeVectorRecords = vectorJsonlRecords.filter(record => (record.layer === "active"));
994
+ const archiveVectorRecords = vectorJsonlRecords.filter(record => (record.layer === "archive"));
995
+ const lancedbDir = path.join(deps.memoryRoot, "vector", "lancedb");
996
+ const lancedbExists = fs.existsSync(lancedbDir);
997
+ let lancedbRecordCount = 0;
998
+ if (lancedbExists) {
999
+ try {
1000
+ const lancedbFiles = fs.readdirSync(lancedbDir).filter(f => f.endsWith(".lance") || f.endsWith(".manifest"));
1001
+ lancedbRecordCount = lancedbFiles.length > 0 ? -1 : 0;
1002
+ }
1003
+ catch {
1004
+ lancedbRecordCount = 0;
1005
+ }
1006
+ }
1007
+ const totalVectorRecords = vectorJsonlRecords.length > 0 ? vectorJsonlRecords.length : (lancedbRecordCount === -1 ? -1 : 0);
1008
+ const vectorStorageType = lancedbExists && lancedbRecordCount === -1 ? "lancedb" : (vectorJsonlRecords.length > 0 ? "jsonl" : "none");
1009
+ const syncState = parseJsonFile(path.join(deps.memoryRoot, ".sync_state.json"));
1010
+ const backfillState = parseJsonFile(path.join(deps.memoryRoot, ".vector_backfill_state.json"));
1011
+ const failureCounts = backfillState && typeof backfillState.failureCounts === "object" && backfillState.failureCounts !== null
1012
+ ? backfillState.failureCounts
1013
+ : {};
1014
+ const pendingRetry = Object.values(failureCounts).filter(value => typeof value === "number" && Number.isFinite(value) && value > 0).length;
1015
+ const lastVectorBackfill = syncState && typeof syncState.lastVectorBackfill === "object" && syncState.lastVectorBackfill !== null
1016
+ ? syncState.lastVectorBackfill
1017
+ : null;
1018
+ const embeddingConnectivity = await probeModelConnection({
1019
+ kind: "embedding",
1020
+ model: deps.embedding?.model || "",
1021
+ apiKey: deps.embedding?.apiKey || "",
1022
+ baseUrl: normalizeBaseUrl(deps.embedding?.baseURL || deps.embedding?.baseUrl),
1023
+ timeoutMs: deps.embedding?.timeoutMs,
1024
+ });
1025
+ const llmConnectivity = await probeModelConnection({
1026
+ kind: "llm",
1027
+ model: deps.llm?.model || "",
1028
+ apiKey: deps.llm?.apiKey || "",
1029
+ baseUrl: normalizeBaseUrl(deps.llm?.baseURL || deps.llm?.baseUrl),
1030
+ timeoutMs: 8000,
1031
+ });
1032
+ const rerankerConnectivity = await probeModelConnection({
1033
+ kind: "reranker",
1034
+ model: deps.reranker?.model || "",
1035
+ apiKey: deps.reranker?.apiKey || "",
1036
+ baseUrl: normalizeBaseUrl(deps.reranker?.baseURL || deps.reranker?.baseUrl),
1037
+ timeoutMs: 8000,
1038
+ });
221
1039
  const checks = [
222
1040
  { name: "Engine mode", passed: true, message: "TS engine active" },
223
1041
  { name: "Active sessions store", passed: fs.existsSync(activePath), message: activePath },
224
1042
  { name: "Archive sessions store", passed: fs.existsSync(archivePath), message: archivePath },
225
1043
  { name: "Core rules store", passed: fs.existsSync(path.join(deps.memoryRoot, "CORTEX_RULES.md")), message: "CORTEX_RULES.md checked" },
1044
+ { name: "Embedding model connectivity", passed: embeddingConnectivity.connected, message: embeddingConnectivity.error || "ok" },
1045
+ { name: "LLM model connectivity", passed: llmConnectivity.connected, message: llmConnectivity.error || "ok" },
1046
+ { name: "Reranker model connectivity", passed: rerankerConnectivity.connected, message: rerankerConnectivity.error || "ok" },
226
1047
  ];
227
1048
  return {
228
1049
  success: true,
229
1050
  data: {
230
1051
  status: "ok",
1052
+ prompt_versions: PROMPT_VERSIONS,
231
1053
  checks,
1054
+ layers: {
1055
+ active: {
1056
+ records: activeRecords.length,
1057
+ path: activePath,
1058
+ },
1059
+ archive: {
1060
+ records: archiveRecords.length,
1061
+ path: archivePath,
1062
+ },
1063
+ vector: {
1064
+ storage_type: vectorStorageType,
1065
+ lancedb_exists: lancedbExists,
1066
+ active_coverage: activeVector.coverage,
1067
+ archive_coverage: archiveVector.coverage,
1068
+ active_unembedded: activeVector.pending + activeVector.failed,
1069
+ archive_unembedded: archiveVector.pending + archiveVector.failed,
1070
+ chunking: {
1071
+ chunk_size: deps.vectorChunking?.chunkSize ?? 600,
1072
+ chunk_overlap: deps.vectorChunking?.chunkOverlap ?? 100,
1073
+ },
1074
+ vector_jsonl_records: vectorJsonlRecords.length,
1075
+ vector_jsonl_by_layer: {
1076
+ active: activeVectorRecords.length,
1077
+ archive: archiveVectorRecords.length,
1078
+ },
1079
+ total_vector_records: totalVectorRecords,
1080
+ last_backfill_summary: lastVectorBackfill,
1081
+ backfill_state: {
1082
+ pending_retry_records: pendingRetry,
1083
+ has_state_file: fs.existsSync(path.join(deps.memoryRoot, ".vector_backfill_state.json")),
1084
+ },
1085
+ },
1086
+ graph_rules: {
1087
+ graph_mutation_log_exists: fs.existsSync(path.join(deps.memoryRoot, "graph", "mutation_log.jsonl")),
1088
+ rules_exists: fs.existsSync(path.join(deps.memoryRoot, "CORTEX_RULES.md")),
1089
+ },
1090
+ },
1091
+ model_connectivity: {
1092
+ embedding: embeddingConnectivity,
1093
+ llm: llmConnectivity,
1094
+ reranker: rerankerConnectivity,
1095
+ },
232
1096
  recommendations: [],
233
1097
  },
234
1098
  };
235
1099
  }
236
1100
  async function searchMemory(args, context) {
237
- if (!args || !args.query) {
1101
+ const argsRecord = asRecord(args) || {};
1102
+ const argsInput = asRecord(argsRecord.input);
1103
+ const queryCandidate = [
1104
+ typeof args.query === "string" ? args.query : "",
1105
+ typeof argsRecord.query === "string" ? String(argsRecord.query) : "",
1106
+ typeof argsRecord.q === "string" ? String(argsRecord.q) : "",
1107
+ typeof argsRecord.keyword === "string" ? String(argsRecord.keyword) : "",
1108
+ typeof argsInput?.query === "string" ? String(argsInput.query) : "",
1109
+ typeof argsInput?.q === "string" ? String(argsInput.q) : "",
1110
+ ].find(item => item.trim());
1111
+ const query = queryCandidate ? queryCandidate.trim() : "";
1112
+ if (!query) {
238
1113
  return {
239
1114
  success: false,
240
1115
  error: "Invalid input provided. Missing 'query' parameter.",
241
1116
  };
242
1117
  }
1118
+ const topKRaw = [
1119
+ typeof args.top_k === "number" ? args.top_k : undefined,
1120
+ typeof argsRecord.top_k === "number" ? Number(argsRecord.top_k) : undefined,
1121
+ typeof argsRecord.topK === "number" ? Number(argsRecord.topK) : undefined,
1122
+ typeof argsInput?.top_k === "number" ? Number(argsInput.top_k) : undefined,
1123
+ typeof argsInput?.topK === "number" ? Number(argsInput.topK) : undefined,
1124
+ ].find(value => typeof value === "number" && Number.isFinite(value));
243
1125
  const result = await deps.readStore.searchMemory({
244
- query: args.query,
245
- topK: typeof args.top_k === "number" && args.top_k > 0 ? Math.floor(args.top_k) : 3,
1126
+ query,
1127
+ topK: typeof topKRaw === "number" && topKRaw > 0 ? Math.floor(topKRaw) : 3,
246
1128
  });
247
1129
  return { success: true, data: result.results };
248
1130
  }
@@ -252,10 +1134,19 @@ function createTsEngine(deps) {
252
1134
  return { success: true, data: result.context };
253
1135
  }
254
1136
  async function getAutoContext(args, context) {
255
- const sessionId = deps.resolveSessionId(context);
1137
+ const argsRecord = asRecord(args) || {};
1138
+ const argsInput = asRecord(argsRecord.input);
1139
+ const includeHotRaw = [
1140
+ typeof args.include_hot === "boolean" ? args.include_hot : undefined,
1141
+ typeof argsRecord.include_hot === "boolean" ? Boolean(argsRecord.include_hot) : undefined,
1142
+ typeof argsRecord.includeHot === "boolean" ? Boolean(argsRecord.includeHot) : undefined,
1143
+ typeof argsInput?.include_hot === "boolean" ? Boolean(argsInput.include_hot) : undefined,
1144
+ typeof argsInput?.includeHot === "boolean" ? Boolean(argsInput.includeHot) : undefined,
1145
+ ].find(value => typeof value === "boolean");
1146
+ const sessionId = deps.resolveSessionId((context || {}));
256
1147
  const cached = deps.getCachedAutoSearch(sessionId);
257
1148
  const result = await deps.readStore.getAutoContext({
258
- includeHot: args.include_hot !== false,
1149
+ includeHot: includeHotRaw !== false,
259
1150
  sessionId,
260
1151
  cachedAutoSearch: cached ?? undefined,
261
1152
  });
@@ -305,12 +1196,15 @@ function createTsEngine(deps) {
305
1196
  const sessionId = deps.resolveSessionId(context, payload);
306
1197
  const syncRecordsRaw = payloadObj?.sync_records;
307
1198
  const syncRecords = typeof syncRecordsRaw === "boolean" ? syncRecordsRaw : deps.defaultAutoSync;
1199
+ const bufferedMessages = sessionMessageBuffer.get(sessionId) || [];
308
1200
  try {
309
1201
  const result = await deps.sessionEnd.onSessionEnd({
310
1202
  sessionId,
311
1203
  syncRecords,
1204
+ messages: bufferedMessages,
312
1205
  });
313
1206
  deps.logger.info(`TS session_end completed for ${sessionId}, events=${result.events_generated}`);
1207
+ sessionMessageBuffer.delete(sessionId);
314
1208
  }
315
1209
  catch (error) {
316
1210
  deps.logger.warn(`TS session_end failed for ${sessionId}: ${error}`);
@@ -323,23 +1217,8 @@ function createTsEngine(deps) {
323
1217
  }
324
1218
  const { text, role, source } = normalized;
325
1219
  const sessionId = deps.resolveSessionId(context, payload);
326
- try {
327
- const writeResult = await deps.writeStore.writeMemory({
328
- text,
329
- role,
330
- source,
331
- sessionId,
332
- });
333
- if (writeResult.status === "ok") {
334
- deps.logger.info(`TS write stored ${role} message for session ${sessionId}`);
335
- }
336
- else {
337
- deps.logger.debug(`TS write skipped for session ${sessionId}: ${writeResult.reason || "unknown"}`);
338
- }
339
- }
340
- catch (error) {
341
- deps.logger.warn(`TS write failed for session ${sessionId}: ${error}`);
342
- }
1220
+ pushSessionMessage(sessionId, { role, text });
1221
+ deps.logger.debug(`TS buffered ${role} message for session ${sessionId} source=${source}`);
343
1222
  if (role === "user" && text.length > 5) {
344
1223
  try {
345
1224
  const searchResult = await deps.readStore.searchMemory({ query: text, topK: 3 });
@@ -381,6 +1260,7 @@ function createTsEngine(deps) {
381
1260
  deleteMemory,
382
1261
  updateMemory,
383
1262
  cleanupMemories,
1263
+ backfillEmbeddings,
384
1264
  runDiagnostics,
385
1265
  onMessage,
386
1266
  onSessionEnd,