@axiom-lattice/pg-stores 3.3.13 → 3.4.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.
- package/.turbo/turbo-build.log +10 -10
- package/CHANGELOG.md +170 -0
- package/dist/index.d.mts +353 -3
- package/dist/index.d.ts +353 -3
- package/dist/index.js +2288 -83
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2294 -82
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/AgentRunRetention.migrations.test.ts +41 -0
- package/src/__tests__/PostgreSQLAgentRunLedger.test.ts +305 -0
- package/src/__tests__/PostgreSQLAgentRunRetention.test.ts +126 -0
- package/src/__tests__/PostgreSQLAgentRunTaskQueue.test.ts +1375 -0
- package/src/__tests__/PostgreSQLChannelReplyJobStore.test.ts +333 -0
- package/src/__tests__/PostgreSQLCheckpointStorage.deleteAfter.test.ts +473 -0
- package/src/__tests__/PostgreSQLCheckpointStorage.test.ts +538 -0
- package/src/__tests__/PostgreSQLStore.test.ts +173 -0
- package/src/__tests__/PostgreSQLThreadStore.test.ts +80 -0
- package/src/__tests__/ThreadMessageQueueStore.integration.test.ts +21 -0
- package/src/__tests__/ThreadMessageQueueStore.test.ts +8 -3
- package/src/__tests__/createPgStoreConfig.test.ts +38 -0
- package/src/createPgStoreConfig.ts +35 -2
- package/src/index.ts +7 -0
- package/src/migrations/agent_run_base_checkpoint_captured_migration.ts +34 -0
- package/src/migrations/agent_run_capability_revision_migration.ts +24 -0
- package/src/migrations/agent_run_ledger_migration.ts +51 -0
- package/src/migrations/agent_run_project_task_migration.ts +34 -0
- package/src/migrations/agent_run_project_task_running_index_migration.ts +33 -0
- package/src/migrations/agent_run_retention_migration.ts +25 -0
- package/src/migrations/agent_run_rollback_failure_migration.ts +28 -0
- package/src/migrations/agent_run_running_index_source_migration.ts +31 -0
- package/src/migrations/agent_run_task_queue_migration.ts +65 -0
- package/src/migrations/channel_reply_job_migration.ts +55 -0
- package/src/migrations/store_migrations.ts +50 -0
- package/src/migrations/thread_migrations.ts +25 -0
- package/src/stores/PostgreSQLAgentRunLedger.ts +1210 -0
- package/src/stores/PostgreSQLChannelReplyJobStore.ts +373 -0
- package/src/stores/PostgreSQLCheckpointStorage.ts +548 -0
- package/src/stores/PostgreSQLStore.ts +167 -0
- package/src/stores/PostgreSQLThreadStore.ts +49 -10
- package/src/stores/ThreadMessageQueueStore.ts +10 -5
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "@jest/globals";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { Pool } from "pg";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
CheckpointBaseNotFoundError,
|
|
7
|
+
PostgreSQLCheckpointStorage,
|
|
8
|
+
} from "../stores/PostgreSQLCheckpointStorage";
|
|
9
|
+
|
|
10
|
+
const TEST_DATABASE_URL = process.env.TEST_DATABASE_URL;
|
|
11
|
+
const maybeDescribe = TEST_DATABASE_URL ? describe : describe.skip;
|
|
12
|
+
|
|
13
|
+
interface CheckpointSeed {
|
|
14
|
+
threadId: string;
|
|
15
|
+
checkpointNs: string;
|
|
16
|
+
checkpointId: string;
|
|
17
|
+
channelVersions: Record<string, string>;
|
|
18
|
+
parentCheckpointId?: string | null;
|
|
19
|
+
metadata?: Record<string, unknown>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface BlobSeed {
|
|
23
|
+
threadId: string;
|
|
24
|
+
checkpointNs: string;
|
|
25
|
+
channel: string;
|
|
26
|
+
version: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface WriteSeed {
|
|
30
|
+
threadId: string;
|
|
31
|
+
checkpointNs: string;
|
|
32
|
+
checkpointId: string;
|
|
33
|
+
taskId: string;
|
|
34
|
+
idx: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
maybeDescribe("PostgreSQLCheckpointStorage.deleteAfter (PostgreSQL)", () => {
|
|
38
|
+
const threadId = randomUUID();
|
|
39
|
+
let pool: Pool;
|
|
40
|
+
let store: PostgreSQLCheckpointStorage;
|
|
41
|
+
|
|
42
|
+
beforeAll(async () => {
|
|
43
|
+
pool = new Pool({ connectionString: TEST_DATABASE_URL! });
|
|
44
|
+
// Both checkpoint suites create the same schema and Jest runs test files
|
|
45
|
+
// in parallel. Serialize the DDL so CREATE TABLE IF NOT EXISTS cannot race
|
|
46
|
+
// on pg_type.
|
|
47
|
+
const client = await pool.connect();
|
|
48
|
+
try {
|
|
49
|
+
await client.query("SELECT pg_advisory_lock(94711231)");
|
|
50
|
+
await client.query(`
|
|
51
|
+
CREATE TABLE IF NOT EXISTS checkpoints (
|
|
52
|
+
thread_id TEXT NOT NULL,
|
|
53
|
+
checkpoint_ns TEXT NOT NULL DEFAULT '',
|
|
54
|
+
checkpoint_id TEXT NOT NULL,
|
|
55
|
+
parent_checkpoint_id TEXT,
|
|
56
|
+
type TEXT,
|
|
57
|
+
checkpoint JSONB NOT NULL,
|
|
58
|
+
metadata JSONB NOT NULL DEFAULT '{}',
|
|
59
|
+
PRIMARY KEY (thread_id, checkpoint_ns, checkpoint_id)
|
|
60
|
+
);
|
|
61
|
+
CREATE TABLE IF NOT EXISTS checkpoint_blobs (
|
|
62
|
+
thread_id TEXT NOT NULL,
|
|
63
|
+
checkpoint_ns TEXT NOT NULL DEFAULT '',
|
|
64
|
+
channel TEXT NOT NULL,
|
|
65
|
+
version TEXT NOT NULL,
|
|
66
|
+
type TEXT NOT NULL,
|
|
67
|
+
blob BYTEA,
|
|
68
|
+
PRIMARY KEY (thread_id, checkpoint_ns, channel, version)
|
|
69
|
+
);
|
|
70
|
+
CREATE TABLE IF NOT EXISTS checkpoint_writes (
|
|
71
|
+
thread_id TEXT NOT NULL,
|
|
72
|
+
checkpoint_ns TEXT NOT NULL DEFAULT '',
|
|
73
|
+
checkpoint_id TEXT NOT NULL,
|
|
74
|
+
task_id TEXT NOT NULL,
|
|
75
|
+
idx INTEGER NOT NULL,
|
|
76
|
+
channel TEXT NOT NULL,
|
|
77
|
+
type TEXT,
|
|
78
|
+
blob BYTEA NOT NULL,
|
|
79
|
+
PRIMARY KEY (thread_id, checkpoint_ns, checkpoint_id, task_id, idx)
|
|
80
|
+
);
|
|
81
|
+
`);
|
|
82
|
+
} finally {
|
|
83
|
+
await client.query("SELECT pg_advisory_unlock(94711231)").catch(() => undefined);
|
|
84
|
+
client.release();
|
|
85
|
+
}
|
|
86
|
+
store = new PostgreSQLCheckpointStorage(pool);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
beforeEach(async () => {
|
|
90
|
+
await removeRows(threadId);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
afterAll(async () => {
|
|
94
|
+
await removeRows(threadId);
|
|
95
|
+
await pool.end();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
async function removeRows(id: string): Promise<void> {
|
|
99
|
+
await pool.query("DELETE FROM checkpoint_writes WHERE thread_id = $1", [id]);
|
|
100
|
+
await pool.query("DELETE FROM checkpoint_blobs WHERE thread_id = $1", [id]);
|
|
101
|
+
await pool.query("DELETE FROM checkpoints WHERE thread_id = $1", [id]);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function insertCheckpoints(rows: CheckpointSeed[]): Promise<void> {
|
|
105
|
+
for (const row of rows) {
|
|
106
|
+
await pool.query(
|
|
107
|
+
`INSERT INTO checkpoints (
|
|
108
|
+
thread_id, checkpoint_ns, checkpoint_id, parent_checkpoint_id, checkpoint, metadata
|
|
109
|
+
)
|
|
110
|
+
VALUES ($1, $2, $3, $4, $5::jsonb, $6::jsonb)`,
|
|
111
|
+
[
|
|
112
|
+
row.threadId,
|
|
113
|
+
row.checkpointNs,
|
|
114
|
+
row.checkpointId,
|
|
115
|
+
row.parentCheckpointId ?? null,
|
|
116
|
+
JSON.stringify({ v: 4, channel_versions: row.channelVersions }),
|
|
117
|
+
JSON.stringify(row.metadata ?? {}),
|
|
118
|
+
],
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function insertBlobs(rows: BlobSeed[]): Promise<void> {
|
|
124
|
+
for (const row of rows) {
|
|
125
|
+
await pool.query(
|
|
126
|
+
`INSERT INTO checkpoint_blobs (thread_id, checkpoint_ns, channel, version, type, blob)
|
|
127
|
+
VALUES ($1, $2, $3, $4, 'msgpack', $5)`,
|
|
128
|
+
[
|
|
129
|
+
row.threadId,
|
|
130
|
+
row.checkpointNs,
|
|
131
|
+
row.channel,
|
|
132
|
+
row.version,
|
|
133
|
+
Buffer.from(`${row.checkpointNs}:${row.channel}:${row.version}`),
|
|
134
|
+
],
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function insertWrites(rows: WriteSeed[]): Promise<void> {
|
|
140
|
+
for (const row of rows) {
|
|
141
|
+
await pool.query(
|
|
142
|
+
`INSERT INTO checkpoint_writes (thread_id, checkpoint_ns, checkpoint_id, task_id, idx, channel, type, blob)
|
|
143
|
+
VALUES ($1, $2, $3, $4, $5, 'messages', 'msgpack', $6)`,
|
|
144
|
+
[
|
|
145
|
+
row.threadId,
|
|
146
|
+
row.checkpointNs,
|
|
147
|
+
row.checkpointId,
|
|
148
|
+
row.taskId,
|
|
149
|
+
row.idx,
|
|
150
|
+
Buffer.from(`${row.checkpointNs}:${row.checkpointId}:${row.idx}`),
|
|
151
|
+
],
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function counts(id: string): Promise<{ checkpoints: number; blobs: number; writes: number }> {
|
|
157
|
+
const result = await pool.query(
|
|
158
|
+
`SELECT
|
|
159
|
+
(SELECT count(*)::int FROM checkpoints WHERE thread_id = $1) AS checkpoints,
|
|
160
|
+
(SELECT count(*)::int FROM checkpoint_blobs WHERE thread_id = $1) AS blobs,
|
|
161
|
+
(SELECT count(*)::int FROM checkpoint_writes WHERE thread_id = $1) AS writes`,
|
|
162
|
+
[id],
|
|
163
|
+
);
|
|
164
|
+
return result.rows[0] as { checkpoints: number; blobs: number; writes: number };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
it("deletes every checkpoint row when the base is null", async () => {
|
|
168
|
+
await insertCheckpoints([
|
|
169
|
+
{ threadId, checkpointNs: "", checkpointId: "cp-1", channelVersions: { messages: "1" } },
|
|
170
|
+
{ threadId, checkpointNs: "sub", checkpointId: "cp-2", channelVersions: { messages: "2" } },
|
|
171
|
+
]);
|
|
172
|
+
await insertBlobs([
|
|
173
|
+
{ threadId, checkpointNs: "", channel: "messages", version: "1" },
|
|
174
|
+
{ threadId, checkpointNs: "sub", channel: "messages", version: "2" },
|
|
175
|
+
]);
|
|
176
|
+
await insertWrites([
|
|
177
|
+
{ threadId, checkpointNs: "", checkpointId: "cp-1", taskId: "task-1", idx: 0 },
|
|
178
|
+
{ threadId, checkpointNs: "sub", checkpointId: "cp-2", taskId: "task-2", idx: 0 },
|
|
179
|
+
]);
|
|
180
|
+
|
|
181
|
+
await store.deleteAfter(threadId, null);
|
|
182
|
+
|
|
183
|
+
await expect(counts(threadId)).resolves.toEqual({ checkpoints: 0, blobs: 0, writes: 0 });
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("follows metadata.parents and same-namespace parent links across subgraph namespaces", async () => {
|
|
187
|
+
await insertCheckpoints([
|
|
188
|
+
{ threadId, checkpointNs: "", checkpointId: "cp-1", channelVersions: { messages: "1" } },
|
|
189
|
+
{ threadId, checkpointNs: "", checkpointId: "cp-2", parentCheckpointId: "cp-1", channelVersions: { messages: "2" } },
|
|
190
|
+
{ threadId, checkpointNs: "", checkpointId: "cp-3", parentCheckpointId: "cp-2", channelVersions: { messages: "3" } },
|
|
191
|
+
{
|
|
192
|
+
threadId,
|
|
193
|
+
checkpointNs: "sub",
|
|
194
|
+
checkpointId: "cp-1",
|
|
195
|
+
channelVersions: { messages: "sub-1" },
|
|
196
|
+
metadata: { parents: { "": "cp-1" } },
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
threadId,
|
|
200
|
+
checkpointNs: "sub",
|
|
201
|
+
checkpointId: "cp-4",
|
|
202
|
+
channelVersions: { messages: "sub-4" },
|
|
203
|
+
metadata: { parents: { "": "cp-2" } },
|
|
204
|
+
},
|
|
205
|
+
{ threadId, checkpointNs: "sub", checkpointId: "cp-5", parentCheckpointId: "cp-4", channelVersions: { messages: "sub-5" } },
|
|
206
|
+
]);
|
|
207
|
+
await insertBlobs([
|
|
208
|
+
{ threadId, checkpointNs: "", channel: "messages", version: "1" },
|
|
209
|
+
{ threadId, checkpointNs: "", channel: "messages", version: "2" },
|
|
210
|
+
{ threadId, checkpointNs: "", channel: "messages", version: "3" },
|
|
211
|
+
{ threadId, checkpointNs: "sub", channel: "messages", version: "sub-1" },
|
|
212
|
+
{ threadId, checkpointNs: "sub", channel: "messages", version: "sub-4" },
|
|
213
|
+
{ threadId, checkpointNs: "sub", channel: "messages", version: "sub-5" },
|
|
214
|
+
{ threadId, checkpointNs: "", channel: "messages", version: "orphan" },
|
|
215
|
+
]);
|
|
216
|
+
await insertWrites([
|
|
217
|
+
{ threadId, checkpointNs: "", checkpointId: "cp-1", taskId: "task-1", idx: 0 },
|
|
218
|
+
{ threadId, checkpointNs: "", checkpointId: "cp-2", taskId: "task-2", idx: 0 },
|
|
219
|
+
{ threadId, checkpointNs: "", checkpointId: "cp-3", taskId: "task-3", idx: 0 },
|
|
220
|
+
{ threadId, checkpointNs: "sub", checkpointId: "cp-1", taskId: "task-sub-1", idx: 0 },
|
|
221
|
+
{ threadId, checkpointNs: "sub", checkpointId: "cp-4", taskId: "task-sub-4", idx: 0 },
|
|
222
|
+
{ threadId, checkpointNs: "sub", checkpointId: "cp-5", taskId: "task-sub-5", idx: 0 },
|
|
223
|
+
]);
|
|
224
|
+
|
|
225
|
+
await store.deleteAfter(threadId, "cp-2");
|
|
226
|
+
|
|
227
|
+
const checkpoints = await pool.query(
|
|
228
|
+
"SELECT checkpoint_ns, checkpoint_id FROM checkpoints WHERE thread_id = $1 ORDER BY checkpoint_ns, checkpoint_id",
|
|
229
|
+
[threadId],
|
|
230
|
+
);
|
|
231
|
+
expect(checkpoints.rows).toEqual([
|
|
232
|
+
{ checkpoint_ns: "", checkpoint_id: "cp-1" },
|
|
233
|
+
{ checkpoint_ns: "", checkpoint_id: "cp-2" },
|
|
234
|
+
{ checkpoint_ns: "sub", checkpoint_id: "cp-1" },
|
|
235
|
+
]);
|
|
236
|
+
|
|
237
|
+
const blobs = await pool.query(
|
|
238
|
+
"SELECT checkpoint_ns, version FROM checkpoint_blobs WHERE thread_id = $1 ORDER BY checkpoint_ns, version",
|
|
239
|
+
[threadId],
|
|
240
|
+
);
|
|
241
|
+
expect(blobs.rows).toEqual([
|
|
242
|
+
{ checkpoint_ns: "", version: "1" },
|
|
243
|
+
{ checkpoint_ns: "", version: "2" },
|
|
244
|
+
{ checkpoint_ns: "sub", version: "sub-1" },
|
|
245
|
+
]);
|
|
246
|
+
|
|
247
|
+
const writes = await pool.query(
|
|
248
|
+
"SELECT checkpoint_ns, checkpoint_id FROM checkpoint_writes WHERE thread_id = $1 ORDER BY checkpoint_ns, checkpoint_id",
|
|
249
|
+
[threadId],
|
|
250
|
+
);
|
|
251
|
+
expect(writes.rows).toEqual([
|
|
252
|
+
{ checkpoint_ns: "", checkpoint_id: "cp-1" },
|
|
253
|
+
{ checkpoint_ns: "sub", checkpoint_id: "cp-1" },
|
|
254
|
+
]);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it("deletes a subgraph checkpoint whose metadata.parents points at the base", async () => {
|
|
258
|
+
await insertCheckpoints([
|
|
259
|
+
{ threadId, checkpointNs: "", checkpointId: "cp-1", channelVersions: {} },
|
|
260
|
+
{ threadId, checkpointNs: "", checkpointId: "cp-2", parentCheckpointId: "cp-1", channelVersions: {} },
|
|
261
|
+
{
|
|
262
|
+
threadId,
|
|
263
|
+
checkpointNs: "sub",
|
|
264
|
+
checkpointId: "sub-1",
|
|
265
|
+
channelVersions: {},
|
|
266
|
+
metadata: { parents: { "": "cp-2" } },
|
|
267
|
+
},
|
|
268
|
+
]);
|
|
269
|
+
|
|
270
|
+
await store.deleteAfter(threadId, "cp-2");
|
|
271
|
+
|
|
272
|
+
const checkpoints = await pool.query(
|
|
273
|
+
"SELECT checkpoint_ns, checkpoint_id FROM checkpoints WHERE thread_id = $1 ORDER BY checkpoint_ns, checkpoint_id",
|
|
274
|
+
[threadId],
|
|
275
|
+
);
|
|
276
|
+
expect(checkpoints.rows).toEqual([
|
|
277
|
+
{ checkpoint_ns: "", checkpoint_id: "cp-1" },
|
|
278
|
+
{ checkpoint_ns: "", checkpoint_id: "cp-2" },
|
|
279
|
+
]);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it("retains a subgraph checkpoint whose metadata.parents points at an earlier root", async () => {
|
|
283
|
+
await insertCheckpoints([
|
|
284
|
+
{ threadId, checkpointNs: "", checkpointId: "cp-1", channelVersions: {} },
|
|
285
|
+
{ threadId, checkpointNs: "", checkpointId: "cp-2", parentCheckpointId: "cp-1", channelVersions: {} },
|
|
286
|
+
{
|
|
287
|
+
threadId,
|
|
288
|
+
checkpointNs: "sub",
|
|
289
|
+
checkpointId: "sub-1",
|
|
290
|
+
channelVersions: {},
|
|
291
|
+
metadata: { parents: { "": "cp-1" } },
|
|
292
|
+
},
|
|
293
|
+
]);
|
|
294
|
+
|
|
295
|
+
await store.deleteAfter(threadId, "cp-2");
|
|
296
|
+
|
|
297
|
+
const checkpoints = await pool.query(
|
|
298
|
+
"SELECT checkpoint_ns, checkpoint_id FROM checkpoints WHERE thread_id = $1 ORDER BY checkpoint_ns, checkpoint_id",
|
|
299
|
+
[threadId],
|
|
300
|
+
);
|
|
301
|
+
expect(checkpoints.rows).toEqual([
|
|
302
|
+
{ checkpoint_ns: "", checkpoint_id: "cp-1" },
|
|
303
|
+
{ checkpoint_ns: "", checkpoint_id: "cp-2" },
|
|
304
|
+
{ checkpoint_ns: "sub", checkpoint_id: "sub-1" },
|
|
305
|
+
]);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it("deletes the rolled-back run's first-step write keyed to the base checkpoint", async () => {
|
|
309
|
+
await insertCheckpoints([
|
|
310
|
+
{ threadId, checkpointNs: "", checkpointId: "cp-1", channelVersions: {} },
|
|
311
|
+
{ threadId, checkpointNs: "", checkpointId: "cp-2", parentCheckpointId: "cp-1", channelVersions: {} },
|
|
312
|
+
]);
|
|
313
|
+
await insertWrites([
|
|
314
|
+
{ threadId, checkpointNs: "", checkpointId: "cp-1", taskId: "earlier-task", idx: 0 },
|
|
315
|
+
{ threadId, checkpointNs: "", checkpointId: "cp-2", taskId: "rolled-back-task", idx: 0 },
|
|
316
|
+
]);
|
|
317
|
+
|
|
318
|
+
await store.deleteAfter(threadId, "cp-2");
|
|
319
|
+
|
|
320
|
+
const writes = await pool.query(
|
|
321
|
+
"SELECT checkpoint_id, task_id FROM checkpoint_writes WHERE thread_id = $1 ORDER BY checkpoint_id",
|
|
322
|
+
[threadId],
|
|
323
|
+
);
|
|
324
|
+
expect(writes.rows).toEqual([{ checkpoint_id: "cp-1", task_id: "earlier-task" }]);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it("is idempotent when no descendants remain", async () => {
|
|
328
|
+
await insertCheckpoints([
|
|
329
|
+
{ threadId, checkpointNs: "", checkpointId: "cp-1", channelVersions: { messages: "1" } },
|
|
330
|
+
{ threadId, checkpointNs: "", checkpointId: "cp-2", parentCheckpointId: "cp-1", channelVersions: { messages: "2" } },
|
|
331
|
+
]);
|
|
332
|
+
await insertBlobs([
|
|
333
|
+
{ threadId, checkpointNs: "", channel: "messages", version: "1" },
|
|
334
|
+
{ threadId, checkpointNs: "", channel: "messages", version: "2" },
|
|
335
|
+
]);
|
|
336
|
+
await insertWrites([
|
|
337
|
+
{ threadId, checkpointNs: "", checkpointId: "cp-1", taskId: "task-1", idx: 0 },
|
|
338
|
+
{ threadId, checkpointNs: "", checkpointId: "cp-2", taskId: "task-2", idx: 0 },
|
|
339
|
+
]);
|
|
340
|
+
|
|
341
|
+
await store.deleteAfter(threadId, "cp-2");
|
|
342
|
+
await store.deleteAfter(threadId, "cp-2");
|
|
343
|
+
|
|
344
|
+
await expect(counts(threadId)).resolves.toEqual({ checkpoints: 2, blobs: 2, writes: 1 });
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it("rejects an empty base checkpoint id instead of deleting every row", async () => {
|
|
348
|
+
await insertCheckpoints([
|
|
349
|
+
{ threadId, checkpointNs: "", checkpointId: "cp-1", channelVersions: { messages: "1" } },
|
|
350
|
+
]);
|
|
351
|
+
await insertBlobs([
|
|
352
|
+
{ threadId, checkpointNs: "", channel: "messages", version: "1" },
|
|
353
|
+
]);
|
|
354
|
+
await insertWrites([
|
|
355
|
+
{ threadId, checkpointNs: "", checkpointId: "cp-1", taskId: "task-1", idx: 0 },
|
|
356
|
+
]);
|
|
357
|
+
|
|
358
|
+
await expect(store.deleteAfter(threadId, "")).rejects.toThrow(
|
|
359
|
+
"baseCheckpointId must be null or a non-empty checkpoint id",
|
|
360
|
+
);
|
|
361
|
+
await expect(counts(threadId)).resolves.toEqual({ checkpoints: 1, blobs: 1, writes: 1 });
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it("deletes by parent lineage, not checkpoint-id ordering", async () => {
|
|
365
|
+
await insertCheckpoints([
|
|
366
|
+
{ threadId, checkpointNs: "", checkpointId: "z-earlier", channelVersions: { messages: "earlier" } },
|
|
367
|
+
{ threadId, checkpointNs: "", checkpointId: "m-base", channelVersions: { messages: "base" } },
|
|
368
|
+
{ threadId, checkpointNs: "", checkpointId: "a-unrelated", channelVersions: { messages: "unrelated" } },
|
|
369
|
+
{ threadId, checkpointNs: "", checkpointId: "z-first", parentCheckpointId: "m-base", channelVersions: { messages: "first" } },
|
|
370
|
+
{
|
|
371
|
+
threadId,
|
|
372
|
+
checkpointNs: "sub",
|
|
373
|
+
checkpointId: "0-sub",
|
|
374
|
+
channelVersions: { messages: "sub" },
|
|
375
|
+
metadata: { parents: { "": "z-first" } },
|
|
376
|
+
},
|
|
377
|
+
{ threadId, checkpointNs: "", checkpointId: "m-unrelated-child", parentCheckpointId: "a-unrelated", channelVersions: { messages: "unrelated-child" } },
|
|
378
|
+
]);
|
|
379
|
+
await insertBlobs([
|
|
380
|
+
{ threadId, checkpointNs: "", channel: "messages", version: "earlier" },
|
|
381
|
+
{ threadId, checkpointNs: "", channel: "messages", version: "base" },
|
|
382
|
+
{ threadId, checkpointNs: "", channel: "messages", version: "unrelated" },
|
|
383
|
+
{ threadId, checkpointNs: "", channel: "messages", version: "first" },
|
|
384
|
+
{ threadId, checkpointNs: "sub", channel: "messages", version: "sub" },
|
|
385
|
+
{ threadId, checkpointNs: "", channel: "messages", version: "unrelated-child" },
|
|
386
|
+
{ threadId, checkpointNs: "", channel: "messages", version: "orphan" },
|
|
387
|
+
]);
|
|
388
|
+
await insertWrites([
|
|
389
|
+
{ threadId, checkpointNs: "", checkpointId: "z-earlier", taskId: "earlier-task", idx: 0 },
|
|
390
|
+
{ threadId, checkpointNs: "", checkpointId: "m-base", taskId: "base-task", idx: 0 },
|
|
391
|
+
{ threadId, checkpointNs: "", checkpointId: "a-unrelated", taskId: "unrelated-task", idx: 0 },
|
|
392
|
+
{ threadId, checkpointNs: "", checkpointId: "z-first", taskId: "first-task", idx: 0 },
|
|
393
|
+
{ threadId, checkpointNs: "sub", checkpointId: "0-sub", taskId: "sub-task", idx: 0 },
|
|
394
|
+
{ threadId, checkpointNs: "", checkpointId: "m-unrelated-child", taskId: "unrelated-child-task", idx: 0 },
|
|
395
|
+
]);
|
|
396
|
+
|
|
397
|
+
await store.deleteAfter(threadId, "m-base");
|
|
398
|
+
|
|
399
|
+
const checkpoints = await pool.query(
|
|
400
|
+
"SELECT checkpoint_ns, checkpoint_id FROM checkpoints WHERE thread_id = $1 ORDER BY checkpoint_ns, checkpoint_id",
|
|
401
|
+
[threadId],
|
|
402
|
+
);
|
|
403
|
+
expect(checkpoints.rows).toEqual([
|
|
404
|
+
{ checkpoint_ns: "", checkpoint_id: "a-unrelated" },
|
|
405
|
+
{ checkpoint_ns: "", checkpoint_id: "m-base" },
|
|
406
|
+
{ checkpoint_ns: "", checkpoint_id: "m-unrelated-child" },
|
|
407
|
+
{ checkpoint_ns: "", checkpoint_id: "z-earlier" },
|
|
408
|
+
]);
|
|
409
|
+
|
|
410
|
+
const writes = await pool.query(
|
|
411
|
+
"SELECT checkpoint_ns, checkpoint_id FROM checkpoint_writes WHERE thread_id = $1 ORDER BY checkpoint_ns, checkpoint_id",
|
|
412
|
+
[threadId],
|
|
413
|
+
);
|
|
414
|
+
expect(writes.rows).toEqual([
|
|
415
|
+
{ checkpoint_ns: "", checkpoint_id: "a-unrelated" },
|
|
416
|
+
{ checkpoint_ns: "", checkpoint_id: "m-unrelated-child" },
|
|
417
|
+
{ checkpoint_ns: "", checkpoint_id: "z-earlier" },
|
|
418
|
+
]);
|
|
419
|
+
|
|
420
|
+
const blobs = await pool.query(
|
|
421
|
+
"SELECT checkpoint_ns, version FROM checkpoint_blobs WHERE thread_id = $1 ORDER BY checkpoint_ns, version",
|
|
422
|
+
[threadId],
|
|
423
|
+
);
|
|
424
|
+
expect(blobs.rows).toEqual([
|
|
425
|
+
{ checkpoint_ns: "", version: "base" },
|
|
426
|
+
{ checkpoint_ns: "", version: "earlier" },
|
|
427
|
+
{ checkpoint_ns: "", version: "unrelated" },
|
|
428
|
+
{ checkpoint_ns: "", version: "unrelated-child" },
|
|
429
|
+
]);
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
it("fails closed when the base checkpoint does not exist", async () => {
|
|
433
|
+
await insertCheckpoints([
|
|
434
|
+
{ threadId, checkpointNs: "", checkpointId: "cp-1", channelVersions: { messages: "1" } },
|
|
435
|
+
]);
|
|
436
|
+
await insertBlobs([
|
|
437
|
+
{ threadId, checkpointNs: "", channel: "messages", version: "1" },
|
|
438
|
+
]);
|
|
439
|
+
await insertWrites([
|
|
440
|
+
{ threadId, checkpointNs: "", checkpointId: "cp-1", taskId: "task-1", idx: 0 },
|
|
441
|
+
]);
|
|
442
|
+
|
|
443
|
+
await expect(store.deleteAfter(threadId, "missing-base")).rejects.toMatchObject({
|
|
444
|
+
name: "CheckpointBaseNotFoundError",
|
|
445
|
+
code: "CHECKPOINT_BASE_NOT_FOUND",
|
|
446
|
+
});
|
|
447
|
+
await expect(store.deleteAfter(threadId, "missing-base"))
|
|
448
|
+
.rejects.toBeInstanceOf(CheckpointBaseNotFoundError);
|
|
449
|
+
await expect(counts(threadId)).resolves.toEqual({ checkpoints: 1, blobs: 1, writes: 1 });
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
it("keeps namespace blobs when a retained checkpoint has no channel versions", async () => {
|
|
453
|
+
await pool.query(
|
|
454
|
+
`INSERT INTO checkpoints (thread_id, checkpoint_ns, checkpoint_id, parent_checkpoint_id, checkpoint, metadata)
|
|
455
|
+
VALUES ($1, '', 'cp-1', NULL, '{"v":4}'::jsonb, '{}'::jsonb),
|
|
456
|
+
($1, '', 'cp-2', 'cp-1', '{"v":4}'::jsonb, '{}'::jsonb)`,
|
|
457
|
+
[threadId],
|
|
458
|
+
);
|
|
459
|
+
await insertBlobs([
|
|
460
|
+
{ threadId, checkpointNs: "", channel: "messages", version: "1" },
|
|
461
|
+
{ threadId, checkpointNs: "", channel: "messages", version: "2" },
|
|
462
|
+
{ threadId, checkpointNs: "", channel: "messages", version: "3" },
|
|
463
|
+
]);
|
|
464
|
+
|
|
465
|
+
await store.deleteAfter(threadId, "cp-1");
|
|
466
|
+
|
|
467
|
+
const blobs = await pool.query(
|
|
468
|
+
"SELECT version FROM checkpoint_blobs WHERE thread_id = $1 ORDER BY version",
|
|
469
|
+
[threadId],
|
|
470
|
+
);
|
|
471
|
+
expect(blobs.rows.map((row) => row.version)).toEqual(["1", "2", "3"]);
|
|
472
|
+
});
|
|
473
|
+
});
|