@shadowforge0/aquifer-memory 1.7.0 → 1.8.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 (39) hide show
  1. package/.env.example +8 -0
  2. package/README.md +66 -0
  3. package/aquifer.config.example.json +19 -0
  4. package/consumers/cli.js +192 -12
  5. package/consumers/codex-active-checkpoint.js +186 -0
  6. package/consumers/codex-current-memory.js +106 -0
  7. package/consumers/codex-handoff.js +442 -3
  8. package/consumers/codex.js +164 -107
  9. package/consumers/mcp.js +144 -6
  10. package/consumers/shared/config.js +60 -1
  11. package/consumers/shared/factory.js +10 -3
  12. package/core/aquifer.js +351 -840
  13. package/core/backends/capabilities.js +89 -0
  14. package/core/backends/local.js +430 -0
  15. package/core/legacy-bootstrap.js +140 -0
  16. package/core/mcp-manifest.js +66 -2
  17. package/core/memory-promotion.js +157 -26
  18. package/core/memory-recall.js +341 -22
  19. package/core/memory-records.js +128 -8
  20. package/core/memory-serving.js +132 -0
  21. package/core/postgres-migrations.js +533 -0
  22. package/core/public-session-filter.js +40 -0
  23. package/core/recall-runtime.js +115 -0
  24. package/core/scope-attribution.js +279 -0
  25. package/core/session-checkpoint-producer.js +412 -0
  26. package/core/session-checkpoints.js +432 -0
  27. package/core/session-finalization.js +82 -1
  28. package/core/storage-checkpoints.js +546 -0
  29. package/core/storage.js +121 -8
  30. package/docs/setup.md +22 -0
  31. package/package.json +8 -4
  32. package/schema/014-v1-checkpoint-runs.sql +349 -0
  33. package/schema/015-v1-evidence-items.sql +92 -0
  34. package/schema/016-v1-evidence-ref-multi-item.sql +19 -0
  35. package/schema/017-v1-memory-record-embeddings.sql +25 -0
  36. package/schema/018-v1-finalization-candidate-envelope.sql +39 -0
  37. package/scripts/codex-checkpoint-commands.js +464 -0
  38. package/scripts/codex-checkpoint-runtime.js +520 -0
  39. package/scripts/codex-recovery.js +105 -0
