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

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 (76) hide show
  1. package/README.md +163 -203
  2. package/SKILL.md +71 -268
  3. package/dist/index.d.ts +88 -15
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +859 -1189
  6. package/dist/index.js.map +1 -1
  7. package/dist/openclaw.plugin.json +362 -14
  8. package/dist/src/dedup/three_stage_deduplicator.d.ts +25 -0
  9. package/dist/src/dedup/three_stage_deduplicator.d.ts.map +1 -0
  10. package/dist/src/dedup/three_stage_deduplicator.js +224 -0
  11. package/dist/src/dedup/three_stage_deduplicator.js.map +1 -0
  12. package/dist/src/engine/memory_engine.d.ts +2 -1
  13. package/dist/src/engine/memory_engine.d.ts.map +1 -1
  14. package/dist/src/engine/ts_engine.d.ts +126 -0
  15. package/dist/src/engine/ts_engine.d.ts.map +1 -1
  16. package/dist/src/engine/ts_engine.js +1172 -44
  17. package/dist/src/engine/ts_engine.js.map +1 -1
  18. package/dist/src/engine/types.d.ts +12 -0
  19. package/dist/src/engine/types.d.ts.map +1 -1
  20. package/dist/src/graph/ontology.d.ts +103 -0
  21. package/dist/src/graph/ontology.d.ts.map +1 -0
  22. package/dist/src/graph/ontology.js +564 -0
  23. package/dist/src/graph/ontology.js.map +1 -0
  24. package/dist/src/quality/llm_output_validator.d.ts +48 -0
  25. package/dist/src/quality/llm_output_validator.d.ts.map +1 -0
  26. package/dist/src/quality/llm_output_validator.js +404 -0
  27. package/dist/src/quality/llm_output_validator.js.map +1 -0
  28. package/dist/src/reflect/reflector.d.ts +7 -0
  29. package/dist/src/reflect/reflector.d.ts.map +1 -1
  30. package/dist/src/reflect/reflector.js +358 -8
  31. package/dist/src/reflect/reflector.js.map +1 -1
  32. package/dist/src/rules/rule_store.d.ts.map +1 -1
  33. package/dist/src/rules/rule_store.js +75 -16
  34. package/dist/src/rules/rule_store.js.map +1 -1
  35. package/dist/src/session/session_end.d.ts +33 -0
  36. package/dist/src/session/session_end.d.ts.map +1 -1
  37. package/dist/src/session/session_end.js +67 -64
  38. package/dist/src/session/session_end.js.map +1 -1
  39. package/dist/src/store/archive_store.d.ts +128 -0
  40. package/dist/src/store/archive_store.d.ts.map +1 -0
  41. package/dist/src/store/archive_store.js +481 -0
  42. package/dist/src/store/archive_store.js.map +1 -0
  43. package/dist/src/store/embedding_utils.d.ts +32 -0
  44. package/dist/src/store/embedding_utils.d.ts.map +1 -0
  45. package/dist/src/store/embedding_utils.js +173 -0
  46. package/dist/src/store/embedding_utils.js.map +1 -0
  47. package/dist/src/store/graph_memory_store.d.ts +44 -0
  48. package/dist/src/store/graph_memory_store.d.ts.map +1 -0
  49. package/dist/src/store/graph_memory_store.js +168 -0
  50. package/dist/src/store/graph_memory_store.js.map +1 -0
  51. package/dist/src/store/read_store.d.ts +86 -0
  52. package/dist/src/store/read_store.d.ts.map +1 -1
  53. package/dist/src/store/read_store.js +1681 -25
  54. package/dist/src/store/read_store.js.map +1 -1
  55. package/dist/src/store/vector_store.d.ts +44 -0
  56. package/dist/src/store/vector_store.d.ts.map +1 -0
  57. package/dist/src/store/vector_store.js +201 -0
  58. package/dist/src/store/vector_store.js.map +1 -0
  59. package/dist/src/store/write_store.d.ts +52 -0
  60. package/dist/src/store/write_store.d.ts.map +1 -1
  61. package/dist/src/store/write_store.js +245 -3
  62. package/dist/src/store/write_store.js.map +1 -1
  63. package/dist/src/sync/session_sync.d.ts +100 -2
  64. package/dist/src/sync/session_sync.d.ts.map +1 -1
  65. package/dist/src/sync/session_sync.js +673 -22
  66. package/dist/src/sync/session_sync.js.map +1 -1
  67. package/dist/src/utils/runtime_env.d.ts +4 -0
  68. package/dist/src/utils/runtime_env.d.ts.map +1 -0
  69. package/dist/src/utils/runtime_env.js +51 -0
  70. package/dist/src/utils/runtime_env.js.map +1 -0
  71. package/openclaw.plugin.json +362 -14
  72. package/package.json +23 -6
  73. package/scripts/cli.js +19 -14
  74. package/scripts/uninstall.js +22 -5
  75. package/index.ts +0 -2092
  76. package/scripts/install.js +0 -27
@@ -37,6 +37,7 @@ exports.createSessionSync = createSessionSync;
37
37
  const crypto = __importStar(require("crypto"));
38
38
  const fs = __importStar(require("fs"));
39
39
  const path = __importStar(require("path"));
