@shadowforge0/aquifer-memory 1.5.12 → 1.7.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 +84 -73
  3. package/README_CN.md +676 -0
  4. package/README_TW.md +684 -0
  5. package/aquifer.config.example.json +34 -0
  6. package/consumers/claude-code.js +11 -11
  7. package/consumers/cli.js +421 -53
  8. package/consumers/codex-handoff.js +258 -0
  9. package/consumers/codex.js +1676 -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 +380 -18
  23. package/core/finalization-review.js +319 -0
  24. package/core/mcp-manifest.js +52 -2
  25. package/core/memory-bootstrap.js +200 -0
  26. package/core/memory-consolidation.js +1590 -0
  27. package/core/memory-promotion.js +544 -0
  28. package/core/memory-recall.js +247 -0
  29. package/core/memory-records.js +797 -0
  30. package/core/memory-safety-gate.js +224 -0
  31. package/core/session-finalization.js +365 -0
  32. package/core/storage.js +385 -2
  33. package/docs/getting-started.md +105 -0
  34. package/docs/postprocess-contract.md +2 -2
  35. package/docs/setup.md +92 -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 +672 -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
package/core/aquifer.js CHANGED
@@ -10,6 +10,7 @@ const { hybridRank } = require('./hybrid-rank');
10
10
  const { summarize } = require('../pipeline/summarize');
11
11
  const { extractEntities } = require('../pipeline/extract-entities');
12
12
  const { createEmbedder } = require('../pipeline/embed');
13
+ const { applyEnrichSafetyGate, sanitizeSummaryResult } = require('./memory-safety-gate');
13
14
 
14
15
  // ---------------------------------------------------------------------------
15
16
  // Schema name validation
