@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
@@ -0,0 +1,797 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+ const { resolveApplicableRecords } = require('./memory-bootstrap');
5
+
6
+ function requireField(obj, field) {
7
+ if (!obj || obj[field] === undefined || obj[field] === null || obj[field] === '') {
8
+ throw new Error(`${field} is required`);
9
+ }
10
+ }
11
+
12
+ function toJson(value, fallback) {
13
+ return JSON.stringify(value === undefined ? fallback : value);
14
+ }
15
+
16
+ function toJsonOrNull(value) {
17
+ return value === undefined || value === null ? null : JSON.stringify(value);
18
+ }
19
+
20
+ function advisoryLockKeys(namespace, value) {
21
+ const digest = crypto.createHash('sha256').update(`${namespace}:${value}`).digest();
22
+ return [digest.readInt32BE(0), digest.readInt32BE(4)];
23
+ }
24
+
25
+ const BOOTSTRAP_ORDER_SQL = `
26
+ CASE m.memory_type
27
+ WHEN 'constraint' THEN 0
28
+ WHEN 'state' THEN 1
29
+ WHEN 'open_loop' THEN 2
30
+ WHEN 'decision' THEN 3
31
+ WHEN 'preference' THEN 4
32
+ WHEN 'fact' THEN 5
33
+ WHEN 'conclusion' THEN 6
34
+ WHEN 'entity_note' THEN 7
35
+ ELSE 99
36
+ END ASC,
37
+ CASE m.authority
38
+ WHEN 'user_explicit' THEN 0
39
+ WHEN 'executable_evidence' THEN 1
40
+ WHEN 'manual' THEN 2
41
+ WHEN 'system' THEN 3
42
+ WHEN 'verified_summary' THEN 4
43
+ WHEN 'llm_inference' THEN 5
44
+ WHEN 'raw_transcript' THEN 6
45
+ ELSE 99
46
+ END ASC,
47
+ m.accepted_at DESC NULLS LAST,
48
+ m.id ASC`;
49
+
50
+ const CURRENT_TYPE_PRIORITY = {
51
+ constraint: 0,
52
+ state: 1,
53
+ open_loop: 2,
54
+ decision: 3,
55
+ preference: 4,
56
+ fact: 5,
57
+ conclusion: 6,
58
+ entity_note: 7,
59
+ };
60
+
61
+ const CURRENT_AUTHORITY_PRIORITY = {
62
+ user_explicit: 0,
63
+ executable_evidence: 1,
64
+ manual: 2,
65
+ system: 3,
66
+ verified_summary: 4,
67
+ llm_inference: 5,
68
+ raw_transcript: 6,
69
+ };
70
+
71
+ function parseTime(value) {
72
+ const parsed = Date.parse(value || '');
73
+ return Number.isFinite(parsed) ? parsed : null;
74
+ }
75
+
76
+ function normalizeScopePath(activeScopePath, activeScopeKey) {
77
+ const source = Array.isArray(activeScopePath)
78
+ ? activeScopePath
79
+ : (typeof activeScopePath === 'string' ? activeScopePath.split(',') : null);
80
+ if (source && source.length > 0) {
81
+ const seen = new Set();
82
+ const path = [];
83
+ for (const value of source) {
84
+ const key = String(value || '').trim();
85
+ if (!key || seen.has(key)) continue;
86
+ seen.add(key);
87
+ path.push(key);
88
+ }
89
+ if (path.length > 0) return path;
90
+ }
91
+ if (activeScopeKey) return [String(activeScopeKey).trim()].filter(Boolean);
92
+ return ['global'];
93
+ }
94
+
95
+ function compareRecordIdAsc(a, b) {
96
+ const left = a.memoryId ?? a.memory_id ?? a.id ?? null;
97
+ const right = b.memoryId ?? b.memory_id ?? b.id ?? null;
98
+ const leftNum = Number(left);
99
+ const rightNum = Number(right);
100
+ if (Number.isFinite(leftNum) && Number.isFinite(rightNum) && leftNum !== rightNum) {
101
+ return leftNum - rightNum;
102
+ }
103
+ return String(left ?? '').localeCompare(String(right ?? ''));
104
+ }
105
+
106
+ function normalizeCurrentMemoryRow(row = {}) {
107
+ const memoryId = row.memoryId ?? row.memory_id ?? row.id ?? null;
108
+ const evidenceRefsValue = row.evidenceRefs ?? row.evidence_refs ?? [];
109
+ const evidenceRefs = Array.isArray(evidenceRefsValue) ? evidenceRefsValue : [];
110
+ return {
111
+ ...row,
112
+ memoryId: memoryId === null ? null : String(memoryId),
113
+ canonicalKey: row.canonicalKey ?? row.canonical_key ?? null,
114
+ memoryType: row.memoryType ?? row.memory_type ?? null,
115
+ scopeKey: row.scopeKey ?? row.scope_key ?? null,
116
+ scopeKind: row.scopeKind ?? row.scope_kind ?? null,
117
+ inheritanceMode: row.inheritanceMode ?? row.inheritance_mode ?? row.scope_inheritance_mode ?? null,
118
+ visibleInBootstrap: row.visibleInBootstrap ?? row.visible_in_bootstrap ?? false,
119
+ visibleInRecall: row.visibleInRecall ?? row.visible_in_recall ?? false,
120
+ acceptedAt: row.acceptedAt ?? row.accepted_at ?? null,
121
+ validFrom: row.validFrom ?? row.valid_from ?? null,
122
+ validTo: row.validTo ?? row.valid_to ?? null,
123
+ staleAfter: row.staleAfter ?? row.stale_after ?? null,
124
+ evidenceRefs,
125
+ evidence_refs: evidenceRefs,
126
+ };
127
+ }
128
+
129
+ function currentScopePriority(record, positions) {
130
+ return positions.get(record.scopeKey ?? record.scope_key) ?? -1;
131
+ }
132
+
133
+ function sortCurrentMemoryRecords(a, b, positions) {
134
+ const leftScope = currentScopePriority(a, positions);
135
+ const rightScope = currentScopePriority(b, positions);
136
+ if (rightScope !== leftScope) return rightScope - leftScope;
137
+
138
+ const leftType = CURRENT_TYPE_PRIORITY[a.memoryType ?? a.memory_type] ?? 99;
139
+ const rightType = CURRENT_TYPE_PRIORITY[b.memoryType ?? b.memory_type] ?? 99;
140
+ if (leftType !== rightType) return leftType - rightType;
141
+
142
+ const leftAuthority = CURRENT_AUTHORITY_PRIORITY[a.authority] ?? 99;
143
+ const rightAuthority = CURRENT_AUTHORITY_PRIORITY[b.authority] ?? 99;
144
+ if (leftAuthority !== rightAuthority) return leftAuthority - rightAuthority;
145
+
146
+ const leftAccepted = parseTime(a.acceptedAt ?? a.accepted_at);
147
+ const rightAccepted = parseTime(b.acceptedAt ?? b.accepted_at);
148
+ if (leftAccepted !== rightAccepted) return (rightAccepted ?? 0) - (leftAccepted ?? 0);
149
+
150
+ return compareRecordIdAsc(a, b);
151
+ }
152
+
153
+ function isCurrentProjectionRow(row, asOf) {
154
+ if ((row.status || 'candidate') !== 'active') return false;
155
+ if (row.visibleInBootstrap !== true && row.visibleInRecall !== true) return false;
156
+ const at = parseTime(asOf);
157
+ if (at === null) return true;
158
+ const validFrom = parseTime(row.validFrom ?? row.valid_from);
159
+ const validTo = parseTime(row.validTo ?? row.valid_to);
160
+ const staleAfter = parseTime(row.staleAfter ?? row.stale_after);
161
+ if (validFrom !== null && validFrom > at) return false;
162
+ if (validTo !== null && validTo <= at) return false;
163
+ if (staleAfter !== null && staleAfter <= at) return false;
164
+ return true;
165
+ }
166
+
167
+ function createMemoryRecords({ pool, schema, defaultTenantId, inTransaction = false }) {
168
+ const scopes = `${schema}.scopes`;
169
+ const versions = `${schema}.versions`;
170
+ const memories = `${schema}.memory_records`;
171
+ const factAssertions = `${schema}.fact_assertions_v1`;
172
+ const evidenceRefs = `${schema}.evidence_refs`;
173
+ const feedback = `${schema}.feedback`;
174
+ const canTransact = typeof pool.connect === 'function';
175
+
176
+ async function upsertScope(input = {}) {
177
+ requireField(input, 'scopeKind');
178
+ requireField(input, 'scopeKey');
179
+ const tenantId = input.tenantId || defaultTenantId;
180
+ const result = await pool.query(
181
+ `INSERT INTO ${scopes} (
182
+ tenant_id, scope_kind, scope_key, parent_scope_id, inheritance_mode,
183
+ context_key, topic_key, metadata, active_from, active_to
184
+ )
185
+ VALUES ($1,$2,$3,$4,COALESCE($5,'defaultable'),$6,$7,COALESCE($8::jsonb,'{}'::jsonb),$9,$10)
186
+ ON CONFLICT (tenant_id, scope_kind, scope_key) DO UPDATE SET
187
+ parent_scope_id = COALESCE(EXCLUDED.parent_scope_id, ${scopes}.parent_scope_id),
188
+ inheritance_mode = EXCLUDED.inheritance_mode,
189
+ context_key = COALESCE(EXCLUDED.context_key, ${scopes}.context_key),
190
+ topic_key = COALESCE(EXCLUDED.topic_key, ${scopes}.topic_key),
191
+ metadata = COALESCE(NULLIF(EXCLUDED.metadata, '{}'::jsonb), ${scopes}.metadata),
192
+ active_from = COALESCE(EXCLUDED.active_from, ${scopes}.active_from),
193
+ active_to = COALESCE(EXCLUDED.active_to, ${scopes}.active_to)
194
+ RETURNING *`,
195
+ [
196
+ tenantId,
197
+ input.scopeKind,
198
+ input.scopeKey,
199
+ input.parentScopeId || null,
200
+ input.inheritanceMode || 'defaultable',
201
+ input.contextKey || null,
202
+ input.topicKey || null,
203
+ toJson(input.metadata, {}),
204
+ input.activeFrom || null,
205
+ input.activeTo || null,
206
+ ]
207
+ );
208
+ return result.rows[0] || null;
209
+ }
210
+
211
+ async function createVersion(input = {}) {
212
+ requireField(input, 'versionKind');
213
+ requireField(input, 'version');
214
+ requireField(input, 'versionHash');
215
+ const tenantId = input.tenantId || defaultTenantId;
216
+ const result = await pool.query(
217
+ `INSERT INTO ${versions} (
218
+ tenant_id, version_kind, version, version_hash, active, metadata,
219
+ released_at, retired_at
220
+ )
221
+ VALUES ($1,$2,$3,$4,COALESCE($5,false),COALESCE($6::jsonb,'{}'::jsonb),COALESCE($7,now()),$8)
222
+ ON CONFLICT (tenant_id, version_kind, version_hash) DO UPDATE SET
223
+ version = EXCLUDED.version,
224
+ metadata = COALESCE(NULLIF(EXCLUDED.metadata, '{}'::jsonb), ${versions}.metadata),
225
+ retired_at = COALESCE(EXCLUDED.retired_at, ${versions}.retired_at)
226
+ RETURNING *`,
227
+ [
228
+ tenantId,
229
+ input.versionKind,
230
+ input.version,
231
+ input.versionHash,
232
+ input.active === true,
233
+ toJson(input.metadata, {}),
234
+ input.releasedAt || null,
235
+ input.retiredAt || null,
236
+ ]
237
+ );
238
+ return result.rows[0] || null;
239
+ }
240
+
241
+ async function upsertMemory(input = {}) {
242
+ requireField(input, 'memoryType');
243
+ requireField(input, 'canonicalKey');
244
+ requireField(input, 'scopeId');
245
+ const tenantId = input.tenantId || defaultTenantId;
246
+ const status = input.status || 'candidate';
247
+ const result = await pool.query(
248
+ `INSERT INTO ${memories} (
249
+ tenant_id, memory_type, canonical_key, scope_id, context_key, topic_key,
250
+ title, summary, payload, status, authority, accepted_at, valid_from,
251
+ valid_to, stale_after, superseded_by, backing_fact_id, observed_at,
252
+ revoked_at, superseded_at, version_id, visible_in_bootstrap,
253
+ visible_in_recall, rank_features, created_by_finalization_id,
254
+ created_by_compaction_run_id
255
+ )
256
+ VALUES (
257
+ $1,$2,$3,$4,$5,$6,$7,COALESCE($8,''),COALESCE($9::jsonb,'{}'::jsonb),
258
+ COALESCE($10,'candidate'),COALESCE($11,'llm_inference'),$12,$13,$14,$15,
259
+ $16,$17,$18,$19,$20,$21,COALESCE($22,false),COALESCE($23,false),COALESCE($24::jsonb,'{}'::jsonb),$25,$26
260
+ )
261
+ ON CONFLICT (tenant_id, canonical_key) WHERE status = 'active' DO UPDATE SET
262
+ scope_id = EXCLUDED.scope_id,
263
+ context_key = COALESCE(EXCLUDED.context_key, ${memories}.context_key),
264
+ topic_key = COALESCE(EXCLUDED.topic_key, ${memories}.topic_key),
265
+ title = COALESCE(EXCLUDED.title, ${memories}.title),
266
+ summary = COALESCE(NULLIF(EXCLUDED.summary, ''), ${memories}.summary),
267
+ payload = COALESCE(NULLIF(EXCLUDED.payload, '{}'::jsonb), ${memories}.payload),
268
+ authority = EXCLUDED.authority,
269
+ accepted_at = COALESCE(EXCLUDED.accepted_at, ${memories}.accepted_at),
270
+ valid_from = COALESCE(EXCLUDED.valid_from, ${memories}.valid_from),
271
+ valid_to = COALESCE(EXCLUDED.valid_to, ${memories}.valid_to),
272
+ stale_after = COALESCE(EXCLUDED.stale_after, ${memories}.stale_after),
273
+ version_id = COALESCE(EXCLUDED.version_id, ${memories}.version_id),
274
+ backing_fact_id = COALESCE(EXCLUDED.backing_fact_id, ${memories}.backing_fact_id),
275
+ observed_at = COALESCE(EXCLUDED.observed_at, ${memories}.observed_at),
276
+ revoked_at = COALESCE(EXCLUDED.revoked_at, ${memories}.revoked_at),
277
+ superseded_at = COALESCE(EXCLUDED.superseded_at, ${memories}.superseded_at),
278
+ visible_in_bootstrap = EXCLUDED.visible_in_bootstrap,
279
+ visible_in_recall = EXCLUDED.visible_in_recall,
280
+ rank_features = COALESCE(NULLIF(EXCLUDED.rank_features, '{}'::jsonb), ${memories}.rank_features),
281
+ created_by_finalization_id = COALESCE(${memories}.created_by_finalization_id, EXCLUDED.created_by_finalization_id),
282
+ created_by_compaction_run_id = COALESCE(${memories}.created_by_compaction_run_id, EXCLUDED.created_by_compaction_run_id)
283
+ RETURNING *`,
284
+ [
285
+ tenantId,
286
+ input.memoryType,
287
+ input.canonicalKey,
288
+ input.scopeId,
289
+ input.contextKey || null,
290
+ input.topicKey || null,
291
+ input.title || null,
292
+ input.summary || '',
293
+ toJson(input.payload, {}),
294
+ status,
295
+ input.authority || 'llm_inference',
296
+ input.acceptedAt || (status === 'active' ? new Date().toISOString() : null),
297
+ input.validFrom || null,
298
+ input.validTo || null,
299
+ input.staleAfter || null,
300
+ input.supersededBy || null,
301
+ input.backingFactId || input.backing_fact_id || null,
302
+ input.observedAt || input.observed_at || null,
303
+ input.revokedAt || input.revoked_at || null,
304
+ input.supersededAt || input.superseded_at || null,
305
+ input.versionId || null,
306
+ input.visibleInBootstrap === true,
307
+ input.visibleInRecall === true,
308
+ toJson(input.rankFeatures, {}),
309
+ input.createdByFinalizationId || input.created_by_finalization_id || null,
310
+ input.createdByCompactionRunId || input.created_by_compaction_run_id || null,
311
+ ]
312
+ );
313
+ return result.rows[0] || null;
314
+ }
315
+
316
+ async function upsertFactAssertion(input = {}) {
317
+ requireField(input, 'canonicalKey');
318
+ requireField(input, 'scopeId');
319
+ requireField(input, 'predicate');
320
+ requireField(input, 'objectKind');
321
+ requireField(input, 'assertionHash');
322
+ const tenantId = input.tenantId || defaultTenantId;
323
+ const status = input.status || 'active';
324
+ const result = await pool.query(
325
+ `INSERT INTO ${factAssertions} (
326
+ tenant_id, canonical_key, scope_id, subject_entity_id, predicate,
327
+ object_kind, object_entity_id, object_value_json, qualifiers_json,
328
+ valid_from, valid_to, observed_at, stale_after, accepted_at,
329
+ revoked_at, superseded_at, status, authority, assertion_hash,
330
+ superseded_by, version_id, metadata, created_by_finalization_id,
331
+ created_by_compaction_run_id
332
+ )
333
+ VALUES (
334
+ $1,$2,$3,$4,$5,$6,$7,$8::jsonb,COALESCE($9::jsonb,'{}'::jsonb),
335
+ $10,$11,$12,$13,$14,$15,$16,COALESCE($17,'active'),COALESCE($18,'verified_summary'),
336
+ $19,$20,$21,COALESCE($22::jsonb,'{}'::jsonb),$23,$24
337
+ )
338
+ ON CONFLICT (tenant_id, canonical_key) WHERE status = 'active' DO UPDATE SET
339
+ scope_id = EXCLUDED.scope_id,
340
+ subject_entity_id = COALESCE(EXCLUDED.subject_entity_id, ${factAssertions}.subject_entity_id),
341
+ predicate = EXCLUDED.predicate,
342
+ object_kind = EXCLUDED.object_kind,
343
+ object_entity_id = COALESCE(EXCLUDED.object_entity_id, ${factAssertions}.object_entity_id),
344
+ object_value_json = EXCLUDED.object_value_json,
345
+ qualifiers_json = COALESCE(NULLIF(EXCLUDED.qualifiers_json, '{}'::jsonb), ${factAssertions}.qualifiers_json),
346
+ valid_from = COALESCE(EXCLUDED.valid_from, ${factAssertions}.valid_from),
347
+ valid_to = COALESCE(EXCLUDED.valid_to, ${factAssertions}.valid_to),
348
+ observed_at = COALESCE(EXCLUDED.observed_at, ${factAssertions}.observed_at),
349
+ stale_after = COALESCE(EXCLUDED.stale_after, ${factAssertions}.stale_after),
350
+ accepted_at = COALESCE(EXCLUDED.accepted_at, ${factAssertions}.accepted_at),
351
+ revoked_at = COALESCE(EXCLUDED.revoked_at, ${factAssertions}.revoked_at),
352
+ superseded_at = COALESCE(EXCLUDED.superseded_at, ${factAssertions}.superseded_at),
353
+ authority = EXCLUDED.authority,
354
+ assertion_hash = EXCLUDED.assertion_hash,
355
+ version_id = COALESCE(EXCLUDED.version_id, ${factAssertions}.version_id),
356
+ metadata = COALESCE(NULLIF(EXCLUDED.metadata, '{}'::jsonb), ${factAssertions}.metadata),
357
+ created_by_finalization_id = COALESCE(${factAssertions}.created_by_finalization_id, EXCLUDED.created_by_finalization_id),
358
+ created_by_compaction_run_id = COALESCE(${factAssertions}.created_by_compaction_run_id, EXCLUDED.created_by_compaction_run_id),
359
+ updated_at = now()
360
+ RETURNING *`,
361
+ [
362
+ tenantId,
363
+ input.canonicalKey,
364
+ input.scopeId,
365
+ input.subjectEntityId || input.subject_entity_id || null,
366
+ input.predicate,
367
+ input.objectKind || input.object_kind,
368
+ input.objectEntityId || input.object_entity_id || null,
369
+ toJsonOrNull(input.objectValueJson ?? input.object_value_json),
370
+ toJson(input.qualifiersJson ?? input.qualifiers_json, {}),
371
+ input.validFrom || input.valid_from || null,
372
+ input.validTo || input.valid_to || null,
373
+ input.observedAt || input.observed_at || null,
374
+ input.staleAfter || input.stale_after || null,
375
+ input.acceptedAt || input.accepted_at || (status === 'active' ? new Date().toISOString() : null),
376
+ input.revokedAt || input.revoked_at || null,
377
+ input.supersededAt || input.superseded_at || null,
378
+ status,
379
+ input.authority || 'verified_summary',
380
+ input.assertionHash || input.assertion_hash,
381
+ input.supersededBy || input.superseded_by || null,
382
+ input.versionId || input.version_id || null,
383
+ toJson(input.metadata, {}),
384
+ input.createdByFinalizationId || input.created_by_finalization_id || null,
385
+ input.createdByCompactionRunId || input.created_by_compaction_run_id || null,
386
+ ]
387
+ );
388
+ return result.rows[0] || null;
389
+ }
390
+
391
+ async function linkEvidence(input = {}) {
392
+ requireField(input, 'ownerKind');
393
+ requireField(input, 'ownerId');
394
+ requireField(input, 'sourceKind');
395
+ requireField(input, 'sourceRef');
396
+ const tenantId = input.tenantId || defaultTenantId;
397
+ const result = await pool.query(
398
+ `INSERT INTO ${evidenceRefs} (
399
+ tenant_id, owner_kind, owner_id, source_kind, source_ref,
400
+ relation_kind, weight, metadata, created_by_finalization_id,
401
+ created_by_compaction_run_id
402
+ )
403
+ VALUES ($1,$2,$3,$4,$5,COALESCE($6,'supporting'),COALESCE($7,1.0),COALESCE($8::jsonb,'{}'::jsonb),$9,$10)
404
+ ON CONFLICT (tenant_id, owner_kind, owner_id, source_kind, source_ref, relation_kind)
405
+ DO UPDATE SET weight = EXCLUDED.weight,
406
+ metadata = COALESCE(NULLIF(EXCLUDED.metadata, '{}'::jsonb), ${evidenceRefs}.metadata),
407
+ created_by_finalization_id = COALESCE(${evidenceRefs}.created_by_finalization_id, EXCLUDED.created_by_finalization_id),
408
+ created_by_compaction_run_id = COALESCE(${evidenceRefs}.created_by_compaction_run_id, EXCLUDED.created_by_compaction_run_id)
409
+ RETURNING *`,
410
+ [
411
+ tenantId,
412
+ input.ownerKind,
413
+ input.ownerId,
414
+ input.sourceKind,
415
+ input.sourceRef,
416
+ input.relationKind || 'supporting',
417
+ input.weight ?? 1.0,
418
+ toJson(input.metadata, {}),
419
+ input.createdByFinalizationId || input.created_by_finalization_id || null,
420
+ input.createdByCompactionRunId || input.created_by_compaction_run_id || null,
421
+ ]
422
+ );
423
+ return result.rows[0] || null;
424
+ }
425
+
426
+ async function recordFeedback(input = {}) {
427
+ requireField(input, 'targetKind');
428
+ requireField(input, 'targetId');
429
+ requireField(input, 'feedbackType');
430
+ const tenantId = input.tenantId || defaultTenantId;
431
+ const result = await pool.query(
432
+ `INSERT INTO ${feedback} (
433
+ tenant_id, target_kind, target_id, feedback_type, actor_kind, actor_id,
434
+ query_fingerprint, note, metadata
435
+ )
436
+ VALUES ($1,$2,$3,$4,COALESCE($5,'user'),$6,$7,$8,COALESCE($9::jsonb,'{}'::jsonb))
437
+ RETURNING *`,
438
+ [
439
+ tenantId,
440
+ input.targetKind,
441
+ String(input.targetId),
442
+ input.feedbackType,
443
+ input.actorKind || 'user',
444
+ input.actorId || null,
445
+ input.queryFingerprint || null,
446
+ input.note || null,
447
+ toJson(input.metadata, {}),
448
+ ]
449
+ );
450
+ return result.rows[0] || null;
451
+ }
452
+
453
+ async function findActiveByCanonicalKey(input = {}) {
454
+ requireField(input, 'canonicalKey');
455
+ const tenantId = input.tenantId || defaultTenantId;
456
+ const lockClause = input.forUpdate === true ? 'FOR UPDATE OF m' : '';
457
+ const result = await pool.query(
458
+ `SELECT m.*, s.scope_kind, s.scope_key, s.inheritance_mode AS scope_inheritance_mode
459
+ FROM ${memories} m
460
+ JOIN ${scopes} s ON s.id = m.scope_id
461
+ WHERE m.tenant_id = $1
462
+ AND m.canonical_key = $2
463
+ AND m.status = 'active'
464
+ ORDER BY m.accepted_at DESC NULLS LAST, m.id ASC
465
+ ${lockClause}`,
466
+ [tenantId, input.canonicalKey]
467
+ );
468
+ return result.rows;
469
+ }
470
+
471
+ async function findActiveFactByCanonicalKey(input = {}) {
472
+ requireField(input, 'canonicalKey');
473
+ const tenantId = input.tenantId || defaultTenantId;
474
+ const lockClause = input.forUpdate === true ? 'FOR UPDATE OF f' : '';
475
+ const result = await pool.query(
476
+ `SELECT f.*, s.scope_kind, s.scope_key, s.inheritance_mode AS scope_inheritance_mode
477
+ FROM ${factAssertions} f
478
+ JOIN ${scopes} s ON s.id = f.scope_id
479
+ WHERE f.tenant_id = $1
480
+ AND f.canonical_key = $2
481
+ AND f.status = 'active'
482
+ ORDER BY f.accepted_at DESC NULLS LAST, f.id ASC
483
+ ${lockClause}`,
484
+ [tenantId, input.canonicalKey]
485
+ );
486
+ return result.rows;
487
+ }
488
+
489
+ async function lockCanonicalKey(input = {}) {
490
+ requireField(input, 'canonicalKey');
491
+ const tenantId = input.tenantId || defaultTenantId;
492
+ const [key1, key2] = advisoryLockKeys(
493
+ 'aquifer.memory_records.active_canonical',
494
+ `${tenantId}:${input.canonicalKey}`,
495
+ );
496
+ await pool.query('SELECT pg_advisory_xact_lock($1, $2)', [key1, key2]);
497
+ }
498
+
499
+ async function updateMemoryStatus(input = {}) {
500
+ requireField(input, 'memoryId');
501
+ requireField(input, 'status');
502
+ const tenantId = input.tenantId || defaultTenantId;
503
+ const visibleBootstrap = input.status === 'active' ? input.visibleInBootstrap === true : false;
504
+ const visibleRecall = input.status === 'active' ? input.visibleInRecall === true : false;
505
+ const result = await pool.query(
506
+ `UPDATE ${memories}
507
+ SET status = $3,
508
+ superseded_by = COALESCE($4, superseded_by),
509
+ valid_to = COALESCE($5, valid_to),
510
+ superseded_at = CASE
511
+ WHEN $3 = 'superseded' THEN COALESCE($8, superseded_at, now())
512
+ ELSE superseded_at
513
+ END,
514
+ revoked_at = CASE
515
+ WHEN $3 = 'revoked' THEN COALESCE($9, revoked_at, now())
516
+ ELSE revoked_at
517
+ END,
518
+ visible_in_bootstrap = $6,
519
+ visible_in_recall = $7,
520
+ updated_at = now()
521
+ WHERE tenant_id = $1 AND id = $2
522
+ RETURNING *`,
523
+ [
524
+ tenantId,
525
+ input.memoryId,
526
+ input.status,
527
+ input.supersededBy || null,
528
+ input.validTo || null,
529
+ visibleBootstrap,
530
+ visibleRecall,
531
+ input.supersededAt || input.superseded_at || null,
532
+ input.revokedAt || input.revoked_at || null,
533
+ ]
534
+ );
535
+ return result.rows[0] || null;
536
+ }
537
+
538
+ async function updateMemoryStatusIfCurrent(input = {}) {
539
+ requireField(input, 'memoryId');
540
+ requireField(input, 'fromStatus');
541
+ requireField(input, 'status');
542
+ const tenantId = input.tenantId || defaultTenantId;
543
+ const visibleBootstrap = input.status === 'active' ? input.visibleInBootstrap === true : false;
544
+ const visibleRecall = input.status === 'active' ? input.visibleInRecall === true : false;
545
+ const result = await pool.query(
546
+ `UPDATE ${memories}
547
+ SET status = $4,
548
+ superseded_by = COALESCE($5, superseded_by),
549
+ valid_to = COALESCE($6, valid_to),
550
+ superseded_at = CASE
551
+ WHEN $4 = 'superseded' THEN COALESCE($9, superseded_at, now())
552
+ ELSE superseded_at
553
+ END,
554
+ revoked_at = CASE
555
+ WHEN $4 = 'revoked' THEN COALESCE($10, revoked_at, now())
556
+ ELSE revoked_at
557
+ END,
558
+ visible_in_bootstrap = $7,
559
+ visible_in_recall = $8,
560
+ updated_at = now()
561
+ WHERE tenant_id = $1 AND id = $2 AND status = $3
562
+ RETURNING *`,
563
+ [
564
+ tenantId,
565
+ input.memoryId,
566
+ input.fromStatus,
567
+ input.status,
568
+ input.supersededBy || null,
569
+ input.validTo || null,
570
+ visibleBootstrap,
571
+ visibleRecall,
572
+ input.supersededAt || input.superseded_at || null,
573
+ input.revokedAt || input.revoked_at || null,
574
+ ]
575
+ );
576
+ return result.rows[0] || null;
577
+ }
578
+
579
+ async function updateFactAssertionStatus(input = {}) {
580
+ requireField(input, 'factId');
581
+ requireField(input, 'status');
582
+ const tenantId = input.tenantId || defaultTenantId;
583
+ const result = await pool.query(
584
+ `UPDATE ${factAssertions}
585
+ SET status = $3,
586
+ superseded_by = COALESCE($4, superseded_by),
587
+ valid_to = COALESCE($5, valid_to),
588
+ superseded_at = CASE
589
+ WHEN $3 = 'superseded' THEN COALESCE($6, superseded_at, now())
590
+ ELSE superseded_at
591
+ END,
592
+ revoked_at = CASE
593
+ WHEN $3 = 'revoked' THEN COALESCE($7, revoked_at, now())
594
+ ELSE revoked_at
595
+ END,
596
+ updated_at = now()
597
+ WHERE tenant_id = $1 AND id = $2
598
+ RETURNING *`,
599
+ [
600
+ tenantId,
601
+ input.factId,
602
+ input.status,
603
+ input.supersededBy || input.superseded_by || null,
604
+ input.validTo || input.valid_to || null,
605
+ input.supersededAt || input.superseded_at || null,
606
+ input.revokedAt || input.revoked_at || null,
607
+ ]
608
+ );
609
+ return result.rows[0] || null;
610
+ }
611
+
612
+ async function listActive(input = {}) {
613
+ const tenantId = input.tenantId || defaultTenantId;
614
+ const params = [tenantId];
615
+ const where = [`m.tenant_id = $1`, `m.status = 'active'`];
616
+ if (input.asOf) {
617
+ params.push(input.asOf);
618
+ const at = `$${params.length}::timestamptz`;
619
+ where.push(`(m.valid_from IS NULL OR m.valid_from <= ${at})`);
620
+ where.push(`(m.valid_to IS NULL OR m.valid_to > ${at})`);
621
+ where.push(`(m.stale_after IS NULL OR m.stale_after > ${at})`);
622
+ }
623
+ if (input.scopeId) {
624
+ params.push(input.scopeId);
625
+ where.push(`m.scope_id = $${params.length}`);
626
+ }
627
+ if (Array.isArray(input.scopeKeys) && input.scopeKeys.length > 0) {
628
+ params.push(input.scopeKeys.map(value => String(value)));
629
+ where.push(`s.scope_key = ANY($${params.length}::text[])`);
630
+ }
631
+ if (input.visibleInBootstrap !== undefined) {
632
+ params.push(input.visibleInBootstrap === true);
633
+ where.push(`m.visible_in_bootstrap = $${params.length}`);
634
+ }
635
+ if (input.visibleInRecall !== undefined) {
636
+ params.push(input.visibleInRecall === true);
637
+ where.push(`m.visible_in_recall = $${params.length}`);
638
+ }
639
+ params.push(Math.max(1, Math.min(200, input.limit || 50)));
640
+ const orderBy = input.visibleInBootstrap === true
641
+ ? BOOTSTRAP_ORDER_SQL
642
+ : `m.accepted_at DESC NULLS LAST, m.id ASC`;
643
+ const result = await pool.query(
644
+ `SELECT m.*, s.scope_kind, s.scope_key, s.inheritance_mode AS scope_inheritance_mode
645
+ FROM ${memories} m
646
+ JOIN ${scopes} s ON s.id = m.scope_id
647
+ WHERE ${where.join(' AND ')}
648
+ ORDER BY ${orderBy}
649
+ LIMIT $${params.length}`,
650
+ params
651
+ );
652
+ return result.rows;
653
+ }
654
+
655
+ async function currentProjection(input = {}) {
656
+ const tenantId = input.tenantId || defaultTenantId;
657
+ let activeScopePath = normalizeScopePath(input.activeScopePath, input.activeScopeKey);
658
+ let activeScopeKey = input.activeScopeKey || activeScopePath[activeScopePath.length - 1] || null;
659
+ if (input.scopeId && !input.activeScopeKey && !input.activeScopePath) {
660
+ const scopeResult = await pool.query(
661
+ `SELECT scope_key FROM ${scopes} WHERE tenant_id = $1 AND id = $2 LIMIT 1`,
662
+ [tenantId, input.scopeId],
663
+ );
664
+ const scopedKey = scopeResult.rows[0]?.scope_key || null;
665
+ if (scopedKey) {
666
+ activeScopePath = [scopedKey];
667
+ activeScopeKey = scopedKey;
668
+ }
669
+ }
670
+ const limit = Math.max(1, Math.min(100, input.limit || 50));
671
+ const fetchLimit = Math.max(limit + 1, Math.min(200, Math.max(limit * 4, 40)));
672
+ const asOf = input.asOf || new Date().toISOString();
673
+ const params = [tenantId, activeScopePath, asOf];
674
+ const where = [
675
+ `m.tenant_id = $1`,
676
+ `m.status = 'active'`,
677
+ `s.scope_key = ANY($2::text[])`,
678
+ `(m.visible_in_bootstrap = true OR m.visible_in_recall = true)`,
679
+ `(m.valid_from IS NULL OR m.valid_from <= $3::timestamptz)`,
680
+ `(m.valid_to IS NULL OR m.valid_to > $3::timestamptz)`,
681
+ `(m.stale_after IS NULL OR m.stale_after > $3::timestamptz)`,
682
+ ];
683
+
684
+ if (input.scopeId) {
685
+ params.push(input.scopeId);
686
+ where.push(`m.scope_id = $${params.length}`);
687
+ }
688
+
689
+ params.push(fetchLimit);
690
+ const limitParam = `$${params.length}`;
691
+ const evidenceRefsSelect = input.includeEvidenceRefs === true
692
+ ? `COALESCE((
693
+ SELECT jsonb_agg(
694
+ jsonb_build_object(
695
+ 'id', e.id,
696
+ 'sourceKind', e.source_kind,
697
+ 'sourceRef', e.source_ref,
698
+ 'relationKind', e.relation_kind,
699
+ 'weight', e.weight,
700
+ 'metadata', e.metadata
701
+ )
702
+ ORDER BY e.id ASC
703
+ )
704
+ FROM ${evidenceRefs} e
705
+ WHERE e.tenant_id = m.tenant_id
706
+ AND e.owner_kind = 'memory_record'
707
+ AND e.owner_id = m.id
708
+ ), '[]'::jsonb)`
709
+ : `'[]'::jsonb`;
710
+
711
+ const result = await pool.query(
712
+ `SELECT
713
+ m.*,
714
+ s.scope_kind,
715
+ s.scope_key,
716
+ s.inheritance_mode AS scope_inheritance_mode,
717
+ ${evidenceRefsSelect} AS evidence_refs
718
+ FROM ${memories} m
719
+ JOIN ${scopes} s ON s.id = m.scope_id
720
+ WHERE ${where.join(' AND ')}
721
+ ORDER BY array_position($2::text[], s.scope_key) DESC NULLS LAST,
722
+ ${BOOTSTRAP_ORDER_SQL}
723
+ LIMIT ${limitParam}`,
724
+ params,
725
+ );
726
+
727
+ const positions = new Map(activeScopePath.map((key, index) => [key, index]));
728
+ const applicable = resolveApplicableRecords(
729
+ result.rows
730
+ .map(normalizeCurrentMemoryRow)
731
+ .filter(row => isCurrentProjectionRow(row, asOf)),
732
+ { activeScopeKey, activeScopePath },
733
+ ).sort((left, right) => sortCurrentMemoryRecords(left, right, positions));
734
+
735
+ const selected = applicable.slice(0, limit);
736
+ const truncated = applicable.length > limit;
737
+ return {
738
+ memories: selected,
739
+ meta: {
740
+ source: 'memory_records',
741
+ servingContract: 'current_memory_v1',
742
+ count: selected.length,
743
+ activeScopeKey,
744
+ activeScopePath,
745
+ asOf,
746
+ truncated,
747
+ degraded: truncated,
748
+ },
749
+ };
750
+ }
751
+
752
+ async function withTransaction(fn) {
753
+ if (inTransaction) {
754
+ return fn(api, { transactional: true });
755
+ }
756
+
757
+ if (!canTransact) {
758
+ return fn(api, { transactional: false });
759
+ }
760
+
761
+ const client = await pool.connect();
762
+ try {
763
+ await client.query('BEGIN');
764
+ const txRecords = createMemoryRecords({ pool: client, schema, defaultTenantId, inTransaction: true });
765
+ const result = await fn(txRecords, { transactional: true });
766
+ await client.query('COMMIT');
767
+ return result;
768
+ } catch (error) {
769
+ await client.query('ROLLBACK').catch(() => {});
770
+ throw error;
771
+ } finally {
772
+ client.release();
773
+ }
774
+ }
775
+
776
+ const api = {
777
+ upsertScope,
778
+ createVersion,
779
+ upsertMemory,
780
+ upsertFactAssertion,
781
+ linkEvidence,
782
+ recordFeedback,
783
+ findActiveByCanonicalKey,
784
+ findActiveFactByCanonicalKey,
785
+ lockCanonicalKey,
786
+ updateMemoryStatus,
787
+ updateMemoryStatusIfCurrent,
788
+ updateFactAssertionStatus,
789
+ listActive,
790
+ currentProjection,
791
+ withTransaction,
792
+ };
793
+
794
+ return api;
795
+ }
796
+
797
+ module.exports = { createMemoryRecords };