@shadowforge0/aquifer-memory 1.5.9 → 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 (65) hide show
  1. package/.env.example +23 -0
  2. package/README.md +96 -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 +374 -39
  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 +131 -7
  14. package/consumers/openclaw-ext/index.js +0 -1
  15. package/consumers/openclaw-plugin.js +44 -4
  16. package/consumers/shared/config.js +28 -0
  17. package/consumers/shared/factory.js +2 -0
  18. package/consumers/shared/ingest.js +1 -1
  19. package/consumers/shared/normalize.js +14 -3
  20. package/consumers/shared/recall-format.js +53 -0
  21. package/consumers/shared/summary-parser.js +151 -0
  22. package/core/aquifer.js +384 -18
  23. package/core/finalization-review.js +319 -0
  24. package/core/insights.js +210 -58
  25. package/core/mcp-manifest.js +69 -2
  26. package/core/memory-bootstrap.js +188 -0
  27. package/core/memory-consolidation.js +1236 -0
  28. package/core/memory-promotion.js +544 -0
  29. package/core/memory-recall.js +247 -0
  30. package/core/memory-records.js +581 -0
  31. package/core/memory-safety-gate.js +224 -0
  32. package/core/session-finalization.js +350 -0
  33. package/core/storage.js +456 -2
  34. package/docs/getting-started.md +99 -0
  35. package/docs/postprocess-contract.md +2 -2
  36. package/docs/setup.md +51 -2
  37. package/package.json +31 -9
  38. package/pipeline/normalize/adapters/codex.js +106 -0
  39. package/pipeline/normalize/detect.js +3 -2
  40. package/schema/001-base.sql +3 -0
  41. package/schema/007-v1-foundation.sql +273 -0
  42. package/schema/008-session-finalizations.sql +50 -0
  43. package/schema/009-v1-assertion-plane.sql +193 -0
  44. package/schema/010-v1-finalization-review.sql +160 -0
  45. package/schema/011-v1-compaction-claim.sql +46 -0
  46. package/schema/012-v1-compaction-lease.sql +39 -0
  47. package/schema/013-v1-compaction-lineage.sql +193 -0
  48. package/scripts/backfill-canonical-key.js +250 -0
  49. package/scripts/codex-recovery.js +532 -0
  50. package/consumers/miranda/context-inject.js +0 -119
  51. package/consumers/miranda/daily-entries.js +0 -224
  52. package/consumers/miranda/index.js +0 -364
  53. package/consumers/miranda/instance.js +0 -55
  54. package/consumers/miranda/llm.js +0 -99
  55. package/consumers/miranda/profile.json +0 -145
  56. package/consumers/miranda/prompts/summary.js +0 -303
  57. package/consumers/miranda/recall-format.js +0 -76
  58. package/consumers/miranda/render-daily-md.js +0 -186
  59. package/consumers/miranda/workspace-files.js +0 -91
  60. package/scripts/drop-entity-state-history.sql +0 -17
  61. package/scripts/drop-insights.sql +0 -12
  62. package/scripts/install-openclaw.sh +0 -59
  63. package/scripts/queries.json +0 -45
  64. package/scripts/retro-recall-bench.js +0 -409
  65. package/scripts/sample-bench-queries.sql +0 -75