@@ -278,6 +279,102 @@ function createAquifer(config = {}) {
278
279
  // Override via config.ftsConfig if you need to force one or the other.
279
280
  let ftsConfig = config.ftsConfig || null;
280
281
 
282
+ const memoryCfg = config.memory || {};
283
+ const memoryServingMode = memoryCfg.servingMode || process.env.AQUIFER_MEMORY_SERVING_MODE || 'legacy';
284
+ function splitScopePath(value) {
285
+ if (Array.isArray(value)) return value.map(v => String(v).trim()).filter(Boolean);
286
+ if (typeof value !== 'string') return null;
287
+ const parts = value.split(',').map(v => v.trim()).filter(Boolean);
288
+ return parts.length > 0 ? parts : null;
289
+ }
290
+ const defaultActiveScopeKey = memoryCfg.activeScopeKey || process.env.AQUIFER_MEMORY_ACTIVE_SCOPE_KEY || null;
291
+ const defaultActiveScopePath = splitScopePath(
292
+ memoryCfg.activeScopePath || process.env.AQUIFER_MEMORY_ACTIVE_SCOPE_PATH || null,
293
+ );
294
+ function resolveMemoryServingMode(opts = {}) {
295
+ const mode = opts.memoryMode || opts.servingMode || memoryServingMode;
296
+ if (mode === 'legacy' || mode === 'evidence') return 'legacy';
297
+ if (mode === 'curated') return 'curated';
298
+ throw new Error(`Invalid memory serving mode: "${mode}". Must be one of: legacy, curated`);
299
+ }
300
+ function withDefaultMemoryScope(opts = {}) {
301
+ const next = { ...opts };
302
+ if (!next.activeScopePath && defaultActiveScopePath) next.activeScopePath = defaultActiveScopePath;
303
+ if (!next.activeScopeKey && defaultActiveScopeKey) next.activeScopeKey = defaultActiveScopeKey;
304
+ return next;
305
+ }
306
+ function assertCuratedRecallOpts(opts = {}) {
307
+ const unsupported = [];
308
+ for (const key of ['agentId', 'agentIds', 'source', 'dateFrom', 'dateTo', 'entities', 'entityMode', 'mode', 'weights', 'rerank', 'allowUnsafeDebug', 'unsafeDebug']) {
309
+ if (opts[key] !== undefined && opts[key] !== null) unsupported.push(key);
310
+ }
311
+ if (unsupported.length > 0) {
312
+ throw new Error(`curated session_recall does not support legacy filters: ${unsupported.join(', ')}. Use activeScopeKey/activeScopePath or evidence_recall.`);
313
+ }
314
+ }
315
+ function assertCuratedBootstrapOpts(opts = {}) {
316
+ const unsupported = [];
317
+ for (const key of ['agentId', 'source', 'lookbackDays', 'dateFrom', 'dateTo']) {
318
+ if (opts[key] !== undefined && opts[key] !== null) unsupported.push(key);
319
+ }
320
+ if (unsupported.length > 0) {
321
+ throw new Error(`curated session_bootstrap does not support legacy filters: ${unsupported.join(', ')}. Use activeScopeKey/activeScopePath.`);
322
+ }
323
+ }
324
+ function hasEvidenceBoundary(opts = {}) {
325
+ return Boolean(
326
+ opts.agentId
327
+ || (Array.isArray(opts.agentIds) && opts.agentIds.length > 0)
328
+ || opts.source
329
+ || opts.dateFrom
330
+ || opts.dateTo
331
+ || opts.host
332
+ || opts.sessionId
333
+ || opts.allowUnsafeDebug === true
334
+ || opts.unsafeDebug === true
335
+ );
336
+ }
337
+ function curatedRecallTitle(row = {}) {
338
+ const title = row.title || row.summary || row.canonical_key || row.canonicalKey || row.memory_type || row.memoryType || 'memory';
339
+ return String(title).trim();
340
+ }
341
+ function curatedRecallSummary(row = {}) {
342
+ const summary = row.summary || row.title || row.canonical_key || row.canonicalKey || '';
343
+ return String(summary).trim();
344
+ }
345
+ function normalizeCuratedRecallRow(row = {}) {
346
+ const memoryId = row.memoryId || row.memory_id || row.id || null;
347
+ const canonicalKey = row.canonicalKey || row.canonical_key || null;
348
+ const memoryType = row.memoryType || row.memory_type || null;
349
+ const scopeKey = row.scopeKey || row.scope_key || null;
350
+ const scopeKind = row.scopeKind || row.scope_kind || null;
351
+ const summaryText = curatedRecallSummary(row) || null;
352
+ const title = curatedRecallTitle(row) || null;
353
+ const scoreValue = row.recall_score ?? row.score ?? row.lexical_rank ?? null;
354
+ const score = scoreValue === null ? null : Number(scoreValue);
355
+ return {
356
+ ...row,
357
+ memoryId: memoryId === null ? null : String(memoryId),
358
+ canonicalKey,
359
+ memoryType,
360
+ scopeKey,
361
+ scopeKind,
362
+ title,
363
+ summaryText,
364
+ structuredSummary: {
365
+ title,
366
+ overview: summaryText,
367
+ },
368
+ startedAt: row.acceptedAt || row.accepted_at || row.observedAt || row.observed_at || null,
369
+ score: Number.isFinite(score) ? score : null,
370
+ feedbackTarget: {
371
+ kind: 'memory_feedback',
372
+ memoryId: memoryId === null ? null : String(memoryId),
373
+ canonicalKey,
374
+ },
375
+ };
376
+ }
377
+
281
378
  // State-change extraction (Q3): off by default. When enabled, enrich() runs
282
379
  // an extra LLM call to capture temporal state transitions on whitelisted
283
380
  // entities. See pipeline/extract-state-changes.js + core/entity-state.js.
@@ -326,6 +423,13 @@ function createAquifer(config = {}) {
326
423
  { id: '004-completion', file: '004-completion.sql', always: true, signature: 'narratives' },
327
424
  { id: '005-entity-state-history',file: '005-entity-state-history.sql',gate: 'entities', signature: 'entity_state_history' },
328
425
  { id: '006-insights', file: '006-insights.sql', always: true, signature: 'insights' },
426
+ { id: '007-v1-foundation', file: '007-v1-foundation.sql', always: true, signature: 'memory_records' },
427
+ { id: '008-session-finalizations',file: '008-session-finalizations.sql',always: true, signature: 'session_finalizations' },
428
+ { id: '009-v1-assertion-plane', file: '009-v1-assertion-plane.sql', always: true, signature: 'fact_assertions_v1' },
429
+ { id: '010-v1-finalization-review',file: '010-v1-finalization-review.sql',always: true, signature: 'finalization_candidates' },
430
+ { id: '011-v1-compaction-claim', file: '011-v1-compaction-claim.sql', always: true, signature: { table: 'compaction_runs', column: 'apply_token' } },
431
+ { id: '012-v1-compaction-lease', file: '012-v1-compaction-lease.sql', always: true, signature: { table: 'compaction_runs', column: 'lease_expires_at' } },
432
+ { id: '013-v1-compaction-lineage', file: '013-v1-compaction-lineage.sql', always: true, signature: 'compaction_candidates' },
329
433
  ];
330
434
 
331
435
  function requiredMigrations() {
@@ -340,15 +444,38 @@ function createAquifer(config = {}) {
340
444
  const required = MIGRATION_PLAN.filter(m => m.always
341
445
  || (m.gate === 'entities' && entitiesEnabled)
342
446
  || (m.gate === 'facts' && factsEnabled));
343
- const signatures = required.map(m => m.signature);
344
- if (signatures.length === 0) return [];
345
- const r = await queryRunner.query(
346
- `SELECT tablename FROM pg_tables
347
- WHERE schemaname = $1 AND tablename = ANY($2::text[])`,
348
- [schema, signatures]
349
- );
350
- const present = new Set(r.rows.map(row => row.tablename));
351
- return required.filter(m => present.has(m.signature)).map(m => m.id);
447
+ const tableSignatures = required
448
+ .map(m => m.signature)
449
+ .filter(signature => typeof signature === 'string');
450
+ const columnSignatures = required
451
+ .map(m => m.signature)
452
+ .filter(signature => signature && typeof signature === 'object');
453
+ const presentTables = new Set();
454
+ const presentColumns = new Set();
455
+ if (tableSignatures.length > 0) {
456
+ const r = await queryRunner.query(
457
+ `SELECT tablename FROM pg_tables
458
+ WHERE schemaname = $1 AND tablename = ANY($2::text[])`,
459
+ [schema, tableSignatures]
460
+ );
461
+ for (const row of r.rows) presentTables.add(row.tablename);
462
+ }
463
+ if (columnSignatures.length > 0) {
464
+ const tables = [...new Set(columnSignatures.map(signature => signature.table))];
465
+ const r = await queryRunner.query(
466
+ `SELECT table_name, column_name
467
+ FROM information_schema.columns
468
+ WHERE table_schema = $1 AND table_name = ANY($2::text[])`,
469
+ [schema, tables]
470
+ );
471
+ for (const row of r.rows) presentColumns.add(`${row.table_name}.${row.column_name}`);
472
+ }
473
+ return required
474
+ .filter(m => {
475
+ if (typeof m.signature === 'string') return presentTables.has(m.signature);
476
+ return presentColumns.has(`${m.signature.table}.${m.signature.column}`);
477
+ })
478
+ .map(m => m.id);
352
479
  }
353
480
 
354
481
  async function buildMigrationPlan(queryRunner) {
@@ -492,6 +619,49 @@ function createAquifer(config = {}) {
492
619
  const insightsSql = loadSql('006-insights.sql', schema);
493
620
  await client.query(insightsSql); ddlExecuted.push('006-insights');
494
621
 
622
+ // 8. v1 curated-memory foundation (always, additive). This creates
623
+ // a sidecar curated plane without changing the legacy session recall
624
+ // or bootstrap serving path.
625
+ const memoryV1Sql = loadSql('007-v1-foundation.sql', schema);
626
+ await client.query(memoryV1Sql); ddlExecuted.push('007-v1-foundation');
627
+
628
+ // 9. v1 session finalization ledger (always, additive). Finalization
629
+ // is the source-of-truth lifecycle for handoff/session-end/recovery
630
+ // triggers; local consumer markers are cache hints only.
631
+ const finalizationSql = loadSql('008-session-finalizations.sql', schema);
632
+ await client.query(finalizationSql); ddlExecuted.push('008-session-finalizations');
633
+
634
+ // 10. v1 structured assertion plane (always, additive). This keeps
635
+ // legacy 004-facts untouched and adds the minimal DB contract for
636
+ // backing structured assertions, tenant-safe scope ancestry guards,
637
+ // and compaction coverage fields.
638
+ const assertionPlaneSql = loadSql('009-v1-assertion-plane.sql', schema);
639
+ await client.query(assertionPlaneSql); ddlExecuted.push('009-v1-assertion-plane');
640
+
641
+ // 11. v1 finalization review and lineage (always, additive). This
642
+ // stores human review text, minimal SessionStart text, finalization
643
+ // candidate rows, and row-level created_by_finalization_id lineage.
644
+ const finalizationReviewSql = loadSql('010-v1-finalization-review.sql', schema);
645
+ await client.query(finalizationReviewSql); ddlExecuted.push('010-v1-finalization-review');
646
+
647
+ // 12. v1 compaction claim/apply guard (always, additive). This adds
648
+ // a minimal claim token and one-live-apply guard for compaction_runs;
649
+ // it does not create or promote aggregate memory.
650
+ const compactionClaimSql = loadSql('011-v1-compaction-claim.sql', schema);
651
+ await client.query(compactionClaimSql); ddlExecuted.push('011-v1-compaction-claim');
652
+
653
+ // 13. v1 compaction claim lease (always, additive). This persists
654
+ // row-level lease expiry so stale applying claims can be reclaimed
655
+ // without relying on caller clocks or changing runtime defaults.
656
+ const compactionLeaseSql = loadSql('012-v1-compaction-lease.sql', schema);
657
+ await client.query(compactionLeaseSql); ddlExecuted.push('012-v1-compaction-lease');
658
+
659
+ // 14. v1 compaction lineage and candidate ledger (always,
660
+ // additive). This records aggregate candidate outcomes and links
661
+ // promoted rows back to the compaction run that created them.
662
+ const compactionLineageSql = loadSql('013-v1-compaction-lineage.sql', schema);
663
+ await client.query(compactionLineageSql); ddlExecuted.push('013-v1-compaction-lineage');
664
+
495
665
  migrated = true;
496
666
  } finally {
497
667
  await client.query('SELECT pg_advisory_unlock($1)', [lockKey]).catch((err) => {
@@ -850,9 +1020,12 @@ function createAquifer(config = {}) {
850
1020
  ? (typeof rawMessages === 'string' ? JSON.parse(rawMessages) : rawMessages)
851
1021
  : null;
852
1022
  const normalized = messages ? (messages.normalized || messages) : [];
1023
+ const safety = applyEnrichSafetyGate(normalized);
1024
+ const safeNormalized = safety.messages;
1025
+ const safetyGate = safety.meta;
853
1026
 
854
1027
  // 2. Extract user turns
855
- const turns = storage.extractUserTurns(normalized);
1028
+ const turns = storage.extractUserTurns(safeNormalized);
856
1029
 
857
1030
  // Collected across pre-tx and tx phases; any non-empty warnings demote
858
1031
  // the final status from 'succeeded' to 'partial' (see step 8 below).
@@ -863,7 +1036,7 @@ function createAquifer(config = {}) {
863
1036
  let entityRaw = null;
864
1037
  let extra = null;
865
1038
 
866
- if (!skipSummary && normalized.length > 0) {
1039
+ if (!skipSummary && safeNormalized.length > 0) {
867
1040
  // Pre-transaction failures (customSummaryFn / summarize throws) would
868
1041
  // otherwise bubble out and leave the session stuck in 'processing'
869
1042
  // until stale reclaim. Capture as a warning so status ends 'partial',
@@ -871,13 +1044,13 @@ function createAquifer(config = {}) {
871
1044
  try {
872
1045
  if (customSummaryFn) {
873
1046
  // Custom pipeline: caller handles LLM call and parsing
874
- summaryResult = await customSummaryFn(normalized);
1047
+ summaryResult = await customSummaryFn(safeNormalized);
875
1048
  if (summaryResult && summaryResult.entityRaw) entityRaw = summaryResult.entityRaw;
876
1049
  if (summaryResult && summaryResult.extra) extra = summaryResult.extra;
877
1050
  } else {
878
1051
  // Built-in pipeline
879
1052
  const doMergeEntities = entitiesEnabled && mergeCall && !skipEntities;
880
- summaryResult = await summarize(normalized, {
1053
+ summaryResult = await summarize(safeNormalized, {
881
1054
  llmFn,
882
1055
  promptFn: summarizePromptFn,
883
1056
  mergeEntities: doMergeEntities,
@@ -890,6 +1063,11 @@ function createAquifer(config = {}) {
890
1063
  warnings.push(`summary step failed: ${e.message}`);
891
1064
  summaryResult = null;
892
1065
  }
1066
+ if (summaryResult) {
1067
+ const sanitizedSummary = sanitizeSummaryResult(summaryResult);
1068
+ summaryResult = sanitizedSummary.summaryResult;
1069
+ safetyGate.summary = sanitizedSummary.meta;
1070
+ }
893
1071
  }
894
1072
 
895
1073
  // 4. Pre-compute all LLM/embed results BEFORE opening transaction
@@ -921,7 +1099,7 @@ function createAquifer(config = {}) {
921
1099
  } else if (entityRaw) {
922
1100
  parsedEntities = entity.parseEntityOutput(entityRaw);
923
1101
  } else if (llmFn && !customSummaryFn) {
924
- parsedEntities = await extractEntities(normalized, { llmFn, promptFn: entityPromptFn });
1102
+ parsedEntities = await extractEntities(safeNormalized, { llmFn, promptFn: entityPromptFn });
925
1103
  }
926
1104
  } catch (e) { warnings.push(`entity extraction failed: ${e.message}`); }
927
1105
  }
@@ -937,7 +1115,7 @@ function createAquifer(config = {}) {
937
1115
  if (scopedEntities.length > 0) {
938
1116
  try {
939
1117
  const { extractStateChanges } = require('../pipeline/extract-state-changes');
940
- const result = await extractStateChanges(normalized, {
1118
+ const result = await extractStateChanges(safeNormalized, {
941
1119
  llmFn,
942
1120
  promptFn: stateChangesPromptFn,
943
1121
  entities: scopedEntities.map(e => ({ name: e.name, aliases: e.aliases || [] })),
@@ -969,9 +1147,9 @@ function createAquifer(config = {}) {
969
1147
  summaryText: summaryResult.summaryText,
970
1148
  structuredSummary: summaryResult.structuredSummary,
971
1149
  model: (optModel !== undefined ? optModel : session.model) || null, sourceHash: null,
972
- msgCount: normalized.length,
1150
+ msgCount: safeNormalized.length,
973
1151
  userCount: turns.length,
974
- assistantCount: normalized.filter(m => m.role === 'assistant').length,
1152
+ assistantCount: safeNormalized.filter(m => m.role === 'assistant').length,
975
1153
  startedAt: session.started_at, endedAt: session.ended_at,
976
1154
  embedding: summaryEmbedding,
977
1155
  });
@@ -1132,8 +1310,10 @@ function createAquifer(config = {}) {
1132
1310
  turnVectors,
1133
1311
  extra,
1134
1312
  normalized,
1313
+ sanitized: safeNormalized,
1135
1314
  parsedEntities,
1136
1315
  skipped: { summary: skipSummary, entities: skipEntities, turns: skipTurnEmbed },
1316
+ safetyGate,
1137
1317
  turnsEmbedded,
1138
1318
  entitiesFound,
1139
1319
  warnings: [...warnings], // defensive copy — caller cannot mutate enrich warnings
@@ -1150,6 +1330,7 @@ function createAquifer(config = {}) {
1150
1330
  entitiesFound,
1151
1331
  warnings,
1152
1332
  extra,
1333
+ safetyGate,
1153
1334
  session: {
1154
1335
  id: session.id,
1155
1336
  sessionId,
@@ -1165,6 +1346,16 @@ function createAquifer(config = {}) {
1165
1346
  // --- read path ---
1166
1347
 
1167
1348
  async recall(query, opts = {}) {
1349
+ if (resolveMemoryServingMode(opts) === 'curated') {
1350
+ assertCuratedRecallOpts(opts);
1351
+ await ensureMigrated();
1352
+ const rows = await aquifer.memory.recall(query, withDefaultMemoryScope(opts));
1353
+ return rows.map(normalizeCuratedRecallRow);
1354
+ }
1355
+ return aquifer.evidenceRecall(query, { ...opts, allowBroadEvidence: true });
1356
+ },
1357
+
1358
+ async evidenceRecall(query, opts = {}) {
1168
1359
  // Contract (aligned across core / manifest / consumer tools): query must
1169
1360
  // be a non-empty string. Empty strings previously short-circuited to []
1170
1361
  // silently — that masks caller bugs. Callers wanting "recent sessions"
@@ -1172,6 +1363,9 @@ function createAquifer(config = {}) {
1172
1363
  if (typeof query !== 'string' || query.trim().length === 0) {
1173
1364
  throw new Error('aquifer.recall(query): query must be a non-empty string');
1174
1365
  }
1366
+ if (opts.allowBroadEvidence !== true && !hasEvidenceBoundary(opts)) {
1367
+ throw new Error('evidence_recall requires an audit boundary filter (agentId, source, dateFrom/dateTo, host, sessionId) or allowUnsafeDebug=true');
1368
+ }
1175
1369
 
1176
1370
  const VALID_MODES = ['fts', 'hybrid', 'vector'];
1177
1371
  const mode = opts.mode !== undefined ? opts.mode : 'hybrid';
@@ -1558,6 +1752,50 @@ function createAquifer(config = {}) {
1558
1752
  });
1559
1753
  },
1560
1754
 
1755
+ async memoryFeedback(memoryId, opts = {}) {
1756
+ let targetMemoryId = memoryId;
1757
+ let canonicalKey = opts.canonicalKey || null;
1758
+ if (memoryId && typeof memoryId === 'object') {
1759
+ targetMemoryId = memoryId.memoryId || memoryId.id || null;
1760
+ canonicalKey = memoryId.canonicalKey || memoryId.canonical_key || canonicalKey;
1761
+ }
1762
+ if (!targetMemoryId && !canonicalKey) {
1763
+ throw new Error('memoryFeedback(memoryId): memoryId or canonicalKey is required');
1764
+ }
1765
+ const feedbackType = opts.feedbackType || opts.verdict;
1766
+ if (!feedbackType) throw new Error('opts.feedbackType is required');
1767
+ await ensureMigrated();
1768
+ if (!targetMemoryId && canonicalKey) {
1769
+ const rows = await memoryRecords.findActiveByCanonicalKey({
1770
+ tenantId: opts.tenantId || tenantId,
1771
+ canonicalKey,
1772
+ });
1773
+ if (!rows[0]) throw new Error(`Active memory not found: ${canonicalKey}`);
1774
+ targetMemoryId = rows[0].id;
1775
+ canonicalKey = rows[0].canonical_key || rows[0].canonicalKey || canonicalKey;
1776
+ }
1777
+ const result = await aquifer.memory.recordFeedback({
1778
+ tenantId: opts.tenantId || tenantId,
1779
+ targetKind: 'memory_record',
1780
+ targetId: targetMemoryId,
1781
+ feedbackType,
1782
+ actorKind: opts.actorKind || 'user',
1783
+ actorId: opts.actorId || opts.agentId || null,
1784
+ queryFingerprint: opts.queryFingerprint || null,
1785
+ note: opts.note || null,
1786
+ metadata: {
1787
+ ...(opts.metadata || {}),
1788
+ publicSurface: 'memoryFeedback',
1789
+ },
1790
+ });
1791
+ return {
1792
+ ...result,
1793
+ memoryId: targetMemoryId === null ? null : String(targetMemoryId),
1794
+ canonicalKey,
1795
+ feedbackType,
1796
+ };
1797
+ },
1798
+
1561
1799
  async feedbackStats(opts = {}) {
1562
1800
  await ensureMigrated();
1563
1801
  return storage.getFeedbackStats(pool, {
@@ -1600,7 +1838,7 @@ function createAquifer(config = {}) {
1600
1838
  // --- public config accessor ---
1601
1839
 
1602
1840
  getConfig() {
1603
- return { schema, tenantId };
1841
+ return { schema, tenantId, memoryServingMode };
1604
1842
  },
1605
1843
 
1606
1844
  // v1.2.0: expose the internal pool so host persona layers can reuse it
@@ -1705,6 +1943,10 @@ function createAquifer(config = {}) {
1705
1943
 
1706
1944
  async bootstrap(opts = {}) {
1707
1945
  await ensureMigrated();
1946
+ if (resolveMemoryServingMode(opts) === 'curated') {
1947
+ assertCuratedBootstrapOpts(opts);
1948
+ return aquifer.memory.bootstrap(withDefaultMemoryScope(opts));
1949
+ }
1708
1950
 
1709
1951
  const agentId = opts.agentId || null;
1710
1952
  const source = opts.source || null;
@@ -1823,6 +2065,12 @@ function createAquifer(config = {}) {
1823
2065
  const { createBundles } = require('./bundles');
1824
2066
  const { createEntityState } = require('./entity-state');
1825
2067
  const { createInsights } = require('./insights');
2068
+ const { createMemoryRecords } = require('./memory-records');
2069
+ const { createMemoryPromotion } = require('./memory-promotion');
2070
+ const { createMemoryBootstrap } = require('./memory-bootstrap');
2071
+ const { createMemoryRecall } = require('./memory-recall');
2072
+ const { createMemoryConsolidation } = require('./memory-consolidation');
2073
+ const { createSessionFinalization } = require('./session-finalization');
1826
2074
  const qSchema = qi(schema);
1827
2075
  aquifer.narratives = createNarratives({ pool, schema: qSchema, defaultTenantId: tenantId });
1828
2076
  aquifer.timeline = createTimeline({ pool, schema: qSchema, defaultTenantId: tenantId });
@@ -1851,6 +2099,120 @@ function createAquifer(config = {}) {
1851
2099
  dedup: config.insights && config.insights.dedup ? config.insights.dedup : undefined,
1852
2100
  });
1853
2101
 
2102
+ const memoryRecords = createMemoryRecords({ pool, schema: qSchema, defaultTenantId: tenantId });
2103
+ const memoryPromotion = createMemoryPromotion({ records: memoryRecords });
2104
+ const memoryBootstrap = createMemoryBootstrap({ records: memoryRecords });
2105
+ const memoryRecall = createMemoryRecall({ pool, schema: qSchema, defaultTenantId: tenantId });
2106
+ const memoryConsolidation = createMemoryConsolidation({
2107
+ pool,
2108
+ schema: qSchema,
2109
+ defaultTenantId: tenantId,
2110
+ records: memoryRecords,
2111
+ });
2112
+ const sessionFinalization = createSessionFinalization({
2113
+ pool,
2114
+ schema,
2115
+ recordsSchema: qSchema,
2116
+ defaultTenantId: tenantId,
2117
+ });
2118
+
2119
+ // v1 curated-memory sidecar. Top-level recall/bootstrap can opt into this
2120
+ // plane through memory.servingMode while legacy/evidence mode remains
2121
+ // available for compatibility and debugging.
2122
+ aquifer.memory = {
2123
+ upsertScope: async (input = {}) => {
2124
+ await ensureMigrated();
2125
+ return memoryRecords.upsertScope(input);
2126
+ },
2127
+ createVersion: async (input = {}) => {
2128
+ await ensureMigrated();
2129
+ return memoryRecords.createVersion(input);
2130
+ },
2131
+ upsertMemory: async (input = {}) => {
2132
+ await ensureMigrated();
2133
+ return memoryRecords.upsertMemory(input);
2134
+ },
2135
+ linkEvidence: async (input = {}) => {
2136
+ await ensureMigrated();
2137
+ return memoryRecords.linkEvidence(input);
2138
+ },
2139
+ recordFeedback: async (input = {}) => {
2140
+ await ensureMigrated();
2141
+ return memoryRecords.recordFeedback(input);
2142
+ },
2143
+ extractCandidates: (input = {}) => memoryPromotion.extractCandidates(input),
2144
+ assessCandidate: (candidate = {}) => memoryPromotion.assessCandidate(candidate),
2145
+ promote: async (candidates = [], opts = {}) => {
2146
+ await ensureMigrated();
2147
+ return memoryPromotion.promote(candidates, opts);
2148
+ },
2149
+ bootstrap: async (opts = {}) => {
2150
+ await ensureMigrated();
2151
+ return memoryBootstrap.bootstrap(opts);
2152
+ },
2153
+ current: async (opts = {}) => {
2154
+ await ensureMigrated();
2155
+ return memoryRecords.currentProjection(withDefaultMemoryScope(opts));
2156
+ },
2157
+ listCurrentMemory: async (opts = {}) => {
2158
+ await ensureMigrated();
2159
+ return memoryRecords.currentProjection(withDefaultMemoryScope(opts));
2160
+ },
2161
+ recall: async (query, opts = {}) => {
2162
+ await ensureMigrated();
2163
+ return memoryRecall.recall(query, opts);
2164
+ },
2165
+ consolidation: {
2166
+ plan: memoryConsolidation.plan,
2167
+ distillArchiveSnapshot: memoryConsolidation.distillArchiveSnapshot,
2168
+ runJob: async (input = {}) => {
2169
+ await ensureMigrated();
2170
+ return memoryConsolidation.runJob(input);
2171
+ },
2172
+ recordRun: async (input = {}) => {
2173
+ await ensureMigrated();
2174
+ return memoryConsolidation.recordRun(input);
2175
+ },
2176
+ claimRun: async (input = {}) => {
2177
+ await ensureMigrated();
2178
+ return memoryConsolidation.claimRun(input);
2179
+ },
2180
+ applyPlan: async (input = {}) => {
2181
+ await ensureMigrated();
2182
+ return memoryConsolidation.applyPlan(input);
2183
+ },
2184
+ executePlan: async (input = {}) => {
2185
+ await ensureMigrated();
2186
+ return memoryConsolidation.executePlan(input);
2187
+ },
2188
+ },
2189
+ };
2190
+
2191
+ aquifer.finalization = {
2192
+ createTask: async (input = {}) => {
2193
+ await ensureMigrated();
2194
+ return sessionFinalization.createTask(input);
2195
+ },
2196
+ get: async (input = {}) => {
2197
+ await ensureMigrated();
2198
+ return sessionFinalization.get(input);
2199
+ },
2200
+ list: async (input = {}) => {
2201
+ await ensureMigrated();
2202
+ return sessionFinalization.list(input);
2203
+ },
2204
+ updateStatus: async (input = {}) => {
2205
+ await ensureMigrated();
2206
+ return sessionFinalization.updateStatus(input);
2207
+ },
2208
+ finalizeSession: async (input = {}) => {
2209
+ await ensureMigrated();
2210
+ return sessionFinalization.finalizeSession(input);
2211
+ },
2212
+ };
2213
+
2214
+ aquifer.finalizeSession = aquifer.finalization.finalizeSession;
2215
+
1854
2216
  return aquifer;
1855
2217
  }
1856
2218