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

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