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

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 (79) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +46 -12
  3. package/SIGNATURE.md +7 -0
  4. package/SKILL.md +18 -3
  5. package/dist/index.d.ts +8 -0
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +148 -6
  8. package/dist/index.js.map +1 -1
  9. package/dist/openclaw.plugin.json +120 -4
  10. package/dist/src/engine/memory_engine.d.ts +5 -1
  11. package/dist/src/engine/memory_engine.d.ts.map +1 -1
  12. package/dist/src/engine/ts_engine.d.ts +116 -0
  13. package/dist/src/engine/ts_engine.d.ts.map +1 -1
  14. package/dist/src/engine/ts_engine.js +417 -102
  15. package/dist/src/engine/ts_engine.js.map +1 -1
  16. package/dist/src/engine/types.d.ts +17 -0
  17. package/dist/src/engine/types.d.ts.map +1 -1
  18. package/dist/src/graph/ontology.d.ts +23 -1
  19. package/dist/src/graph/ontology.d.ts.map +1 -1
  20. package/dist/src/graph/ontology.js +743 -70
  21. package/dist/src/graph/ontology.js.map +1 -1
  22. package/dist/src/quality/llm_output_validator.d.ts +20 -2
  23. package/dist/src/quality/llm_output_validator.d.ts.map +1 -1
  24. package/dist/src/quality/llm_output_validator.js +296 -41
  25. package/dist/src/quality/llm_output_validator.js.map +1 -1
  26. package/dist/src/store/archive_store.d.ts +8 -0
  27. package/dist/src/store/archive_store.d.ts.map +1 -1
  28. package/dist/src/store/archive_store.js +244 -84
  29. package/dist/src/store/archive_store.js.map +1 -1
  30. package/dist/src/store/graph_memory_store.d.ts +72 -2
  31. package/dist/src/store/graph_memory_store.d.ts.map +1 -1
  32. package/dist/src/store/graph_memory_store.js +723 -50
  33. package/dist/src/store/graph_memory_store.js.map +1 -1
  34. package/dist/src/store/read_store.d.ts +3 -0
  35. package/dist/src/store/read_store.d.ts.map +1 -1
  36. package/dist/src/store/read_store.js +1004 -209
  37. package/dist/src/store/read_store.js.map +1 -1
  38. package/dist/src/store/vector_store.d.ts +1 -0
  39. package/dist/src/store/vector_store.d.ts.map +1 -1
  40. package/dist/src/store/vector_store.js +1 -0
  41. package/dist/src/store/vector_store.js.map +1 -1
  42. package/dist/src/store/write_store.d.ts +2 -0
  43. package/dist/src/store/write_store.d.ts.map +1 -1
  44. package/dist/src/store/write_store.js +45 -3
  45. package/dist/src/store/write_store.js.map +1 -1
  46. package/dist/src/sync/session_sync.d.ts +20 -1
  47. package/dist/src/sync/session_sync.d.ts.map +1 -1
  48. package/dist/src/sync/session_sync.js +1810 -161
  49. package/dist/src/sync/session_sync.js.map +1 -1
  50. package/dist/src/wiki/wiki_linter.d.ts +25 -0
  51. package/dist/src/wiki/wiki_linter.d.ts.map +1 -0
  52. package/dist/src/wiki/wiki_linter.js +268 -0
  53. package/dist/src/wiki/wiki_linter.js.map +1 -0
  54. package/dist/src/wiki/wiki_logger.d.ts +10 -0
  55. package/dist/src/wiki/wiki_logger.d.ts.map +1 -0
  56. package/dist/src/wiki/wiki_logger.js +78 -0
  57. package/dist/src/wiki/wiki_logger.js.map +1 -0
  58. package/dist/src/wiki/wiki_maintainer.d.ts +36 -0
  59. package/dist/src/wiki/wiki_maintainer.d.ts.map +1 -0
  60. package/dist/src/wiki/wiki_maintainer.js +38 -0
  61. package/dist/src/wiki/wiki_maintainer.js.map +1 -0
  62. package/dist/src/wiki/wiki_projector.d.ts +33 -0
  63. package/dist/src/wiki/wiki_projector.d.ts.map +1 -0
  64. package/dist/src/wiki/wiki_projector.js +633 -0
  65. package/dist/src/wiki/wiki_projector.js.map +1 -0
  66. package/dist/src/wiki/wiki_queue.d.ts +29 -0
  67. package/dist/src/wiki/wiki_queue.d.ts.map +1 -0
  68. package/dist/src/wiki/wiki_queue.js +137 -0
  69. package/dist/src/wiki/wiki_queue.js.map +1 -0
  70. package/openclaw.plugin.json +120 -4
  71. package/package.json +8 -4
  72. package/schema/graph.schema.yaml +188 -33
  73. package/skills/cortex-memory/SKILL.md +49 -0
  74. package/skills/cortex-memory/references/agent-manual.md +115 -0
  75. package/skills/cortex-memory/references/configuration.md +92 -0
  76. package/skills/cortex-memory/references/publish-checklist.md +46 -0
  77. package/skills/cortex-memory/references/system-prompt-template.md +27 -0
  78. package/skills/cortex-memory/references/tools.md +181 -0
  79. package/skills/cortex-memory/scripts/smoke-check.ps1 +56 -0
