@shadowforge0/aquifer-memory 1.5.12 → 1.6.0

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 (60) hide show
  1. package/.env.example +23 -0
  2. package/README.md +78 -73
  3. package/README_CN.md +659 -0
  4. package/README_TW.md +680 -0
  5. package/aquifer.config.example.json +34 -0
  6. package/consumers/claude-code.js +11 -11
  7. package/consumers/cli.js +353 -52
  8. package/consumers/codex-handoff.js +152 -0
  9. package/consumers/codex.js +1549 -0
  10. package/consumers/default/daily-entries.js +23 -4
  11. package/consumers/default/index.js +2 -2
  12. package/consumers/default/prompts/summary.js +6 -6
  13. package/consumers/mcp.js +96 -5
  14. package/consumers/openclaw-ext/index.js +0 -1
  15. package/consumers/openclaw-plugin.js +1 -1
  16. package/consumers/shared/config.js +8 -0
  17. package/consumers/shared/factory.js +1 -0
  18. package/consumers/shared/ingest.js +1 -1
  19. package/consumers/shared/normalize.js +14 -3
  20. package/consumers/shared/recall-format.js +27 -0
  21. package/consumers/shared/summary-parser.js +151 -0
  22. package/core/aquifer.js +372 -18
  23. package/core/finalization-review.js +319 -0
  24. package/core/mcp-manifest.js +52 -2
  25. package/core/memory-bootstrap.js +188 -0
  26. package/core/memory-consolidation.js +1236 -0
  27. package/core/memory-promotion.js +544 -0
  28. package/core/memory-recall.js +247 -0
  29. package/core/memory-records.js +581 -0
  30. package/core/memory-safety-gate.js +224 -0
  31. package/core/session-finalization.js +350 -0
  32. package/core/storage.js +385 -2
  33. package/docs/getting-started.md +99 -0
  34. package/docs/postprocess-contract.md +2 -2
  35. package/docs/setup.md +51 -2
  36. package/package.json +25 -11
  37. package/pipeline/normalize/adapters/codex.js +106 -0
  38. package/pipeline/normalize/detect.js +3 -2
  39. package/schema/001-base.sql +3 -0
  40. package/schema/007-v1-foundation.sql +273 -0
  41. package/schema/008-session-finalizations.sql +50 -0
  42. package/schema/009-v1-assertion-plane.sql +193 -0
  43. package/schema/010-v1-finalization-review.sql +160 -0
  44. package/schema/011-v1-compaction-claim.sql +46 -0
  45. package/schema/012-v1-compaction-lease.sql +39 -0
  46. package/schema/013-v1-compaction-lineage.sql +193 -0
  47. package/scripts/codex-recovery.js +532 -0
  48. package/consumers/miranda/context-inject.js +0 -120
  49. package/consumers/miranda/daily-entries.js +0 -224
  50. package/consumers/miranda/index.js +0 -364
  51. package/consumers/miranda/instance.js +0 -55
  52. package/consumers/miranda/llm.js +0 -99
  53. package/consumers/miranda/profile.json +0 -145
  54. package/consumers/miranda/prompts/summary.js +0 -303
  55. package/consumers/miranda/recall-format.js +0 -76
  56. package/consumers/miranda/render-daily-md.js +0 -186
  57. package/consumers/miranda/workspace-files.js +0 -91
  58. package/scripts/drop-entity-state-history.sql +0 -17
  59. package/scripts/drop-insights.sql +0 -12
  60. package/scripts/install-openclaw.sh +0 -59
