@shadowforge0/aquifer-memory 1.7.0 → 1.8.1
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.
- package/.env.example +8 -0
- package/README.md +66 -0
- package/aquifer.config.example.json +19 -0
- package/consumers/cli.js +217 -14
- package/consumers/codex-active-checkpoint.js +186 -0
- package/consumers/codex-current-memory.js +106 -0
- package/consumers/codex-handoff.js +442 -3
- package/consumers/codex.js +164 -107
- package/consumers/mcp.js +144 -6
- package/consumers/shared/config.js +60 -1
- package/consumers/shared/factory.js +10 -3
- package/core/aquifer.js +351 -840
- package/core/backends/capabilities.js +89 -0
- package/core/backends/local.js +430 -0
- package/core/legacy-bootstrap.js +140 -0
- package/core/mcp-manifest.js +66 -2
- package/core/memory-promotion.js +157 -26
- package/core/memory-recall.js +341 -22
- package/core/memory-records.js +128 -8
- package/core/memory-serving.js +132 -0
- package/core/postgres-migrations.js +533 -0
- package/core/public-session-filter.js +40 -0
- package/core/recall-runtime.js +115 -0
- package/core/scope-attribution.js +279 -0
- package/core/session-checkpoint-producer.js +412 -0
- package/core/session-checkpoints.js +432 -0
- package/core/session-finalization.js +82 -1
- package/core/storage-checkpoints.js +546 -0
- package/core/storage.js +121 -8
- package/docs/setup.md +22 -0
- package/package.json +8 -4
- package/schema/014-v1-checkpoint-runs.sql +349 -0
- package/schema/015-v1-evidence-items.sql +92 -0
- package/schema/016-v1-evidence-ref-multi-item.sql +19 -0
- package/schema/017-v1-memory-record-embeddings.sql +25 -0
- package/schema/018-v1-finalization-candidate-envelope.sql +39 -0
- package/scripts/codex-checkpoint-commands.js +464 -0
- package/scripts/codex-checkpoint-runtime.js +520 -0
- 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
|
+
};
|