@axiom-lattice/pg-stores 3.3.12 → 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 +187 -0
- package/dist/index.d.mts +353 -3
- package/dist/index.d.ts +353 -3
- package/dist/index.js +2323 -93
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2329 -92
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/PGVectorStoreProvider.ts +19 -13
- 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 +36 -1
- 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/model_provider_migrations.ts +27 -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/PostgreSQLModelProviderStore.ts +25 -7
- package/src/stores/PostgreSQLStore.ts +167 -0
- package/src/stores/PostgreSQLThreadStore.ts +49 -10
- package/src/stores/ThreadMessageQueueStore.ts +10 -5
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import type { Pool } from "pg";
|
|
2
|
+
import {
|
|
3
|
+
assertStoreIdentity,
|
|
4
|
+
assertStoreNamespacePrefix,
|
|
5
|
+
storeExpiresAt,
|
|
6
|
+
storePaginationValue,
|
|
7
|
+
type StoreItem,
|
|
8
|
+
type StoreProtocol,
|
|
9
|
+
type StorePutInput,
|
|
10
|
+
type StoreSearchInput,
|
|
11
|
+
} from "@axiom-lattice/protocols";
|
|
12
|
+
|
|
13
|
+
interface StoreRow {
|
|
14
|
+
tenant_id: string;
|
|
15
|
+
namespace: string[];
|
|
16
|
+
key: string;
|
|
17
|
+
value: Record<string, unknown> | null;
|
|
18
|
+
created_at: Date;
|
|
19
|
+
updated_at: Date;
|
|
20
|
+
expires_at: Date | null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function mapRow(row: StoreRow): StoreItem {
|
|
24
|
+
return {
|
|
25
|
+
namespace: row.namespace,
|
|
26
|
+
key: row.key,
|
|
27
|
+
value: row.value ?? {},
|
|
28
|
+
createdAt: row.created_at,
|
|
29
|
+
updatedAt: row.updated_at,
|
|
30
|
+
expiresAt: row.expires_at ?? undefined,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* PostgreSQL tenant-scoped namespaced key-value store.
|
|
36
|
+
*
|
|
37
|
+
* @remarks
|
|
38
|
+
* - `namespace` is a `TEXT[]`; prefix search compares `namespace[1:n]`.
|
|
39
|
+
* - `filter` uses JSONB containment (`value @> $filter`).
|
|
40
|
+
* - `query` is a case-insensitive substring over `value::text`.
|
|
41
|
+
* - Expired items are excluded from reads/searches and hard-deleted by
|
|
42
|
+
* {@link deleteExpired}.
|
|
43
|
+
*/
|
|
44
|
+
export class PostgreSQLStore implements StoreProtocol {
|
|
45
|
+
/**
|
|
46
|
+
* @param pool - Pool shared with the rest of the store config.
|
|
47
|
+
*/
|
|
48
|
+
constructor(private readonly pool: Pool) {}
|
|
49
|
+
|
|
50
|
+
async put(tenantId: string, input: StorePutInput): Promise<StoreItem> {
|
|
51
|
+
assertStoreIdentity(input.namespace, input.key);
|
|
52
|
+
const now = new Date();
|
|
53
|
+
const expiresAt = storeExpiresAt(input.ttlMinutes, now) ?? null;
|
|
54
|
+
const result = await this.pool.query<StoreRow>(
|
|
55
|
+
`INSERT INTO lattice_store_items
|
|
56
|
+
(tenant_id, namespace, key, value, created_at, updated_at, expires_at)
|
|
57
|
+
VALUES ($1, $2, $3, $4::jsonb, $5, $5, $6)
|
|
58
|
+
ON CONFLICT (tenant_id, namespace, key) DO UPDATE SET
|
|
59
|
+
value = EXCLUDED.value,
|
|
60
|
+
updated_at = EXCLUDED.updated_at,
|
|
61
|
+
expires_at = EXCLUDED.expires_at
|
|
62
|
+
RETURNING tenant_id, namespace, key, value, created_at, updated_at, expires_at`,
|
|
63
|
+
[tenantId, input.namespace, input.key, JSON.stringify(input.value), now, expiresAt],
|
|
64
|
+
);
|
|
65
|
+
return mapRow(result.rows[0]);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async get(tenantId: string, namespace: string[], key: string): Promise<StoreItem | null> {
|
|
69
|
+
assertStoreIdentity(namespace, key);
|
|
70
|
+
const result = await this.pool.query<StoreRow>(
|
|
71
|
+
`SELECT tenant_id, namespace, key, value, created_at, updated_at, expires_at
|
|
72
|
+
FROM lattice_store_items
|
|
73
|
+
WHERE tenant_id = $1 AND namespace = $2 AND key = $3
|
|
74
|
+
AND (expires_at IS NULL OR expires_at > $4)`,
|
|
75
|
+
[tenantId, namespace, key, new Date()],
|
|
76
|
+
);
|
|
77
|
+
return result.rows[0] ? mapRow(result.rows[0]) : null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async delete(tenantId: string, namespace: string[], key: string): Promise<boolean> {
|
|
81
|
+
assertStoreIdentity(namespace, key);
|
|
82
|
+
const result = await this.pool.query(
|
|
83
|
+
`DELETE FROM lattice_store_items
|
|
84
|
+
WHERE tenant_id = $1 AND namespace = $2 AND key = $3`,
|
|
85
|
+
[tenantId, namespace, key],
|
|
86
|
+
);
|
|
87
|
+
return (result.rowCount ?? 0) > 0;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async search(tenantId: string, input: StoreSearchInput): Promise<StoreItem[]> {
|
|
91
|
+
const conditions = [
|
|
92
|
+
"tenant_id = $1",
|
|
93
|
+
"(expires_at IS NULL OR expires_at > $2)",
|
|
94
|
+
];
|
|
95
|
+
const params: unknown[] = [tenantId, new Date()];
|
|
96
|
+
if (input.namespacePrefix) {
|
|
97
|
+
assertStoreNamespacePrefix(input.namespacePrefix);
|
|
98
|
+
}
|
|
99
|
+
if (input.namespacePrefix && input.namespacePrefix.length > 0) {
|
|
100
|
+
params.push(input.namespacePrefix);
|
|
101
|
+
conditions.push(`namespace[1:array_length($${params.length}::text[], 1)] = $${params.length}`);
|
|
102
|
+
}
|
|
103
|
+
if (input.filter !== undefined) {
|
|
104
|
+
params.push(JSON.stringify(input.filter));
|
|
105
|
+
conditions.push(`value @> $${params.length}::jsonb`);
|
|
106
|
+
}
|
|
107
|
+
if (input.query !== undefined && input.query !== "") {
|
|
108
|
+
params.push(input.query.replace(/[\\%_]/g, "\\$&"));
|
|
109
|
+
conditions.push(`value::text ILIKE '%' || $${params.length} || '%' ESCAPE '\\'`);
|
|
110
|
+
}
|
|
111
|
+
params.push(storePaginationValue(input.limit, 10));
|
|
112
|
+
const limitParam = params.length;
|
|
113
|
+
params.push(storePaginationValue(input.offset, 0));
|
|
114
|
+
const offsetParam = params.length;
|
|
115
|
+
const result = await this.pool.query<StoreRow>(
|
|
116
|
+
`SELECT tenant_id, namespace, key, value, created_at, updated_at, expires_at
|
|
117
|
+
FROM lattice_store_items
|
|
118
|
+
WHERE ${conditions.join(" AND ")}
|
|
119
|
+
ORDER BY updated_at DESC, namespace, key
|
|
120
|
+
LIMIT $${limitParam} OFFSET $${offsetParam}`,
|
|
121
|
+
params,
|
|
122
|
+
);
|
|
123
|
+
return result.rows.map(mapRow);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async listNamespaces(
|
|
127
|
+
tenantId: string,
|
|
128
|
+
prefix: string[] = [],
|
|
129
|
+
limit = 100,
|
|
130
|
+
): Promise<string[][]> {
|
|
131
|
+
assertStoreNamespacePrefix(prefix);
|
|
132
|
+
const conditions = [
|
|
133
|
+
"tenant_id = $1",
|
|
134
|
+
"(expires_at IS NULL OR expires_at > $2)",
|
|
135
|
+
];
|
|
136
|
+
const params: unknown[] = [tenantId, new Date()];
|
|
137
|
+
if (prefix.length > 0) {
|
|
138
|
+
params.push(prefix);
|
|
139
|
+
conditions.push(`namespace[1:array_length($${params.length}::text[], 1)] = $${params.length}`);
|
|
140
|
+
}
|
|
141
|
+
params.push(storePaginationValue(limit, 100));
|
|
142
|
+
const result = await this.pool.query<{ namespace: string[] }>(
|
|
143
|
+
`SELECT DISTINCT namespace
|
|
144
|
+
FROM lattice_store_items
|
|
145
|
+
WHERE ${conditions.join(" AND ")}
|
|
146
|
+
ORDER BY namespace
|
|
147
|
+
LIMIT $${params.length}`,
|
|
148
|
+
params,
|
|
149
|
+
);
|
|
150
|
+
return result.rows.map((row) => row.namespace);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async deleteExpired(now: Date, limit: number): Promise<number> {
|
|
154
|
+
const bounded = storePaginationValue(limit, 0);
|
|
155
|
+
const result = await this.pool.query(
|
|
156
|
+
`WITH expired AS (
|
|
157
|
+
SELECT ctid FROM lattice_store_items
|
|
158
|
+
WHERE expires_at IS NOT NULL AND expires_at <= $1
|
|
159
|
+
ORDER BY expires_at
|
|
160
|
+
LIMIT $2
|
|
161
|
+
)
|
|
162
|
+
DELETE FROM lattice_store_items WHERE ctid IN (SELECT ctid FROM expired)`,
|
|
163
|
+
[now, bounded],
|
|
164
|
+
);
|
|
165
|
+
return result.rowCount ?? 0;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
CreateThreadRequest,
|
|
11
11
|
} from "@axiom-lattice/protocols";
|
|
12
12
|
import { MigrationManager } from "../migrations/migration";
|
|
13
|
-
import { createThreadsTable } from "../migrations/thread_migrations";
|
|
13
|
+
import { addThreadExpiresAt, createThreadsTable } from "../migrations/thread_migrations";
|
|
14
14
|
import { addThreadTenantId } from "../migrations/thread_tenant_migration";
|
|
15
15
|
import { changeThreadPrimaryKey } from "../migrations/thread_pk_migration";
|
|
16
16
|
|
|
@@ -72,6 +72,7 @@ export class PostgreSQLThreadStore implements ThreadStore {
|
|
|
72
72
|
this.migrationManager.register(createThreadsTable);
|
|
73
73
|
this.migrationManager.register(addThreadTenantId);
|
|
74
74
|
this.migrationManager.register(changeThreadPrimaryKey);
|
|
75
|
+
this.migrationManager.register(addThreadExpiresAt);
|
|
75
76
|
|
|
76
77
|
// Auto-migrate by default
|
|
77
78
|
if (options.autoMigrate !== false) {
|
|
@@ -134,9 +135,10 @@ export class PostgreSQLThreadStore implements ThreadStore {
|
|
|
134
135
|
metadata: any;
|
|
135
136
|
created_at: Date;
|
|
136
137
|
updated_at: Date;
|
|
138
|
+
expires_at: Date | null;
|
|
137
139
|
}>(
|
|
138
140
|
`
|
|
139
|
-
SELECT id, tenant_id, assistant_id, metadata, created_at, updated_at
|
|
141
|
+
SELECT id, tenant_id, assistant_id, metadata, created_at, updated_at, expires_at
|
|
140
142
|
FROM lattice_threads
|
|
141
143
|
WHERE tenant_id = $1 AND assistant_id = $2
|
|
142
144
|
ORDER BY created_at DESC
|
|
@@ -174,9 +176,10 @@ export class PostgreSQLThreadStore implements ThreadStore {
|
|
|
174
176
|
metadata: any;
|
|
175
177
|
created_at: Date;
|
|
176
178
|
updated_at: Date;
|
|
179
|
+
expires_at: Date | null;
|
|
177
180
|
}>(
|
|
178
181
|
`
|
|
179
|
-
SELECT id, tenant_id, assistant_id, metadata, created_at, updated_at
|
|
182
|
+
SELECT id, tenant_id, assistant_id, metadata, created_at, updated_at, expires_at
|
|
180
183
|
FROM lattice_threads
|
|
181
184
|
WHERE tenant_id = $1 AND id = $2
|
|
182
185
|
`,
|
|
@@ -206,14 +209,15 @@ export class PostgreSQLThreadStore implements ThreadStore {
|
|
|
206
209
|
|
|
207
210
|
await this.pool.query(
|
|
208
211
|
`
|
|
209
|
-
INSERT INTO lattice_threads (id, tenant_id, assistant_id, metadata, created_at, updated_at)
|
|
210
|
-
VALUES ($1, $2, $3, $4, $5, $6)
|
|
212
|
+
INSERT INTO lattice_threads (id, tenant_id, assistant_id, metadata, created_at, updated_at, expires_at)
|
|
213
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
|
211
214
|
ON CONFLICT (id, tenant_id) DO UPDATE SET
|
|
212
215
|
assistant_id = EXCLUDED.assistant_id,
|
|
213
216
|
metadata = EXCLUDED.metadata,
|
|
214
|
-
updated_at = EXCLUDED.updated_at
|
|
217
|
+
updated_at = EXCLUDED.updated_at,
|
|
218
|
+
expires_at = EXCLUDED.expires_at
|
|
215
219
|
`,
|
|
216
|
-
[threadId, tenantId, assistantId, JSON.stringify(metadata), now, now]
|
|
220
|
+
[threadId, tenantId, assistantId, JSON.stringify(metadata), now, now, data.expiresAt ?? null]
|
|
217
221
|
);
|
|
218
222
|
|
|
219
223
|
return {
|
|
@@ -223,6 +227,7 @@ export class PostgreSQLThreadStore implements ThreadStore {
|
|
|
223
227
|
metadata,
|
|
224
228
|
createdAt: now,
|
|
225
229
|
updatedAt: now,
|
|
230
|
+
expiresAt: data.expiresAt,
|
|
226
231
|
};
|
|
227
232
|
}
|
|
228
233
|
|
|
@@ -249,20 +254,24 @@ export class PostgreSQLThreadStore implements ThreadStore {
|
|
|
249
254
|
};
|
|
250
255
|
|
|
251
256
|
const now = new Date();
|
|
257
|
+
const expiresAt = updates.expiresAt !== undefined
|
|
258
|
+
? updates.expiresAt
|
|
259
|
+
: existing.expiresAt ?? null;
|
|
252
260
|
|
|
253
261
|
await this.pool.query(
|
|
254
262
|
`
|
|
255
263
|
UPDATE lattice_threads
|
|
256
|
-
SET metadata = $1, updated_at = $2
|
|
257
|
-
WHERE tenant_id = $
|
|
264
|
+
SET metadata = $1, updated_at = $2, expires_at = $3
|
|
265
|
+
WHERE tenant_id = $4 AND id = $5
|
|
258
266
|
`,
|
|
259
|
-
[JSON.stringify(updatedMetadata), now, tenantId, threadId]
|
|
267
|
+
[JSON.stringify(updatedMetadata), now, expiresAt, tenantId, threadId]
|
|
260
268
|
);
|
|
261
269
|
|
|
262
270
|
return {
|
|
263
271
|
...existing,
|
|
264
272
|
metadata: updatedMetadata,
|
|
265
273
|
updatedAt: now,
|
|
274
|
+
expiresAt: expiresAt ?? undefined,
|
|
266
275
|
};
|
|
267
276
|
}
|
|
268
277
|
|
|
@@ -301,6 +310,34 @@ export class PostgreSQLThreadStore implements ThreadStore {
|
|
|
301
310
|
return result.rows.length > 0;
|
|
302
311
|
}
|
|
303
312
|
|
|
313
|
+
/**
|
|
314
|
+
* List expired threads across all tenants, oldest expiry first.
|
|
315
|
+
*/
|
|
316
|
+
async listExpiredThreads(now: Date, limit: number): Promise<Thread[]> {
|
|
317
|
+
await this.ensureInitialized();
|
|
318
|
+
|
|
319
|
+
const result = await this.pool.query<{
|
|
320
|
+
id: string;
|
|
321
|
+
tenant_id: string;
|
|
322
|
+
assistant_id: string;
|
|
323
|
+
metadata: any;
|
|
324
|
+
created_at: Date;
|
|
325
|
+
updated_at: Date;
|
|
326
|
+
expires_at: Date | null;
|
|
327
|
+
}>(
|
|
328
|
+
`
|
|
329
|
+
SELECT id, tenant_id, assistant_id, metadata, created_at, updated_at, expires_at
|
|
330
|
+
FROM lattice_threads
|
|
331
|
+
WHERE expires_at IS NOT NULL AND expires_at <= $1
|
|
332
|
+
ORDER BY expires_at ASC
|
|
333
|
+
LIMIT $2
|
|
334
|
+
`,
|
|
335
|
+
[now, Math.max(0, limit)]
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
return result.rows.map(this.mapRowToThread);
|
|
339
|
+
}
|
|
340
|
+
|
|
304
341
|
/**
|
|
305
342
|
* Ensure store is initialized
|
|
306
343
|
*/
|
|
@@ -320,6 +357,7 @@ export class PostgreSQLThreadStore implements ThreadStore {
|
|
|
320
357
|
metadata: any;
|
|
321
358
|
created_at: Date;
|
|
322
359
|
updated_at: Date;
|
|
360
|
+
expires_at: Date | null;
|
|
323
361
|
}): Thread {
|
|
324
362
|
return {
|
|
325
363
|
id: row.id,
|
|
@@ -331,6 +369,7 @@ export class PostgreSQLThreadStore implements ThreadStore {
|
|
|
331
369
|
: row.metadata || {},
|
|
332
370
|
createdAt: row.created_at,
|
|
333
371
|
updatedAt: row.updated_at,
|
|
372
|
+
expiresAt: row.expires_at ?? undefined,
|
|
334
373
|
};
|
|
335
374
|
}
|
|
336
375
|
}
|
|
@@ -277,21 +277,26 @@ export class ThreadMessageQueueStore implements IMessageQueueStore {
|
|
|
277
277
|
|
|
278
278
|
/**
|
|
279
279
|
* Get all threads with pending or processing messages
|
|
280
|
+
*
|
|
281
|
+
* @returns One entry per full scope, with `hasPending` distinguishing
|
|
282
|
+
* processing-only threads (no pending row) from threads with work to drain.
|
|
280
283
|
*/
|
|
281
284
|
async getThreadsWithPendingMessages(): Promise<ThreadInfo[]> {
|
|
282
285
|
const result = await this.pool.query(
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
286
|
+
`SELECT tenant_id, assistant_id, thread_id, workspace_id, project_id,
|
|
287
|
+
bool_or(status = 'pending') AS has_pending
|
|
288
|
+
FROM lattice_thread_message_queue
|
|
289
|
+
WHERE status IN ('pending', 'processing')
|
|
290
|
+
GROUP BY tenant_id, assistant_id, thread_id, workspace_id, project_id`
|
|
287
291
|
);
|
|
288
292
|
|
|
289
|
-
return result.rows.map(row => ({
|
|
293
|
+
return result.rows.map((row) => ({
|
|
290
294
|
tenantId: row.tenant_id,
|
|
291
295
|
assistantId: row.assistant_id,
|
|
292
296
|
threadId: row.thread_id,
|
|
293
297
|
workspaceId: row.workspace_id,
|
|
294
298
|
projectId: row.project_id,
|
|
299
|
+
hasPending: row.has_pending,
|
|
295
300
|
}));
|
|
296
301
|
}
|
|
297
302
|
|