@@ -0,0 +1,544 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+
5
+ const ACTIVE_V1_TYPES = new Set([
6
+ 'fact',
7
+ 'state',
8
+ 'decision',
9
+ 'preference',
10
+ 'constraint',
11
+ 'entity_note',
12
+ 'open_loop',
13
+ 'conclusion',
14
+ ]);
15
+
16
+ const AUTHORITY_RANK = {
17
+ raw_transcript: 0,
18
+ llm_inference: 1,
19
+ verified_summary: 2,
20
+ manual: 3,
21
+ system: 4,
22
+ executable_evidence: 5,
23
+ user_explicit: 6,
24
+ };
25
+
26
+ const DEFAULT_INHERITANCE = {
27
+ constraint: 'additive',
28
+ preference: 'defaultable',
29
+ state: 'defaultable',
30
+ fact: 'defaultable',
31
+ conclusion: 'defaultable',
32
+ entity_note: 'defaultable',
33
+ decision: 'non_inheritable',
34
+ open_loop: 'non_inheritable',
35
+ };
36
+
37
+ const DEFAULT_ASPECT = {
38
+ fact: 'fact',
39
+ state: 'state',
40
+ decision: 'decision',
41
+ preference: 'preference',
42
+ constraint: 'constraint',
43
+ entity_note: 'entity_note',
44
+ open_loop: 'open_loop',
45
+ conclusion: 'conclusion',
46
+ };
47
+
48
+ const FORBIDDEN_TAGS = new Set([
49
+ 'commentary',
50
+ 'tool_narration',
51
+ 'failed_hypothesis',
52
+ 'wrapper_metadata',
53
+ 'session_injected_context',
54
+ 'rendered_artifact',
55
+ 'stack_trace',
56
+ 'secret',
57
+ 'secret_risk',
58
+ 'tool_output',
59
+ 'env_dump',
60
+ ]);
61
+
62
+ function normalizeText(value) {
63
+ return String(value || '').trim().replace(/\s+/g, ' ');
64
+ }
65
+
66
+ function normalizeType(value) {
67
+ return normalizeText(value).toLowerCase();
68
+ }
69
+
70
+ function stableHash(value) {
71
+ return crypto
72
+ .createHash('sha256')
73
+ .update(String(value || '').trim().toLowerCase())
74
+ .digest('hex')
75
+ .slice(0, 16);
76
+ }
77
+
78
+ function stableJson(value) {
79
+ if (value === null || value === undefined) return JSON.stringify(null);
80
+ if (Array.isArray(value)) return `[${value.map(stableJson).join(',')}]`;
81
+ if (typeof value === 'object') {
82
+ return `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${stableJson(value[key])}`).join(',')}}`;
83
+ }
84
+ return JSON.stringify(value);
85
+ }
86
+
87
+ function assertionHash(value) {
88
+ return crypto
89
+ .createHash('sha256')
90
+ .update(stableJson(value))
91
+ .digest('hex');
92
+ }
93
+
94
+ function defaultInheritanceForType(memoryType) {
95
+ return DEFAULT_INHERITANCE[normalizeType(memoryType)] || 'defaultable';
96
+ }
97
+
98
+ function authorityRank(authority) {
99
+ return AUTHORITY_RANK[String(authority || 'llm_inference').toLowerCase()] ?? 0;
100
+ }
101
+
102
+ function buildCanonicalKey({ memoryType, scopeKey, contextKey, topicKey, subject, aspect }) {
103
+ const type = normalizeType(memoryType || 'memory');
104
+ const scope = normalizeType(scopeKey || 'unspecified');
105
+ const context = normalizeType(contextKey || '');
106
+ const topic = normalizeType(topicKey || '');
107
+ const scopePart = [scope, context, topic].filter(Boolean).join('|');
108
+ const subj = normalizeType(subject || 'session');
109
+ const asp = normalizeType(aspect || DEFAULT_ASPECT[type] || type);
110
+ return [type, scopePart, subj, asp].join(':');
111
+ }
112
+
113
+ function textFromItem(item, keys) {
114
+ if (typeof item === 'string') return normalizeText(item);
115
+ for (const key of keys) {
116
+ const text = normalizeText(item && item[key]);
117
+ if (text) return text;
118
+ }
119
+ return '';
120
+ }
121
+
122
+ function normalizeEvidenceRefs(candidate = {}) {
123
+ return candidate.evidenceRefs || candidate.evidence_refs || [];
124
+ }
125
+
126
+ function buildFactAssertion(candidate = {}, opts = {}) {
127
+ const memoryType = normalizeType(candidate.memoryType || candidate.memory_type);
128
+ if (memoryType !== 'fact') return null;
129
+
130
+ const payload = candidate.payload && typeof candidate.payload === 'object' ? candidate.payload : {};
131
+ const subject = normalizeText(payload.subject || candidate.subject || 'session');
132
+ const predicate = normalizeText(
133
+ payload.predicate ||
134
+ payload.aspect ||
135
+ payload.attribute ||
136
+ candidate.predicate ||
137
+ candidate.aspect ||
138
+ 'fact',
139
+ );
140
+ const statement = normalizeText(
141
+ payload.statement ||
142
+ payload.fact ||
143
+ payload.summary ||
144
+ payload.text ||
145
+ candidate.summary ||
146
+ candidate.title,
147
+ );
148
+ if (!predicate || !statement) return null;
149
+
150
+ const rawObjectKind = normalizeText(payload.object_kind || payload.objectKind || 'value') || 'value';
151
+ const objectKind = ['entity', 'value', 'none'].includes(rawObjectKind) ? rawObjectKind : 'value';
152
+ const objectEntityId = payload.object_entity_id || payload.objectEntityId || null;
153
+ const objectValueJson = objectKind === 'none' || objectKind === 'entity'
154
+ ? null
155
+ : (payload.object_value_json !== undefined
156
+ ? payload.object_value_json
157
+ : {
158
+ statement,
159
+ subject,
160
+ });
161
+ const qualifiersJson = {
162
+ subject,
163
+ statement,
164
+ ...(payload.qualifiers && typeof payload.qualifiers === 'object' ? payload.qualifiers : {}),
165
+ };
166
+ const acceptedAt = candidate.acceptedAt || opts.acceptedAt || new Date().toISOString();
167
+ const observedAt = candidate.observedAt || candidate.observed_at || payload.observed_at || payload.observedAt || acceptedAt;
168
+ const assertion = {
169
+ tenantId: opts.tenantId,
170
+ canonicalKey: candidate.canonicalKey || candidate.canonical_key,
171
+ scopeId: opts.scopeId,
172
+ subjectEntityId: payload.subject_entity_id || payload.subjectEntityId || candidate.subjectEntityId || null,
173
+ predicate,
174
+ objectKind,
175
+ objectEntityId: objectKind === 'entity' ? objectEntityId : null,
176
+ objectValueJson,
177
+ qualifiersJson,
178
+ validFrom: candidate.validFrom || candidate.valid_from || payload.valid_from || payload.validFrom || null,
179
+ validTo: candidate.validTo || candidate.valid_to || payload.valid_to || payload.validTo || null,
180
+ observedAt,
181
+ staleAfter: candidate.staleAfter || candidate.stale_after || payload.stale_after || payload.staleAfter || null,
182
+ acceptedAt,
183
+ status: 'active',
184
+ authority: candidate.authority || 'verified_summary',
185
+ versionId: candidate.versionId || candidate.version_id || null,
186
+ createdByFinalizationId: opts.createdByFinalizationId || opts.created_by_finalization_id || null,
187
+ createdByCompactionRunId: opts.createdByCompactionRunId || opts.created_by_compaction_run_id || null,
188
+ metadata: {
189
+ source: 'memory_promotion',
190
+ memoryType,
191
+ createdByFinalizationId: opts.createdByFinalizationId || opts.created_by_finalization_id || null,
192
+ createdByCompactionRunId: opts.createdByCompactionRunId || opts.created_by_compaction_run_id || null,
193
+ },
194
+ };
195
+ assertion.assertionHash = assertionHash({
196
+ canonicalKey: assertion.canonicalKey,
197
+ predicate: assertion.predicate,
198
+ objectKind: assertion.objectKind,
199
+ objectEntityId: assertion.objectEntityId,
200
+ objectValueJson: assertion.objectValueJson,
201
+ qualifiersJson: assertion.qualifiersJson,
202
+ validFrom: assertion.validFrom,
203
+ validTo: assertion.validTo,
204
+ observedAt: assertion.observedAt,
205
+ authority: assertion.authority,
206
+ });
207
+ return assertion;
208
+ }
209
+
210
+ function sameClaim(a, b) {
211
+ return normalizeText(a && (a.summary || a.title)).toLowerCase()
212
+ === normalizeText(b && (b.summary || b.title)).toLowerCase();
213
+ }
214
+
215
+ function pushStructuredCandidates(candidates, items, spec) {
216
+ for (const item of items) {
217
+ const text = textFromItem(item, spec.keys);
218
+ if (!text) continue;
219
+ const itemObj = item && typeof item === 'object' ? item : null;
220
+ const explicitSubject = normalizeText(itemObj && (itemObj.subject || itemObj.entity || itemObj.name));
221
+ const explicitAspect = normalizeText(itemObj && (itemObj.aspect || itemObj.predicate || itemObj.attribute));
222
+ const subject = explicitSubject || normalizeText(spec.subject);
223
+ let aspect = explicitAspect || normalizeText(spec.aspect);
224
+ if (!explicitAspect) aspect = `${aspect}:${stableHash(text)}`;
225
+ candidates.push({
226
+ memoryType: spec.memoryType,
227
+ canonicalKey: buildCanonicalKey({
228
+ memoryType: spec.memoryType,
229
+ scopeKey: spec.scopeKey,
230
+ contextKey: spec.contextKey,
231
+ topicKey: spec.topicKey,
232
+ subject,
233
+ aspect,
234
+ }),
235
+ scopeKind: spec.scopeKind,
236
+ scopeKey: spec.scopeKey,
237
+ contextKey: spec.contextKey,
238
+ topicKey: spec.topicKey,
239
+ inheritanceMode: spec.inheritanceMode || defaultInheritanceForType(spec.memoryType),
240
+ title: text.slice(0, 120),
241
+ summary: text,
242
+ payload: typeof item === 'string' ? { [spec.payloadKey]: text } : { ...item, [spec.payloadKey]: text },
243
+ authority: spec.authority,
244
+ evidenceRefs: spec.evidenceRefs,
245
+ visibleInBootstrap: true,
246
+ visibleInRecall: true,
247
+ });
248
+ }
249
+ }
250
+
251
+ function extractCandidatesFromStructuredSummary(input = {}) {
252
+ const structuredSummary = input.structuredSummary || {};
253
+ const sessionId = input.sessionId || null;
254
+ const scopeKey = input.scopeKey || (sessionId ? `session:${sessionId}` : 'global');
255
+ const scopeKind = input.scopeKind || (sessionId ? 'session' : 'global');
256
+ const subject = input.subject || 'session';
257
+ const contextKey = input.contextKey || null;
258
+ const topicKey = input.topicKey || null;
259
+ const authority = input.authority || 'verified_summary';
260
+ const evidenceRefs = input.evidenceRefs || (sessionId
261
+ ? [{ sourceKind: 'session_summary', sourceRef: sessionId, relationKind: 'primary' }]
262
+ : []);
263
+ const candidates = [];
264
+
265
+ const specs = [
266
+ ['decision', ['decisions'], ['decision', 'summary', 'text'], 'decision', input.inheritanceMode || 'non_inheritable'],
267
+ ['open_loop', ['open_loops'], ['item', 'summary', 'text'], 'item', input.inheritanceMode || 'non_inheritable'],
268
+ ['fact', ['important_facts', 'facts'], ['statement', 'fact', 'summary', 'text'], 'statement'],
269
+ ['preference', ['preferences'], ['preference', 'summary', 'text'], 'preference'],
270
+ ['constraint', ['constraints'], ['constraint', 'summary', 'text'], 'constraint'],
271
+ ['conclusion', ['conclusions'], ['conclusion', 'summary', 'text'], 'conclusion'],
272
+ ['state', ['states', 'state'], ['state', 'summary', 'text', 'value'], 'state'],
273
+ ['entity_note', ['entity_notes'], ['note', 'summary', 'text'], 'note'],
274
+ ];
275
+
276
+ for (const [memoryType, fieldNames, keys, payloadKey, inheritanceMode] of specs) {
277
+ const items = fieldNames.flatMap(field => Array.isArray(structuredSummary[field]) ? structuredSummary[field] : []);
278
+ const filtered = memoryType === 'open_loop'
279
+ ? items.filter(item => {
280
+ const text = textFromItem(item, keys);
281
+ return text && !['none', 'n/a', 'na', 'done', '無'].includes(text.toLowerCase());
282
+ })
283
+ : items;
284
+ pushStructuredCandidates(candidates, filtered, {
285
+ memoryType,
286
+ keys,
287
+ payloadKey,
288
+ scopeKind,
289
+ scopeKey,
290
+ contextKey,
291
+ topicKey,
292
+ subject,
293
+ aspect: DEFAULT_ASPECT[memoryType],
294
+ inheritanceMode,
295
+ authority,
296
+ evidenceRefs,
297
+ });
298
+ }
299
+
300
+ return candidates;
301
+ }
302
+
303
+ function assessCandidate(candidate = {}, opts = {}) {
304
+ const memoryType = normalizeType(candidate.memoryType || candidate.memory_type);
305
+ const tags = new Set([
306
+ ...(candidate.pollutionTags || []),
307
+ ...(candidate.tags || []),
308
+ ].map(tag => String(tag || '').trim().toLowerCase()).filter(Boolean));
309
+
310
+ for (const tag of tags) {
311
+ if (FORBIDDEN_TAGS.has(tag)) {
312
+ return { action: 'quarantine', reason: `forbidden_${tag}` };
313
+ }
314
+ }
315
+
316
+ if (!ACTIVE_V1_TYPES.has(memoryType)) {
317
+ return { action: 'quarantine', reason: 'unsupported_memory_type' };
318
+ }
319
+
320
+ const evidenceRefs = normalizeEvidenceRefs(candidate);
321
+ if (!Array.isArray(evidenceRefs) || evidenceRefs.length === 0) {
322
+ return { action: 'quarantine', reason: 'missing_provenance' };
323
+ }
324
+
325
+ const authority = String(candidate.authority || 'llm_inference').toLowerCase();
326
+ if (authority === 'raw_transcript') {
327
+ return { action: 'quarantine', reason: 'raw_transcript_not_authoritative' };
328
+ }
329
+ if (authorityRank(authority) < authorityRank('verified_summary')) {
330
+ return { action: 'quarantine', reason: 'insufficient_authority' };
331
+ }
332
+
333
+ if (!candidate.scopeId && !candidate.scope_id && !(candidate.scopeKey || candidate.scope_key)) {
334
+ return { action: 'quarantine', reason: 'missing_scope' };
335
+ }
336
+
337
+ const activeConflicts = opts.existingActiveRecords || [];
338
+ for (const existing of activeConflicts) {
339
+ if (sameClaim(candidate, existing)) continue;
340
+ const incomingRank = authorityRank(authority);
341
+ const existingRank = authorityRank(existing.authority);
342
+ if (incomingRank < existingRank) {
343
+ return { action: 'quarantine', reason: 'lower_authority_conflict', conflictWith: existing.id || existing.memory_id };
344
+ }
345
+ if (incomingRank === existingRank) {
346
+ return { action: 'quarantine', reason: 'unresolved_active_conflict', conflictWith: existing.id || existing.memory_id };
347
+ }
348
+ return {
349
+ action: 'promote',
350
+ reason: 'higher_authority_supersedes',
351
+ supersedeId: existing.id || existing.memory_id,
352
+ };
353
+ }
354
+
355
+ return { action: 'promote', reason: 'v1_foundation_allowed' };
356
+ }
357
+
358
+ function createMemoryPromotion({ records }) {
359
+ async function promoteOne(candidate, opts = {}, candidateRecords = records, tx = {}) {
360
+ if (tx.inTransaction && candidateRecords.lockCanonicalKey && candidate.canonicalKey) {
361
+ await candidateRecords.lockCanonicalKey({
362
+ tenantId: opts.tenantId,
363
+ canonicalKey: candidate.canonicalKey,
364
+ });
365
+ }
366
+
367
+ const existingActiveRecords = candidateRecords.findActiveByCanonicalKey && candidate.canonicalKey
368
+ ? await candidateRecords.findActiveByCanonicalKey({
369
+ tenantId: opts.tenantId,
370
+ canonicalKey: candidate.canonicalKey,
371
+ forUpdate: tx.inTransaction === true,
372
+ })
373
+ : [];
374
+ const assessment = assessCandidate(candidate, { existingActiveRecords });
375
+ if (assessment.action !== 'promote') {
376
+ return { candidate, ...assessment };
377
+ }
378
+
379
+ if (assessment.supersedeId && candidateRecords.updateMemoryStatus) {
380
+ await candidateRecords.updateMemoryStatus({
381
+ tenantId: opts.tenantId,
382
+ memoryId: assessment.supersedeId,
383
+ status: 'superseded',
384
+ });
385
+ }
386
+
387
+ const memoryType = normalizeType(candidate.memoryType || candidate.memory_type);
388
+ const acceptedAt = candidate.acceptedAt || opts.acceptedAt || new Date().toISOString();
389
+ let scopeId = candidate.scopeId || candidate.scope_id || null;
390
+ if (!scopeId) {
391
+ const scope = await candidateRecords.upsertScope({
392
+ tenantId: opts.tenantId,
393
+ scopeKind: candidate.scopeKind || candidate.scope_kind || 'session',
394
+ scopeKey: candidate.scopeKey || candidate.scope_key,
395
+ inheritanceMode: candidate.inheritanceMode || candidate.inheritance_mode || defaultInheritanceForType(memoryType),
396
+ contextKey: candidate.contextKey || candidate.context_key || null,
397
+ topicKey: candidate.topicKey || candidate.topic_key || null,
398
+ });
399
+ scopeId = scope.id;
400
+ }
401
+
402
+ let backingFact = null;
403
+ const factAssertion = buildFactAssertion(candidate, {
404
+ tenantId: opts.tenantId,
405
+ scopeId,
406
+ acceptedAt,
407
+ createdByFinalizationId: opts.createdByFinalizationId || opts.created_by_finalization_id || null,
408
+ createdByCompactionRunId: opts.createdByCompactionRunId || opts.created_by_compaction_run_id || null,
409
+ });
410
+ const supersededFacts = [];
411
+ if (factAssertion && candidateRecords.upsertFactAssertion) {
412
+ const existingFacts = candidateRecords.findActiveFactByCanonicalKey
413
+ ? await candidateRecords.findActiveFactByCanonicalKey({
414
+ tenantId: opts.tenantId,
415
+ canonicalKey: candidate.canonicalKey,
416
+ forUpdate: tx.inTransaction === true,
417
+ })
418
+ : [];
419
+
420
+ for (const fact of existingFacts) {
421
+ if (fact.assertion_hash === factAssertion.assertionHash) continue;
422
+ if (!candidateRecords.updateFactAssertionStatus) continue;
423
+ await candidateRecords.updateFactAssertionStatus({
424
+ tenantId: opts.tenantId,
425
+ factId: fact.id,
426
+ status: 'superseded',
427
+ validTo: factAssertion.validFrom || acceptedAt,
428
+ supersededAt: acceptedAt,
429
+ });
430
+ supersededFacts.push(fact);
431
+ }
432
+
433
+ backingFact = await candidateRecords.upsertFactAssertion(factAssertion);
434
+
435
+ for (const fact of supersededFacts) {
436
+ await candidateRecords.updateFactAssertionStatus({
437
+ tenantId: opts.tenantId,
438
+ factId: fact.id,
439
+ status: 'superseded',
440
+ supersededBy: backingFact.id,
441
+ validTo: factAssertion.validFrom || acceptedAt,
442
+ supersededAt: acceptedAt,
443
+ });
444
+ }
445
+
446
+ for (const ref of normalizeEvidenceRefs(candidate)) {
447
+ await candidateRecords.linkEvidence({
448
+ tenantId: opts.tenantId,
449
+ ownerKind: 'fact',
450
+ ownerId: backingFact.id,
451
+ sourceKind: ref.sourceKind || ref.source_kind,
452
+ sourceRef: ref.sourceRef || ref.source_ref,
453
+ relationKind: ref.relationKind || ref.relation_kind || 'supporting',
454
+ weight: ref.weight ?? 1.0,
455
+ metadata: ref.metadata || {},
456
+ createdByFinalizationId: opts.createdByFinalizationId || opts.created_by_finalization_id || null,
457
+ createdByCompactionRunId: opts.createdByCompactionRunId || opts.created_by_compaction_run_id || null,
458
+ });
459
+ }
460
+ }
461
+
462
+ const memory = await candidateRecords.upsertMemory({
463
+ tenantId: opts.tenantId,
464
+ memoryType,
465
+ canonicalKey: candidate.canonicalKey,
466
+ scopeId,
467
+ contextKey: candidate.contextKey || null,
468
+ topicKey: candidate.topicKey || null,
469
+ title: candidate.title || null,
470
+ summary: candidate.summary || '',
471
+ payload: candidate.payload || {},
472
+ status: 'active',
473
+ authority: candidate.authority || 'verified_summary',
474
+ acceptedAt,
475
+ validFrom: candidate.validFrom || null,
476
+ validTo: candidate.validTo || null,
477
+ staleAfter: candidate.staleAfter || null,
478
+ backingFactId: backingFact ? backingFact.id : null,
479
+ observedAt: factAssertion ? factAssertion.observedAt : (candidate.observedAt || candidate.observed_at || null),
480
+ createdByFinalizationId: opts.createdByFinalizationId || opts.created_by_finalization_id || null,
481
+ createdByCompactionRunId: opts.createdByCompactionRunId || opts.created_by_compaction_run_id || null,
482
+ visibleInBootstrap: candidate.visibleInBootstrap !== false,
483
+ visibleInRecall: candidate.visibleInRecall !== false,
484
+ rankFeatures: candidate.rankFeatures || {},
485
+ });
486
+
487
+ if (assessment.supersedeId && candidateRecords.updateMemoryStatus) {
488
+ await candidateRecords.updateMemoryStatus({
489
+ tenantId: opts.tenantId,
490
+ memoryId: assessment.supersedeId,
491
+ status: 'superseded',
492
+ supersededBy: memory.id,
493
+ validTo: candidate.validFrom || acceptedAt,
494
+ });
495
+ }
496
+
497
+ for (const ref of normalizeEvidenceRefs(candidate)) {
498
+ await candidateRecords.linkEvidence({
499
+ tenantId: opts.tenantId,
500
+ ownerKind: 'memory_record',
501
+ ownerId: memory.id,
502
+ sourceKind: ref.sourceKind || ref.source_kind,
503
+ sourceRef: ref.sourceRef || ref.source_ref,
504
+ relationKind: ref.relationKind || ref.relation_kind || 'supporting',
505
+ weight: ref.weight ?? 1.0,
506
+ metadata: ref.metadata || {},
507
+ createdByFinalizationId: opts.createdByFinalizationId || opts.created_by_finalization_id || null,
508
+ createdByCompactionRunId: opts.createdByCompactionRunId || opts.created_by_compaction_run_id || null,
509
+ });
510
+ }
511
+
512
+ return { candidate, action: 'promote', reason: assessment.reason, memory, backingFact };
513
+ }
514
+
515
+ async function promote(candidates = [], opts = {}) {
516
+ const results = [];
517
+ for (const candidate of candidates) {
518
+ const result = records.withTransaction
519
+ ? await records.withTransaction((txRecords, meta = {}) => promoteOne(candidate, opts, txRecords, {
520
+ inTransaction: meta.transactional !== false,
521
+ }))
522
+ : await promoteOne(candidate, opts, records, { inTransaction: false });
523
+ results.push(result);
524
+ }
525
+ return results;
526
+ }
527
+
528
+ return {
529
+ extractCandidates: extractCandidatesFromStructuredSummary,
530
+ assessCandidate,
531
+ promote,
532
+ };
533
+ }
534
+
535
+ module.exports = {
536
+ ACTIVE_V1_TYPES,
537
+ FORBIDDEN_TAGS,
538
+ AUTHORITY_RANK,
539
+ defaultInheritanceForType,
540
+ buildCanonicalKey,
541
+ extractCandidatesFromStructuredSummary,
542
+ assessCandidate,
543
+ createMemoryPromotion,
544
+ };