@@ -0,0 +1,581 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+
5
+ function requireField(obj, field) {
6
+ if (!obj || obj[field] === undefined || obj[field] === null || obj[field] === '') {
7
+ throw new Error(`${field} is required`);
8
+ }
9
+ }
10
+
11
+ function toJson(value, fallback) {
12
+ return JSON.stringify(value === undefined ? fallback : value);
13
+ }
14
+
15
+ function toJsonOrNull(value) {
16
+ return value === undefined || value === null ? null : JSON.stringify(value);
17
+ }
18
+
19
+ function advisoryLockKeys(namespace, value) {
20
+ const digest = crypto.createHash('sha256').update(`${namespace}:${value}`).digest();
21
+ return [digest.readInt32BE(0), digest.readInt32BE(4)];
22
+ }
23
+
24
+ const BOOTSTRAP_ORDER_SQL = `
25
+ CASE m.memory_type
26
+ WHEN 'constraint' THEN 0
27
+ WHEN 'preference' THEN 1
28
+ WHEN 'state' THEN 2
29
+ WHEN 'open_loop' THEN 3
30
+ WHEN 'decision' THEN 4
31
+ WHEN 'fact' THEN 5
32
+ WHEN 'conclusion' THEN 6
33
+ WHEN 'entity_note' THEN 7
34
+ ELSE 99
35
+ END ASC,
36
+ CASE m.authority
37
+ WHEN 'user_explicit' THEN 0
38
+ WHEN 'executable_evidence' THEN 1
39
+ WHEN 'manual' THEN 2
40
+ WHEN 'system' THEN 3
41
+ WHEN 'verified_summary' THEN 4
42
+ WHEN 'llm_inference' THEN 5
43
+ WHEN 'raw_transcript' THEN 6
44
+ ELSE 99
45
+ END ASC,
46
+ m.accepted_at DESC NULLS LAST,
47
+ m.id ASC`;
48
+
49
+ function createMemoryRecords({ pool, schema, defaultTenantId, inTransaction = false }) {
50
+ const scopes = `${schema}.scopes`;
51
+ const versions = `${schema}.versions`;
52
+ const memories = `${schema}.memory_records`;
53
+ const factAssertions = `${schema}.fact_assertions_v1`;
54
+ const evidenceRefs = `${schema}.evidence_refs`;
55
+ const feedback = `${schema}.feedback`;
56
+ const canTransact = typeof pool.connect === 'function';
57
+
58
+ async function upsertScope(input = {}) {
59
+ requireField(input, 'scopeKind');
60
+ requireField(input, 'scopeKey');
61
+ const tenantId = input.tenantId || defaultTenantId;
62
+ const result = await pool.query(
63
+ `INSERT INTO ${scopes} (
64
+ tenant_id, scope_kind, scope_key, parent_scope_id, inheritance_mode,
65
+ context_key, topic_key, metadata, active_from, active_to
66
+ )
67
+ VALUES ($1,$2,$3,$4,COALESCE($5,'defaultable'),$6,$7,COALESCE($8::jsonb,'{}'::jsonb),$9,$10)
68
+ ON CONFLICT (tenant_id, scope_kind, scope_key) DO UPDATE SET
69
+ parent_scope_id = COALESCE(EXCLUDED.parent_scope_id, ${scopes}.parent_scope_id),
70
+ inheritance_mode = EXCLUDED.inheritance_mode,
71
+ context_key = COALESCE(EXCLUDED.context_key, ${scopes}.context_key),
72
+ topic_key = COALESCE(EXCLUDED.topic_key, ${scopes}.topic_key),
73
+ metadata = COALESCE(NULLIF(EXCLUDED.metadata, '{}'::jsonb), ${scopes}.metadata),
74
+ active_from = COALESCE(EXCLUDED.active_from, ${scopes}.active_from),
75
+ active_to = COALESCE(EXCLUDED.active_to, ${scopes}.active_to)
76
+ RETURNING *`,
77
+ [
78
+ tenantId,
79
+ input.scopeKind,
80
+ input.scopeKey,
81
+ input.parentScopeId || null,
82
+ input.inheritanceMode || 'defaultable',
83
+ input.contextKey || null,
84
+ input.topicKey || null,
85
+ toJson(input.metadata, {}),
86
+ input.activeFrom || null,
87
+ input.activeTo || null,
88
+ ]
89
+ );
90
+ return result.rows[0] || null;
91
+ }
92
+
93
+ async function createVersion(input = {}) {
94
+ requireField(input, 'versionKind');
95
+ requireField(input, 'version');
96
+ requireField(input, 'versionHash');
97
+ const tenantId = input.tenantId || defaultTenantId;
98
+ const result = await pool.query(
99
+ `INSERT INTO ${versions} (
100
+ tenant_id, version_kind, version, version_hash, active, metadata,
101
+ released_at, retired_at
102
+ )
103
+ VALUES ($1,$2,$3,$4,COALESCE($5,false),COALESCE($6::jsonb,'{}'::jsonb),COALESCE($7,now()),$8)
104
+ ON CONFLICT (tenant_id, version_kind, version_hash) DO UPDATE SET
105
+ version = EXCLUDED.version,
106
+ metadata = COALESCE(NULLIF(EXCLUDED.metadata, '{}'::jsonb), ${versions}.metadata),
107
+ retired_at = COALESCE(EXCLUDED.retired_at, ${versions}.retired_at)
108
+ RETURNING *`,
109
+ [
110
+ tenantId,
111
+ input.versionKind,
112
+ input.version,
113
+ input.versionHash,
114
+ input.active === true,
115
+ toJson(input.metadata, {}),
116
+ input.releasedAt || null,
117
+ input.retiredAt || null,
118
+ ]
119
+ );
120
+ return result.rows[0] || null;
121
+ }
122
+
123
+ async function upsertMemory(input = {}) {
124
+ requireField(input, 'memoryType');
125
+ requireField(input, 'canonicalKey');
126
+ requireField(input, 'scopeId');
127
+ const tenantId = input.tenantId || defaultTenantId;
128
+ const status = input.status || 'candidate';
129
+ const result = await pool.query(
130
+ `INSERT INTO ${memories} (
131
+ tenant_id, memory_type, canonical_key, scope_id, context_key, topic_key,
132
+ title, summary, payload, status, authority, accepted_at, valid_from,
133
+ valid_to, stale_after, superseded_by, backing_fact_id, observed_at,
134
+ revoked_at, superseded_at, version_id, visible_in_bootstrap,
135
+ visible_in_recall, rank_features, created_by_finalization_id,
136
+ created_by_compaction_run_id
137
+ )
138
+ VALUES (
139
+ $1,$2,$3,$4,$5,$6,$7,COALESCE($8,''),COALESCE($9::jsonb,'{}'::jsonb),
140
+ COALESCE($10,'candidate'),COALESCE($11,'llm_inference'),$12,$13,$14,$15,
141
+ $16,$17,$18,$19,$20,$21,COALESCE($22,false),COALESCE($23,false),COALESCE($24::jsonb,'{}'::jsonb),$25,$26
142
+ )
143
+ ON CONFLICT (tenant_id, canonical_key) WHERE status = 'active' DO UPDATE SET
144
+ scope_id = EXCLUDED.scope_id,
145
+ context_key = COALESCE(EXCLUDED.context_key, ${memories}.context_key),
146
+ topic_key = COALESCE(EXCLUDED.topic_key, ${memories}.topic_key),
147
+ title = COALESCE(EXCLUDED.title, ${memories}.title),
148
+ summary = COALESCE(NULLIF(EXCLUDED.summary, ''), ${memories}.summary),
149
+ payload = COALESCE(NULLIF(EXCLUDED.payload, '{}'::jsonb), ${memories}.payload),
150
+ authority = EXCLUDED.authority,
151
+ accepted_at = COALESCE(EXCLUDED.accepted_at, ${memories}.accepted_at),
152
+ valid_from = COALESCE(EXCLUDED.valid_from, ${memories}.valid_from),
153
+ valid_to = COALESCE(EXCLUDED.valid_to, ${memories}.valid_to),
154
+ stale_after = COALESCE(EXCLUDED.stale_after, ${memories}.stale_after),
155
+ version_id = COALESCE(EXCLUDED.version_id, ${memories}.version_id),
156
+ backing_fact_id = COALESCE(EXCLUDED.backing_fact_id, ${memories}.backing_fact_id),
157
+ observed_at = COALESCE(EXCLUDED.observed_at, ${memories}.observed_at),
158
+ revoked_at = COALESCE(EXCLUDED.revoked_at, ${memories}.revoked_at),
159
+ superseded_at = COALESCE(EXCLUDED.superseded_at, ${memories}.superseded_at),
160
+ visible_in_bootstrap = EXCLUDED.visible_in_bootstrap,
161
+ visible_in_recall = EXCLUDED.visible_in_recall,
162
+ rank_features = COALESCE(NULLIF(EXCLUDED.rank_features, '{}'::jsonb), ${memories}.rank_features),
163
+ created_by_finalization_id = COALESCE(${memories}.created_by_finalization_id, EXCLUDED.created_by_finalization_id),
164
+ created_by_compaction_run_id = COALESCE(${memories}.created_by_compaction_run_id, EXCLUDED.created_by_compaction_run_id)
165
+ RETURNING *`,
166
+ [
167
+ tenantId,
168
+ input.memoryType,
169
+ input.canonicalKey,
170
+ input.scopeId,
171
+ input.contextKey || null,
172
+ input.topicKey || null,
173
+ input.title || null,
174
+ input.summary || '',
175
+ toJson(input.payload, {}),
176
+ status,
177
+ input.authority || 'llm_inference',
178
+ input.acceptedAt || (status === 'active' ? new Date().toISOString() : null),
179
+ input.validFrom || null,
180
+ input.validTo || null,
181
+ input.staleAfter || null,
182
+ input.supersededBy || null,
183
+ input.backingFactId || input.backing_fact_id || null,
184
+ input.observedAt || input.observed_at || null,
185
+ input.revokedAt || input.revoked_at || null,
186
+ input.supersededAt || input.superseded_at || null,
187
+ input.versionId || null,
188
+ input.visibleInBootstrap === true,
189
+ input.visibleInRecall === true,
190
+ toJson(input.rankFeatures, {}),
191
+ input.createdByFinalizationId || input.created_by_finalization_id || null,
192
+ input.createdByCompactionRunId || input.created_by_compaction_run_id || null,
193
+ ]
194
+ );
195
+ return result.rows[0] || null;
196
+ }
197
+
198
+ async function upsertFactAssertion(input = {}) {
199
+ requireField(input, 'canonicalKey');
200
+ requireField(input, 'scopeId');
201
+ requireField(input, 'predicate');
202
+ requireField(input, 'objectKind');
203
+ requireField(input, 'assertionHash');
204
+ const tenantId = input.tenantId || defaultTenantId;
205
+ const status = input.status || 'active';
206
+ const result = await pool.query(
207
+ `INSERT INTO ${factAssertions} (
208
+ tenant_id, canonical_key, scope_id, subject_entity_id, predicate,
209
+ object_kind, object_entity_id, object_value_json, qualifiers_json,
210
+ valid_from, valid_to, observed_at, stale_after, accepted_at,
211
+ revoked_at, superseded_at, status, authority, assertion_hash,
212
+ superseded_by, version_id, metadata, created_by_finalization_id,
213
+ created_by_compaction_run_id
214
+ )
215
+ VALUES (
216
+ $1,$2,$3,$4,$5,$6,$7,$8::jsonb,COALESCE($9::jsonb,'{}'::jsonb),
217
+ $10,$11,$12,$13,$14,$15,$16,COALESCE($17,'active'),COALESCE($18,'verified_summary'),
218
+ $19,$20,$21,COALESCE($22::jsonb,'{}'::jsonb),$23,$24
219
+ )
220
+ ON CONFLICT (tenant_id, canonical_key) WHERE status = 'active' DO UPDATE SET
221
+ scope_id = EXCLUDED.scope_id,
222
+ subject_entity_id = COALESCE(EXCLUDED.subject_entity_id, ${factAssertions}.subject_entity_id),
223
+ predicate = EXCLUDED.predicate,
224
+ object_kind = EXCLUDED.object_kind,
225
+ object_entity_id = COALESCE(EXCLUDED.object_entity_id, ${factAssertions}.object_entity_id),
226
+ object_value_json = EXCLUDED.object_value_json,
227
+ qualifiers_json = COALESCE(NULLIF(EXCLUDED.qualifiers_json, '{}'::jsonb), ${factAssertions}.qualifiers_json),
228
+ valid_from = COALESCE(EXCLUDED.valid_from, ${factAssertions}.valid_from),
229
+ valid_to = COALESCE(EXCLUDED.valid_to, ${factAssertions}.valid_to),
230
+ observed_at = COALESCE(EXCLUDED.observed_at, ${factAssertions}.observed_at),
231
+ stale_after = COALESCE(EXCLUDED.stale_after, ${factAssertions}.stale_after),
232
+ accepted_at = COALESCE(EXCLUDED.accepted_at, ${factAssertions}.accepted_at),
233
+ revoked_at = COALESCE(EXCLUDED.revoked_at, ${factAssertions}.revoked_at),
234
+ superseded_at = COALESCE(EXCLUDED.superseded_at, ${factAssertions}.superseded_at),
235
+ authority = EXCLUDED.authority,
236
+ assertion_hash = EXCLUDED.assertion_hash,
237
+ version_id = COALESCE(EXCLUDED.version_id, ${factAssertions}.version_id),
238
+ metadata = COALESCE(NULLIF(EXCLUDED.metadata, '{}'::jsonb), ${factAssertions}.metadata),
239
+ created_by_finalization_id = COALESCE(${factAssertions}.created_by_finalization_id, EXCLUDED.created_by_finalization_id),
240
+ created_by_compaction_run_id = COALESCE(${factAssertions}.created_by_compaction_run_id, EXCLUDED.created_by_compaction_run_id),
241
+ updated_at = now()
242
+ RETURNING *`,
243
+ [
244
+ tenantId,
245
+ input.canonicalKey,
246
+ input.scopeId,
247
+ input.subjectEntityId || input.subject_entity_id || null,
248
+ input.predicate,
249
+ input.objectKind || input.object_kind,
250
+ input.objectEntityId || input.object_entity_id || null,
251
+ toJsonOrNull(input.objectValueJson ?? input.object_value_json),
252
+ toJson(input.qualifiersJson ?? input.qualifiers_json, {}),
253
+ input.validFrom || input.valid_from || null,
254
+ input.validTo || input.valid_to || null,
255
+ input.observedAt || input.observed_at || null,
256
+ input.staleAfter || input.stale_after || null,
257
+ input.acceptedAt || input.accepted_at || (status === 'active' ? new Date().toISOString() : null),
258
+ input.revokedAt || input.revoked_at || null,
259
+ input.supersededAt || input.superseded_at || null,
260
+ status,
261
+ input.authority || 'verified_summary',
262
+ input.assertionHash || input.assertion_hash,
263
+ input.supersededBy || input.superseded_by || null,
264
+ input.versionId || input.version_id || null,
265
+ toJson(input.metadata, {}),
266
+ input.createdByFinalizationId || input.created_by_finalization_id || null,
267
+ input.createdByCompactionRunId || input.created_by_compaction_run_id || null,
268
+ ]
269
+ );
270
+ return result.rows[0] || null;
271
+ }
272
+
273
+ async function linkEvidence(input = {}) {
274
+ requireField(input, 'ownerKind');
275
+ requireField(input, 'ownerId');
276
+ requireField(input, 'sourceKind');
277
+ requireField(input, 'sourceRef');
278
+ const tenantId = input.tenantId || defaultTenantId;
279
+ const result = await pool.query(
280
+ `INSERT INTO ${evidenceRefs} (
281
+ tenant_id, owner_kind, owner_id, source_kind, source_ref,
282
+ relation_kind, weight, metadata, created_by_finalization_id,
283
+ created_by_compaction_run_id
284
+ )
285
+ VALUES ($1,$2,$3,$4,$5,COALESCE($6,'supporting'),COALESCE($7,1.0),COALESCE($8::jsonb,'{}'::jsonb),$9,$10)
286
+ ON CONFLICT (tenant_id, owner_kind, owner_id, source_kind, source_ref, relation_kind)
287
+ DO UPDATE SET weight = EXCLUDED.weight,
288
+ metadata = COALESCE(NULLIF(EXCLUDED.metadata, '{}'::jsonb), ${evidenceRefs}.metadata),
289
+ created_by_finalization_id = COALESCE(${evidenceRefs}.created_by_finalization_id, EXCLUDED.created_by_finalization_id),
290
+ created_by_compaction_run_id = COALESCE(${evidenceRefs}.created_by_compaction_run_id, EXCLUDED.created_by_compaction_run_id)
291
+ RETURNING *`,
292
+ [
293
+ tenantId,
294
+ input.ownerKind,
295
+ input.ownerId,
296
+ input.sourceKind,
297
+ input.sourceRef,
298
+ input.relationKind || 'supporting',
299
+ input.weight ?? 1.0,
300
+ toJson(input.metadata, {}),
301
+ input.createdByFinalizationId || input.created_by_finalization_id || null,
302
+ input.createdByCompactionRunId || input.created_by_compaction_run_id || null,
303
+ ]
304
+ );
305
+ return result.rows[0] || null;
306
+ }
307
+
308
+ async function recordFeedback(input = {}) {
309
+ requireField(input, 'targetKind');
310
+ requireField(input, 'targetId');
311
+ requireField(input, 'feedbackType');
312
+ const tenantId = input.tenantId || defaultTenantId;
313
+ const result = await pool.query(
314
+ `INSERT INTO ${feedback} (
315
+ tenant_id, target_kind, target_id, feedback_type, actor_kind, actor_id,
316
+ query_fingerprint, note, metadata
317
+ )
318
+ VALUES ($1,$2,$3,$4,COALESCE($5,'user'),$6,$7,$8,COALESCE($9::jsonb,'{}'::jsonb))
319
+ RETURNING *`,
320
+ [
321
+ tenantId,
322
+ input.targetKind,
323
+ String(input.targetId),
324
+ input.feedbackType,
325
+ input.actorKind || 'user',
326
+ input.actorId || null,
327
+ input.queryFingerprint || null,
328
+ input.note || null,
329
+ toJson(input.metadata, {}),
330
+ ]
331
+ );
332
+ return result.rows[0] || null;
333
+ }
334
+
335
+ async function findActiveByCanonicalKey(input = {}) {
336
+ requireField(input, 'canonicalKey');
337
+ const tenantId = input.tenantId || defaultTenantId;
338
+ const lockClause = input.forUpdate === true ? 'FOR UPDATE OF m' : '';
339
+ const result = await pool.query(
340
+ `SELECT m.*, s.scope_kind, s.scope_key, s.inheritance_mode AS scope_inheritance_mode
341
+ FROM ${memories} m
342
+ JOIN ${scopes} s ON s.id = m.scope_id
343
+ WHERE m.tenant_id = $1
344
+ AND m.canonical_key = $2
345
+ AND m.status = 'active'
346
+ ORDER BY m.accepted_at DESC NULLS LAST, m.id ASC
347
+ ${lockClause}`,
348
+ [tenantId, input.canonicalKey]
349
+ );
350
+ return result.rows;
351
+ }
352
+
353
+ async function findActiveFactByCanonicalKey(input = {}) {
354
+ requireField(input, 'canonicalKey');
355
+ const tenantId = input.tenantId || defaultTenantId;
356
+ const lockClause = input.forUpdate === true ? 'FOR UPDATE OF f' : '';
357
+ const result = await pool.query(
358
+ `SELECT f.*, s.scope_kind, s.scope_key, s.inheritance_mode AS scope_inheritance_mode
359
+ FROM ${factAssertions} f
360
+ JOIN ${scopes} s ON s.id = f.scope_id
361
+ WHERE f.tenant_id = $1
362
+ AND f.canonical_key = $2
363
+ AND f.status = 'active'
364
+ ORDER BY f.accepted_at DESC NULLS LAST, f.id ASC
365
+ ${lockClause}`,
366
+ [tenantId, input.canonicalKey]
367
+ );
368
+ return result.rows;
369
+ }
370
+
371
+ async function lockCanonicalKey(input = {}) {
372
+ requireField(input, 'canonicalKey');
373
+ const tenantId = input.tenantId || defaultTenantId;
374
+ const [key1, key2] = advisoryLockKeys(
375
+ 'aquifer.memory_records.active_canonical',
376
+ `${tenantId}:${input.canonicalKey}`,
377
+ );
378
+ await pool.query('SELECT pg_advisory_xact_lock($1, $2)', [key1, key2]);
379
+ }
380
+
381
+ async function updateMemoryStatus(input = {}) {
382
+ requireField(input, 'memoryId');
383
+ requireField(input, 'status');
384
+ const tenantId = input.tenantId || defaultTenantId;
385
+ const visibleBootstrap = input.status === 'active' ? input.visibleInBootstrap === true : false;
386
+ const visibleRecall = input.status === 'active' ? input.visibleInRecall === true : false;
387
+ const result = await pool.query(
388
+ `UPDATE ${memories}
389
+ SET status = $3,
390
+ superseded_by = COALESCE($4, superseded_by),
391
+ valid_to = COALESCE($5, valid_to),
392
+ superseded_at = CASE
393
+ WHEN $3 = 'superseded' THEN COALESCE($8, superseded_at, now())
394
+ ELSE superseded_at
395
+ END,
396
+ revoked_at = CASE
397
+ WHEN $3 = 'revoked' THEN COALESCE($9, revoked_at, now())
398
+ ELSE revoked_at
399
+ END,
400
+ visible_in_bootstrap = $6,
401
+ visible_in_recall = $7,
402
+ updated_at = now()
403
+ WHERE tenant_id = $1 AND id = $2
404
+ RETURNING *`,
405
+ [
406
+ tenantId,
407
+ input.memoryId,
408
+ input.status,
409
+ input.supersededBy || null,
410
+ input.validTo || null,
411
+ visibleBootstrap,
412
+ visibleRecall,
413
+ input.supersededAt || input.superseded_at || null,
414
+ input.revokedAt || input.revoked_at || null,
415
+ ]
416
+ );
417
+ return result.rows[0] || null;
418
+ }
419
+
420
+ async function updateMemoryStatusIfCurrent(input = {}) {
421
+ requireField(input, 'memoryId');
422
+ requireField(input, 'fromStatus');
423
+ requireField(input, 'status');
424
+ const tenantId = input.tenantId || defaultTenantId;
425
+ const visibleBootstrap = input.status === 'active' ? input.visibleInBootstrap === true : false;
426
+ const visibleRecall = input.status === 'active' ? input.visibleInRecall === true : false;
427
+ const result = await pool.query(
428
+ `UPDATE ${memories}
429
+ SET status = $4,
430
+ superseded_by = COALESCE($5, superseded_by),
431
+ valid_to = COALESCE($6, valid_to),
432
+ superseded_at = CASE
433
+ WHEN $4 = 'superseded' THEN COALESCE($9, superseded_at, now())
434
+ ELSE superseded_at
435
+ END,
436
+ revoked_at = CASE
437
+ WHEN $4 = 'revoked' THEN COALESCE($10, revoked_at, now())
438
+ ELSE revoked_at
439
+ END,
440
+ visible_in_bootstrap = $7,
441
+ visible_in_recall = $8,
442
+ updated_at = now()
443
+ WHERE tenant_id = $1 AND id = $2 AND status = $3
444
+ RETURNING *`,
445
+ [
446
+ tenantId,
447
+ input.memoryId,
448
+ input.fromStatus,
449
+ input.status,
450
+ input.supersededBy || null,
451
+ input.validTo || null,
452
+ visibleBootstrap,
453
+ visibleRecall,
454
+ input.supersededAt || input.superseded_at || null,
455
+ input.revokedAt || input.revoked_at || null,
456
+ ]
457
+ );
458
+ return result.rows[0] || null;
459
+ }
460
+
461
+ async function updateFactAssertionStatus(input = {}) {
462
+ requireField(input, 'factId');
463
+ requireField(input, 'status');
464
+ const tenantId = input.tenantId || defaultTenantId;
465
+ const result = await pool.query(
466
+ `UPDATE ${factAssertions}
467
+ SET status = $3,
468
+ superseded_by = COALESCE($4, superseded_by),
469
+ valid_to = COALESCE($5, valid_to),
470
+ superseded_at = CASE
471
+ WHEN $3 = 'superseded' THEN COALESCE($6, superseded_at, now())
472
+ ELSE superseded_at
473
+ END,
474
+ revoked_at = CASE
475
+ WHEN $3 = 'revoked' THEN COALESCE($7, revoked_at, now())
476
+ ELSE revoked_at
477
+ END,
478
+ updated_at = now()
479
+ WHERE tenant_id = $1 AND id = $2
480
+ RETURNING *`,
481
+ [
482
+ tenantId,
483
+ input.factId,
484
+ input.status,
485
+ input.supersededBy || input.superseded_by || null,
486
+ input.validTo || input.valid_to || null,
487
+ input.supersededAt || input.superseded_at || null,
488
+ input.revokedAt || input.revoked_at || null,
489
+ ]
490
+ );
491
+ return result.rows[0] || null;
492
+ }
493
+
494
+ async function listActive(input = {}) {
495
+ const tenantId = input.tenantId || defaultTenantId;
496
+ const params = [tenantId];
497
+ const where = [`m.tenant_id = $1`, `m.status = 'active'`];
498
+ if (input.asOf) {
499
+ params.push(input.asOf);
500
+ const at = `$${params.length}::timestamptz`;
501
+ where.push(`(m.valid_from IS NULL OR m.valid_from <= ${at})`);
502
+ where.push(`(m.valid_to IS NULL OR m.valid_to > ${at})`);
503
+ where.push(`(m.stale_after IS NULL OR m.stale_after > ${at})`);
504
+ }
505
+ if (input.scopeId) {
506
+ params.push(input.scopeId);
507
+ where.push(`m.scope_id = $${params.length}`);
508
+ }
509
+ if (Array.isArray(input.scopeKeys) && input.scopeKeys.length > 0) {
510
+ params.push(input.scopeKeys.map(value => String(value)));
511
+ where.push(`s.scope_key = ANY($${params.length}::text[])`);
512
+ }
513
+ if (input.visibleInBootstrap !== undefined) {
514
+ params.push(input.visibleInBootstrap === true);
515
+ where.push(`m.visible_in_bootstrap = $${params.length}`);
516
+ }
517
+ if (input.visibleInRecall !== undefined) {
518
+ params.push(input.visibleInRecall === true);
519
+ where.push(`m.visible_in_recall = $${params.length}`);
520
+ }
521
+ params.push(Math.max(1, Math.min(200, input.limit || 50)));
522
+ const orderBy = input.visibleInBootstrap === true
523
+ ? BOOTSTRAP_ORDER_SQL
524
+ : `m.accepted_at DESC NULLS LAST, m.id ASC`;
525
+ const result = await pool.query(
526
+ `SELECT m.*, s.scope_kind, s.scope_key, s.inheritance_mode AS scope_inheritance_mode
527
+ FROM ${memories} m
528
+ JOIN ${scopes} s ON s.id = m.scope_id
529
+ WHERE ${where.join(' AND ')}
530
+ ORDER BY ${orderBy}
531
+ LIMIT $${params.length}`,
532
+ params
533
+ );
534
+ return result.rows;
535
+ }
536
+
537
+ async function withTransaction(fn) {
538
+ if (inTransaction) {
539
+ return fn(api, { transactional: true });
540
+ }
541
+
542
+ if (!canTransact) {
543
+ return fn(api, { transactional: false });
544
+ }
545
+
546
+ const client = await pool.connect();
547
+ try {
548
+ await client.query('BEGIN');
549
+ const txRecords = createMemoryRecords({ pool: client, schema, defaultTenantId, inTransaction: true });
550
+ const result = await fn(txRecords, { transactional: true });
551
+ await client.query('COMMIT');
552
+ return result;
553
+ } catch (error) {
554
+ await client.query('ROLLBACK').catch(() => {});
555
+ throw error;
556
+ } finally {
557
+ client.release();
558
+ }
559
+ }
560
+
561
+ const api = {
562
+ upsertScope,
563
+ createVersion,
564
+ upsertMemory,
565
+ upsertFactAssertion,
566
+ linkEvidence,
567
+ recordFeedback,
568
+ findActiveByCanonicalKey,
569
+ findActiveFactByCanonicalKey,
570
+ lockCanonicalKey,
571
+ updateMemoryStatus,
572
+ updateMemoryStatusIfCurrent,
573
+ updateFactAssertionStatus,
574
+ listActive,
575
+ withTransaction,
576
+ };
577
+
578
+ return api;
579
+ }
580
+
581
+ module.exports = { createMemoryRecords };