@@ -0,0 +1,546 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+
5
+ function qi(identifier) { return `"${identifier}"`; }
6
+
7
+ const CHECKPOINT_RUN_STATUSES = new Set([
8
+ 'pending',
9
+ 'processing',
10
+ 'finalized',
11
+ 'failed',
12
+ 'skipped',
13
+ ]);
14
+ const CHECKPOINT_RUN_TERMINAL_STATUSES = new Set(['finalized', 'skipped']);
15
+
16
+ function requireField(obj, field) {
17
+ if (!obj || obj[field] === undefined || obj[field] === null || obj[field] === '') {
18
+ throw new Error(`${field} is required`);
19
+ }
20
+ }
21
+
22
+ function toJson(value, fallback) {
23
+ return JSON.stringify(value === undefined ? fallback : value);
24
+ }
25
+
26
+ function normalizeCheckpointRunStatus(status) {
27
+ const out = status || 'pending';
28
+ if (!CHECKPOINT_RUN_STATUSES.has(out)) throw new Error(`Invalid checkpoint run status: ${out}`);
29
+ return out;
30
+ }
31
+
32
+ function checkpointRunTerminalSql(tableName) {
33
+ return `${tableName}.status IN (${[...CHECKPOINT_RUN_TERMINAL_STATUSES].map(value => `'${value}'`).join(',')})`;
34
+ }
35
+
36
+ function normalizeNonNegativeInteger(value, field) {
37
+ if (value === undefined || value === null) return null;
38
+ const out = Number(value);
39
+ if (!Number.isInteger(out) || out < 0) {
40
+ throw new Error(`${field} must be a non-negative integer`);
41
+ }
42
+ return out;
43
+ }
44
+
45
+ function normalizePositiveInteger(value, field) {
46
+ if (value === undefined || value === null) return null;
47
+ const out = Number(value);
48
+ if (!Number.isInteger(out) || out <= 0) {
49
+ throw new Error(`${field} must be a positive integer`);
50
+ }
51
+ return out;
52
+ }
53
+
54
+ function checkpointRunRange(input = {}) {
55
+ const from = normalizeNonNegativeInteger(
56
+ input.fromFinalizationIdExclusive ?? input.from_finalization_id_exclusive,
57
+ 'fromFinalizationIdExclusive'
58
+ );
59
+ const to = normalizePositiveInteger(
60
+ input.toFinalizationIdInclusive ?? input.to_finalization_id_inclusive,
61
+ 'toFinalizationIdInclusive'
62
+ );
63
+ if (from !== null && to !== null && to <= from) {
64
+ throw new Error('toFinalizationIdInclusive must be greater than fromFinalizationIdExclusive');
65
+ }
66
+ return {
67
+ from: from === null ? 0 : from,
68
+ to,
69
+ };
70
+ }
71
+
72
+ function checkpointRunRangesEqual(left, right) {
73
+ return left && right
74
+ && left.from === right.from
75
+ && left.to === right.to;
76
+ }
77
+
78
+ function advisoryLockKeys(namespace, value) {
79
+ const digest = crypto.createHash('sha256').update(`${namespace}:${value}`).digest();
80
+ return [digest.readInt32BE(0), digest.readInt32BE(4)];
81
+ }
82
+
83
+ async function withTransaction(queryable, fn) {
84
+ if (!queryable || typeof queryable.connect !== 'function') {
85
+ return fn(queryable);
86
+ }
87
+ const client = await queryable.connect();
88
+ try {
89
+ await client.query('BEGIN');
90
+ const out = await fn(client);
91
+ await client.query('COMMIT');
92
+ return out;
93
+ } catch (err) {
94
+ try {
95
+ await client.query('ROLLBACK');
96
+ } catch {
97
+ // Ignore rollback failure and surface the original error.
98
+ }
99
+ throw err;
100
+ } finally {
101
+ if (typeof client.release === 'function') client.release();
102
+ }
103
+ }
104
+
105
+ async function lockCheckpointRunScope(queryable, tenantId, scopeId) {
106
+ const [key1, key2] = advisoryLockKeys(
107
+ 'aquifer.checkpoint_runs.scope',
108
+ `${tenantId}:${scopeId}`,
109
+ );
110
+ await queryable.query('SELECT pg_advisory_xact_lock($1, $2)', [key1, key2]);
111
+ }
112
+
113
+ function defaultCheckpointKey(scopeId, range) {
114
+ if (range.to === null) return null;
115
+ return `scope:${scopeId}:finalization:${range.from}-${range.to}`;
116
+ }
117
+
118
+ function checkpointRunRowRange(row = {}) {
119
+ return checkpointRunRange({
120
+ fromFinalizationIdExclusive: row.from_finalization_id_exclusive,
121
+ toFinalizationIdInclusive: row.to_finalization_id_inclusive,
122
+ });
123
+ }
124
+
125
+ function checkpointRunIsTerminal(row = {}) {
126
+ return CHECKPOINT_RUN_TERMINAL_STATUSES.has(row.status);
127
+ }
128
+
129
+ async function getCheckpointRunByKey(pool, input = {}, { schema, tenantId }) {
130
+ const scopeId = input.scopeId || input.scope_id;
131
+ const checkpointKey = input.checkpointKey || input.checkpoint_key;
132
+ if (!scopeId || !checkpointKey) return null;
133
+ const result = await pool.query(
134
+ `SELECT *
135
+ FROM ${qi(schema)}.checkpoint_runs
136
+ WHERE tenant_id = $1
137
+ AND scope_id = $2
138
+ AND checkpoint_key = $3
139
+ LIMIT 1`,
140
+ [tenantId, scopeId, checkpointKey]
141
+ );
142
+ return result.rows[0] || null;
143
+ }
144
+
145
+ async function getCheckpointRunById(pool, input = {}, { schema, tenantId }) {
146
+ if (!input.id) return null;
147
+ const result = await pool.query(
148
+ `SELECT *
149
+ FROM ${qi(schema)}.checkpoint_runs
150
+ WHERE tenant_id = $1
151
+ AND id = $2
152
+ LIMIT 1`,
153
+ [tenantId, input.id]
154
+ );
155
+ return result.rows[0] || null;
156
+ }
157
+
158
+ async function getCheckpointRunByExactRange(pool, input = {}, { schema, tenantId }) {
159
+ const scopeId = input.scopeId || input.scope_id;
160
+ const range = checkpointRunRange(input);
161
+ if (!scopeId || range.to === null) return null;
162
+ const result = await pool.query(
163
+ `SELECT *
164
+ FROM ${qi(schema)}.checkpoint_runs
165
+ WHERE tenant_id = $1
166
+ AND scope_id = $2
167
+ AND from_finalization_id_exclusive = $3
168
+ AND to_finalization_id_inclusive = $4
169
+ LIMIT 1`,
170
+ [tenantId, scopeId, range.from, range.to]
171
+ );
172
+ return result.rows[0] || null;
173
+ }
174
+
175
+ async function assertNoCheckpointRangeOverlap(pool, input = {}, { schema, tenantId }) {
176
+ const scopeId = input.scopeId || input.scope_id;
177
+ const range = checkpointRunRange(input);
178
+ if (range.to === null) return;
179
+ const params = [tenantId, scopeId, range.from, range.to];
180
+ const where = [
181
+ 'tenant_id = $1',
182
+ 'scope_id = $2',
183
+ "status IN ('processing','finalized')",
184
+ 'to_finalization_id_inclusive IS NOT NULL',
185
+ 'from_finalization_id_exclusive < $4',
186
+ 'to_finalization_id_inclusive > $3',
187
+ ];
188
+ if (input.id) {
189
+ params.push(input.id);
190
+ where.push(`id <> $${params.length}`);
191
+ }
192
+ if (input.checkpointKey || input.checkpoint_key) {
193
+ params.push(input.checkpointKey || input.checkpoint_key);
194
+ where.push(`checkpoint_key <> $${params.length}`);
195
+ }
196
+ const result = await pool.query(
197
+ `SELECT id, checkpoint_key
198
+ FROM ${qi(schema)}.checkpoint_runs
199
+ WHERE ${where.join(' AND ')}
200
+ LIMIT 1`,
201
+ params
202
+ );
203
+ if (result.rows && result.rows.length > 0) {
204
+ const existing = result.rows[0];
205
+ throw new Error(`checkpoint range overlaps existing run ${existing.id || existing.checkpoint_key}`);
206
+ }
207
+ }
208
+
209
+ async function upsertCheckpointRun(pool, input = {}, { schema, tenantId: defaultTenantId } = {}) {
210
+ const scopeId = input.scopeId || input.scope_id;
211
+ requireField({ scopeId }, 'scopeId');
212
+ const range = checkpointRunRange(input);
213
+ const requestedCheckpointKey = input.checkpointKey || input.checkpoint_key || defaultCheckpointKey(scopeId, range);
214
+ requireField({ checkpointKey: requestedCheckpointKey }, 'checkpointKey');
215
+ const tenantId = input.tenantId || defaultTenantId || 'default';
216
+ const status = normalizeCheckpointRunStatus(input.status || 'pending');
217
+ return withTransaction(pool, async (queryable) => {
218
+ await lockCheckpointRunScope(queryable, tenantId, scopeId);
219
+ const existingByKey = await getCheckpointRunByKey(queryable, {
220
+ scopeId,
221
+ checkpointKey: requestedCheckpointKey,
222
+ }, { schema, tenantId });
223
+ const existingByRange = await getCheckpointRunByExactRange(queryable, {
224
+ scopeId,
225
+ fromFinalizationIdExclusive: range.from,
226
+ toFinalizationIdInclusive: range.to,
227
+ }, { schema, tenantId });
228
+ if (existingByKey && existingByRange && existingByKey.id !== existingByRange.id) {
229
+ throw new Error(`checkpointKey ${requestedCheckpointKey} already maps to a different checkpoint run`);
230
+ }
231
+ const targetRow = existingByRange || existingByKey || null;
232
+ if (targetRow && checkpointRunIsTerminal(targetRow)
233
+ && !checkpointRunRangesEqual(checkpointRunRowRange(targetRow), range)) {
234
+ throw new Error(`checkpoint run ${targetRow.id || targetRow.checkpoint_key} is terminal and cannot change finalization range`);
235
+ }
236
+ const checkpointKey = targetRow ? targetRow.checkpoint_key : requestedCheckpointKey;
237
+ await assertNoCheckpointRangeOverlap(queryable, {
238
+ ...input,
239
+ id: targetRow ? targetRow.id : input.id,
240
+ scopeId,
241
+ checkpointKey,
242
+ fromFinalizationIdExclusive: range.from,
243
+ toFinalizationIdInclusive: range.to,
244
+ }, { schema, tenantId });
245
+ const preserveTerminal = `${checkpointRunTerminalSql(qi(schema) + '.checkpoint_runs')}
246
+ AND ${qi(schema)}.checkpoint_runs.status <> EXCLUDED.status`;
247
+ const result = await queryable.query(
248
+ `INSERT INTO ${qi(schema)}.checkpoint_runs (
249
+ tenant_id, scope_id, checkpoint_key, from_finalization_id_exclusive,
250
+ to_finalization_id_inclusive, status, window_start, window_end,
251
+ scope_snapshot, checkpoint_text, checkpoint_payload, error,
252
+ metadata, claimed_at, finalized_at
253
+ )
254
+ VALUES (
255
+ $1,$2,$3,$4,$5,$6,$7,$8,COALESCE($9::jsonb,'{}'::jsonb),$10,
256
+ COALESCE($11::jsonb,'{}'::jsonb),$12,COALESCE($13::jsonb,'{}'::jsonb),$14,$15
257
+ )
258
+ ON CONFLICT (tenant_id, scope_id, checkpoint_key)
259
+ DO UPDATE SET
260
+ status = CASE
261
+ WHEN ${preserveTerminal}
262
+ THEN ${qi(schema)}.checkpoint_runs.status
263
+ ELSE EXCLUDED.status
264
+ END,
265
+ from_finalization_id_exclusive = CASE
266
+ WHEN ${preserveTerminal}
267
+ THEN ${qi(schema)}.checkpoint_runs.from_finalization_id_exclusive
268
+ ELSE EXCLUDED.from_finalization_id_exclusive
269
+ END,
270
+ to_finalization_id_inclusive = CASE
271
+ WHEN ${preserveTerminal}
272
+ THEN ${qi(schema)}.checkpoint_runs.to_finalization_id_inclusive
273
+ ELSE COALESCE(EXCLUDED.to_finalization_id_inclusive, ${qi(schema)}.checkpoint_runs.to_finalization_id_inclusive)
274
+ END,
275
+ window_start = CASE
276
+ WHEN ${preserveTerminal}
277
+ THEN ${qi(schema)}.checkpoint_runs.window_start
278
+ ELSE COALESCE(EXCLUDED.window_start, ${qi(schema)}.checkpoint_runs.window_start)
279
+ END,
280
+ window_end = CASE
281
+ WHEN ${preserveTerminal}
282
+ THEN ${qi(schema)}.checkpoint_runs.window_end
283
+ ELSE COALESCE(EXCLUDED.window_end, ${qi(schema)}.checkpoint_runs.window_end)
284
+ END,
285
+ scope_snapshot = CASE
286
+ WHEN ${preserveTerminal}
287
+ THEN ${qi(schema)}.checkpoint_runs.scope_snapshot
288
+ ELSE COALESCE(NULLIF(EXCLUDED.scope_snapshot, '{}'::jsonb), ${qi(schema)}.checkpoint_runs.scope_snapshot)
289
+ END,
290
+ checkpoint_text = CASE
291
+ WHEN ${preserveTerminal}
292
+ THEN ${qi(schema)}.checkpoint_runs.checkpoint_text
293
+ ELSE COALESCE(EXCLUDED.checkpoint_text, ${qi(schema)}.checkpoint_runs.checkpoint_text)
294
+ END,
295
+ checkpoint_payload = CASE
296
+ WHEN ${preserveTerminal}
297
+ THEN ${qi(schema)}.checkpoint_runs.checkpoint_payload
298
+ ELSE COALESCE(NULLIF(EXCLUDED.checkpoint_payload, '{}'::jsonb), ${qi(schema)}.checkpoint_runs.checkpoint_payload)
299
+ END,
300
+ error = CASE
301
+ WHEN ${preserveTerminal}
302
+ THEN ${qi(schema)}.checkpoint_runs.error
303
+ ELSE EXCLUDED.error
304
+ END,
305
+ metadata = CASE
306
+ WHEN ${preserveTerminal}
307
+ THEN ${qi(schema)}.checkpoint_runs.metadata
308
+ ELSE COALESCE(NULLIF(EXCLUDED.metadata, '{}'::jsonb), ${qi(schema)}.checkpoint_runs.metadata)
309
+ END,
310
+ claimed_at = CASE
311
+ WHEN ${preserveTerminal}
312
+ THEN ${qi(schema)}.checkpoint_runs.claimed_at
313
+ ELSE COALESCE(EXCLUDED.claimed_at, ${qi(schema)}.checkpoint_runs.claimed_at)
314
+ END,
315
+ finalized_at = CASE
316
+ WHEN ${preserveTerminal}
317
+ THEN ${qi(schema)}.checkpoint_runs.finalized_at
318
+ WHEN EXCLUDED.status = 'finalized'
319
+ THEN COALESCE(EXCLUDED.finalized_at, ${qi(schema)}.checkpoint_runs.finalized_at, now())
320
+ ELSE COALESCE(EXCLUDED.finalized_at, ${qi(schema)}.checkpoint_runs.finalized_at)
321
+ END,
322
+ updated_at = CASE
323
+ WHEN ${preserveTerminal}
324
+ THEN ${qi(schema)}.checkpoint_runs.updated_at
325
+ ELSE now()
326
+ END
327
+ RETURNING *`,
328
+ [
329
+ tenantId,
330
+ scopeId,
331
+ checkpointKey,
332
+ range.from,
333
+ range.to,
334
+ status,
335
+ input.windowStart || input.window_start || null,
336
+ input.windowEnd || input.window_end || null,
337
+ toJson(input.scopeSnapshot || input.scope_snapshot, {}),
338
+ input.checkpointText || input.checkpoint_text || null,
339
+ toJson(input.checkpointPayload || input.checkpoint_payload, {}),
340
+ input.error || null,
341
+ toJson(input.metadata, {}),
342
+ input.claimedAt || input.claimed_at || (status === 'processing' ? new Date().toISOString() : null),
343
+ input.finalizedAt || input.finalized_at || (status === 'finalized' ? new Date().toISOString() : null),
344
+ ]
345
+ );
346
+ return result.rows[0] || null;
347
+ });
348
+ }
349
+
350
+ async function updateCheckpointRunStatus(pool, input = {}, { schema, tenantId: defaultTenantId } = {}) {
351
+ const tenantId = input.tenantId || defaultTenantId || 'default';
352
+ const status = normalizeCheckpointRunStatus(input.status);
353
+ return withTransaction(pool, async (queryable) => {
354
+ const existing = input.id
355
+ ? await getCheckpointRunById(queryable, input, { schema, tenantId })
356
+ : await getCheckpointRunByKey(queryable, input, { schema, tenantId });
357
+ if (!existing) return null;
358
+ await lockCheckpointRunScope(queryable, tenantId, existing.scope_id);
359
+ const current = await getCheckpointRunById(queryable, { id: existing.id }, { schema, tenantId }) || existing;
360
+ const currentRange = checkpointRunRowRange(current);
361
+ const nextRange = checkpointRunRange({
362
+ fromFinalizationIdExclusive: (
363
+ input.fromFinalizationIdExclusive ?? input.from_finalization_id_exclusive ?? currentRange.from
364
+ ),
365
+ toFinalizationIdInclusive: (
366
+ input.toFinalizationIdInclusive ?? input.to_finalization_id_inclusive ?? currentRange.to
367
+ ),
368
+ });
369
+ if (checkpointRunIsTerminal(current) && !checkpointRunRangesEqual(currentRange, nextRange)) {
370
+ throw new Error(`checkpoint run ${current.id || current.checkpoint_key} is terminal and cannot change finalization range`);
371
+ }
372
+ if ((status === 'processing' || status === 'finalized') && nextRange.to !== null) {
373
+ await assertNoCheckpointRangeOverlap(queryable, {
374
+ id: current.id,
375
+ scopeId: current.scope_id,
376
+ checkpointKey: current.checkpoint_key,
377
+ fromFinalizationIdExclusive: nextRange.from,
378
+ toFinalizationIdInclusive: nextRange.to,
379
+ }, { schema, tenantId });
380
+ }
381
+ const params = [
382
+ tenantId,
383
+ status,
384
+ nextRange.from,
385
+ nextRange.to,
386
+ input.error || null,
387
+ input.checkpointText || input.checkpoint_text || null,
388
+ toJson(input.checkpointPayload || input.checkpoint_payload, {}),
389
+ toJson(input.metadata, {}),
390
+ input.claimedAt || input.claimed_at || (status === 'processing' ? new Date().toISOString() : null),
391
+ input.finalizedAt || input.finalized_at || (status === 'finalized' ? new Date().toISOString() : null),
392
+ current.id,
393
+ ];
394
+ const result = await queryable.query(
395
+ `UPDATE ${qi(schema)}.checkpoint_runs
396
+ SET status = $2,
397
+ from_finalization_id_exclusive = $3,
398
+ to_finalization_id_inclusive = $4,
399
+ error = $5,
400
+ checkpoint_text = COALESCE($6, checkpoint_text),
401
+ checkpoint_payload = COALESCE(NULLIF($7::jsonb, '{}'::jsonb), checkpoint_payload),
402
+ metadata = COALESCE(NULLIF($8::jsonb, '{}'::jsonb), metadata),
403
+ claimed_at = CASE WHEN $2 = 'processing' THEN COALESCE(claimed_at, $9::timestamptz, now()) ELSE claimed_at END,
404
+ finalized_at = CASE WHEN $2 = 'finalized' THEN COALESCE(finalized_at, $10::timestamptz, now()) ELSE finalized_at END,
405
+ updated_at = now()
406
+ WHERE tenant_id = $1
407
+ AND id = $11
408
+ AND (
409
+ status NOT IN (${[...CHECKPOINT_RUN_TERMINAL_STATUSES].map(value => `'${value}'`).join(',')})
410
+ OR status = $2
411
+ )
412
+ RETURNING *`,
413
+ params
414
+ );
415
+ return result.rows[0] || null;
416
+ });
417
+ }
418
+
419
+ async function listCheckpointRuns(pool, input = {}, { schema, tenantId: defaultTenantId } = {}) {
420
+ const tenantId = input.tenantId || defaultTenantId || 'default';
421
+ const params = [tenantId];
422
+ const where = ['tenant_id = $1'];
423
+ if (input.scopeId || input.scope_id) {
424
+ params.push(input.scopeId || input.scope_id);
425
+ where.push(`scope_id = $${params.length}`);
426
+ }
427
+ if (input.status) {
428
+ const statuses = Array.isArray(input.status) ? input.status : [input.status];
429
+ for (const status of statuses) normalizeCheckpointRunStatus(status);
430
+ params.push(statuses);
431
+ where.push(`status = ANY($${params.length}::text[])`);
432
+ }
433
+ if (input.checkpointKey || input.checkpoint_key) {
434
+ params.push(input.checkpointKey || input.checkpoint_key);
435
+ where.push(`checkpoint_key = $${params.length}`);
436
+ }
437
+ if (input.id) {
438
+ params.push(input.id);
439
+ where.push(`id = $${params.length}`);
440
+ }
441
+ params.push(Math.max(1, Math.min(200, input.limit || 50)));
442
+ const result = await pool.query(
443
+ `SELECT *
444
+ FROM ${qi(schema)}.checkpoint_runs
445
+ WHERE ${where.join(' AND ')}
446
+ ORDER BY updated_at DESC, id DESC
447
+ LIMIT $${params.length}`,
448
+ params
449
+ );
450
+ return result.rows;
451
+ }
452
+
453
+ async function upsertCheckpointRunSources(pool, rows = [], input = {}, { schema, tenantId: defaultTenantId } = {}) {
454
+ if (!Array.isArray(rows) || rows.length === 0) return [];
455
+ const checkpointRunId = input.checkpointRunId || input.checkpoint_run_id;
456
+ requireField({ checkpointRunId }, 'checkpointRunId');
457
+ const tenantId = input.tenantId || defaultTenantId || 'default';
458
+ const out = [];
459
+ for (let i = 0; i < rows.length; i++) {
460
+ const row = rows[i] || {};
461
+ const finalization = row.finalization || {};
462
+ const finalizationId = row.finalizationId || row.finalization_id || finalization.id;
463
+ requireField({ finalizationId }, 'finalizationId');
464
+ const sourceIndex = normalizeNonNegativeInteger(
465
+ row.sourceIndex !== undefined ? row.sourceIndex : (
466
+ row.source_index !== undefined ? row.source_index : i
467
+ ),
468
+ 'sourceIndex'
469
+ );
470
+ const result = await pool.query(
471
+ `INSERT INTO ${qi(schema)}.checkpoint_run_sources (
472
+ tenant_id, checkpoint_run_id, finalization_id, source_index, scope_id,
473
+ scope_snapshot, session_row_id, session_id, transcript_hash,
474
+ summary_row_id, finalized_at, metadata
475
+ )
476
+ VALUES (
477
+ $1,$2,$3,$4,$5,COALESCE($6::jsonb,'{}'::jsonb),$7,$8,$9,$10,$11,COALESCE($12::jsonb,'{}'::jsonb)
478
+ )
479
+ ON CONFLICT (tenant_id, checkpoint_run_id, finalization_id)
480
+ DO UPDATE SET
481
+ source_index = EXCLUDED.source_index,
482
+ scope_id = COALESCE(EXCLUDED.scope_id, ${qi(schema)}.checkpoint_run_sources.scope_id),
483
+ scope_snapshot = COALESCE(NULLIF(EXCLUDED.scope_snapshot, '{}'::jsonb), ${qi(schema)}.checkpoint_run_sources.scope_snapshot),
484
+ session_row_id = COALESCE(EXCLUDED.session_row_id, ${qi(schema)}.checkpoint_run_sources.session_row_id),
485
+ session_id = COALESCE(EXCLUDED.session_id, ${qi(schema)}.checkpoint_run_sources.session_id),
486
+ transcript_hash = COALESCE(EXCLUDED.transcript_hash, ${qi(schema)}.checkpoint_run_sources.transcript_hash),
487
+ summary_row_id = COALESCE(EXCLUDED.summary_row_id, ${qi(schema)}.checkpoint_run_sources.summary_row_id),
488
+ finalized_at = COALESCE(EXCLUDED.finalized_at, ${qi(schema)}.checkpoint_run_sources.finalized_at),
489
+ metadata = COALESCE(NULLIF(EXCLUDED.metadata, '{}'::jsonb), ${qi(schema)}.checkpoint_run_sources.metadata),
490
+ updated_at = now()
491
+ RETURNING *`,
492
+ [
493
+ tenantId,
494
+ checkpointRunId,
495
+ finalizationId,
496
+ sourceIndex,
497
+ row.scopeId || row.scope_id || finalization.scopeId || finalization.scope_id || null,
498
+ toJson(row.scopeSnapshot || row.scope_snapshot || finalization.scopeSnapshot || finalization.scope_snapshot, {}),
499
+ row.sessionRowId || row.session_row_id || finalization.sessionRowId || finalization.session_row_id || null,
500
+ row.sessionId || row.session_id || finalization.sessionId || finalization.session_id || null,
501
+ row.transcriptHash || row.transcript_hash || finalization.transcriptHash || finalization.transcript_hash || null,
502
+ row.summaryRowId || row.summary_row_id || finalization.summaryRowId || finalization.summary_row_id || null,
503
+ row.finalizedAt || row.finalized_at || finalization.finalizedAt || finalization.finalized_at || null,
504
+ toJson(row.metadata, {}),
505
+ ]
506
+ );
507
+ out.push(result.rows[0] || null);
508
+ }
509
+ return out;
510
+ }
511
+
512
+ async function listCheckpointRunSources(pool, input = {}, { schema, tenantId: defaultTenantId } = {}) {
513
+ const tenantId = input.tenantId || defaultTenantId || 'default';
514
+ const params = [tenantId];
515
+ const where = ['tenant_id = $1'];
516
+ if (input.checkpointRunId || input.checkpoint_run_id) {
517
+ params.push(input.checkpointRunId || input.checkpoint_run_id);
518
+ where.push(`checkpoint_run_id = $${params.length}`);
519
+ }
520
+ if (input.finalizationId || input.finalization_id) {
521
+ params.push(input.finalizationId || input.finalization_id);
522
+ where.push(`finalization_id = $${params.length}`);
523
+ }
524
+ if (input.scopeId || input.scope_id) {
525
+ params.push(input.scopeId || input.scope_id);
526
+ where.push(`scope_id = $${params.length}`);
527
+ }
528
+ params.push(Math.max(1, Math.min(500, input.limit || 200)));
529
+ const result = await pool.query(
530
+ `SELECT *
531
+ FROM ${qi(schema)}.checkpoint_run_sources
532
+ WHERE ${where.join(' AND ')}
533
+ ORDER BY checkpoint_run_id DESC, source_index ASC, id ASC
534
+ LIMIT $${params.length}`,
535
+ params
536
+ );
537
+ return result.rows;
538
+ }
539
+
540
+ module.exports = {
541
+ upsertCheckpointRun,
542
+ updateCheckpointRunStatus,
543
+ listCheckpointRuns,
544
+ upsertCheckpointRunSources,
545
+ listCheckpointRunSources,
546
+ };