40
+ const llm_output_validator_1 = require("../quality/llm_output_validator");
40
41
  function asRecord(value) {
41
42
  if (typeof value === "object" && value !== null) {
42
43
  return value;
@@ -51,23 +52,33 @@ function firstString(values) {
51
52
  }
52
53
  return undefined;
53
54
  }
55
+ const SYNC_STATE_VERSION = "2";
56
+ function createDefaultState() {
57
+ return { version: SYNC_STATE_VERSION, files: {}, markdowns: {} };
58
+ }
54
59
  function readState(filePath) {
55
60
  try {
56
61
  if (!fs.existsSync(filePath)) {
57
- return { files: {} };
62
+ return createDefaultState();
58
63
  }
59
64
  const content = fs.readFileSync(filePath, "utf-8").trim();
60
65
  if (!content) {
61
- return { files: {} };
66
+ return createDefaultState();
62
67
  }
63
68
  const parsed = JSON.parse(content);
64
69
  if (!parsed.files || typeof parsed.files !== "object") {
65
- return { files: {} };
70
+ return createDefaultState();
71
+ }
72
+ if (!parsed.markdowns || typeof parsed.markdowns !== "object") {
73
+ parsed.markdowns = {};
66
74
  }
75
+ parsed.version = typeof parsed.version === "string" && parsed.version.trim()
76
+ ? parsed.version
77
+ : SYNC_STATE_VERSION;
67
78
  return parsed;
68
79
  }
69
80
  catch {
70
- return { files: {} };
81
+ return createDefaultState();
71
82
  }
72
83
  }
73
84
  function writeState(filePath, state) {
@@ -75,6 +86,7 @@ function writeState(filePath, state) {
75
86
  if (!fs.existsSync(dir)) {
76
87
  fs.mkdirSync(dir, { recursive: true });
77
88
  }
89
+ state.version = SYNC_STATE_VERSION;
78
90
  fs.writeFileSync(filePath, JSON.stringify(state, null, 2), "utf-8");
79
91
  }
80
92
  function gatherSessionFiles(openclawBasePath, memoryRoot) {
@@ -93,10 +105,35 @@ function gatherSessionFiles(openclawBasePath, memoryRoot) {
93
105
  }
94
106
  return [...results];
95
107
  }
108
+ function gatherDailySummaryFiles(openclawBasePath) {
109
+ const summaryDir = path.join(openclawBasePath, "workspace", "memory");
110
+ if (!fs.existsSync(summaryDir) || !fs.statSync(summaryDir).isDirectory()) {
111
+ return [];
112
+ }
113
+ const files = [];
114
+ for (const entry of fs.readdirSync(summaryDir)) {
115
+ if (!entry.toLowerCase().endsWith(".md")) {
116
+ continue;
117
+ }
118
+ const filePath = path.join(summaryDir, entry);
119
+ if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
120
+ files.push(filePath);
121
+ }
122
+ }
123
+ return files;
124
+ }
96
125
  function inferOpenclawBasePath(projectRoot) {
97
- const fromEnv = process.env.OPENCLAW_BASE_PATH;
98
- if (fromEnv && fs.existsSync(fromEnv)) {
99
- return fromEnv;
126
+ const configPath = process.env.OPENCLAW_CONFIG_PATH;
127
+ if (configPath && fs.existsSync(configPath)) {
128
+ return path.dirname(configPath);
129
+ }
130
+ const stateDir = process.env.OPENCLAW_STATE_DIR;
131
+ if (stateDir && fs.existsSync(stateDir)) {
132
+ return stateDir;
133
+ }
134
+ const basePath = process.env.OPENCLAW_BASE_PATH;
135
+ if (basePath && fs.existsSync(basePath)) {
136
+ return basePath;
100
137
  }
101
138
  const home = process.env.USERPROFILE || process.env.HOME || "";
102
139
  if (home) {
@@ -143,16 +180,595 @@ function getSessionId(record, fallbackSeed) {
143
180
  record.id,
144
181
  ]) || `sync:${fallbackSeed}`);
145
182
  }
183
+ function parseDailySummary(content) {
184
+ const normalized = content
185
+ .replace(/\r\n/g, "\n")
186
+ .split("\n")
187
+ .map(line => line.trim())
188
+ .filter(Boolean)
189
+ .filter(line => !line.startsWith("```"));
190
+ const chunks = [];
191
+ let current = [];
192
+ for (const line of normalized) {
193
+ const isHeader = line.startsWith("#");
194
+ const isBullet = /^[-*]\s+/.test(line);
195
+ if (isHeader && current.length > 0) {
196
+ chunks.push(current.join("\n"));
197
+ current = [];
198
+ }
199
+ current.push(line);
200
+ if (isBullet && current.length >= 6) {
201
+ chunks.push(current.join("\n"));
202
+ current = [];
203
+ }
204
+ }
205
+ if (current.length > 0) {
206
+ chunks.push(current.join("\n"));
207
+ }
208
+ return chunks.map(chunk => chunk.trim()).filter(chunk => chunk.length >= 10);
209
+ }
210
+ function normalizeBaseUrl(value) {
211
+ if (!value)
212
+ return "";
213
+ return value.endsWith("/") ? value.slice(0, -1) : value;
214
+ }
215
+ function buildEventSnippet(text) {
216
+ const lines = text
217
+ .split(/\r?\n/)
218
+ .map(line => line.trim())
219
+ .filter(Boolean)
220
+ .filter(line => line.length >= 8);
221
+ const actionPattern = /(决定|完成|修复|阻塞|失败|成功|上线|部署|实现|依赖|owner|blocked|resolved|fixed|depends|decide|complete)/i;
222
+ const picked = lines.filter(line => actionPattern.test(line));
223
+ const use = picked.length > 0 ? picked : lines.slice(-20);
224
+ return use.slice(-30).join("\n").slice(-8000);
225
+ }
226
+ const WRITE_GATE_PROMPT_VERSION = "write-gate.v1.3.0";
227
+ const WRITE_GATE_REGRESSION_SAMPLES = [
228
+ "鏍蜂緥A: 鈥滀粖澶╄璁轰簡涓夌鏂规锛屽皻鏈喅绛栤€?=> active_only",
229
+ "鏍蜂緥B: 鈥滃喅瀹氶噰鐢˙鏂规骞跺畬鎴愪笂绾匡紝閿欒鐜囦笅闄嶅埌0.2%鈥?=> archive_event",
230
+ "鏍蜂緥C: 鈥滃ソ鐨勬敹鍒拌阿璋⑩€?=> skip",
231
+ ];
232
+ function parseArchiveEventPayload(value) {
233
+ if (!value || typeof value !== "object") {
234
+ return null;
235
+ }
236
+ const obj = value;
237
+ const eventType = typeof obj.event_type === "string" ? obj.event_type.trim() : "";
238
+ const summary = typeof obj.summary === "string" ? obj.summary.trim() : "";
239
+ if (!eventType || !summary) {
240
+ return null;
241
+ }
242
+ const entities = Array.isArray(obj.entities)
243
+ ? obj.entities.map(v => (typeof v === "string" ? v.trim() : "")).filter(Boolean)
244
+ : [];
245
+ const relations = Array.isArray(obj.relations)
246
+ ? obj.relations
247
+ .map(valueItem => {
248
+ if (!valueItem || typeof valueItem !== "object")
249
+ return null;
250
+ const relation = valueItem;
251
+ const source = typeof relation.source === "string" ? relation.source.trim() : "";
252
+ const target = typeof relation.target === "string" ? relation.target.trim() : "";
253
+ const type = typeof relation.type === "string" ? relation.type.trim() : "related_to";
254
+ const evidenceSpan = typeof relation.evidence_span === "string" ? relation.evidence_span.trim() : "";
255
+ const confidence = typeof relation.confidence === "number"
256
+ ? Math.max(0, Math.min(1, relation.confidence))
257
+ : undefined;
258
+ if (!source || !target)
259
+ return null;
260
+ return { source, target, type, evidence_span: evidenceSpan || undefined, confidence };
261
+ })
262
+ .filter(Boolean)
263
+ : [];
264
+ const entity_types = typeof obj.entity_types === "object" && obj.entity_types !== null && !Array.isArray(obj.entity_types)
265
+ ? Object.fromEntries(Object.entries(obj.entity_types)
266
+ .filter(([key, value]) => typeof key === "string" && key.trim().length > 0 && typeof value === "string" && value.trim().length > 0)
267
+ .map(([key, value]) => [key.trim(), value.trim()]))
268
+ : undefined;
269
+ return {
270
+ event_type: eventType,
271
+ summary,
272
+ entities,
273
+ entity_types,
274
+ relations,
275
+ outcome: typeof obj.outcome === "string" ? obj.outcome.trim() : "",
276
+ confidence: typeof obj.confidence === "number" ? Math.max(0, Math.min(1, obj.confidence)) : 0.6,
277
+ };
278
+ }
279
+ function parseGraphPayload(value) {
280
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
281
+ return null;
282
+ }
283
+ const obj = value;
284
+ const entities = Array.isArray(obj.entities)
285
+ ? obj.entities.map(v => (typeof v === "string" ? v.trim() : "")).filter(Boolean)
286
+ : [];
287
+ const entity_types = typeof obj.entity_types === "object" && obj.entity_types !== null && !Array.isArray(obj.entity_types)
288
+ ? Object.fromEntries(Object.entries(obj.entity_types)
289
+ .filter(([key, val]) => typeof key === "string" && key.trim() && typeof val === "string" && val.trim())
290
+ .map(([key, val]) => [key.trim(), val.trim()]))
291
+ : undefined;
292
+ const relations = Array.isArray(obj.relations)
293
+ ? obj.relations
294
+ .map(item => {
295
+ if (!item || typeof item !== "object")
296
+ return null;
297
+ const rel = item;
298
+ const source = typeof rel.source === "string" ? rel.source.trim() : "";
299
+ const target = typeof rel.target === "string" ? rel.target.trim() : "";
300
+ const type = typeof rel.type === "string" && rel.type.trim() ? rel.type.trim() : "related_to";
301
+ if (!source || !target)
302
+ return null;
303
+ const evidenceSpan = typeof rel.evidence_span === "string" ? rel.evidence_span.trim() : "";
304
+ const confidence = typeof rel.confidence === "number"
305
+ ? Math.max(0, Math.min(1, rel.confidence))
306
+ : undefined;
307
+ return {
308
+ source,
309
+ target,
310
+ type,
311
+ ...(evidenceSpan ? { evidence_span: evidenceSpan } : {}),
312
+ ...(typeof confidence === "number" ? { confidence } : {}),
313
+ };
314
+ })
315
+ .filter((item) => item !== null)
316
+ : [];
317
+ if (entities.length === 0 || relations.length === 0) {
318
+ return null;
319
+ }
320
+ return {
321
+ entities,
322
+ entity_types,
323
+ relations,
324
+ confidence: typeof obj.confidence === "number" ? Math.max(0, Math.min(1, obj.confidence)) : undefined,
325
+ };
326
+ }
327
+ function parseLlmGateDecisions(raw, logger) {
328
+ const validation = (0, llm_output_validator_1.validateLlmJsonOutput)(raw);
329
+ if (!validation.valid) {
330
+ if (logger) {
331
+ logger.warn(`quality_gate_decisions_invalid errors=${validation.errors.join("|")}`);
332
+ }
333
+ return [];
334
+ }
335
+ if (validation.warnings.length > 0 && logger) {
336
+ logger.debug(`quality_gate_decisions_warnings warnings=${validation.warnings.join("|")}`);
337
+ }
338
+ const root = validation.data;
339
+ const parsed = Array.isArray(root) ? root : root;
340
+ const output = [];
341
+ const pushDecision = (obj, target) => {
342
+ let event = null;
343
+ if (target === "archive_event") {
344
+ const eventValidation = (0, llm_output_validator_1.validateArchiveEvent)(obj.event || obj);
345
+ if (eventValidation.valid && eventValidation.cleaned) {
346
+ event = {
347
+ event_type: eventValidation.cleaned.event_type || "insight",
348
+ summary: eventValidation.cleaned.summary,
349
+ entities: eventValidation.cleaned.entities,
350
+ entity_types: eventValidation.cleaned.entity_types,
351
+ relations: eventValidation.cleaned.relations,
352
+ outcome: eventValidation.cleaned.outcome || "",
353
+ confidence: eventValidation.cleaned.confidence,
354
+ };
355
+ }
356
+ else {
357
+ if (logger) {
358
+ logger.warn(`quality_event_invalid errors=${eventValidation.errors.join("|")}`);
359
+ }
360
+ return;
361
+ }
362
+ }
363
+ output.push({
364
+ candidate_id: typeof obj.candidate_id === "string" ? obj.candidate_id.trim() : "",
365
+ target_layer: target,
366
+ active_text: typeof obj.active_text === "string" ? obj.active_text.trim() : "",
367
+ event: event || undefined,
368
+ graph: parseGraphPayload(obj.graph) || undefined,
369
+ reason: typeof obj.reason === "string" ? obj.reason.trim() : "",
370
+ });
371
+ };
372
+ if (Array.isArray(parsed)) {
373
+ if (logger) {
374
+ logger.warn("quality_gate_decisions_invalid format=array_not_supported_require_routing_plan");
375
+ }
376
+ }
377
+ else if (parsed && typeof parsed === "object") {
378
+ const rootObj = parsed;
379
+ const routingPlan = (typeof rootObj.routing_plan === "object" && rootObj.routing_plan !== null)
380
+ ? rootObj.routing_plan
381
+ : null;
382
+ if (routingPlan) {
383
+ const buckets = [
384
+ { key: "archive_event", items: routingPlan.archive_event },
385
+ { key: "active_only", items: routingPlan.active_only },
386
+ { key: "skip", items: routingPlan.skip },
387
+ ];
388
+ for (const bucket of buckets) {
389
+ if (!Array.isArray(bucket.items))
390
+ continue;
391
+ for (const item of bucket.items) {
392
+ if (!item || typeof item !== "object")
393
+ continue;
394
+ pushDecision(item, bucket.key);
395
+ }
396
+ }
397
+ }
398
+ else if (logger) {
399
+ logger.warn("quality_gate_decisions_invalid missing_routing_plan");
400
+ }
401
+ }
402
+ if (output.length === 0 && logger) {
403
+ logger.warn("quality_gate_decisions_empty");
404
+ }
405
+ const deduped = [];
406
+ const seen = new Set();
407
+ for (const item of output) {
408
+ const key = `${item.target_layer}|${item.event?.summary || item.active_text || item.reason || ""}`;
409
+ if (seen.has(key))
410
+ continue;
411
+ seen.add(key);
412
+ deduped.push(item);
413
+ }
414
+ return deduped;
415
+ }
416
+ async function extractGateDecisionsWithLlm(args) {
417
+ const endpoint = args.llm.baseUrl.endsWith("/chat/completions")
418
+ ? args.llm.baseUrl
419
+ : `${args.llm.baseUrl}/chat/completions`;
420
+ const body = {
421
+ model: args.llm.model,
422
+ temperature: 0.1,
423
+ response_format: { type: "json_object" },
424
+ messages: [
425
+ { role: "system", content: "You are a memory write-gate router. Output JSON only." },
426
+ {
427
+ role: "user",
428
+ content: [
429
+ `prompt_version=${WRITE_GATE_PROMPT_VERSION}`,
430
+ "Execute in 3 stages:",
431
+ "Stage1) Split transcript into candidate_events[]. One candidate should contain one principal event only.",
432
+ "Stage2) Route each candidate_event into target_layer: active_only | archive_event | skip.",
433
+ "Stage3) Output routing plan only. Do NOT claim data has been written.",
434
+ "Classification:",
435
+ "A) active_only: process context, ongoing discussion, temporary status, no stable conclusion.",
436
+ "B) archive_event: reusable event with clear subject + action/decision + outcome/phase conclusion.",
437
+ "C) skip: noise/repetition/chitchat/no clear business value.",
438
+ "Constraints:",
439
+ "- For archive_event: if confidence < 0.35, prefer skip.",
440
+ "- Relations must be grounded in source text. Do not fabricate.",
441
+ "- For active_only: active_text is required.",
442
+ "- Optional graph for active_only: graph={entities[],entity_types,relations[],confidence}.",
443
+ "- For relations: each relation should include source,target,type,evidence_span,confidence.",
444
+ "- If evidence_span or confidence is missing, do not output that relation.",
445
+ "Output JSON schema:",
446
+ "{\"candidate_events\":[{\"candidate_id\":\"c1\",\"span\":\"...\",\"normalized_text\":\"...\"}],\"routing_plan\":{\"archive_event\":[{\"candidate_id\":\"c1\",\"event\":{\"event_type\":\"decision\",\"summary\":\"...\",\"entities\":[\"A\"],\"entity_types\":{\"A\":\"Project\"},\"relations\":[],\"outcome\":\"...\",\"confidence\":0.82}}],\"active_only\":[],\"skip\":[]}}",
447
+ "routing_plan.archive_event[] item: {candidate_id,event}",
448
+ "routing_plan.active_only[] item: {candidate_id,active_text,graph}",
449
+ "routing_plan.skip[] item: {candidate_id,reason}",
450
+ ...WRITE_GATE_REGRESSION_SAMPLES,
451
+ "Output JSON only.",
452
+ "",
453
+ buildEventSnippet(args.transcript),
454
+ ].join("\n"),
455
+ },
456
+ ],
457
+ };
458
+ let lastError = null;
459
+ for (let attempt = 0; attempt < 3; attempt += 1) {
460
+ const controller = new AbortController();
461
+ const timeoutId = setTimeout(() => controller.abort(), 25000);
462
+ try {
463
+ const response = await fetch(endpoint, {
464
+ method: "POST",
465
+ headers: {
466
+ "content-type": "application/json",
467
+ authorization: `Bearer ${args.llm.apiKey}`,
468
+ },
469
+ body: JSON.stringify(body),
470
+ signal: controller.signal,
471
+ });
472
+ clearTimeout(timeoutId);
473
+ if (!response.ok) {
474
+ lastError = new Error(`sync_llm_http_${response.status}`);
475
+ continue;
476
+ }
477
+ const json = await response.json();
478
+ const content = json?.choices?.[0]?.message?.content || "";
479
+ if (!content.trim()) {
480
+ lastError = new Error("sync_llm_empty");
481
+ continue;
482
+ }
483
+ return parseLlmGateDecisions(content, args.logger);
484
+ }
485
+ catch (error) {
486
+ clearTimeout(timeoutId);
487
+ lastError = error;
488
+ }
489
+ }
490
+ args.logger.warn(`Sync LLM extraction failed: ${String(lastError || "unknown")}`);
491
+ return [];
492
+ }
146
493
  function createSessionSync(options) {
147
494
  const memoryRoot = options.dbPath ? path.resolve(options.dbPath) : path.join(options.projectRoot, "data", "memory");
148
495
  const statePath = path.join(memoryRoot, ".sync_state.json");
149
496
  const openclawBasePath = inferOpenclawBasePath(options.projectRoot);
497
+ const llmModel = options.llm?.model || "";
498
+ const llmApiKey = options.llm?.apiKey || "";
499
+ const llmBaseUrl = normalizeBaseUrl(options.llm?.baseURL || options.llm?.baseUrl);
500
+ const requireLlmForWrite = options.requireLlmForWrite !== false;
501
+ options.logger.info(`sync_gate_prompt_version=${WRITE_GATE_PROMPT_VERSION}`);
502
+ if (!fs.existsSync(statePath)) {
503
+ options.logger.warn("sync_state_missing: deleting state file triggers full re-import");
504
+ }
505
+ async function storeFromTranscript(args) {
506
+ const skipReasons = {};
507
+ const activeTextMaxChars = typeof options.writePolicy?.activeTextMaxChars === "number"
508
+ ? Math.max(500, Math.min(20000, Math.floor(options.writePolicy.activeTextMaxChars)))
509
+ : 4000;
510
+ const archiveSourceTextMaxChars = typeof options.writePolicy?.archiveSourceTextMaxChars === "number"
511
+ ? Math.max(1000, Math.min(30000, Math.floor(options.writePolicy.archiveSourceTextMaxChars)))
512
+ : 8000;
513
+ function bumpReason(reason) {
514
+ const key = reason || "unknown";
515
+ skipReasons[key] = (skipReasons[key] || 0) + 1;
516
+ }
517
+ if (!args.transcript.trim()) {
518
+ options.logger.info(`sync_skip reason=no_active_records session=${args.sessionId}`);
519
+ bumpReason("no_active_records");
520
+ return { imported: 0, skipped: 1, ok: true, llmDecisions: 0, activeOnly: 0, archiveEvent: 0, skipReasons };
521
+ }
522
+ if (!llmModel || !llmApiKey || !llmBaseUrl) {
523
+ if (requireLlmForWrite) {
524
+ options.logger.warn(`sync_skip reason=llm_not_configured session=${args.sessionId}`);
525
+ bumpReason("llm_not_configured");
526
+ return { imported: 0, skipped: 1, ok: false, llmDecisions: 0, activeOnly: 0, archiveEvent: 0, skipReasons };
527
+ }
528
+ options.logger.warn(`Sync gate degraded to active_only for ${args.sessionId}: llm_not_configured`);
529
+ const fallbackWrite = await options.writeStore.writeMemory({
530
+ text: args.transcript.slice(-activeTextMaxChars),
531
+ role: "system",
532
+ source: `sync_gate_fallback:${args.sourceFile}`,
533
+ sessionId: args.sessionId,
534
+ });
535
+ if (fallbackWrite.status === "ok") {
536
+ return { imported: 1, skipped: 0, ok: true, llmDecisions: 1, activeOnly: 1, archiveEvent: 0, skipReasons };
537
+ }
538
+ bumpReason(fallbackWrite.reason || "active_only_fallback_failed");
539
+ return { imported: 0, skipped: 1, ok: false, llmDecisions: 1, activeOnly: 0, archiveEvent: 0, skipReasons };
540
+ }
541
+ const decisions = await extractGateDecisionsWithLlm({
542
+ llm: { model: llmModel, apiKey: llmApiKey, baseUrl: llmBaseUrl },
543
+ transcript: args.transcript,
544
+ logger: options.logger,
545
+ });
546
+ if (decisions.length === 0) {
547
+ options.logger.info(`sync_skip reason=llm_extract_empty session=${args.sessionId}`);
548
+ bumpReason("llm_extract_empty");
549
+ return { imported: 0, skipped: 1, ok: true, llmDecisions: 0, activeOnly: 0, archiveEvent: 0, skipReasons };
550
+ }
551
+ let llmDecisions = 0;
552
+ let imported = 0;
553
+ let skipped = 0;
554
+ let activeOnly = 0;
555
+ let archiveEvent = 0;
556
+ let activeAttempted = 0;
557
+ let archiveAttempted = 0;
558
+ let graphAttempted = 0;
559
+ let graphStored = 0;
560
+ let graphSkipped = 0;
561
+ const archiveInputs = [];
562
+ for (const decision of decisions) {
563
+ llmDecisions += 1;
564
+ if (decision.target_layer === "skip") {
565
+ skipped += 1;
566
+ bumpReason(decision.reason || "llm_gate_skip");
567
+ continue;
568
+ }
569
+ if (decision.target_layer === "active_only") {
570
+ activeAttempted += 1;
571
+ const activeText = (decision.active_text || args.transcript).trim().slice(-activeTextMaxChars);
572
+ if (!activeText) {
573
+ skipped += 1;
574
+ bumpReason("active_only_empty");
575
+ continue;
576
+ }
577
+ const writeResult = await options.writeStore.writeMemory({
578
+ text: activeText,
579
+ role: "system",
580
+ source: `sync_gate_active:${args.sourceFile}`,
581
+ sessionId: args.sessionId,
582
+ });
583
+ if (writeResult.status === "ok") {
584
+ imported += 1;
585
+ activeOnly += 1;
586
+ if (options.graphMemoryStore && decision.graph) {
587
+ graphAttempted += 1;
588
+ const relationFingerprint = (decision.graph.relations || [])
589
+ .map(rel => `${rel.source}|${rel.type}|${rel.target}|${rel.evidence_span || ""}`)
590
+ .sort()
591
+ .join("||");
592
+ const activeSourceEventId = `active:${args.sessionId}:${crypto.createHash("sha1").update(relationFingerprint || activeText).digest("hex").slice(0, 16)}`;
593
+ const graphResult = await options.graphMemoryStore.append({
594
+ sourceEventId: activeSourceEventId,
595
+ sourceLayer: "active_only",
596
+ sessionId: args.sessionId,
597
+ sourceFile: args.sourceFile,
598
+ eventType: "insight",
599
+ entities: decision.graph.entities,
600
+ entity_types: decision.graph.entity_types,
601
+ relations: decision.graph.relations,
602
+ gateSource: "sync",
603
+ confidence: decision.graph.confidence,
604
+ sourceText: activeText,
605
+ });
606
+ if (!graphResult.success) {
607
+ graphSkipped += 1;
608
+ options.logger.info(`graph_skip_reason=${graphResult.reason} source_event_id=${activeSourceEventId}`);
609
+ }
610
+ else {
611
+ graphStored += 1;
612
+ }
613
+ }
614
+ }
615
+ else {
616
+ skipped += 1;
617
+ bumpReason(writeResult.reason || "active_only_write_skipped");
618
+ }
619
+ continue;
620
+ }
621
+ if (decision.target_layer === "archive_event") {
622
+ archiveAttempted += 1;
623
+ if (!decision.event) {
624
+ skipped += 1;
625
+ bumpReason("archive_event_missing_payload");
626
+ continue;
627
+ }
628
+ archiveInputs.push({
629
+ event_type: decision.event.event_type,
630
+ summary: decision.event.summary,
631
+ entities: decision.event.entities,
632
+ relations: decision.event.relations,
633
+ entity_types: decision.event.entity_types,
634
+ outcome: decision.event.outcome,
635
+ confidence: decision.event.confidence,
636
+ session_id: args.sessionId,
637
+ source_file: args.sourceFile,
638
+ source_text: args.transcript.slice(-archiveSourceTextMaxChars),
639
+ source_event_id: decision.candidate_id
640
+ ? `candidate:${args.sessionId}:${decision.candidate_id}`
641
+ : `candidate:${args.sessionId}:${crypto.createHash("sha1").update(decision.event.summary).digest("hex").slice(0, 16)}`,
642
+ actor: "sync_llm_gate",
643
+ });
644
+ }
645
+ }
646
+ if (archiveInputs.length > 0) {
647
+ let archivedSuccess = 0;
648
+ let archivedSkipped = 0;
649
+ for (const inputRecord of archiveInputs) {
650
+ const archiveResult = await options.archiveStore.storeEvents([inputRecord]);
651
+ imported += archiveResult.stored.length;
652
+ skipped += archiveResult.skipped.length;
653
+ archiveEvent += archiveResult.stored.length;
654
+ archivedSuccess += archiveResult.stored.length;
655
+ archivedSkipped += archiveResult.skipped.length;
656
+ for (const skip of archiveResult.skipped) {
657
+ bumpReason(skip.reason || "archive_store_skipped");
658
+ }
659
+ const archiveRecord = archiveResult.stored[0];
660
+ if (!archiveRecord) {
661
+ continue;
662
+ }
663
+ if (!options.graphMemoryStore) {
664
+ continue;
665
+ }
666
+ graphAttempted += 1;
667
+ const graphResult = await options.graphMemoryStore.append({
668
+ // Graph trace points to persisted archive record id for stable lookup.
669
+ sourceEventId: archiveRecord.id,
670
+ sourceLayer: "archive_event",
671
+ archiveEventId: archiveRecord.id,
672
+ sessionId: args.sessionId,
673
+ sourceFile: args.sourceFile,
674
+ eventType: inputRecord.event_type,
675
+ entities: inputRecord.entities,
676
+ entity_types: inputRecord.entity_types,
677
+ relations: inputRecord.relations,
678
+ gateSource: "sync",
679
+ confidence: inputRecord.confidence,
680
+ sourceText: args.transcript,
681
+ });
682
+ if (!graphResult.success) {
683
+ graphSkipped += 1;
684
+ options.logger.info(`graph_skip_reason=${graphResult.reason} source_event_id=${archiveRecord.id}`);
685
+ }
686
+ else {
687
+ graphStored += 1;
688
+ }
689
+ }
690
+ options.logger.info(`sync_archive_result session=${args.sessionId} archived_success=${archivedSuccess} skipped=${archivedSkipped}`);
691
+ }
692
+ options.logger.info(`sync_gate_result session=${args.sessionId} llm_decisions=${llmDecisions} active_only=${activeOnly} archive_event=${archiveEvent} skipped=${skipped}`);
693
+ options.logger.info(`sync_gate_metrics session=${args.sessionId} active_attempted=${activeAttempted} archive_attempted=${archiveAttempted} graph_attempted=${graphAttempted} graph_stored=${graphStored} graph_skipped=${graphSkipped} skip_reason_kinds=${Object.keys(skipReasons).length}`);
694
+ return {
695
+ imported,
696
+ skipped,
697
+ ok: true,
698
+ llmDecisions,
699
+ activeOnly,
700
+ archiveEvent,
701
+ skipReasons,
702
+ };
703
+ }
704
+ async function syncDailySummaries() {
705
+ const files = gatherDailySummaryFiles(openclawBasePath);
706
+ const state = readState(statePath);
707
+ if (!state.markdowns || typeof state.markdowns !== "object") {
708
+ state.markdowns = {};
709
+ }
710
+ let imported = 0;
711
+ let skipped = 0;
712
+ let filesProcessed = 0;
713
+ let llmDecisions = 0;
714
+ let activeOnly = 0;
715
+ let archiveEvent = 0;
716
+ const skipReasons = {};
717
+ for (const filePath of files) {
718
+ if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
719
+ continue;
720
+ }
721
+ const content = fs.readFileSync(filePath, "utf-8");
722
+ const digest = crypto.createHash("sha1").update(content).digest("hex");
723
+ const prev = state.markdowns[filePath];
724
+ if (prev && prev.digest === digest) {
725
+ skipped += 1;
726
+ continue;
727
+ }
728
+ const chunks = parseDailySummary(content);
729
+ if (chunks.length === 0) {
730
+ state.markdowns[filePath] = { digest, importedAt: new Date().toISOString() };
731
+ skipped += 1;
732
+ continue;
733
+ }
734
+ const summarySessionId = `daily_summary:${path.basename(filePath)}`;
735
+ const transcript = chunks.join("\n");
736
+ const result = await storeFromTranscript({
737
+ sessionId: summarySessionId,
738
+ sourceFile: `daily_summary_sync:${path.basename(filePath)}`,
739
+ transcript,
740
+ });
741
+ imported += result.imported;
742
+ skipped += result.skipped;
743
+ llmDecisions += result.llmDecisions;
744
+ activeOnly += result.activeOnly;
745
+ archiveEvent += result.archiveEvent;
746
+ for (const [key, count] of Object.entries(result.skipReasons)) {
747
+ skipReasons[key] = (skipReasons[key] || 0) + count;
748
+ }
749
+ if (!result.ok) {
750
+ continue;
751
+ }
752
+ state.markdowns[filePath] = { digest, importedAt: new Date().toISOString() };
753
+ filesProcessed += 1;
754
+ }
755
+ writeState(statePath, state);
756
+ options.logger.info(`TS daily summary sync completed: imported=${imported}, skipped=${skipped}, files=${filesProcessed}`);
757
+ return { imported, skipped, filesProcessed, llmDecisions, activeOnly, archiveEvent, skipReasons };
758
+ }
150
759
  async function syncMemory() {
151
760
  const files = gatherSessionFiles(openclawBasePath, memoryRoot);
761
+ if (files.length === 0) {
762
+ options.logger.info("sync_skip reason=no_active_records");
763
+ }
152
764
  const state = readState(statePath);
153
765
  let imported = 0;
154
766
  let skipped = 0;
155
767
  let filesProcessed = 0;
768
+ let llmDecisions = 0;
769
+ let activeOnly = 0;
770
+ let archiveEvent = 0;
771
+ const skipReasons = {};
156
772
  for (const filePath of files) {
157
773
  if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
158
774
  continue;
@@ -169,6 +785,8 @@ function createSessionSync(options) {
169
785
  state.files[filePath] = { size: stat.size, lineCount: lines.length };
170
786
  continue;
171
787
  }
788
+ const bySession = new Map();
789
+ let fileHasFailure = false;
172
790
  for (let i = startIndex; i < lines.length; i++) {
173
791
  const line = lines[i].trim();
174
792
  if (!line)
@@ -183,18 +801,10 @@ function createSessionSync(options) {
183
801
  }
184
802
  const sessionId = getSessionId(record, `${path.basename(filePath)}:${hash}`);
185
803
  for (const msg of messages) {
186
- const result = await options.writeStore.writeMemory({
187
- text: msg.text,
188
- role: msg.role,
189
- source: "sync",
190
- sessionId,
191
- });
192
- if (result.status === "ok") {
193
- imported += 1;
194
- }
195
- else {
196
- skipped += 1;
804
+ if (!bySession.has(sessionId)) {
805
+ bySession.set(sessionId, []);
197
806
  }
807
+ bySession.get(sessionId)?.push(`[${msg.role}] ${msg.text}`);
198
808
  }
199
809
  }
200
810
  catch (error) {
@@ -202,13 +812,54 @@ function createSessionSync(options) {
202
812
  skipped += 1;
203
813
  }
204
814
  }
815
+ for (const [sessionId, messages] of bySession.entries()) {
816
+ const transcript = messages.join("\n");
817
+ const result = await storeFromTranscript({
818
+ sessionId,
819
+ sourceFile: `sync:${path.basename(filePath)}`,
820
+ transcript,
821
+ });
822
+ imported += result.imported;
823
+ skipped += result.skipped;
824
+ llmDecisions += result.llmDecisions;
825
+ activeOnly += result.activeOnly;
826
+ archiveEvent += result.archiveEvent;
827
+ for (const [key, count] of Object.entries(result.skipReasons)) {
828
+ skipReasons[key] = (skipReasons[key] || 0) + count;
829
+ }
830
+ if (!result.ok) {
831
+ fileHasFailure = true;
832
+ }
833
+ }
205
834
  filesProcessed += 1;
206
- state.files[filePath] = { size: stat.size, lineCount: lines.length };
835
+ if (!fileHasFailure) {
836
+ state.files[filePath] = { size: stat.size, lineCount: lines.length };
837
+ }
207
838
  }
208
839
  writeState(statePath, state);
209
- options.logger.info(`TS sync completed: imported=${imported}, skipped=${skipped}, files=${filesProcessed}`);
210
- return { imported, skipped, filesProcessed };
840
+ const summary = await syncDailySummaries();
841
+ llmDecisions += summary.llmDecisions;
842
+ activeOnly += summary.activeOnly;
843
+ archiveEvent += summary.archiveEvent;
844
+ for (const [key, count] of Object.entries(summary.skipReasons)) {
845
+ skipReasons[key] = (skipReasons[key] || 0) + count;
846
+ }
847
+ options.logger.info(`TS sync completed: imported=${imported}, skipped=${skipped}, files=${filesProcessed}, summaryImported=${summary.imported}, summarySkipped=${summary.skipped}, llmDecisions=${llmDecisions}, activeOnly=${activeOnly}, archiveEvent=${archiveEvent}`);
848
+ return {
849
+ imported,
850
+ skipped,
851
+ filesProcessed,
852
+ summaryImported: summary.imported,
853
+ summarySkipped: summary.skipped,
854
+ llmDecisions,
855
+ activeOnly,
856
+ archiveEvent,
857
+ skipReasons,
858
+ };
859
+ }
860
+ async function routeTranscript(args) {
861
+ return storeFromTranscript(args);
211
862
  }
212
- return { syncMemory };
863
+ return { syncMemory, syncDailySummaries, routeTranscript };
213
864
  }
214
865
  //# sourceMappingURL=session_sync.js.map