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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +347 -290
  3. package/SIGNATURE.md +7 -0
  4. package/SKILL.md +96 -345
  5. package/dist/index.d.ts +69 -23
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +1130 -1330
  8. package/dist/index.js.map +1 -1
  9. package/dist/openclaw.plugin.json +397 -18
  10. package/dist/src/dedup/three_stage_deduplicator.d.ts.map +1 -1
  11. package/dist/src/dedup/three_stage_deduplicator.js +13 -3
  12. package/dist/src/dedup/three_stage_deduplicator.js.map +1 -1
  13. package/dist/src/engine/memory_engine.d.ts +5 -1
  14. package/dist/src/engine/memory_engine.d.ts.map +1 -1
  15. package/dist/src/engine/ts_engine.d.ts +149 -0
  16. package/dist/src/engine/ts_engine.d.ts.map +1 -1
  17. package/dist/src/engine/ts_engine.js +863 -203
  18. package/dist/src/engine/ts_engine.js.map +1 -1
  19. package/dist/src/engine/types.d.ts +20 -0
  20. package/dist/src/engine/types.d.ts.map +1 -1
  21. package/dist/src/graph/ontology.d.ts +87 -15
  22. package/dist/src/graph/ontology.d.ts.map +1 -1
  23. package/dist/src/graph/ontology.js +999 -12
  24. package/dist/src/graph/ontology.js.map +1 -1
  25. package/dist/src/net/http_post.d.ts +17 -0
  26. package/dist/src/net/http_post.d.ts.map +1 -0
  27. package/dist/src/net/http_post.js +56 -0
  28. package/dist/src/net/http_post.js.map +1 -0
  29. package/dist/src/quality/llm_output_validator.d.ts +65 -0
  30. package/dist/src/quality/llm_output_validator.d.ts.map +1 -0
  31. package/dist/src/quality/llm_output_validator.js +635 -0
  32. package/dist/src/quality/llm_output_validator.js.map +1 -0
  33. package/dist/src/reflect/reflector.d.ts.map +1 -1
  34. package/dist/src/reflect/reflector.js +296 -26
  35. package/dist/src/reflect/reflector.js.map +1 -1
  36. package/dist/src/rules/rule_store.d.ts.map +1 -1
  37. package/dist/src/rules/rule_store.js +75 -16
  38. package/dist/src/rules/rule_store.js.map +1 -1
  39. package/dist/src/session/session_end.d.ts +20 -42
  40. package/dist/src/session/session_end.d.ts.map +1 -1
  41. package/dist/src/session/session_end.js +21 -218
  42. package/dist/src/session/session_end.js.map +1 -1
  43. package/dist/src/store/archive_store.d.ts +28 -7
  44. package/dist/src/store/archive_store.d.ts.map +1 -1
  45. package/dist/src/store/archive_store.js +367 -130
  46. package/dist/src/store/archive_store.js.map +1 -1
  47. package/dist/src/store/graph_memory_store.d.ts +115 -0
  48. package/dist/src/store/graph_memory_store.d.ts.map +1 -0
  49. package/dist/src/store/graph_memory_store.js +1061 -0
  50. package/dist/src/store/graph_memory_store.js.map +1 -0
  51. package/dist/src/store/read_store.d.ts +75 -0
  52. package/dist/src/store/read_store.d.ts.map +1 -1
  53. package/dist/src/store/read_store.js +1837 -312
  54. package/dist/src/store/read_store.js.map +1 -1
  55. package/dist/src/store/vector_store.d.ts +2 -0
  56. package/dist/src/store/vector_store.d.ts.map +1 -1
  57. package/dist/src/store/vector_store.js +19 -3
  58. package/dist/src/store/vector_store.js.map +1 -1
  59. package/dist/src/store/write_store.d.ts +11 -0
  60. package/dist/src/store/write_store.d.ts.map +1 -1
  61. package/dist/src/store/write_store.js +242 -42
  62. package/dist/src/store/write_store.js.map +1 -1
  63. package/dist/src/sync/session_sync.d.ts +72 -1
  64. package/dist/src/sync/session_sync.d.ts.map +1 -1
  65. package/dist/src/sync/session_sync.js +2246 -126
  66. package/dist/src/sync/session_sync.js.map +1 -1
  67. package/dist/src/wiki/wiki_linter.d.ts +26 -0
  68. package/dist/src/wiki/wiki_linter.d.ts.map +1 -0
  69. package/dist/src/wiki/wiki_linter.js +339 -0
  70. package/dist/src/wiki/wiki_linter.js.map +1 -0
  71. package/dist/src/wiki/wiki_logger.d.ts +10 -0
  72. package/dist/src/wiki/wiki_logger.d.ts.map +1 -0
  73. package/dist/src/wiki/wiki_logger.js +78 -0
  74. package/dist/src/wiki/wiki_logger.js.map +1 -0
  75. package/dist/src/wiki/wiki_maintainer.d.ts +39 -0
  76. package/dist/src/wiki/wiki_maintainer.d.ts.map +1 -0
  77. package/dist/src/wiki/wiki_maintainer.js +38 -0
  78. package/dist/src/wiki/wiki_maintainer.js.map +1 -0
  79. package/dist/src/wiki/wiki_projector.d.ts +35 -0
  80. package/dist/src/wiki/wiki_projector.d.ts.map +1 -0
  81. package/dist/src/wiki/wiki_projector.js +1151 -0
  82. package/dist/src/wiki/wiki_projector.js.map +1 -0
  83. package/dist/src/wiki/wiki_queue.d.ts +29 -0
  84. package/dist/src/wiki/wiki_queue.d.ts.map +1 -0
  85. package/dist/src/wiki/wiki_queue.js +137 -0
  86. package/dist/src/wiki/wiki_queue.js.map +1 -0
  87. package/openclaw.plugin.json +397 -18
  88. package/package.json +51 -5
  89. package/schema/graph.schema.yaml +330 -0
  90. package/scripts/cli.js +67 -13
  91. package/scripts/repair-memory.js +321 -0
  92. package/skills/cortex-memory/SKILL.md +83 -0
  93. package/skills/cortex-memory/references/agent-manual.md +127 -0
  94. package/skills/cortex-memory/references/configuration.md +109 -0
  95. package/skills/cortex-memory/references/publish-checklist.md +45 -0
  96. package/skills/cortex-memory/references/system-prompt-template.md +27 -0
  97. package/skills/cortex-memory/references/tools.md +191 -0