@@ -39,6 +39,15 @@ const path = __importStar(require("path"));
39
39
  const crypto = __importStar(require("crypto"));
40
40
  const ontology_1 = require("../graph/ontology");
41
41
  const llm_output_validator_1 = require("../quality/llm_output_validator");
42
+ const wiki_queue_1 = require("../wiki/wiki_queue");
43
+ const wiki_logger_1 = require("../wiki/wiki_logger");
44
+ const wiki_maintainer_1 = require("../wiki/wiki_maintainer");
45
+ const SINGLE_VALUE_RELATION_TYPES = new Set([
46
+ "birthday_on",
47
+ "anniversary_on",
48
+ "has_spouse",
49
+ "lives_in",
50
+ ]);
42
51
  function ensureDirForFile(filePath) {
43
52
  const dir = path.dirname(filePath);
44
53
  if (!fs.existsSync(dir)) {
@@ -48,10 +57,362 @@ function ensureDirForFile(filePath) {
48
57
  function generateGraphId() {
49
58
  return `gph_${Date.now().toString(36)}_${crypto.randomBytes(4).toString("hex")}`;
50
59
  }
60
+ function generateConflictId() {
61
+ return `gcf_${Date.now().toString(36)}_${crypto.randomBytes(4).toString("hex")}`;
62
+ }
63
+ function relationKey(relation) {
64
+ const source = (relation.source || "").trim().toLowerCase();
65
+ const type = (relation.type || "related_to").trim().toLowerCase();
66
+ const target = (relation.target || "").trim().toLowerCase();
67
+ return `${source}|${type}|${target}`;
68
+ }
69
+ function relationBucketKey(relation) {
70
+ const source = (relation.source || "").trim().toLowerCase();
71
+ const type = (relation.type || "related_to").trim().toLowerCase();
72
+ return `${source}|${type}`;
73
+ }
74
+ function normalizeSummary(value) {
75
+ return String(value || "").trim().replace(/\s+/g, " ").toLowerCase();
76
+ }
77
+ function buildStructureSignature(record) {
78
+ const entities = (record.entities || [])
79
+ .map(item => String(item || "").trim().toLowerCase())
80
+ .filter(Boolean)
81
+ .sort();
82
+ const relations = (record.relations || [])
83
+ .map(item => {
84
+ const source = String(item.source || "").trim().toLowerCase();
85
+ const type = String(item.type || "").trim().toLowerCase();
86
+ const target = String(item.target || "").trim().toLowerCase();
87
+ const evidence = String(item.evidence_span || "").trim().toLowerCase();
88
+ const context = String(item.context_chunk || "").trim().toLowerCase();
89
+ return `${source}|${type}|${target}|${evidence}|${context}`;
90
+ })
91
+ .filter(Boolean)
92
+ .sort();
93
+ return `${entities.join("||")}#${relations.join("||")}`;
94
+ }
95
+ function buildFallbackSummary(entities, relations) {
96
+ const entityText = (entities || []).map(item => String(item || "").trim()).filter(Boolean).join(", ");
97
+ const relationText = (relations || [])
98
+ .slice(0, 3)
99
+ .map(item => `${item.source} ${item.type} ${item.target}`)
100
+ .join("; ");
101
+ return `Graph memory update covering entities [${entityText || "n/a"}] with relations [${relationText || "n/a"}].`;
102
+ }
103
+ function toConflictSummary(record) {
104
+ return {
105
+ conflict_id: record.conflict_id,
106
+ status: record.status,
107
+ created_at: record.created_at,
108
+ updated_at: record.updated_at,
109
+ source_event_id: record.source_event_id,
110
+ session_id: record.session_id,
111
+ conflict_reason: record.conflict_reason,
112
+ conflict_types: record.conflict_types,
113
+ existing_relation_keys: record.existing_relation_keys,
114
+ candidate_relations: (record.candidate.relations || []).map(rel => ({
115
+ source: rel.source,
116
+ type: rel.type,
117
+ target: rel.target,
118
+ })),
119
+ resolution: record.resolution,
120
+ };
121
+ }
51
122
  function createGraphMemoryStore(options) {
52
123
  const graphMemoryPath = path.join(options.memoryRoot, "graph", "memory.jsonl");
53
124
  const mutationLogPath = path.join(options.memoryRoot, "graph", "mutation_log.jsonl");
125
+ const conflictQueuePath = path.join(options.memoryRoot, "graph", "conflict_queue.jsonl");
126
+ const supersededRelationPath = path.join(options.memoryRoot, "graph", "superseded_relations.jsonl");
54
127
  const graphSchema = (0, ontology_1.loadGraphSchema)(options.projectRoot);
128
+ const wikiProjectionEnabled = options.wikiProjection?.enabled === true && options.wikiProjection?.mode !== "off";
129
+ const wikiProjectionMaxBatch = typeof options.wikiProjection?.maxBatch === "number" && Number.isFinite(options.wikiProjection.maxBatch)
130
+ ? Math.max(1, Math.min(1000, Math.floor(options.wikiProjection.maxBatch)))
131
+ : 100;
132
+ function triggerWikiMaintenance(input) {
133
+ if (!wikiProjectionEnabled) {
134
+ return;
135
+ }
136
+ try {
137
+ (0, wiki_queue_1.appendWikiRebuildEvent)({
138
+ memoryRoot: options.memoryRoot,
139
+ type: input.type,
140
+ source_event_id: input.sourceEventId,
141
+ conflict_id: input.conflictId,
142
+ entities: input.entities,
143
+ relation_types: input.relationTypes,
144
+ });
145
+ (0, wiki_logger_1.appendWikiLog)({
146
+ memoryRoot: options.memoryRoot,
147
+ type: input.type,
148
+ source_event_id: input.sourceEventId,
149
+ conflict_id: input.conflictId,
150
+ message: input.message,
151
+ });
152
+ const graphView = exportGraphView();
153
+ (0, wiki_maintainer_1.maintainWikiProjection)({
154
+ memoryRoot: options.memoryRoot,
155
+ graphView,
156
+ maxBatch: wikiProjectionMaxBatch,
157
+ logger: options.logger,
158
+ });
159
+ }
160
+ catch (error) {
161
+ options.logger.warn(`wiki_projection_maintenance_failed reason=${String(error)}`);
162
+ }
163
+ }
164
+ function loadSupersededRelationEntries() {
165
+ if (!fs.existsSync(supersededRelationPath)) {
166
+ return [];
167
+ }
168
+ const lines = fs.readFileSync(supersededRelationPath, "utf-8").split(/\r?\n/).filter(Boolean);
169
+ const entries = [];
170
+ for (const line of lines) {
171
+ try {
172
+ const parsed = JSON.parse(line);
173
+ const relationKeyRaw = typeof parsed.relation_key === "string" ? parsed.relation_key.trim().toLowerCase() : "";
174
+ if (!relationKeyRaw) {
175
+ continue;
176
+ }
177
+ entries.push({
178
+ relation_key: relationKeyRaw,
179
+ superseded_at: typeof parsed.superseded_at === "string" ? parsed.superseded_at : "",
180
+ conflict_id: typeof parsed.conflict_id === "string" ? parsed.conflict_id : undefined,
181
+ note: typeof parsed.note === "string" ? parsed.note : undefined,
182
+ });
183
+ }
184
+ catch {
185
+ options.logger.warn(`graph_superseded_parse_error line=${line.slice(0, 120)}`);
186
+ }
187
+ }
188
+ return entries;
189
+ }
190
+ function loadSupersededRelationKeys() {
191
+ const keys = new Set();
192
+ for (const entry of loadSupersededRelationEntries()) {
193
+ if (entry.relation_key) {
194
+ keys.add(entry.relation_key);
195
+ }
196
+ }
197
+ return keys;
198
+ }
199
+ function appendSupersededRelationEntries(entries) {
200
+ if (entries.length === 0) {
201
+ return;
202
+ }
203
+ ensureDirForFile(supersededRelationPath);
204
+ const lines = entries.map(item => JSON.stringify(item));
205
+ fs.appendFileSync(supersededRelationPath, `${lines.join("\n")}\n`, "utf-8");
206
+ }
207
+ function filterRecordBySuperseded(record, supersededKeys) {
208
+ const relations = Array.isArray(record.relations)
209
+ ? record.relations.filter(rel => !supersededKeys.has(relationKey(rel)))
210
+ : [];
211
+ if (relations.length === 0) {
212
+ return null;
213
+ }
214
+ return {
215
+ ...record,
216
+ relations,
217
+ };
218
+ }
219
+ function loadAllRaw() {
220
+ if (!fs.existsSync(graphMemoryPath)) {
221
+ return [];
222
+ }
223
+ const content = fs.readFileSync(graphMemoryPath, "utf-8");
224
+ const lines = content.split(/\r?\n/).filter(Boolean);
225
+ const records = [];
226
+ for (const line of lines) {
227
+ try {
228
+ const parsed = JSON.parse(line);
229
+ if (parsed && parsed.id && (parsed.source_event_id || parsed.archive_event_id)) {
230
+ if (!parsed.source_event_id && parsed.archive_event_id) {
231
+ parsed.source_event_id = parsed.archive_event_id;
232
+ parsed.source_layer = "archive_event";
233
+ }
234
+ records.push(parsed);
235
+ }
236
+ }
237
+ catch {
238
+ options.logger.warn(`graph_memory_parse_error line=${line.slice(0, 100)}`);
239
+ }
240
+ }
241
+ return records;
242
+ }
243
+ function loadAllEffective() {
244
+ const supersededKeys = loadSupersededRelationKeys();
245
+ const raw = loadAllRaw();
246
+ const output = [];
247
+ for (const record of raw) {
248
+ const filtered = filterRecordBySuperseded(record, supersededKeys);
249
+ if (filtered) {
250
+ output.push(filtered);
251
+ }
252
+ }
253
+ return output;
254
+ }
255
+ function shouldRejectStaleSummary(record) {
256
+ const sameSourceRecords = loadAllEffective()
257
+ .filter(item => item.source_event_id === record.source_event_id)
258
+ .sort((a, b) => Date.parse(b.timestamp || "") - Date.parse(a.timestamp || ""));
259
+ if (sameSourceRecords.length === 0) {
260
+ return false;
261
+ }
262
+ const latest = sameSourceRecords[0];
263
+ const previousSummary = normalizeSummary(latest.summary);
264
+ const nextSummary = normalizeSummary(record.summary);
265
+ if (!previousSummary || !nextSummary || previousSummary !== nextSummary) {
266
+ return false;
267
+ }
268
+ const previousSignature = buildStructureSignature(latest);
269
+ const nextSignature = buildStructureSignature(record);
270
+ return previousSignature !== nextSignature;
271
+ }
272
+ function loadConflictQueue() {
273
+ if (!fs.existsSync(conflictQueuePath)) {
274
+ return [];
275
+ }
276
+ const lines = fs.readFileSync(conflictQueuePath, "utf-8").split(/\r?\n/).filter(Boolean);
277
+ const records = [];
278
+ for (const line of lines) {
279
+ try {
280
+ const parsed = JSON.parse(line);
281
+ if (parsed && typeof parsed.conflict_id === "string" && parsed.conflict_id.trim()) {
282
+ records.push(parsed);
283
+ }
284
+ }
285
+ catch {
286
+ options.logger.warn(`graph_conflict_parse_error line=${line.slice(0, 120)}`);
287
+ }
288
+ }
289
+ return records;
290
+ }
291
+ function saveConflictQueue(records) {
292
+ ensureDirForFile(conflictQueuePath);
293
+ const lines = records.map(item => JSON.stringify(item));
294
+ fs.writeFileSync(conflictQueuePath, `${lines.join("\n")}${lines.length > 0 ? "\n" : ""}`, "utf-8");
295
+ }
296
+ function appendGraphRecord(args) {
297
+ const line = JSON.stringify(args.record);
298
+ const qualityCheck = (0, llm_output_validator_1.validateGraphJsonlLine)(line);
299
+ if (!qualityCheck.valid) {
300
+ throw new Error(`graph_record_quality_invalid:${qualityCheck.errors.join("|")}`);
301
+ }
302
+ if (qualityCheck.warnings.length > 0) {
303
+ options.logger.warn(`graph_quality_warning source_event_id=${args.record.source_event_id} warnings=${qualityCheck.warnings.join("|")}`);
304
+ }
305
+ const mutationEntry = {
306
+ op: args.conflictId ? "insert_graph_conflict_resolved" : "insert_graph",
307
+ id: args.record.id,
308
+ source_event_id: args.record.source_event_id,
309
+ source_layer: args.record.source_layer,
310
+ archive_event_id: args.record.archive_event_id,
311
+ timestamp: args.record.timestamp,
312
+ session_id: args.record.session_id,
313
+ gate_source: args.record.gate_source,
314
+ entity_count: args.record.entities.length,
315
+ relation_count: args.record.relations.length,
316
+ conflict_id: args.conflictId,
317
+ note: args.note,
318
+ };
319
+ ensureDirForFile(graphMemoryPath);
320
+ ensureDirForFile(mutationLogPath);
321
+ fs.appendFileSync(graphMemoryPath, `${line}\n`, "utf-8");
322
+ fs.appendFileSync(mutationLogPath, `${JSON.stringify(mutationEntry)}\n`, "utf-8");
323
+ }
324
+ function appendSupersedeMutationLogs(entries, conflictId, sourceEventId) {
325
+ if (entries.length === 0) {
326
+ return;
327
+ }
328
+ ensureDirForFile(mutationLogPath);
329
+ const nowIso = new Date().toISOString();
330
+ const lines = entries.map(item => JSON.stringify({
331
+ op: "supersede_relation",
332
+ id: `sup_${crypto.createHash("sha1").update(`${item.relation_key}|${item.superseded_at}`).digest("hex").slice(0, 12)}`,
333
+ source_event_id: sourceEventId || "conflict_resolution",
334
+ source_layer: "active_only",
335
+ timestamp: nowIso,
336
+ session_id: "system",
337
+ gate_source: "manual",
338
+ entity_count: 0,
339
+ relation_count: 0,
340
+ relation_key: item.relation_key,
341
+ conflict_id: conflictId,
342
+ note: item.note,
343
+ }));
344
+ fs.appendFileSync(mutationLogPath, `${lines.join("\n")}\n`, "utf-8");
345
+ }
346
+ function detectConflicts(candidate) {
347
+ const existing = loadAllEffective();
348
+ const candidateSingleValued = (candidate.relations || []).filter(rel => SINGLE_VALUE_RELATION_TYPES.has((rel.type || "").trim().toLowerCase()));
349
+ if (candidateSingleValued.length === 0) {
350
+ return {
351
+ hasConflict: false,
352
+ reason: "",
353
+ conflictTypes: [],
354
+ existingRelations: [],
355
+ existingRelationKeys: [],
356
+ };
357
+ }
358
+ const existingByBucket = new Map();
359
+ for (const record of existing) {
360
+ for (const rel of record.relations || []) {
361
+ const type = (rel.type || "").trim().toLowerCase();
362
+ if (!SINGLE_VALUE_RELATION_TYPES.has(type)) {
363
+ continue;
364
+ }
365
+ const key = relationKey(rel);
366
+ const bucket = relationBucketKey(rel);
367
+ if (!existingByBucket.has(bucket)) {
368
+ existingByBucket.set(bucket, []);
369
+ }
370
+ existingByBucket.get(bucket)?.push({ key, relation: rel });
371
+ }
372
+ }
373
+ const conflictTypes = new Set();
374
+ const existingRelations = [];
375
+ const existingRelationKeySet = new Set();
376
+ for (const rel of candidateSingleValued) {
377
+ const candidateKey = relationKey(rel);
378
+ const bucket = relationBucketKey(rel);
379
+ const sameBucket = existingByBucket.get(bucket) || [];
380
+ for (const existingItem of sameBucket) {
381
+ if (existingItem.key === candidateKey) {
382
+ continue;
383
+ }
384
+ existingRelationKeySet.add(existingItem.key);
385
+ existingRelations.push(existingItem);
386
+ conflictTypes.add((rel.type || "").trim().toLowerCase());
387
+ }
388
+ }
389
+ const existingRelationKeys = [...existingRelationKeySet].sort();
390
+ if (existingRelationKeys.length === 0) {
391
+ return {
392
+ hasConflict: false,
393
+ reason: "",
394
+ conflictTypes: [],
395
+ existingRelations: [],
396
+ existingRelationKeys: [],
397
+ };
398
+ }
399
+ return {
400
+ hasConflict: true,
401
+ reason: `single_value_relation_conflict:${[...conflictTypes].sort().join(",")}`,
402
+ conflictTypes: [...conflictTypes].sort(),
403
+ existingRelations,
404
+ existingRelationKeys,
405
+ };
406
+ }
407
+ function buildConflictFingerprint(input) {
408
+ const payload = JSON.stringify({
409
+ source_event_id: input.sourceEventId,
410
+ conflict_types: [...input.conflictTypes].sort(),
411
+ existing_relation_keys: [...input.existingRelationKeys].sort(),
412
+ candidate_relation_keys: input.candidateRelations.map(relationKey).sort(),
413
+ });
414
+ return crypto.createHash("sha1").update(payload).digest("hex");
415
+ }
55
416
  options.logger.info(`Graph memory store initialized at ${graphMemoryPath}`);
56
417
  async function append(input) {
57
418
  const validation = (0, ontology_1.validateGraphPayload)({
@@ -60,6 +421,8 @@ function createGraphMemoryStore(options) {
60
421
  archiveEventId: input.archiveEventId,
61
422
  sessionId: input.sessionId,
62
423
  sourceFile: input.sourceFile,
424
+ source_text_nav: input.source_text_nav,
425
+ summary: input.summary,
63
426
  eventType: input.eventType,
64
427
  entities: input.entities,
65
428
  entity_types: input.entity_types,
@@ -80,66 +443,98 @@ function createGraphMemoryStore(options) {
80
443
  const record = validation.normalized;
81
444
  record.id = generateGraphId();
82
445
  record.timestamp = new Date().toISOString();
83
- const line = JSON.stringify(record);
84
- const qualityCheck = (0, llm_output_validator_1.validateGraphJsonlLine)(line);
85
- if (!qualityCheck.valid) {
86
- const reason = `graph_record_quality_invalid:${qualityCheck.errors.join("|")}`;
446
+ if (shouldRejectStaleSummary(record)) {
447
+ options.logger.info(`graph_skip_reason=stale_summary_after_update source_event_id=${record.source_event_id}`);
448
+ return { success: false, reason: "stale_summary_after_update" };
449
+ }
450
+ const conflictDetection = detectConflicts(record);
451
+ if (conflictDetection.hasConflict) {
452
+ const queue = loadConflictQueue();
453
+ const fingerprint = buildConflictFingerprint({
454
+ sourceEventId: record.source_event_id,
455
+ conflictTypes: conflictDetection.conflictTypes,
456
+ existingRelationKeys: conflictDetection.existingRelationKeys,
457
+ candidateRelations: record.relations,
458
+ });
459
+ const existingPending = queue.find(item => item.status === "pending" && item.fingerprint === fingerprint);
460
+ if (existingPending) {
461
+ options.logger.info(`graph_conflict_pending_reuse conflict_id=${existingPending.conflict_id} source_event_id=${record.source_event_id}`);
462
+ return { success: false, reason: `graph_conflict_pending:${existingPending.conflict_id}` };
463
+ }
464
+ const now = new Date().toISOString();
465
+ const conflictRecord = {
466
+ conflict_id: generateConflictId(),
467
+ status: "pending",
468
+ created_at: now,
469
+ updated_at: now,
470
+ source_event_id: record.source_event_id,
471
+ source_layer: record.source_layer,
472
+ session_id: record.session_id,
473
+ source_file: record.source_file,
474
+ conflict_reason: conflictDetection.reason,
475
+ conflict_types: conflictDetection.conflictTypes,
476
+ existing_relation_keys: conflictDetection.existingRelationKeys,
477
+ existing_relations: conflictDetection.existingRelations,
478
+ candidate: {
479
+ summary: record.summary,
480
+ source_text_nav: record.source_text_nav,
481
+ entities: record.entities,
482
+ entity_types: record.entity_types,
483
+ relations: record.relations,
484
+ event_type: record.event_type,
485
+ gate_source: record.gate_source,
486
+ confidence: record.confidence,
487
+ source_text: input.sourceText,
488
+ },
489
+ fingerprint,
490
+ };
491
+ queue.push(conflictRecord);
492
+ saveConflictQueue(queue);
493
+ options.logger.info(`graph_conflict_pending id=${conflictRecord.conflict_id} source_event_id=${record.source_event_id} types=${conflictRecord.conflict_types.join(",")}`);
494
+ triggerWikiMaintenance({
495
+ type: "conflict_pending",
496
+ message: "Conflict pending and awaiting confirmation",
497
+ sourceEventId: record.source_event_id,
498
+ conflictId: conflictRecord.conflict_id,
499
+ entities: record.entities,
500
+ relationTypes: (record.relations || []).map(item => item.type),
501
+ });
502
+ return { success: false, reason: `graph_conflict_pending:${conflictRecord.conflict_id}` };
503
+ }
504
+ try {
505
+ appendGraphRecord({ record });
506
+ options.logger.info(`graph_write id=${record.id} source_event_id=${record.source_event_id} source_layer=${record.source_layer} entities=${record.entities.length} relations=${record.relations.length} gate_source=${record.gate_source}`);
507
+ triggerWikiMaintenance({
508
+ type: "graph_append",
509
+ message: "Graph relation appended",
510
+ sourceEventId: record.source_event_id,
511
+ entities: record.entities,
512
+ relationTypes: (record.relations || []).map(item => item.type),
513
+ });
514
+ return { success: true, record };
515
+ }
516
+ catch (error) {
517
+ const reason = String(error);
87
518
  options.logger.warn(`graph_skip_reason=${reason} source_event_id=${input.sourceEventId}`);
88
519
  return { success: false, reason };
89
520
  }
90
- if (qualityCheck.warnings.length > 0) {
91
- options.logger.warn(`graph_quality_warning source_event_id=${input.sourceEventId} warnings=${qualityCheck.warnings.join("|")}`);
92
- }
93
- const mutationEntry = {
94
- op: "insert_graph",
95
- id: record.id,
96
- source_event_id: record.source_event_id,
97
- source_layer: record.source_layer,
98
- archive_event_id: record.archive_event_id,
99
- timestamp: record.timestamp,
100
- session_id: record.session_id,
101
- gate_source: record.gate_source,
102
- entity_count: record.entities.length,
103
- relation_count: record.relations.length,
104
- };
105
- const mutationLine = JSON.stringify(mutationEntry);
106
- ensureDirForFile(graphMemoryPath);
107
- ensureDirForFile(mutationLogPath);
108
- fs.appendFileSync(graphMemoryPath, `${line}\n`, "utf-8");
109
- fs.appendFileSync(mutationLogPath, `${mutationLine}\n`, "utf-8");
110
- options.logger.info(`graph_write id=${record.id} source_event_id=${record.source_event_id} source_layer=${record.source_layer} entities=${record.entities.length} relations=${record.relations.length} gate_source=${record.gate_source}`);
111
- return { success: true, record };
112
521
  }
113
522
  function loadAll() {
114
- if (!fs.existsSync(graphMemoryPath)) {
115
- return [];
116
- }
117
- const content = fs.readFileSync(graphMemoryPath, "utf-8");
118
- const lines = content.split(/\r?\n/).filter(Boolean);
119
- const records = [];
120
- for (const line of lines) {
121
- try {
122
- const parsed = JSON.parse(line);
123
- if (parsed && parsed.id && (parsed.source_event_id || parsed.archive_event_id)) {
124
- if (!parsed.source_event_id && parsed.archive_event_id) {
125
- parsed.source_event_id = parsed.archive_event_id;
126
- parsed.source_layer = "archive_event";
127
- }
128
- records.push(parsed);
129
- }
130
- }
131
- catch {
132
- options.logger.warn(`graph_memory_parse_error line=${line.slice(0, 100)}`);
133
- }
134
- }
135
- return records;
523
+ return loadAllEffective();
136
524
  }
137
525
  function loadByArchiveEventId(archiveEventId) {
138
526
  return loadAll().filter(record => record.archive_event_id === archiveEventId || record.source_event_id === archiveEventId);
139
527
  }
140
528
  function loadByEntity(entityName) {
141
- const normalized = entityName.trim().toLowerCase();
142
- return loadAll().filter(record => record.entities.some(entity => entity.trim().toLowerCase() === normalized));
529
+ const normalized = entityName.trim();
530
+ const queryKeySet = new Set((0, ontology_1.getEntityMatchKeys)(normalized, graphSchema));
531
+ if (queryKeySet.size === 0) {
532
+ return [];
533
+ }
534
+ return loadAll().filter(record => record.entities.some(entity => {
535
+ const entityKeys = (0, ontology_1.getEntityMatchKeys)(entity, graphSchema);
536
+ return entityKeys.some(key => queryKeySet.has(key));
537
+ }));
143
538
  }
144
539
  function getStats() {
145
540
  const all = loadAll();
@@ -157,12 +552,290 @@ function createGraphMemoryStore(options) {
157
552
  totalRelations,
158
553
  };
159
554
  }
555
+ function parseRelationKey(key) {
556
+ const parts = key.split("|");
557
+ return {
558
+ source: parts[0] || "",
559
+ type: parts[1] || "related_to",
560
+ target: parts.slice(2).join("|") || "",
561
+ };
562
+ }
563
+ function exportGraphView() {
564
+ const rawRecords = loadAllRaw();
565
+ const activeRecords = loadAllEffective();
566
+ const conflictQueue = loadConflictQueue();
567
+ const supersededEntries = loadSupersededRelationEntries();
568
+ const nodes = new Map();
569
+ const edges = [];
570
+ const dedupe = new Set();
571
+ let latestTimestampMs = 0;
572
+ function updateLatest(iso) {
573
+ const ms = Date.parse(iso || "");
574
+ if (Number.isFinite(ms) && ms > latestTimestampMs) {
575
+ latestTimestampMs = ms;
576
+ }
577
+ }
578
+ function addNode(id) {
579
+ const trimmed = id.trim();
580
+ if (!trimmed) {
581
+ return;
582
+ }
583
+ const key = trimmed.toLowerCase();
584
+ if (!nodes.has(key)) {
585
+ nodes.set(key, { id: trimmed, type: "entity" });
586
+ }
587
+ }
588
+ function addEdge(input) {
589
+ const source = input.source.trim();
590
+ const target = input.target.trim();
591
+ const type = (input.type || "related_to").trim().toLowerCase();
592
+ if (!source || !target || !type) {
593
+ return;
594
+ }
595
+ const relation_key = `${source.toLowerCase()}|${type}|${target.toLowerCase()}`;
596
+ const dedupeKey = `${relation_key}|${input.status}|${(input.conflict_id || "").trim().toLowerCase()}`;
597
+ if (dedupe.has(dedupeKey)) {
598
+ return;
599
+ }
600
+ dedupe.add(dedupeKey);
601
+ addNode(source);
602
+ addNode(target);
603
+ edges.push({
604
+ source,
605
+ target,
606
+ type,
607
+ status: input.status,
608
+ relation_key,
609
+ source_event_id: input.source_event_id,
610
+ evidence_span: input.evidence_span,
611
+ confidence: input.confidence,
612
+ conflict_id: input.conflict_id,
613
+ });
614
+ }
615
+ const relationMetaByKey = new Map();
616
+ for (const record of rawRecords) {
617
+ updateLatest(record.timestamp);
618
+ for (const rel of record.relations || []) {
619
+ const key = relationKey(rel);
620
+ const prev = relationMetaByKey.get(key);
621
+ if (!prev || Date.parse(record.timestamp || "") >= Date.parse(prev.timestamp || "")) {
622
+ relationMetaByKey.set(key, {
623
+ source_event_id: record.source_event_id,
624
+ evidence_span: rel.evidence_span,
625
+ confidence: rel.confidence,
626
+ timestamp: record.timestamp,
627
+ });
628
+ }
629
+ }
630
+ }
631
+ for (const record of activeRecords) {
632
+ updateLatest(record.timestamp);
633
+ for (const rel of record.relations || []) {
634
+ addEdge({
635
+ source: rel.source,
636
+ target: rel.target,
637
+ type: rel.type,
638
+ status: "active",
639
+ source_event_id: record.source_event_id,
640
+ evidence_span: rel.evidence_span,
641
+ confidence: rel.confidence,
642
+ });
643
+ }
644
+ }
645
+ for (const entry of supersededEntries) {
646
+ updateLatest(entry.superseded_at);
647
+ const parsed = parseRelationKey(entry.relation_key);
648
+ const meta = relationMetaByKey.get(entry.relation_key);
649
+ addEdge({
650
+ source: parsed.source,
651
+ target: parsed.target,
652
+ type: parsed.type,
653
+ status: "superseded",
654
+ source_event_id: meta?.source_event_id,
655
+ evidence_span: meta?.evidence_span,
656
+ confidence: meta?.confidence,
657
+ conflict_id: entry.conflict_id,
658
+ });
659
+ }
660
+ for (const item of conflictQueue) {
661
+ updateLatest(item.updated_at);
662
+ if (item.status !== "pending" && item.status !== "rejected") {
663
+ continue;
664
+ }
665
+ const status = item.status === "pending" ? "pending_conflict" : "rejected";
666
+ for (const rel of item.candidate.relations || []) {
667
+ addEdge({
668
+ source: rel.source,
669
+ target: rel.target,
670
+ type: rel.type,
671
+ status,
672
+ source_event_id: item.source_event_id,
673
+ evidence_span: rel.evidence_span,
674
+ confidence: rel.confidence,
675
+ conflict_id: item.conflict_id,
676
+ });
677
+ }
678
+ }
679
+ const status_counts = {
680
+ active: 0,
681
+ pending_conflict: 0,
682
+ superseded: 0,
683
+ rejected: 0,
684
+ };
685
+ for (const edge of edges) {
686
+ status_counts[edge.status] += 1;
687
+ }
688
+ edges.sort((a, b) => {
689
+ const left = `${a.status}|${a.source.toLowerCase()}|${a.type}|${a.target.toLowerCase()}|${a.conflict_id || ""}`;
690
+ const right = `${b.status}|${b.source.toLowerCase()}|${b.type}|${b.target.toLowerCase()}|${b.conflict_id || ""}`;
691
+ return left.localeCompare(right);
692
+ });
693
+ const nodeList = [...nodes.values()].sort((a, b) => a.id.toLowerCase().localeCompare(b.id.toLowerCase()));
694
+ return {
695
+ nodes: nodeList,
696
+ edges,
697
+ status_counts,
698
+ updated_at: latestTimestampMs > 0 ? new Date(latestTimestampMs).toISOString() : new Date().toISOString(),
699
+ };
700
+ }
701
+ function listConflicts(args) {
702
+ const status = args?.status || "pending";
703
+ const limit = typeof args?.limit === "number" && Number.isFinite(args.limit) && args.limit > 0
704
+ ? Math.min(500, Math.floor(args.limit))
705
+ : 50;
706
+ const queue = loadConflictQueue();
707
+ const filtered = queue
708
+ .filter(item => status === "all" ? true : item.status === status)
709
+ .sort((a, b) => Date.parse(b.updated_at) - Date.parse(a.updated_at));
710
+ return filtered.slice(0, limit).map(toConflictSummary);
711
+ }
712
+ function getConflictStats() {
713
+ const queue = loadConflictQueue();
714
+ let pending = 0;
715
+ let accepted = 0;
716
+ let rejected = 0;
717
+ for (const item of queue) {
718
+ if (item.status === "pending")
719
+ pending += 1;
720
+ if (item.status === "accepted")
721
+ accepted += 1;
722
+ if (item.status === "rejected")
723
+ rejected += 1;
724
+ }
725
+ return { pending, accepted, rejected };
726
+ }
727
+ async function resolveConflict(args) {
728
+ const conflictId = (args.conflictId || "").trim();
729
+ if (!conflictId) {
730
+ return { success: false, reason: "conflict_id_empty" };
731
+ }
732
+ const queue = loadConflictQueue();
733
+ const index = queue.findIndex(item => item.conflict_id === conflictId);
734
+ if (index < 0) {
735
+ return { success: false, reason: "conflict_not_found" };
736
+ }
737
+ const current = queue[index];
738
+ if (current.status !== "pending") {
739
+ return { success: false, reason: `conflict_already_${current.status}` };
740
+ }
741
+ const now = new Date().toISOString();
742
+ if (args.action === "reject") {
743
+ current.status = "rejected";
744
+ current.updated_at = now;
745
+ current.resolution = {
746
+ action: "reject",
747
+ note: args.note,
748
+ resolved_at: now,
749
+ };
750
+ queue[index] = current;
751
+ saveConflictQueue(queue);
752
+ options.logger.info(`graph_conflict_rejected id=${conflictId}`);
753
+ triggerWikiMaintenance({
754
+ type: "conflict_rejected",
755
+ message: "Conflict rejected; canonical graph unchanged",
756
+ sourceEventId: current.source_event_id,
757
+ conflictId,
758
+ entities: current.candidate.entities,
759
+ relationTypes: (current.candidate.relations || []).map(item => item.type),
760
+ });
761
+ return { success: true };
762
+ }
763
+ const supersedeEntries = current.existing_relation_keys.map(key => ({
764
+ relation_key: key,
765
+ superseded_at: now,
766
+ conflict_id: conflictId,
767
+ note: args.note || "conflict_accept_replace",
768
+ }));
769
+ appendSupersededRelationEntries(supersedeEntries);
770
+ appendSupersedeMutationLogs(supersedeEntries, conflictId, current.source_event_id);
771
+ const record = {
772
+ id: generateGraphId(),
773
+ summary: (current.candidate.summary || "").trim() || buildFallbackSummary(current.candidate.entities, current.candidate.relations),
774
+ source_text_nav: current.candidate.source_text_nav || {
775
+ layer: current.source_layer,
776
+ session_id: current.session_id,
777
+ source_file: current.source_file || "unknown",
778
+ source_memory_id: current.source_event_id,
779
+ source_event_id: current.source_event_id,
780
+ },
781
+ source_event_id: current.source_event_id,
782
+ source_layer: current.source_layer,
783
+ archive_event_id: current.source_layer === "archive_event" ? current.source_event_id : undefined,
784
+ session_id: current.session_id,
785
+ source_file: current.source_file,
786
+ timestamp: now,
787
+ entities: current.candidate.entities,
788
+ entity_types: current.candidate.entity_types,
789
+ relations: current.candidate.relations,
790
+ gate_source: current.candidate.gate_source,
791
+ event_type: current.candidate.event_type,
792
+ schema_version: "1.0.0",
793
+ confidence: current.candidate.confidence,
794
+ };
795
+ if (shouldRejectStaleSummary(record)) {
796
+ return { success: false, reason: "stale_summary_after_update" };
797
+ }
798
+ try {
799
+ appendGraphRecord({
800
+ record,
801
+ conflictId,
802
+ note: args.note,
803
+ });
804
+ }
805
+ catch (error) {
806
+ return { success: false, reason: String(error) };
807
+ }
808
+ current.status = "accepted";
809
+ current.updated_at = now;
810
+ current.resolution = {
811
+ action: "accept",
812
+ note: args.note,
813
+ resolved_at: now,
814
+ applied_record_id: record.id,
815
+ };
816
+ queue[index] = current;
817
+ saveConflictQueue(queue);
818
+ options.logger.info(`graph_conflict_accepted id=${conflictId} record_id=${record.id}`);
819
+ triggerWikiMaintenance({
820
+ type: "conflict_accepted",
821
+ message: "Conflict accepted; canonical graph updated",
822
+ sourceEventId: current.source_event_id,
823
+ conflictId,
824
+ entities: current.candidate.entities,
825
+ relationTypes: (current.candidate.relations || []).map(item => item.type),
826
+ });
827
+ return { success: true, appliedRecordId: record.id };
828
+ }
160
829
  return {
161
830
  append,
162
831
  loadAll,
163
832
  loadByArchiveEventId,
164
833
  loadByEntity,
165
834
  getStats,
835
+ exportGraphView,
836
+ listConflicts,
837
+ resolveConflict,
838
+ getConflictStats,
166
839
  };
167
840
  }
168
841
  //# sourceMappingURL=graph_memory_store.js.map