@@ -36,22 +36,197 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.createArchiveStore = createArchiveStore;
37
37
  const fs = __importStar(require("fs"));
38
38
  const path = __importStar(require("path"));
39
+ const http_post_1 = require("../net/http_post");
39
40
  const ontology_1 = require("../graph/ontology");
41
+ const llm_output_validator_1 = require("../quality/llm_output_validator");
40
42
  function normalizeBaseUrl(value) {
41
43
  if (!value)
42
44
  return "";
43
45
  return value.endsWith("/") ? value.slice(0, -1) : value;
44
46
  }
45
- function scoreQuality(text) {
46
- const length = text.length;
47
- const score = Math.max(0, Math.min(1, Number((Math.min(length, 320) / 320).toFixed(2))));
48
- if (score >= 0.75) {
49
- return { score, level: "high" };
47
+ function resolveArchiveSourceCharLimit(value) {
48
+ if (typeof value === "number" && Number.isFinite(value)) {
49
+ return Math.max(1000, Math.floor(value));
50
50
  }
51
- if (score >= 0.4) {
52
- return { score, level: "medium" };
51
+ return 500000;
52
+ }
53
+ function clampTailText(text, maxChars) {
54
+ const source = (text || "").trim();
55
+ if (!source)
56
+ return "";
57
+ if (!Number.isFinite(maxChars) || maxChars <= 0 || source.length <= maxChars) {
58
+ return source;
59
+ }
60
+ return source.slice(-Math.floor(maxChars)).trim();
61
+ }
62
+ const ARCHIVE_LOW_INFORMATION_LINE = /^(ok|okay|got it|roger|noted|sure|thanks|thank you|received|copy that|understood|好的|收到|明白|了解|谢谢|感谢|可以|行|嗯|嗯嗯|没问题)(?:\b|$)/i;
63
+ function denoiseArchiveSourceText(text) {
64
+ const raw = (text || "").trim();
65
+ if (!raw)
66
+ return "";
67
+ const output = [];
68
+ const seen = new Set();
69
+ for (const line of raw.split(/\r?\n/)) {
70
+ const trimmed = line.trim();
71
+ if (!trimmed)
72
+ continue;
73
+ const content = trimmed.replace(/^\[[^\]]+\]\s*/, "").trim();
74
+ if (!content)
75
+ continue;
76
+ const hasSignal = /(https?:\/\/|www\.|[A-Za-z0-9._-]+\.[A-Za-z]{2,}|[`#/:\\]|@\w+|\b\d{2,}\b)/.test(content);
77
+ if (!hasSignal && ARCHIVE_LOW_INFORMATION_LINE.test(content)) {
78
+ continue;
79
+ }
80
+ const dedupKey = content.toLowerCase();
81
+ if (!hasSignal && seen.has(dedupKey)) {
82
+ continue;
83
+ }
84
+ seen.add(dedupKey);
85
+ output.push(trimmed);
86
+ }
87
+ return output.length > 0 ? output.join("\n") : raw;
88
+ }
89
+ function normalizeOneLineText(value) {
90
+ return String(value || "").replace(/\s+/g, " ").trim();
91
+ }
92
+ const ARCHIVE_TASK_INSTRUCTION_PATTERNS = [
93
+ /请|需要|帮我|麻烦|任务|需求|实现|修复|排查|优化|上线|部署|整理|编写|启用|查看|检查|分析|导入|生成|提高|改进/,
94
+ /please|can you|need to|task|implement|fix|investigate|optimi[sz]e|deploy|enable|review/i,
95
+ ];
96
+ const ARCHIVE_COMPLETION_REPORT_PATTERNS = [
97
+ /已完成|完成了|处理完|搞定|已修复|修复了|已实现|已上线|已部署|结果|汇报|完成情况|报告|已通过|验证通过|测试通过/,
98
+ /done|completed|fixed|implemented|deployed|resolved|report|summary|finished/i,
99
+ ];
100
+ const ARCHIVE_USER_ACCEPTANCE_PATTERNS = [
101
+ /确认|认可|通过|验收|OK|可以|好的|收到|辛苦|谢谢|没问题|就这样|接受|效果可以/,
102
+ /approved|accepted|looks good|great|works|thank you|confirmed/i,
103
+ ];
104
+ const ARCHIVE_ACTION_PATTERNS = [
105
+ /决定|完成|修复|发布|上线|部署|提交|交付|验证|关闭|推进|落地|实施|启用|导入|生成|优化|改进/,
106
+ /decide|complete|fix|release|deploy|ship|deliver|verify|close|implement|enable|migrate/i,
107
+ ];
108
+ const ARCHIVE_FAILURE_PATTERNS = [
109
+ /失败|报错|错误|异常|阻塞|卡住|不行|超时|回滚|故障|不通过|无法/,
110
+ /failed|error|exception|blocked|timeout|rollback|incident/i,
111
+ ];
112
+ const ARCHIVE_SUCCESS_PATTERNS = [
113
+ /成功|完成|修复|解决|通过|已上线|稳定|正常|恢复|可用|生效/,
114
+ /success|completed|fixed|resolved|passed|stable|recovered|works/i,
115
+ ];
116
+ function matchesAnyPattern(text, patterns) {
117
+ return patterns.some(pattern => pattern.test(text));
118
+ }
119
+ function firstMatchIndex(text, patterns) {
120
+ let minIndex = -1;
121
+ for (const pattern of patterns) {
122
+ const idx = text.search(pattern);
123
+ if (idx < 0) {
124
+ continue;
125
+ }
126
+ if (minIndex < 0 || idx < minIndex) {
127
+ minIndex = idx;
128
+ }
129
+ }
130
+ return minIndex;
131
+ }
132
+ function scoreQuality(args) {
133
+ const summary = (args.summary || "").trim();
134
+ const cause = (args.cause || "").trim();
135
+ const process = (args.process || "").trim();
136
+ const result = (args.result || "").trim();
137
+ const outcome = (args.outcome || "").trim();
138
+ const sourceText = (args.sourceText || "").trim();
139
+ const mergedText = [summary, cause, process, result, outcome, sourceText].filter(Boolean).join("\n");
140
+ const hasStructuredTriplet = cause.length > 0 && process.length > 0 && result.length > 0;
141
+ const hasTaskInstruction = matchesAnyPattern(mergedText, ARCHIVE_TASK_INSTRUCTION_PATTERNS);
142
+ const hasCompletionReport = matchesAnyPattern(mergedText, ARCHIVE_COMPLETION_REPORT_PATTERNS);
143
+ const hasUserAcceptance = matchesAnyPattern(mergedText, ARCHIVE_USER_ACCEPTANCE_PATTERNS);
144
+ const hasAction = matchesAnyPattern(mergedText, ARCHIVE_ACTION_PATTERNS);
145
+ const hasFailure = matchesAnyPattern(mergedText, ARCHIVE_FAILURE_PATTERNS);
146
+ const hasSuccess = matchesAnyPattern(mergedText, ARCHIVE_SUCCESS_PATTERNS);
147
+ const hasOutcome = outcome.length >= 6 || hasSuccess;
148
+ const firstFailureIdx = hasFailure ? firstMatchIndex(mergedText, ARCHIVE_FAILURE_PATTERNS) : -1;
149
+ const firstSuccessIdx = hasSuccess ? firstMatchIndex(mergedText, ARCHIVE_SUCCESS_PATTERNS) : -1;
150
+ const failThenSuccess = hasFailure && hasSuccess && firstFailureIdx >= 0 && firstSuccessIdx > firstFailureIdx;
151
+ const workflowComplete = hasStructuredTriplet || (hasTaskInstruction && hasCompletionReport && hasUserAcceptance);
152
+ let score = 0;
153
+ if (summary.length >= 24)
154
+ score += 0.1;
155
+ if (summary.length >= 60)
156
+ score += 0.1;
157
+ if (summary.length >= 120)
158
+ score += 0.06;
159
+ if (summary.length >= 180)
160
+ score += 0.04;
161
+ if (hasStructuredTriplet)
162
+ score += 0.22;
163
+ if (hasAction)
164
+ score += 0.14;
165
+ if (hasOutcome)
166
+ score += 0.12;
167
+ if (hasTaskInstruction)
168
+ score += 0.12;
169
+ if (hasCompletionReport)
170
+ score += 0.12;
171
+ if (hasUserAcceptance)
172
+ score += 0.14;
173
+ if (workflowComplete)
174
+ score += 0.12;
175
+ if (failThenSuccess)
176
+ score += 0.1;
177
+ const normalizedScore = Math.max(0, Math.min(1, Number(score.toFixed(2))));
178
+ if (normalizedScore >= 0.75) {
179
+ return {
180
+ score: normalizedScore,
181
+ level: "high",
182
+ signals: { hasStructuredTriplet, hasTaskInstruction, hasCompletionReport, hasUserAcceptance, workflowComplete, failThenSuccess },
183
+ };
53
184
  }
54
- return { score, level: "low" };
185
+ if (normalizedScore >= 0.4) {
186
+ return {
187
+ score: normalizedScore,
188
+ level: "medium",
189
+ signals: { hasStructuredTriplet, hasTaskInstruction, hasCompletionReport, hasUserAcceptance, workflowComplete, failThenSuccess },
190
+ };
191
+ }
192
+ return {
193
+ score: normalizedScore,
194
+ level: "low",
195
+ signals: { hasStructuredTriplet, hasTaskInstruction, hasCompletionReport, hasUserAcceptance, workflowComplete, failThenSuccess },
196
+ };
197
+ }
198
+ function normalizeArchiveDedupText(value) {
199
+ return String(value || "")
200
+ .toLowerCase()
201
+ .replace(/[^\p{L}\p{N}:./\\#@_-]+/gu, " ")
202
+ .replace(/\s+/g, " ")
203
+ .trim();
204
+ }
205
+ function buildArchiveDedupText(args) {
206
+ const relationText = Array.isArray(args.relations)
207
+ ? args.relations
208
+ .map(relation => `${relation.source || ""}|${relation.type || ""}|${relation.target || ""}`)
209
+ .filter(item => item.replace(/\|/g, "").trim().length > 0)
210
+ .sort()
211
+ .join("\n")
212
+ : "";
213
+ const entityText = Array.isArray(args.entities)
214
+ ? args.entities.map(item => String(item || "").trim()).filter(Boolean).sort().join("\n")
215
+ : "";
216
+ return [
217
+ `canonical:${args.canonicalId || ""}`,
218
+ `event_type:${args.eventType}`,
219
+ `summary:${args.summary}`,
220
+ `cause:${args.cause || ""}`,
221
+ `process:${args.process || ""}`,
222
+ `result:${args.result || args.outcome || ""}`,
223
+ entityText ? `entities:${entityText}` : "",
224
+ relationText ? `relations:${relationText}` : "",
225
+ ]
226
+ .filter(Boolean)
227
+ .map(normalizeArchiveDedupText)
228
+ .filter(Boolean)
229
+ .join("\n");
55
230
  }
56
231
  async function requestEmbedding(args) {
57
232
  const endpoint = args.baseUrl.endsWith("/embeddings") ? args.baseUrl : `${args.baseUrl}/embeddings`;
@@ -70,24 +245,18 @@ async function requestEmbedding(args) {
70
245
  : 4;
71
246
  let lastError = null;
72
247
  for (let attempt = 0; attempt < maxRetries; attempt += 1) {
73
- const controller = new AbortController();
74
- const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
248
+ const response = await (0, http_post_1.postJsonWithTimeout)({
249
+ endpoint,
250
+ apiKey: args.apiKey,
251
+ body,
252
+ timeoutMs,
253
+ });
254
+ if (!response.ok) {
255
+ lastError = new Error(response.status > 0 ? `embedding_http_${response.status}` : (response.error || "embedding_network_error"));
256
+ continue;
257
+ }
75
258
  try {
76
- const response = await fetch(endpoint, {
77
- method: "POST",
78
- headers: {
79
- "content-type": "application/json",
80
- authorization: `Bearer ${args.apiKey}`,
81
- },
82
- body: JSON.stringify(body),
83
- signal: controller.signal,
84
- });
85
- clearTimeout(timeoutId);
86
- if (!response.ok) {
87
- lastError = new Error(`embedding_http_${response.status}`);
88
- continue;
89
- }
90
- const json = await response.json();
259
+ const json = (response.json || {});
91
260
  const embedding = json?.data?.[0]?.embedding;
92
261
  if (Array.isArray(embedding) && embedding.length > 0) {
93
262
  return embedding.filter(item => Number.isFinite(item));
@@ -95,7 +264,6 @@ async function requestEmbedding(args) {
95
264
  lastError = new Error("embedding_empty");
96
265
  }
97
266
  catch (error) {
98
- clearTimeout(timeoutId);
99
267
  lastError = error;
100
268
  }
101
269
  if (attempt < maxRetries - 1) {
@@ -131,34 +299,6 @@ function inferGateSource(event) {
131
299
  }
132
300
  return "manual";
133
301
  }
134
- function buildArchiveVectorText(event, normalizedEventType, entities) {
135
- const lines = [
136
- `event_type: ${normalizedEventType}`,
137
- `summary: ${(event.summary || "").trim()}`,
138
- `outcome: ${(event.outcome || "").trim()}`,
139
- `entities: ${entities.join(", ")}`,
140
- `source_file: ${(event.source_file || "").trim()}`,
141
- `actor: ${(event.actor || "").trim()}`,
142
- ].map(line => line.trim()).filter(Boolean);
143
- if (Array.isArray(event.relations) && event.relations.length > 0) {
144
- const relationLines = event.relations
145
- .map(relation => {
146
- if (!relation || typeof relation !== "object")
147
- return "";
148
- const source = typeof relation.source === "string" ? relation.source.trim() : "";
149
- const target = typeof relation.target === "string" ? relation.target.trim() : "";
150
- const type = typeof relation.type === "string" ? relation.type.trim() : "related_to";
151
- if (!source || !target)
152
- return "";
153
- return `${source} -[${type}]-> ${target}`;
154
- })
155
- .filter(Boolean);
156
- if (relationLines.length > 0) {
157
- lines.push(`relations: ${relationLines.join(" ; ")}`);
158
- }
159
- }
160
- return lines.join("\n").trim();
161
- }
162
302
  function splitTextChunks(text, chunkSize, chunkOverlap) {
163
303
  const normalizedSize = Number.isFinite(chunkSize) && chunkSize >= 200 ? Math.floor(chunkSize) : 600;
164
304
  const normalizedOverlap = Number.isFinite(chunkOverlap) && chunkOverlap >= 0
@@ -210,6 +350,32 @@ function splitTextChunks(text, chunkSize, chunkOverlap) {
210
350
  }
211
351
  return output;
212
352
  }
353
+ function pickEvidenceChunks(chunks, maxCount) {
354
+ if (!chunks.length || maxCount <= 0)
355
+ return [];
356
+ if (chunks.length <= maxCount)
357
+ return chunks;
358
+ const picked = new Map();
359
+ picked.set(chunks[0].index, chunks[0]);
360
+ if (maxCount >= 2) {
361
+ const mid = chunks[Math.floor(chunks.length / 2)];
362
+ picked.set(mid.index, mid);
363
+ }
364
+ if (maxCount >= 3) {
365
+ const last = chunks[chunks.length - 1];
366
+ picked.set(last.index, last);
367
+ }
368
+ if (picked.size < maxCount) {
369
+ for (const chunk of chunks) {
370
+ if (!picked.has(chunk.index)) {
371
+ picked.set(chunk.index, chunk);
372
+ }
373
+ if (picked.size >= maxCount)
374
+ break;
375
+ }
376
+ }
377
+ return [...picked.values()].sort((a, b) => a.index - b.index).slice(0, maxCount);
378
+ }
213
379
  async function mapWithConcurrency(items, maxConcurrency, mapper) {
214
380
  if (items.length === 0) {
215
381
  return [];
@@ -232,8 +398,9 @@ async function mapWithConcurrency(items, maxConcurrency, mapper) {
232
398
  }
233
399
  function createArchiveStore(options) {
234
400
  const archivePath = path.join(options.memoryRoot, "sessions", "archive", "sessions.jsonl");
235
- const mutationLogPath = path.join(options.memoryRoot, "graph", "mutation_log.jsonl");
401
+ const mutationLogPath = path.join(options.memoryRoot, "sessions", "archive", "mutation_log.jsonl");
236
402
  const graphSchema = (0, ontology_1.loadGraphSchema)(options.projectRoot);
403
+ const archiveSourceTextMaxChars = resolveArchiveSourceCharLimit(options.writePolicy?.archiveSourceTextMaxChars);
237
404
  async function storeEvents(events) {
238
405
  const stored = [];
239
406
  const skipped = [];
@@ -243,46 +410,112 @@ function createArchiveStore(options) {
243
410
  const lines = [];
244
411
  const mutationLines = [];
245
412
  for (const event of events) {
246
- const summary = (event.summary || "").trim();
247
- if (!summary) {
413
+ const rawSummary = normalizeOneLineText(event.summary || "");
414
+ if (!rawSummary) {
248
415
  skipped.push({ summary: "", reason: "empty_summary" });
249
416
  options.logger.info("archive_skip reason=empty_summary");
250
417
  continue;
251
418
  }
419
+ const cause = normalizeOneLineText(event.cause || "");
420
+ const process = normalizeOneLineText(event.process || "");
421
+ const result = normalizeOneLineText(event.result || event.outcome || "");
422
+ const summary = rawSummary;
252
423
  const confidence = typeof event.confidence === "number"
253
424
  ? Math.max(0, Math.min(1, event.confidence))
254
425
  : undefined;
255
- if (typeof confidence === "number" && confidence < 0.35) {
256
- skipped.push({ summary, reason: "low_confidence" });
257
- options.logger.info("archive_skip reason=filtered_low_quality detail=low_confidence");
426
+ const quality = scoreQuality({
427
+ summary,
428
+ cause,
429
+ process,
430
+ result,
431
+ outcome: event.outcome,
432
+ sourceText: event.source_text,
433
+ });
434
+ const gateSource = inferGateSource(event);
435
+ const lifecycleComplete = quality.signals.workflowComplete;
436
+ if (gateSource === "sync" && !quality.signals.hasStructuredTriplet) {
437
+ skipped.push({ summary, reason: "incomplete_cause_process_result" });
438
+ options.logger.info("archive_skip reason=incomplete_cause_process_result gate_source=sync");
258
439
  continue;
259
440
  }
260
- const quality = scoreQuality(summary);
261
- if (quality.level === "low") {
262
- skipped.push({ summary, reason: "low_quality" });
263
- options.logger.info("archive_skip reason=filtered_low_quality detail=low_quality");
264
- continue;
441
+ const archiveMinConfidence = typeof options.writePolicy?.archiveMinConfidence === "number"
442
+ ? Math.max(0, Math.min(1, options.writePolicy.archiveMinConfidence))
443
+ : 0.35;
444
+ if (typeof confidence === "number" && confidence < archiveMinConfidence) {
445
+ if (!lifecycleComplete) {
446
+ skipped.push({ summary, reason: "low_confidence" });
447
+ options.logger.info("archive_skip reason=filtered_low_quality detail=low_confidence");
448
+ continue;
449
+ }
450
+ options.logger.info(`archive_confidence_override reason=workflow_complete confidence=${confidence.toFixed(2)} threshold=${archiveMinConfidence.toFixed(2)}`);
451
+ }
452
+ const archiveMinQualityScore = typeof options.writePolicy?.archiveMinQualityScore === "number"
453
+ ? Math.max(0, Math.min(1, options.writePolicy.archiveMinQualityScore))
454
+ : 0.4;
455
+ if (quality.score < archiveMinQualityScore) {
456
+ if (!lifecycleComplete) {
457
+ skipped.push({ summary, reason: "low_quality" });
458
+ options.logger.info("archive_skip reason=filtered_low_quality detail=low_quality");
459
+ continue;
460
+ }
461
+ options.logger.info(`archive_quality_override reason=workflow_complete quality=${quality.score.toFixed(2)} threshold=${archiveMinQualityScore.toFixed(2)}`);
265
462
  }
266
463
  const normalizedEventType = (0, ontology_1.normalizeEventType)(event.event_type || "insight", graphSchema);
267
- const entities = Array.isArray(event.entities)
268
- ? [...new Set(event.entities.map(item => (typeof item === "string" ? item.trim() : "")).filter(Boolean))]
269
- : [];
270
- const relationValidation = (0, ontology_1.validateRelations)({
271
- relations: Array.isArray(event.relations) ? event.relations : [],
272
- entities,
273
- entityTypes: event.entity_types,
274
- schema: graphSchema,
464
+ const id = `evt_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
465
+ const sourceTextRaw = typeof event.source_text === "string" ? event.source_text : "";
466
+ const sourceText = clampTailText(denoiseArchiveSourceText(sourceTextRaw), archiveSourceTextMaxChars);
467
+ const canonicalId = event.canonical_id || (0, ontology_1.buildCanonicalId)({
468
+ eventType: normalizedEventType,
469
+ summary,
470
+ outcome: event.outcome,
471
+ });
472
+ const dedupText = buildArchiveDedupText({
473
+ eventType: normalizedEventType,
474
+ summary,
475
+ cause,
476
+ process,
477
+ result,
478
+ outcome: event.outcome,
479
+ canonicalId,
480
+ entities: event.entities,
481
+ relations: event.relations,
482
+ });
483
+ const dedup = options.deduplicator.check({
484
+ id,
485
+ summary: dedupText || `${normalizedEventType}: ${summary}`,
275
486
  });
276
- if (relationValidation.accepted.length === 0 && Array.isArray(event.relations) && event.relations.length > 0) {
277
- skipped.push({ summary, reason: "relation_validation_failed" });
278
- options.logger.info("archive_skip reason=relation_validation_failed");
487
+ if (dedup.duplicate) {
488
+ skipped.push({ summary, reason: `duplicate_${dedup.stage || "unknown"}` });
489
+ options.logger.info(`archive_skip reason=duplicate_dedup_stage_${dedup.stage || "unknown"}`);
279
490
  continue;
280
491
  }
281
- const id = `evt_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
492
+ const record = {
493
+ id,
494
+ timestamp: new Date().toISOString(),
495
+ layer: "archive",
496
+ event_type: normalizedEventType,
497
+ summary,
498
+ cause,
499
+ process,
500
+ result,
501
+ source_text: sourceText || undefined,
502
+ outcome: event.outcome,
503
+ session_id: event.session_id,
504
+ source_file: event.source_file,
505
+ gate_source: gateSource,
506
+ embedding_status: "pending",
507
+ quality_score: quality.score,
508
+ quality_level: quality.level,
509
+ char_count: (sourceText || summary).length,
510
+ token_count: estimateTokenCount(sourceText || summary),
511
+ vector_chunks_total: 0,
512
+ vector_chunks_ok: 0,
513
+ confidence,
514
+ source_event_id: event.source_event_id,
515
+ actor: event.actor || "system",
516
+ canonical_id: canonicalId,
517
+ };
282
518
  let embedding = undefined;
283
- let embeddingStatus = "pending";
284
- let vectorChunksTotal = 0;
285
- let vectorChunksOk = 0;
286
519
  const vectorUpsertRows = [];
287
520
  const embeddingModel = options.embedding?.model || "";
288
521
  const embeddingApiKey = options.embedding?.apiKey || "";
@@ -291,10 +524,38 @@ function createArchiveStore(options) {
291
524
  if (embeddingModel && embeddingApiKey && embeddingBaseUrl) {
292
525
  const chunkSize = options.vectorChunking?.chunkSize ?? 600;
293
526
  const chunkOverlap = options.vectorChunking?.chunkOverlap ?? 100;
294
- const vectorText = buildArchiveVectorText(event, normalizedEventType, entities);
295
- const chunks = splitTextChunks(vectorText, chunkSize, chunkOverlap);
296
- vectorChunksTotal = chunks.length;
297
- const chunkEmbeddings = await mapWithConcurrency(chunks, maxParallel, async (chunk) => {
527
+ const evidenceMaxChunks = typeof options.vectorChunking?.evidenceMaxChunks === "number"
528
+ ? Math.max(0, Math.min(8, Math.floor(options.vectorChunking.evidenceMaxChunks)))
529
+ : 2;
530
+ const summaryText = (record.summary || "").trim();
531
+ const evidenceChunks = record.source_text
532
+ ? pickEvidenceChunks(splitTextChunks(record.source_text, chunkSize, chunkOverlap), evidenceMaxChunks)
533
+ : [];
534
+ const summaryChunk = summaryText
535
+ ? [
536
+ {
537
+ text: summaryText,
538
+ source_field: "summary",
539
+ index: 0,
540
+ total: 1 + evidenceChunks.length,
541
+ start: 0,
542
+ end: summaryText.length,
543
+ },
544
+ ]
545
+ : [];
546
+ const embeddingInputs = [
547
+ ...summaryChunk,
548
+ ...evidenceChunks.map((chunk, idx) => ({
549
+ text: chunk.text,
550
+ source_field: "evidence",
551
+ index: idx + summaryChunk.length,
552
+ total: summaryChunk.length + evidenceChunks.length,
553
+ start: chunk.start,
554
+ end: chunk.end,
555
+ })),
556
+ ];
557
+ record.vector_chunks_total = embeddingInputs.length;
558
+ const chunkEmbeddings = await mapWithConcurrency(embeddingInputs, maxParallel, async (chunk) => {
298
559
  try {
299
560
  const chunkEmbedding = await requestEmbedding({
300
561
  text: chunk.text,
@@ -314,14 +575,18 @@ function createArchiveStore(options) {
314
575
  return null;
315
576
  }
316
577
  catch (error) {
317
- options.logger.warn(`Archive chunk embedding failed id=${id} chunk=${chunk.index} error=${error}`);
578
+ options.logger.warn(`Archive chunk embedding failed id=${id} chunk=${chunk.index} field=${chunk.source_field} error=${error}`);
318
579
  return null;
319
580
  }
320
581
  });
321
582
  const validEmbeddings = chunkEmbeddings
322
583
  .filter((item) => Boolean(item))
323
584
  .sort((a, b) => a.chunk.index - b.chunk.index);
324
- if (validEmbeddings.length > 0) {
585
+ const primary = validEmbeddings.find(item => item.chunk.source_field === "summary");
586
+ if (primary) {
587
+ embedding = primary.embedding;
588
+ }
589
+ else if (validEmbeddings.length > 0) {
325
590
  embedding = validEmbeddings[0].embedding;
326
591
  }
327
592
  for (const item of validEmbeddings) {
@@ -329,58 +594,24 @@ function createArchiveStore(options) {
329
594
  id: `${id}_c${item.chunk.index}`,
330
595
  summary: item.chunk.text,
331
596
  embedding: item.embedding,
597
+ source_field: item.chunk.source_field,
332
598
  chunk_index: item.chunk.index,
333
- chunk_total: chunks.length,
599
+ chunk_total: item.chunk.total,
334
600
  chunk_start: item.chunk.start,
335
601
  chunk_end: item.chunk.end,
336
602
  });
337
603
  }
338
- vectorChunksOk = validEmbeddings.length;
339
- embeddingStatus = vectorChunksTotal > 0 && vectorChunksOk === vectorChunksTotal ? "ok" : "failed";
604
+ record.vector_chunks_ok = validEmbeddings.length;
605
+ record.embedding_status = record.vector_chunks_total > 0 && record.vector_chunks_ok === record.vector_chunks_total
606
+ ? "ok"
607
+ : "failed";
340
608
  }
341
- const dedup = options.deduplicator.check({
342
- id,
343
- summary: `${normalizedEventType}: ${summary}`,
344
- embedding,
345
- });
346
- if (dedup.duplicate) {
347
- skipped.push({ summary, reason: `duplicate_${dedup.stage || "unknown"}` });
348
- options.logger.info(`archive_skip reason=duplicate_dedup_stage_${dedup.stage || "unknown"}`);
349
- continue;
350
- }
351
- const gateSource = inferGateSource(event);
352
- const record = {
353
- ...event,
354
- event_type: normalizedEventType,
355
- entities,
356
- relations: relationValidation.accepted,
357
- canonical_id: event.canonical_id || (0, ontology_1.buildCanonicalId)({
358
- eventType: normalizedEventType,
359
- summary,
360
- entities,
361
- relations: relationValidation.accepted,
362
- outcome: event.outcome,
363
- }),
364
- actor: event.actor || "system",
365
- confidence,
366
- id,
367
- timestamp: new Date().toISOString(),
368
- layer: "archive",
369
- gate_source: gateSource,
370
- embedding_status: embeddingStatus,
371
- quality_score: quality.score,
372
- quality_level: quality.level,
373
- char_count: buildArchiveVectorText(event, normalizedEventType, entities).length,
374
- token_count: estimateTokenCount(buildArchiveVectorText(event, normalizedEventType, entities)),
375
- vector_chunks_total: vectorChunksTotal,
376
- vector_chunks_ok: vectorChunksOk,
377
- embedding,
378
- };
609
+ record.embedding = embedding;
379
610
  lines.push(JSON.stringify(record));
380
611
  stored.push(record);
381
612
  options.deduplicator.append({
382
613
  id: record.id,
383
- summary: `${record.event_type}: ${summary}`,
614
+ summary: dedupText || `${record.event_type}: ${summary}`,
384
615
  embedding: embedding,
385
616
  });
386
617
  mutationLines.push(JSON.stringify({
@@ -405,13 +636,13 @@ function createArchiveStore(options) {
405
636
  summary: chunkRow.summary,
406
637
  timestamp: record.timestamp,
407
638
  outcome: record.outcome,
408
- entities: record.entities,
409
- relations: record.relations,
410
639
  embedding: chunkRow.embedding,
411
640
  quality_score: record.quality_score,
412
641
  layer: "archive",
413
642
  source_memory_id: record.id,
414
643
  source_memory_canonical_id: record.canonical_id,
644
+ source_event_id: record.source_event_id || record.id,
645
+ source_field: chunkRow.source_field,
415
646
  char_count: chunkRow.summary.length,
416
647
  token_count: estimateTokenCount(chunkRow.summary),
417
648
  chunk_index: chunkRow.chunk_index,
@@ -435,6 +666,12 @@ function createArchiveStore(options) {
435
666
  if (lines.length > 0) {
436
667
  ensureDirForFile(archivePath);
437
668
  fs.appendFileSync(archivePath, `${lines.join("\n")}\n`, "utf-8");
669
+ for (let i = 0; i < lines.length; i++) {
670
+ const validation = (0, llm_output_validator_1.validateJsonlLine)(lines[i]);
671
+ if (!validation.valid && validation.errors.length > 0) {
672
+ options.logger.warn(`archive_write_integrity_check_failed line=${i} errors=${validation.errors.join("|")}`);
673
+ }
674
+ }
438
675
  ensureDirForFile(mutationLogPath);
439
676
  fs.appendFileSync(mutationLogPath, `${mutationLines.join("\n")}\n`, "utf-8");
440
677
  }