@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
package/dist/index.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import { Pool as
|
|
2
|
+
import { Pool as Pool35 } from "pg";
|
|
3
3
|
|
|
4
4
|
// src/createPgStoreConfig.ts
|
|
5
|
-
import { Pool as
|
|
5
|
+
import { Pool as Pool32 } from "pg";
|
|
6
6
|
|
|
7
7
|
// src/migrations/migration.ts
|
|
8
8
|
var MigrationManager = class {
|
|
@@ -215,6 +215,25 @@ var createThreadsTable = {
|
|
|
215
215
|
await client.query("DROP TABLE IF EXISTS lattice_threads");
|
|
216
216
|
}
|
|
217
217
|
};
|
|
218
|
+
var addThreadExpiresAt = {
|
|
219
|
+
version: 194,
|
|
220
|
+
name: "add_thread_expires_at",
|
|
221
|
+
up: async (client) => {
|
|
222
|
+
await client.query(`
|
|
223
|
+
ALTER TABLE lattice_threads
|
|
224
|
+
ADD COLUMN IF NOT EXISTS expires_at TIMESTAMPTZ
|
|
225
|
+
`);
|
|
226
|
+
await client.query(`
|
|
227
|
+
CREATE INDEX IF NOT EXISTS idx_lattice_threads_expires_at
|
|
228
|
+
ON lattice_threads(expires_at)
|
|
229
|
+
WHERE expires_at IS NOT NULL
|
|
230
|
+
`);
|
|
231
|
+
},
|
|
232
|
+
down: async (client) => {
|
|
233
|
+
await client.query("DROP INDEX IF EXISTS idx_lattice_threads_expires_at");
|
|
234
|
+
await client.query("ALTER TABLE lattice_threads DROP COLUMN IF EXISTS expires_at");
|
|
235
|
+
}
|
|
236
|
+
};
|
|
218
237
|
|
|
219
238
|
// src/migrations/thread_tenant_migration.ts
|
|
220
239
|
var addThreadTenantId = {
|
|
@@ -342,6 +361,7 @@ var PostgreSQLThreadStore = class {
|
|
|
342
361
|
this.migrationManager.register(createThreadsTable);
|
|
343
362
|
this.migrationManager.register(addThreadTenantId);
|
|
344
363
|
this.migrationManager.register(changeThreadPrimaryKey);
|
|
364
|
+
this.migrationManager.register(addThreadExpiresAt);
|
|
345
365
|
if (options.autoMigrate !== false) {
|
|
346
366
|
this.initialize().catch((error) => {
|
|
347
367
|
console.error("Failed to initialize PostgreSQLThreadStore:", error);
|
|
@@ -386,7 +406,7 @@ var PostgreSQLThreadStore = class {
|
|
|
386
406
|
await this.ensureInitialized();
|
|
387
407
|
const result = await this.pool.query(
|
|
388
408
|
`
|
|
389
|
-
SELECT id, tenant_id, assistant_id, metadata, created_at, updated_at
|
|
409
|
+
SELECT id, tenant_id, assistant_id, metadata, created_at, updated_at, expires_at
|
|
390
410
|
FROM lattice_threads
|
|
391
411
|
WHERE tenant_id = $1 AND assistant_id = $2
|
|
392
412
|
ORDER BY created_at DESC
|
|
@@ -410,7 +430,7 @@ var PostgreSQLThreadStore = class {
|
|
|
410
430
|
await this.ensureInitialized();
|
|
411
431
|
const result = await this.pool.query(
|
|
412
432
|
`
|
|
413
|
-
SELECT id, tenant_id, assistant_id, metadata, created_at, updated_at
|
|
433
|
+
SELECT id, tenant_id, assistant_id, metadata, created_at, updated_at, expires_at
|
|
414
434
|
FROM lattice_threads
|
|
415
435
|
WHERE tenant_id = $1 AND id = $2
|
|
416
436
|
`,
|
|
@@ -430,14 +450,15 @@ var PostgreSQLThreadStore = class {
|
|
|
430
450
|
const metadata = data.metadata || {};
|
|
431
451
|
await this.pool.query(
|
|
432
452
|
`
|
|
433
|
-
INSERT INTO lattice_threads (id, tenant_id, assistant_id, metadata, created_at, updated_at)
|
|
434
|
-
VALUES ($1, $2, $3, $4, $5, $6)
|
|
453
|
+
INSERT INTO lattice_threads (id, tenant_id, assistant_id, metadata, created_at, updated_at, expires_at)
|
|
454
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
|
435
455
|
ON CONFLICT (id, tenant_id) DO UPDATE SET
|
|
436
456
|
assistant_id = EXCLUDED.assistant_id,
|
|
437
457
|
metadata = EXCLUDED.metadata,
|
|
438
|
-
updated_at = EXCLUDED.updated_at
|
|
458
|
+
updated_at = EXCLUDED.updated_at,
|
|
459
|
+
expires_at = EXCLUDED.expires_at
|
|
439
460
|
`,
|
|
440
|
-
[threadId, tenantId, assistantId, JSON.stringify(metadata), now, now]
|
|
461
|
+
[threadId, tenantId, assistantId, JSON.stringify(metadata), now, now, data.expiresAt ?? null]
|
|
441
462
|
);
|
|
442
463
|
return {
|
|
443
464
|
id: threadId,
|
|
@@ -445,7 +466,8 @@ var PostgreSQLThreadStore = class {
|
|
|
445
466
|
assistantId,
|
|
446
467
|
metadata,
|
|
447
468
|
createdAt: now,
|
|
448
|
-
updatedAt: now
|
|
469
|
+
updatedAt: now,
|
|
470
|
+
expiresAt: data.expiresAt
|
|
449
471
|
};
|
|
450
472
|
}
|
|
451
473
|
/**
|
|
@@ -462,18 +484,20 @@ var PostgreSQLThreadStore = class {
|
|
|
462
484
|
...updates.metadata || {}
|
|
463
485
|
};
|
|
464
486
|
const now = /* @__PURE__ */ new Date();
|
|
487
|
+
const expiresAt = updates.expiresAt !== void 0 ? updates.expiresAt : existing.expiresAt ?? null;
|
|
465
488
|
await this.pool.query(
|
|
466
489
|
`
|
|
467
490
|
UPDATE lattice_threads
|
|
468
|
-
SET metadata = $1, updated_at = $2
|
|
469
|
-
WHERE tenant_id = $
|
|
491
|
+
SET metadata = $1, updated_at = $2, expires_at = $3
|
|
492
|
+
WHERE tenant_id = $4 AND id = $5
|
|
470
493
|
`,
|
|
471
|
-
[JSON.stringify(updatedMetadata), now, tenantId, threadId]
|
|
494
|
+
[JSON.stringify(updatedMetadata), now, expiresAt, tenantId, threadId]
|
|
472
495
|
);
|
|
473
496
|
return {
|
|
474
497
|
...existing,
|
|
475
498
|
metadata: updatedMetadata,
|
|
476
|
-
updatedAt: now
|
|
499
|
+
updatedAt: now,
|
|
500
|
+
expiresAt: expiresAt ?? void 0
|
|
477
501
|
};
|
|
478
502
|
}
|
|
479
503
|
/**
|
|
@@ -505,6 +529,23 @@ var PostgreSQLThreadStore = class {
|
|
|
505
529
|
);
|
|
506
530
|
return result.rows.length > 0;
|
|
507
531
|
}
|
|
532
|
+
/**
|
|
533
|
+
* List expired threads across all tenants, oldest expiry first.
|
|
534
|
+
*/
|
|
535
|
+
async listExpiredThreads(now, limit) {
|
|
536
|
+
await this.ensureInitialized();
|
|
537
|
+
const result = await this.pool.query(
|
|
538
|
+
`
|
|
539
|
+
SELECT id, tenant_id, assistant_id, metadata, created_at, updated_at, expires_at
|
|
540
|
+
FROM lattice_threads
|
|
541
|
+
WHERE expires_at IS NOT NULL AND expires_at <= $1
|
|
542
|
+
ORDER BY expires_at ASC
|
|
543
|
+
LIMIT $2
|
|
544
|
+
`,
|
|
545
|
+
[now, Math.max(0, limit)]
|
|
546
|
+
);
|
|
547
|
+
return result.rows.map(this.mapRowToThread);
|
|
548
|
+
}
|
|
508
549
|
/**
|
|
509
550
|
* Ensure store is initialized
|
|
510
551
|
*/
|
|
@@ -523,7 +564,8 @@ var PostgreSQLThreadStore = class {
|
|
|
523
564
|
assistantId: row.assistant_id,
|
|
524
565
|
metadata: typeof row.metadata === "string" ? JSON.parse(row.metadata) : row.metadata || {},
|
|
525
566
|
createdAt: row.created_at,
|
|
526
|
-
updatedAt: row.updated_at
|
|
567
|
+
updatedAt: row.updated_at,
|
|
568
|
+
expiresAt: row.expires_at ?? void 0
|
|
527
569
|
};
|
|
528
570
|
}
|
|
529
571
|
};
|
|
@@ -5443,20 +5485,25 @@ var ThreadMessageQueueStore = class {
|
|
|
5443
5485
|
}
|
|
5444
5486
|
/**
|
|
5445
5487
|
* Get all threads with pending or processing messages
|
|
5488
|
+
*
|
|
5489
|
+
* @returns One entry per full scope, with `hasPending` distinguishing
|
|
5490
|
+
* processing-only threads (no pending row) from threads with work to drain.
|
|
5446
5491
|
*/
|
|
5447
5492
|
async getThreadsWithPendingMessages() {
|
|
5448
5493
|
const result = await this.pool.query(
|
|
5449
|
-
`SELECT
|
|
5450
|
-
|
|
5451
|
-
|
|
5452
|
-
|
|
5494
|
+
`SELECT tenant_id, assistant_id, thread_id, workspace_id, project_id,
|
|
5495
|
+
bool_or(status = 'pending') AS has_pending
|
|
5496
|
+
FROM lattice_thread_message_queue
|
|
5497
|
+
WHERE status IN ('pending', 'processing')
|
|
5498
|
+
GROUP BY tenant_id, assistant_id, thread_id, workspace_id, project_id`
|
|
5453
5499
|
);
|
|
5454
5500
|
return result.rows.map((row) => ({
|
|
5455
5501
|
tenantId: row.tenant_id,
|
|
5456
5502
|
assistantId: row.assistant_id,
|
|
5457
5503
|
threadId: row.thread_id,
|
|
5458
5504
|
workspaceId: row.workspace_id,
|
|
5459
|
-
projectId: row.project_id
|
|
5505
|
+
projectId: row.project_id,
|
|
5506
|
+
hasPending: row.has_pending
|
|
5460
5507
|
}));
|
|
5461
5508
|
}
|
|
5462
5509
|
/**
|
|
@@ -9478,19 +9525,18 @@ var PGVectorStoreProvider = class {
|
|
|
9478
9525
|
}
|
|
9479
9526
|
async create(params) {
|
|
9480
9527
|
const { tableName, embeddingKey } = params;
|
|
9481
|
-
|
|
9482
|
-
|
|
9483
|
-
|
|
9484
|
-
|
|
9485
|
-
|
|
9486
|
-
|
|
9487
|
-
|
|
9488
|
-
|
|
9489
|
-
|
|
9490
|
-
|
|
9491
|
-
|
|
9492
|
-
|
|
9493
|
-
}
|
|
9528
|
+
const tenantId = params.tenantId ?? "default";
|
|
9529
|
+
const embeddings = await embeddingsLatticeManager.getEmbeddingsClientWithTenant(
|
|
9530
|
+
tenantId,
|
|
9531
|
+
embeddingKey
|
|
9532
|
+
) ?? await embeddingsLatticeManager.getEmbeddingsClientWithTenant(
|
|
9533
|
+
tenantId,
|
|
9534
|
+
"default"
|
|
9535
|
+
);
|
|
9536
|
+
if (!embeddings) {
|
|
9537
|
+
throw new Error(
|
|
9538
|
+
`No embedding model "${embeddingKey}" registered for tenant "${tenantId}", and no deployment default. Configure one in the model provider panel.`
|
|
9539
|
+
);
|
|
9494
9540
|
}
|
|
9495
9541
|
return PGVectorStore.initialize(embeddings, {
|
|
9496
9542
|
postgresConnectionOptions: { connectionString: this.connectionString },
|
|
@@ -9891,6 +9937,299 @@ var createCollectionsTable = {
|
|
|
9891
9937
|
}
|
|
9892
9938
|
};
|
|
9893
9939
|
|
|
9940
|
+
// src/migrations/agent_run_ledger_migration.ts
|
|
9941
|
+
var createAgentRunLedgerTable = {
|
|
9942
|
+
version: 193,
|
|
9943
|
+
name: "create_lattice_agent_runs_table",
|
|
9944
|
+
up: async (client) => {
|
|
9945
|
+
await client.query(`
|
|
9946
|
+
CREATE TABLE IF NOT EXISTS lattice_agent_runs (
|
|
9947
|
+
run_id UUID PRIMARY KEY,
|
|
9948
|
+
tenant_id VARCHAR(255) NOT NULL,
|
|
9949
|
+
workspace_id VARCHAR(255),
|
|
9950
|
+
project_id VARCHAR(255),
|
|
9951
|
+
assistant_id VARCHAR(255) NOT NULL,
|
|
9952
|
+
thread_id VARCHAR(255) NOT NULL,
|
|
9953
|
+
status VARCHAR(32) NOT NULL,
|
|
9954
|
+
owner VARCHAR(255) NOT NULL,
|
|
9955
|
+
lease_expires_at TIMESTAMPTZ NOT NULL,
|
|
9956
|
+
input_message_ids TEXT[] NOT NULL DEFAULT '{}',
|
|
9957
|
+
attempts INTEGER NOT NULL DEFAULT 1,
|
|
9958
|
+
error TEXT,
|
|
9959
|
+
cancel_requested BOOLEAN NOT NULL DEFAULT FALSE,
|
|
9960
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
9961
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
9962
|
+
started_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
9963
|
+
finished_at TIMESTAMPTZ
|
|
9964
|
+
)
|
|
9965
|
+
`);
|
|
9966
|
+
await client.query(`
|
|
9967
|
+
CREATE UNIQUE INDEX IF NOT EXISTS uq_lattice_agent_runs_one_running
|
|
9968
|
+
ON lattice_agent_runs (tenant_id, thread_id) WHERE status = 'running'
|
|
9969
|
+
`);
|
|
9970
|
+
await client.query(`
|
|
9971
|
+
CREATE INDEX IF NOT EXISTS idx_lattice_agent_runs_history
|
|
9972
|
+
ON lattice_agent_runs (tenant_id, thread_id, created_at DESC)
|
|
9973
|
+
`);
|
|
9974
|
+
await client.query(`
|
|
9975
|
+
CREATE INDEX IF NOT EXISTS idx_lattice_agent_runs_reap
|
|
9976
|
+
ON lattice_agent_runs (status, lease_expires_at)
|
|
9977
|
+
`);
|
|
9978
|
+
},
|
|
9979
|
+
down: async (client) => {
|
|
9980
|
+
await client.query("DROP TABLE IF EXISTS lattice_agent_runs");
|
|
9981
|
+
}
|
|
9982
|
+
};
|
|
9983
|
+
|
|
9984
|
+
// src/migrations/channel_reply_job_migration.ts
|
|
9985
|
+
var DB_CLOCK_MS = "FLOOR(EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint";
|
|
9986
|
+
var createChannelReplyJobsTable = {
|
|
9987
|
+
version: 200,
|
|
9988
|
+
name: "create_lattice_channel_reply_jobs",
|
|
9989
|
+
up: async (client) => {
|
|
9990
|
+
await client.query(`
|
|
9991
|
+
CREATE TABLE IF NOT EXISTS lattice_channel_reply_jobs (
|
|
9992
|
+
run_id TEXT PRIMARY KEY,
|
|
9993
|
+
tenant_id VARCHAR(255) NOT NULL,
|
|
9994
|
+
assistant_id TEXT NOT NULL,
|
|
9995
|
+
thread_id TEXT NOT NULL,
|
|
9996
|
+
adapter_channel TEXT NOT NULL,
|
|
9997
|
+
channel_installation_id TEXT NOT NULL,
|
|
9998
|
+
reply_target JSONB NOT NULL,
|
|
9999
|
+
input_message_id TEXT NOT NULL,
|
|
10000
|
+
stream_cursor TEXT NULL,
|
|
10001
|
+
status TEXT NOT NULL,
|
|
10002
|
+
attempts INT NOT NULL DEFAULT 0,
|
|
10003
|
+
available_at BIGINT NOT NULL,
|
|
10004
|
+
lease_owner TEXT NULL,
|
|
10005
|
+
lease_expires_at BIGINT NULL,
|
|
10006
|
+
last_error TEXT NULL,
|
|
10007
|
+
output_message_id TEXT NULL,
|
|
10008
|
+
created_at BIGINT NOT NULL DEFAULT (${DB_CLOCK_MS}),
|
|
10009
|
+
updated_at BIGINT NOT NULL DEFAULT (${DB_CLOCK_MS}),
|
|
10010
|
+
delivered_at BIGINT NULL
|
|
10011
|
+
)
|
|
10012
|
+
`);
|
|
10013
|
+
await client.query(`
|
|
10014
|
+
CREATE INDEX IF NOT EXISTS idx_lattice_channel_reply_jobs_due
|
|
10015
|
+
ON lattice_channel_reply_jobs (status, available_at)
|
|
10016
|
+
`);
|
|
10017
|
+
await client.query(`
|
|
10018
|
+
CREATE INDEX IF NOT EXISTS idx_lattice_channel_reply_jobs_tenant_thread
|
|
10019
|
+
ON lattice_channel_reply_jobs (tenant_id, thread_id)
|
|
10020
|
+
`);
|
|
10021
|
+
},
|
|
10022
|
+
down: async (client) => {
|
|
10023
|
+
await client.query("DROP INDEX IF EXISTS idx_lattice_channel_reply_jobs_tenant_thread");
|
|
10024
|
+
await client.query("DROP INDEX IF EXISTS idx_lattice_channel_reply_jobs_due");
|
|
10025
|
+
await client.query("DROP TABLE IF EXISTS lattice_channel_reply_jobs");
|
|
10026
|
+
}
|
|
10027
|
+
};
|
|
10028
|
+
|
|
10029
|
+
// src/migrations/agent_run_task_queue_migration.ts
|
|
10030
|
+
var createAgentRunTaskQueueMigration = {
|
|
10031
|
+
version: 196,
|
|
10032
|
+
name: "expand_lattice_agent_runs_task_queue",
|
|
10033
|
+
up: async (client) => {
|
|
10034
|
+
await client.query(`
|
|
10035
|
+
ALTER TABLE lattice_agent_runs
|
|
10036
|
+
ALTER COLUMN owner DROP NOT NULL,
|
|
10037
|
+
ALTER COLUMN lease_expires_at DROP NOT NULL,
|
|
10038
|
+
ALTER COLUMN started_at DROP NOT NULL,
|
|
10039
|
+
ALTER COLUMN started_at DROP DEFAULT
|
|
10040
|
+
`);
|
|
10041
|
+
await client.query(`
|
|
10042
|
+
ALTER TABLE lattice_agent_runs
|
|
10043
|
+
ADD COLUMN IF NOT EXISTS source VARCHAR(32) NOT NULL DEFAULT 'legacy',
|
|
10044
|
+
ADD COLUMN IF NOT EXISTS input JSONB,
|
|
10045
|
+
ADD COLUMN IF NOT EXISTS command JSONB,
|
|
10046
|
+
ADD COLUMN IF NOT EXISTS config JSONB,
|
|
10047
|
+
ADD COLUMN IF NOT EXISTS metadata JSONB,
|
|
10048
|
+
ADD COLUMN IF NOT EXISTS base_checkpoint_id TEXT,
|
|
10049
|
+
ADD COLUMN IF NOT EXISTS available_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
10050
|
+
ADD COLUMN IF NOT EXISTS rollback_requested BOOLEAN NOT NULL DEFAULT FALSE
|
|
10051
|
+
`);
|
|
10052
|
+
await client.query(`
|
|
10053
|
+
ALTER TABLE lattice_agent_runs
|
|
10054
|
+
ALTER COLUMN source SET DEFAULT 'legacy'
|
|
10055
|
+
`);
|
|
10056
|
+
await client.query(`
|
|
10057
|
+
CREATE INDEX IF NOT EXISTS idx_lattice_agent_runs_run_queue
|
|
10058
|
+
ON lattice_agent_runs (source, status, available_at, created_at)
|
|
10059
|
+
`);
|
|
10060
|
+
},
|
|
10061
|
+
down: async (client) => {
|
|
10062
|
+
await client.query(`
|
|
10063
|
+
DROP INDEX IF EXISTS idx_lattice_agent_runs_run_queue
|
|
10064
|
+
`);
|
|
10065
|
+
await client.query(`
|
|
10066
|
+
ALTER TABLE lattice_agent_runs
|
|
10067
|
+
DROP COLUMN IF EXISTS rollback_requested,
|
|
10068
|
+
DROP COLUMN IF EXISTS available_at,
|
|
10069
|
+
DROP COLUMN IF EXISTS base_checkpoint_id,
|
|
10070
|
+
DROP COLUMN IF EXISTS metadata,
|
|
10071
|
+
DROP COLUMN IF EXISTS config,
|
|
10072
|
+
DROP COLUMN IF EXISTS command,
|
|
10073
|
+
DROP COLUMN IF EXISTS input,
|
|
10074
|
+
DROP COLUMN IF EXISTS source
|
|
10075
|
+
`);
|
|
10076
|
+
await client.query(`
|
|
10077
|
+
ALTER TABLE lattice_agent_runs
|
|
10078
|
+
ALTER COLUMN started_at SET DEFAULT now()
|
|
10079
|
+
`);
|
|
10080
|
+
}
|
|
10081
|
+
};
|
|
10082
|
+
|
|
10083
|
+
// src/migrations/agent_run_capability_revision_migration.ts
|
|
10084
|
+
var createAgentRunCapabilityRevisionMigration = {
|
|
10085
|
+
version: 197,
|
|
10086
|
+
name: "add_agent_run_capability_revision",
|
|
10087
|
+
up: async (client) => {
|
|
10088
|
+
await client.query(`
|
|
10089
|
+
ALTER TABLE lattice_agent_runs
|
|
10090
|
+
ADD COLUMN IF NOT EXISTS capability_runtime_revision TEXT
|
|
10091
|
+
`);
|
|
10092
|
+
},
|
|
10093
|
+
down: async (client) => {
|
|
10094
|
+
await client.query(`
|
|
10095
|
+
ALTER TABLE lattice_agent_runs
|
|
10096
|
+
DROP COLUMN IF EXISTS capability_runtime_revision
|
|
10097
|
+
`);
|
|
10098
|
+
}
|
|
10099
|
+
};
|
|
10100
|
+
|
|
10101
|
+
// src/migrations/agent_run_base_checkpoint_captured_migration.ts
|
|
10102
|
+
var createAgentRunBaseCheckpointCapturedMigration = {
|
|
10103
|
+
version: 198,
|
|
10104
|
+
name: "add_agent_run_base_checkpoint_captured",
|
|
10105
|
+
up: async (client) => {
|
|
10106
|
+
await client.query(`
|
|
10107
|
+
ALTER TABLE lattice_agent_runs
|
|
10108
|
+
ADD COLUMN IF NOT EXISTS base_checkpoint_captured BOOLEAN NOT NULL DEFAULT FALSE
|
|
10109
|
+
`);
|
|
10110
|
+
await client.query(`
|
|
10111
|
+
UPDATE lattice_agent_runs
|
|
10112
|
+
SET base_checkpoint_captured = TRUE
|
|
10113
|
+
WHERE base_checkpoint_id IS NOT NULL
|
|
10114
|
+
`);
|
|
10115
|
+
},
|
|
10116
|
+
down: async (client) => {
|
|
10117
|
+
await client.query(`
|
|
10118
|
+
ALTER TABLE lattice_agent_runs
|
|
10119
|
+
DROP COLUMN IF EXISTS base_checkpoint_captured
|
|
10120
|
+
`);
|
|
10121
|
+
}
|
|
10122
|
+
};
|
|
10123
|
+
|
|
10124
|
+
// src/migrations/agent_run_rollback_failure_migration.ts
|
|
10125
|
+
var createAgentRunRollbackFailureMigration = {
|
|
10126
|
+
version: 199,
|
|
10127
|
+
name: "add_agent_run_rollback_failure",
|
|
10128
|
+
up: async (client) => {
|
|
10129
|
+
await client.query(`
|
|
10130
|
+
ALTER TABLE lattice_agent_runs
|
|
10131
|
+
ADD COLUMN IF NOT EXISTS rollback_attempts INTEGER NOT NULL DEFAULT 0,
|
|
10132
|
+
ADD COLUMN IF NOT EXISTS rollback_error TEXT
|
|
10133
|
+
`);
|
|
10134
|
+
},
|
|
10135
|
+
down: async (client) => {
|
|
10136
|
+
await client.query(`
|
|
10137
|
+
ALTER TABLE lattice_agent_runs
|
|
10138
|
+
DROP COLUMN IF EXISTS rollback_error,
|
|
10139
|
+
DROP COLUMN IF EXISTS rollback_attempts
|
|
10140
|
+
`);
|
|
10141
|
+
}
|
|
10142
|
+
};
|
|
10143
|
+
|
|
10144
|
+
// src/migrations/agent_run_running_index_source_migration.ts
|
|
10145
|
+
var agentRunRunningIndexSourceMigration = {
|
|
10146
|
+
version: 202,
|
|
10147
|
+
name: "scope_agent_run_running_index_to_run_api",
|
|
10148
|
+
up: async (client) => {
|
|
10149
|
+
await client.query("DROP INDEX IF EXISTS uq_lattice_agent_runs_one_running");
|
|
10150
|
+
await client.query(`
|
|
10151
|
+
CREATE UNIQUE INDEX IF NOT EXISTS uq_lattice_agent_runs_one_running
|
|
10152
|
+
ON lattice_agent_runs (tenant_id, thread_id)
|
|
10153
|
+
WHERE status = 'running' AND source = 'run_api'
|
|
10154
|
+
`);
|
|
10155
|
+
},
|
|
10156
|
+
down: async (client) => {
|
|
10157
|
+
await client.query("DROP INDEX IF EXISTS uq_lattice_agent_runs_one_running");
|
|
10158
|
+
await client.query(`
|
|
10159
|
+
CREATE UNIQUE INDEX IF NOT EXISTS uq_lattice_agent_runs_one_running
|
|
10160
|
+
ON lattice_agent_runs (tenant_id, thread_id)
|
|
10161
|
+
WHERE status = 'running'
|
|
10162
|
+
`);
|
|
10163
|
+
}
|
|
10164
|
+
};
|
|
10165
|
+
|
|
10166
|
+
// src/migrations/agent_run_retention_migration.ts
|
|
10167
|
+
var createAgentRunRetentionMigration = {
|
|
10168
|
+
version: 201,
|
|
10169
|
+
name: "add_agent_run_retention_index",
|
|
10170
|
+
up: async (client) => {
|
|
10171
|
+
await client.query(`
|
|
10172
|
+
CREATE INDEX IF NOT EXISTS idx_lattice_agent_runs_terminal_finished_at
|
|
10173
|
+
ON lattice_agent_runs (finished_at)
|
|
10174
|
+
WHERE status IN ('success', 'error', 'interrupted', 'cancelled', 'timeout')
|
|
10175
|
+
`);
|
|
10176
|
+
},
|
|
10177
|
+
down: async (client) => {
|
|
10178
|
+
await client.query(`
|
|
10179
|
+
DROP INDEX IF EXISTS idx_lattice_agent_runs_terminal_finished_at
|
|
10180
|
+
`);
|
|
10181
|
+
}
|
|
10182
|
+
};
|
|
10183
|
+
|
|
10184
|
+
// src/migrations/agent_run_project_task_migration.ts
|
|
10185
|
+
var createAgentRunProjectTaskMigration = {
|
|
10186
|
+
version: 203,
|
|
10187
|
+
name: "add_agent_run_project_task",
|
|
10188
|
+
up: async (client) => {
|
|
10189
|
+
await client.query(`
|
|
10190
|
+
ALTER TABLE lattice_agent_runs
|
|
10191
|
+
ADD COLUMN IF NOT EXISTS task_id TEXT NULL
|
|
10192
|
+
`);
|
|
10193
|
+
await client.query(`
|
|
10194
|
+
CREATE INDEX IF NOT EXISTS idx_lattice_agent_runs_project_task
|
|
10195
|
+
ON lattice_agent_runs (tenant_id, task_id, status, created_at DESC)
|
|
10196
|
+
WHERE task_id IS NOT NULL
|
|
10197
|
+
`);
|
|
10198
|
+
},
|
|
10199
|
+
down: async (client) => {
|
|
10200
|
+
await client.query(`
|
|
10201
|
+
DROP INDEX IF EXISTS idx_lattice_agent_runs_project_task
|
|
10202
|
+
`);
|
|
10203
|
+
await client.query(`
|
|
10204
|
+
ALTER TABLE lattice_agent_runs
|
|
10205
|
+
DROP COLUMN IF EXISTS task_id
|
|
10206
|
+
`);
|
|
10207
|
+
}
|
|
10208
|
+
};
|
|
10209
|
+
|
|
10210
|
+
// src/migrations/agent_run_project_task_running_index_migration.ts
|
|
10211
|
+
var agentRunProjectTaskRunningIndexMigration = {
|
|
10212
|
+
version: 204,
|
|
10213
|
+
name: "scope_agent_run_running_index_to_queue_sources",
|
|
10214
|
+
up: async (client) => {
|
|
10215
|
+
await client.query("DROP INDEX IF EXISTS uq_lattice_agent_runs_one_running");
|
|
10216
|
+
await client.query(`
|
|
10217
|
+
CREATE UNIQUE INDEX IF NOT EXISTS uq_lattice_agent_runs_one_running
|
|
10218
|
+
ON lattice_agent_runs (tenant_id, thread_id)
|
|
10219
|
+
WHERE status = 'running'
|
|
10220
|
+
AND source IN ('run_api', 'project_task')
|
|
10221
|
+
`);
|
|
10222
|
+
},
|
|
10223
|
+
down: async (client) => {
|
|
10224
|
+
await client.query("DROP INDEX IF EXISTS uq_lattice_agent_runs_one_running");
|
|
10225
|
+
await client.query(`
|
|
10226
|
+
CREATE UNIQUE INDEX IF NOT EXISTS uq_lattice_agent_runs_one_running
|
|
10227
|
+
ON lattice_agent_runs (tenant_id, thread_id)
|
|
10228
|
+
WHERE status = 'running' AND source = 'run_api'
|
|
10229
|
+
`);
|
|
10230
|
+
}
|
|
10231
|
+
};
|
|
10232
|
+
|
|
9894
10233
|
// src/migrations/capability_bundle_migration.ts
|
|
9895
10234
|
var createCapabilityBundlesTable = {
|
|
9896
10235
|
version: 170,
|
|
@@ -10350,6 +10689,21 @@ var addModelProviderModelDefault = {
|
|
|
10350
10689
|
`);
|
|
10351
10690
|
}
|
|
10352
10691
|
};
|
|
10692
|
+
var addModelProviderModelKind = {
|
|
10693
|
+
version: 193,
|
|
10694
|
+
name: "add_model_provider_model_kind",
|
|
10695
|
+
up: async (client) => {
|
|
10696
|
+
await client.query(`
|
|
10697
|
+
ALTER TABLE lattice_model_provider_models
|
|
10698
|
+
ADD COLUMN IF NOT EXISTS kind VARCHAR(16) NOT NULL DEFAULT 'chat'
|
|
10699
|
+
`);
|
|
10700
|
+
},
|
|
10701
|
+
down: async (client) => {
|
|
10702
|
+
await client.query(`
|
|
10703
|
+
ALTER TABLE lattice_model_provider_models DROP COLUMN IF EXISTS kind
|
|
10704
|
+
`);
|
|
10705
|
+
}
|
|
10706
|
+
};
|
|
10353
10707
|
|
|
10354
10708
|
// src/stores/PostgreSQLCapabilityBundleStore.ts
|
|
10355
10709
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
@@ -11343,6 +11697,7 @@ var PostgreSQLModelProviderStore = class {
|
|
|
11343
11697
|
this.migrationManager.register(addModelProviderModelStale);
|
|
11344
11698
|
this.migrationManager.register(addModelProviderModelExtra);
|
|
11345
11699
|
this.migrationManager.register(addModelProviderModelDefault);
|
|
11700
|
+
this.migrationManager.register(addModelProviderModelKind);
|
|
11346
11701
|
if (options.autoMigrate !== false) {
|
|
11347
11702
|
this.initialize().catch((error) => {
|
|
11348
11703
|
console.error("Failed to initialize PostgreSQLModelProviderStore:", error);
|
|
@@ -11589,7 +11944,7 @@ var PostgreSQLModelProviderStore = class {
|
|
|
11589
11944
|
const result = providerId ? await this.pool.query(
|
|
11590
11945
|
`
|
|
11591
11946
|
SELECT id, tenant_id, provider_id, model_id, display_name, owned_by,
|
|
11592
|
-
upstream_created, enabled, stale, extra, is_default, discovered_at
|
|
11947
|
+
upstream_created, enabled, stale, extra, kind, is_default, discovered_at
|
|
11593
11948
|
FROM lattice_model_provider_models
|
|
11594
11949
|
WHERE tenant_id = $1 AND provider_id = $2
|
|
11595
11950
|
ORDER BY model_id ASC
|
|
@@ -11598,7 +11953,7 @@ var PostgreSQLModelProviderStore = class {
|
|
|
11598
11953
|
) : await this.pool.query(
|
|
11599
11954
|
`
|
|
11600
11955
|
SELECT id, tenant_id, provider_id, model_id, display_name, owned_by,
|
|
11601
|
-
upstream_created, enabled, stale, extra, is_default, discovered_at
|
|
11956
|
+
upstream_created, enabled, stale, extra, kind, is_default, discovered_at
|
|
11602
11957
|
FROM lattice_model_provider_models
|
|
11603
11958
|
WHERE tenant_id = $1
|
|
11604
11959
|
ORDER BY model_id ASC
|
|
@@ -11617,7 +11972,7 @@ var PostgreSQLModelProviderStore = class {
|
|
|
11617
11972
|
const result = await this.pool.query(
|
|
11618
11973
|
`
|
|
11619
11974
|
SELECT id, tenant_id, provider_id, model_id, display_name, owned_by,
|
|
11620
|
-
upstream_created, enabled, stale, extra, is_default, discovered_at
|
|
11975
|
+
upstream_created, enabled, stale, extra, kind, is_default, discovered_at
|
|
11621
11976
|
FROM lattice_model_provider_models
|
|
11622
11977
|
WHERE tenant_id = $1 AND provider_id = $2 AND model_id = $3
|
|
11623
11978
|
`,
|
|
@@ -11650,7 +12005,7 @@ var PostgreSQLModelProviderStore = class {
|
|
|
11650
12005
|
await client.query("BEGIN");
|
|
11651
12006
|
const current = await client.query(
|
|
11652
12007
|
`
|
|
11653
|
-
SELECT model_id, enabled, extra, is_default FROM lattice_model_provider_models
|
|
12008
|
+
SELECT model_id, enabled, extra, kind, is_default FROM lattice_model_provider_models
|
|
11654
12009
|
WHERE tenant_id = $1 AND provider_id = $2
|
|
11655
12010
|
`,
|
|
11656
12011
|
[tenantId, providerId]
|
|
@@ -11661,6 +12016,9 @@ var PostgreSQLModelProviderStore = class {
|
|
|
11661
12016
|
const currentExtra = new Map(
|
|
11662
12017
|
current.rows.map((row) => [row.model_id, row.extra])
|
|
11663
12018
|
);
|
|
12019
|
+
const currentKind = new Map(
|
|
12020
|
+
current.rows.map((row) => [row.model_id, row.kind])
|
|
12021
|
+
);
|
|
11664
12022
|
const currentIsDefault = new Map(
|
|
11665
12023
|
current.rows.map((row) => [row.model_id, row.is_default])
|
|
11666
12024
|
);
|
|
@@ -11675,6 +12033,9 @@ var PostgreSQLModelProviderStore = class {
|
|
|
11675
12033
|
const effectiveExtra = rows.map(
|
|
11676
12034
|
(model) => model.extra ?? currentExtra.get(model.id) ?? {}
|
|
11677
12035
|
);
|
|
12036
|
+
const effectiveKind = rows.map(
|
|
12037
|
+
(model) => model.kind ?? currentKind.get(model.id) ?? "chat"
|
|
12038
|
+
);
|
|
11678
12039
|
const namesDefault = rows.some((model) => model.isDefault === true);
|
|
11679
12040
|
if (namesDefault) {
|
|
11680
12041
|
await client.query(
|
|
@@ -11711,15 +12072,15 @@ var PostgreSQLModelProviderStore = class {
|
|
|
11711
12072
|
`
|
|
11712
12073
|
INSERT INTO lattice_model_provider_models (
|
|
11713
12074
|
id, tenant_id, provider_id, model_id, display_name, owned_by,
|
|
11714
|
-
upstream_created, raw, enabled, stale, extra, is_default, discovered_at
|
|
12075
|
+
upstream_created, raw, enabled, stale, extra, kind, is_default, discovered_at
|
|
11715
12076
|
)
|
|
11716
12077
|
SELECT * FROM unnest(
|
|
11717
12078
|
$1::text[], $2::text[], $3::text[], $4::text[], $5::text[],
|
|
11718
12079
|
$6::text[], $7::timestamp[], $8::jsonb[], $9::boolean[], $10::boolean[],
|
|
11719
|
-
$11::jsonb[], $12::
|
|
12080
|
+
$11::jsonb[], $12::text[], $13::boolean[], $14::timestamp[]
|
|
11720
12081
|
) AS t(
|
|
11721
12082
|
id, tenant_id, provider_id, model_id, display_name, owned_by,
|
|
11722
|
-
upstream_created, raw, enabled, stale, extra, is_default, discovered_at
|
|
12083
|
+
upstream_created, raw, enabled, stale, extra, kind, is_default, discovered_at
|
|
11723
12084
|
)
|
|
11724
12085
|
ON CONFLICT (tenant_id, provider_id, model_id) DO UPDATE SET
|
|
11725
12086
|
display_name = EXCLUDED.display_name,
|
|
@@ -11729,6 +12090,7 @@ var PostgreSQLModelProviderStore = class {
|
|
|
11729
12090
|
enabled = EXCLUDED.enabled,
|
|
11730
12091
|
stale = EXCLUDED.stale,
|
|
11731
12092
|
extra = EXCLUDED.extra,
|
|
12093
|
+
kind = EXCLUDED.kind,
|
|
11732
12094
|
is_default = EXCLUDED.is_default,
|
|
11733
12095
|
discovered_at = EXCLUDED.discovered_at
|
|
11734
12096
|
`,
|
|
@@ -11748,6 +12110,7 @@ var PostgreSQLModelProviderStore = class {
|
|
|
11748
12110
|
effectiveEnabled,
|
|
11749
12111
|
rows.map((model) => model.stale ?? false),
|
|
11750
12112
|
effectiveExtra.map((extra) => JSON.stringify(extra)),
|
|
12113
|
+
effectiveKind,
|
|
11751
12114
|
effectiveIsDefault,
|
|
11752
12115
|
rows.map(() => (/* @__PURE__ */ new Date()).toISOString())
|
|
11753
12116
|
]
|
|
@@ -11854,6 +12217,7 @@ var PostgreSQLModelProviderStore = class {
|
|
|
11854
12217
|
enabled: row.enabled,
|
|
11855
12218
|
stale: row.stale,
|
|
11856
12219
|
extra: extraOrUndefined(row.extra),
|
|
12220
|
+
kind: row.kind || "chat",
|
|
11857
12221
|
isDefault: row.is_default,
|
|
11858
12222
|
discoveredAt: row.discovered_at
|
|
11859
12223
|
};
|
|
@@ -12207,53 +12571,1899 @@ var PostgreSQLPluginDeploymentStore = class {
|
|
|
12207
12571
|
}
|
|
12208
12572
|
};
|
|
12209
12573
|
|
|
12210
|
-
// src/
|
|
12211
|
-
|
|
12212
|
-
|
|
12213
|
-
|
|
12214
|
-
|
|
12215
|
-
|
|
12216
|
-
|
|
12217
|
-
|
|
12218
|
-
|
|
12219
|
-
|
|
12220
|
-
|
|
12221
|
-
|
|
12222
|
-
|
|
12223
|
-
|
|
12224
|
-
|
|
12225
|
-
|
|
12226
|
-
|
|
12227
|
-
|
|
12228
|
-
|
|
12229
|
-
|
|
12230
|
-
|
|
12231
|
-
|
|
12232
|
-
|
|
12233
|
-
|
|
12234
|
-
|
|
12235
|
-
|
|
12236
|
-
|
|
12237
|
-
|
|
12238
|
-
|
|
12239
|
-
|
|
12240
|
-
|
|
12241
|
-
|
|
12242
|
-
|
|
12243
|
-
|
|
12244
|
-
|
|
12245
|
-
|
|
12246
|
-
|
|
12247
|
-
|
|
12248
|
-
|
|
12249
|
-
|
|
12574
|
+
// src/stores/PostgreSQLAgentRunLedger.ts
|
|
12575
|
+
import { Pool as Pool30 } from "pg";
|
|
12576
|
+
import {
|
|
12577
|
+
AgentRunAlreadyActiveError,
|
|
12578
|
+
AgentRunDuplicateMessageIdError,
|
|
12579
|
+
AgentRunInputCommandConflictError,
|
|
12580
|
+
AgentRunInvalidTaskIdError,
|
|
12581
|
+
AgentRunTenantQuotaExceededError,
|
|
12582
|
+
assertQueueSources,
|
|
12583
|
+
extractAgentRunInputMessageIds,
|
|
12584
|
+
normalizeAgentRunMessageIds
|
|
12585
|
+
} from "@axiom-lattice/core";
|
|
12586
|
+
var DEFAULT_LIST_LIMIT = 50;
|
|
12587
|
+
var MAX_LIST_LIMIT = 200;
|
|
12588
|
+
var CLAIM_RUN_SQL = `UPDATE lattice_agent_runs AS r
|
|
12589
|
+
SET status = 'running',
|
|
12590
|
+
owner = $2,
|
|
12591
|
+
lease_expires_at = now() + ($3::bigint || ' milliseconds')::interval,
|
|
12592
|
+
started_at = now(),
|
|
12593
|
+
attempts = r.attempts + 1,
|
|
12594
|
+
input_message_ids = COALESCE($4::text[], r.input_message_ids),
|
|
12595
|
+
updated_at = now()
|
|
12596
|
+
WHERE r.run_id = $1
|
|
12597
|
+
AND r.source = ANY($5::text[])
|
|
12598
|
+
AND r.status = 'pending'
|
|
12599
|
+
AND r.available_at <= now()
|
|
12600
|
+
AND NOT EXISTS (
|
|
12601
|
+
SELECT 1 FROM lattice_agent_runs AS o
|
|
12602
|
+
WHERE o.tenant_id = r.tenant_id
|
|
12603
|
+
AND o.thread_id = r.thread_id
|
|
12604
|
+
AND o.source IN ('run_api', 'project_task')
|
|
12605
|
+
AND o.status = 'running'
|
|
12606
|
+
)
|
|
12607
|
+
AND NOT EXISTS (
|
|
12608
|
+
SELECT 1 FROM lattice_agent_runs AS b
|
|
12609
|
+
WHERE b.tenant_id = r.tenant_id
|
|
12610
|
+
AND b.thread_id = r.thread_id
|
|
12611
|
+
AND b.source = 'run_api'
|
|
12612
|
+
AND b.rollback_requested = TRUE
|
|
12613
|
+
)
|
|
12614
|
+
RETURNING r.*`;
|
|
12615
|
+
var PostgreSQLAgentRunLedger = class {
|
|
12616
|
+
/**
|
|
12617
|
+
* @param options An externally managed pool, or a pool config/connection URL.
|
|
12618
|
+
*/
|
|
12619
|
+
constructor(options) {
|
|
12620
|
+
this.ownsPool = true;
|
|
12621
|
+
this.initialized = false;
|
|
12622
|
+
this.initPromise = null;
|
|
12623
|
+
if (options.pool) {
|
|
12624
|
+
this.pool = options.pool;
|
|
12625
|
+
this.ownsPool = false;
|
|
12626
|
+
this.initialized = true;
|
|
12627
|
+
return;
|
|
12628
|
+
}
|
|
12629
|
+
if (typeof options.poolConfig === "string") {
|
|
12630
|
+
this.pool = new Pool30({ connectionString: options.poolConfig });
|
|
12631
|
+
} else if (options.poolConfig) {
|
|
12632
|
+
this.pool = new Pool30(options.poolConfig);
|
|
12633
|
+
} else {
|
|
12634
|
+
throw new Error("Either pool or poolConfig must be provided");
|
|
12635
|
+
}
|
|
12636
|
+
this.migrationManager = new MigrationManager(this.pool);
|
|
12637
|
+
this.migrationManager.register(createAgentRunLedgerTable);
|
|
12638
|
+
this.migrationManager.register(createAgentRunTaskQueueMigration);
|
|
12639
|
+
this.migrationManager.register(createAgentRunCapabilityRevisionMigration);
|
|
12640
|
+
this.migrationManager.register(createAgentRunBaseCheckpointCapturedMigration);
|
|
12641
|
+
this.migrationManager.register(createAgentRunRollbackFailureMigration);
|
|
12642
|
+
this.migrationManager.register(createAgentRunRetentionMigration);
|
|
12643
|
+
this.migrationManager.register(agentRunRunningIndexSourceMigration);
|
|
12644
|
+
this.migrationManager.register(createAgentRunProjectTaskMigration);
|
|
12645
|
+
this.migrationManager.register(agentRunProjectTaskRunningIndexMigration);
|
|
12646
|
+
if (options.autoMigrate !== false) {
|
|
12647
|
+
this.initialize().catch((error) => {
|
|
12648
|
+
console.error("Failed to initialize PostgreSQLAgentRunLedger:", error);
|
|
12649
|
+
throw error;
|
|
12650
|
+
});
|
|
12651
|
+
}
|
|
12250
12652
|
}
|
|
12251
|
-
|
|
12252
|
-
|
|
12253
|
-
|
|
12254
|
-
|
|
12255
|
-
|
|
12256
|
-
|
|
12653
|
+
/** Runs pending migrations once. */
|
|
12654
|
+
async initialize() {
|
|
12655
|
+
if (this.initialized) return;
|
|
12656
|
+
if (this.initPromise) return this.initPromise;
|
|
12657
|
+
this.initPromise = (async () => {
|
|
12658
|
+
try {
|
|
12659
|
+
await this.migrationManager.migrate();
|
|
12660
|
+
this.initialized = true;
|
|
12661
|
+
} finally {
|
|
12662
|
+
this.initPromise = null;
|
|
12663
|
+
}
|
|
12664
|
+
})();
|
|
12665
|
+
return this.initPromise;
|
|
12666
|
+
}
|
|
12667
|
+
/** Ends the pool when this store owns it. */
|
|
12668
|
+
async dispose() {
|
|
12669
|
+
if (this.ownsPool && this.pool) {
|
|
12670
|
+
await this.pool.end();
|
|
12671
|
+
}
|
|
12672
|
+
}
|
|
12673
|
+
/**
|
|
12674
|
+
* Creates a running legacy run, reconciling any existing running row first.
|
|
12675
|
+
*
|
|
12676
|
+
* @remarks The caller holds the `thread-run` lease, so a surviving `running`
|
|
12677
|
+
* row is stale (its owner can no longer hold the lease) even when its ledger
|
|
12678
|
+
* lease has not expired yet.
|
|
12679
|
+
*/
|
|
12680
|
+
async beginRun(input) {
|
|
12681
|
+
const client = await this.pool.connect();
|
|
12682
|
+
try {
|
|
12683
|
+
await client.query("BEGIN");
|
|
12684
|
+
await client.query(
|
|
12685
|
+
`UPDATE lattice_agent_runs
|
|
12686
|
+
SET status = 'interrupted',
|
|
12687
|
+
error = COALESCE(error, 'superseded by a new run on the same thread'),
|
|
12688
|
+
finished_at = now(),
|
|
12689
|
+
updated_at = now()
|
|
12690
|
+
WHERE tenant_id = $1 AND thread_id = $2
|
|
12691
|
+
AND status = 'running'`,
|
|
12692
|
+
[input.tenantId, input.threadId]
|
|
12693
|
+
);
|
|
12694
|
+
const result = await client.query(
|
|
12695
|
+
`INSERT INTO lattice_agent_runs
|
|
12696
|
+
(run_id, tenant_id, workspace_id, project_id, assistant_id, thread_id,
|
|
12697
|
+
status, source, owner, lease_expires_at, input_message_ids, attempts,
|
|
12698
|
+
started_at)
|
|
12699
|
+
VALUES ($1, $2, $3, $4, $5, $6, 'running', 'legacy', $7,
|
|
12700
|
+
now() + ($8::bigint || ' milliseconds')::interval, $9, 1, now())
|
|
12701
|
+
RETURNING *`,
|
|
12702
|
+
[
|
|
12703
|
+
input.runId,
|
|
12704
|
+
input.tenantId,
|
|
12705
|
+
input.workspaceId ?? null,
|
|
12706
|
+
input.projectId ?? null,
|
|
12707
|
+
input.assistantId,
|
|
12708
|
+
input.threadId,
|
|
12709
|
+
input.owner,
|
|
12710
|
+
input.leaseMs,
|
|
12711
|
+
input.inputMessageIds
|
|
12712
|
+
]
|
|
12713
|
+
);
|
|
12714
|
+
await client.query("COMMIT");
|
|
12715
|
+
return rowToRecord(result.rows[0]);
|
|
12716
|
+
} catch (error) {
|
|
12717
|
+
await client.query("ROLLBACK");
|
|
12718
|
+
if (isActiveRunViolation(error)) {
|
|
12719
|
+
throw new AgentRunAlreadyActiveError();
|
|
12720
|
+
}
|
|
12721
|
+
throw error;
|
|
12722
|
+
} finally {
|
|
12723
|
+
client.release();
|
|
12724
|
+
}
|
|
12725
|
+
}
|
|
12726
|
+
/** Creates an unowned pending run-queue row. */
|
|
12727
|
+
async createPendingRun(input) {
|
|
12728
|
+
const source = input.source ?? "run_api";
|
|
12729
|
+
if (source === "project_task") {
|
|
12730
|
+
if (input.taskId === void 0 || input.taskId.trim().length === 0) {
|
|
12731
|
+
throw new AgentRunInvalidTaskIdError(
|
|
12732
|
+
"A project_task run requires a non-empty taskId"
|
|
12733
|
+
);
|
|
12734
|
+
}
|
|
12735
|
+
} else if (input.taskId !== void 0) {
|
|
12736
|
+
throw new AgentRunInvalidTaskIdError(
|
|
12737
|
+
`A ${source} run cannot carry a taskId`
|
|
12738
|
+
);
|
|
12739
|
+
}
|
|
12740
|
+
if (input.input != null && input.command != null) {
|
|
12741
|
+
throw new AgentRunInputCommandConflictError();
|
|
12742
|
+
}
|
|
12743
|
+
const inputMessageIds = input.inputMessageIds ? normalizeAgentRunMessageIds(input.inputMessageIds) : extractAgentRunInputMessageIds(input.input);
|
|
12744
|
+
const availableAt = input.availableAt ?? Date.now();
|
|
12745
|
+
const limit = input.maxActiveRunsForTenant;
|
|
12746
|
+
const client = await this.pool.connect();
|
|
12747
|
+
try {
|
|
12748
|
+
await client.query("BEGIN");
|
|
12749
|
+
if (limit !== void 0 && limit > 0) {
|
|
12750
|
+
await client.query("SELECT pg_advisory_xact_lock(hashtext($1))", [
|
|
12751
|
+
input.tenantId
|
|
12752
|
+
]);
|
|
12753
|
+
}
|
|
12754
|
+
await client.query("SELECT pg_advisory_xact_lock(hashtext($1))", [
|
|
12755
|
+
`agent-run:${input.tenantId}:${input.threadId}`
|
|
12756
|
+
]);
|
|
12757
|
+
if (inputMessageIds.length > 0) {
|
|
12758
|
+
const duplicate = await client.query(
|
|
12759
|
+
`SELECT input_message_ids
|
|
12760
|
+
FROM lattice_agent_runs
|
|
12761
|
+
WHERE tenant_id = $1 AND thread_id = $2
|
|
12762
|
+
AND status IN ('pending', 'running')
|
|
12763
|
+
AND input_message_ids && $3::text[]
|
|
12764
|
+
LIMIT 1`,
|
|
12765
|
+
[input.tenantId, input.threadId, inputMessageIds]
|
|
12766
|
+
);
|
|
12767
|
+
if ((duplicate.rowCount ?? 0) > 0) {
|
|
12768
|
+
const existing = duplicate.rows[0].input_message_ids;
|
|
12769
|
+
const messageId = existing.find((id) => inputMessageIds.includes(id)) ?? inputMessageIds[0];
|
|
12770
|
+
throw new AgentRunDuplicateMessageIdError(messageId);
|
|
12771
|
+
}
|
|
12772
|
+
}
|
|
12773
|
+
if (limit !== void 0 && limit > 0) {
|
|
12774
|
+
const active = await client.query(
|
|
12775
|
+
`SELECT count(*)::text AS count
|
|
12776
|
+
FROM lattice_agent_runs
|
|
12777
|
+
WHERE tenant_id = $1
|
|
12778
|
+
AND source = 'run_api'
|
|
12779
|
+
AND status IN ('pending', 'running')`,
|
|
12780
|
+
[input.tenantId]
|
|
12781
|
+
);
|
|
12782
|
+
if (Number(active.rows[0].count) >= limit) {
|
|
12783
|
+
throw new AgentRunTenantQuotaExceededError(input.tenantId, limit);
|
|
12784
|
+
}
|
|
12785
|
+
}
|
|
12786
|
+
const result = await client.query(
|
|
12787
|
+
`INSERT INTO lattice_agent_runs
|
|
12788
|
+
(run_id, tenant_id, workspace_id, project_id, assistant_id, thread_id,
|
|
12789
|
+
status, source, owner, lease_expires_at, input_message_ids, attempts,
|
|
12790
|
+
input, command, config, metadata, capability_runtime_revision,
|
|
12791
|
+
available_at, created_at, updated_at, task_id)
|
|
12792
|
+
VALUES ($1, $2, $3, $4, $5, $6, 'pending', $7, NULL, NULL, $8, 0,
|
|
12793
|
+
$9::jsonb, $10::jsonb, $11::jsonb, $12::jsonb, $13,
|
|
12794
|
+
to_timestamp($14::double precision / 1000.0),
|
|
12795
|
+
date_trunc('milliseconds', now()),
|
|
12796
|
+
date_trunc('milliseconds', now()), $15)
|
|
12797
|
+
RETURNING *`,
|
|
12798
|
+
[
|
|
12799
|
+
input.runId,
|
|
12800
|
+
input.tenantId,
|
|
12801
|
+
input.workspaceId ?? null,
|
|
12802
|
+
input.projectId ?? null,
|
|
12803
|
+
input.assistantId,
|
|
12804
|
+
input.threadId,
|
|
12805
|
+
source,
|
|
12806
|
+
inputMessageIds,
|
|
12807
|
+
jsonParam(input.input),
|
|
12808
|
+
jsonParam(input.command),
|
|
12809
|
+
jsonParam(input.config),
|
|
12810
|
+
jsonParam(input.metadata),
|
|
12811
|
+
input.capabilityRuntimeRevision ?? null,
|
|
12812
|
+
availableAt,
|
|
12813
|
+
input.taskId ?? null
|
|
12814
|
+
]
|
|
12815
|
+
);
|
|
12816
|
+
await client.query("COMMIT");
|
|
12817
|
+
return rowToRecord(result.rows[0]);
|
|
12818
|
+
} catch (error) {
|
|
12819
|
+
await client.query("ROLLBACK");
|
|
12820
|
+
throw error;
|
|
12821
|
+
} finally {
|
|
12822
|
+
client.release();
|
|
12823
|
+
}
|
|
12824
|
+
}
|
|
12825
|
+
/** Lists claimable run-queue rows, oldest available first. */
|
|
12826
|
+
async listPendingRuns(options) {
|
|
12827
|
+
const limit = clampLimit(options?.limit);
|
|
12828
|
+
const after = options?.after;
|
|
12829
|
+
const fairTenants = options?.fairTenants === true;
|
|
12830
|
+
if (fairTenants && after !== void 0) {
|
|
12831
|
+
throw new RangeError(
|
|
12832
|
+
"listPendingRuns fairTenants does not support the after cursor"
|
|
12833
|
+
);
|
|
12834
|
+
}
|
|
12835
|
+
const maxRunning = positiveLimit(options?.maxRunningPerTenant);
|
|
12836
|
+
const sources = options?.sources === void 0 ? ["run_api"] : [...options.sources];
|
|
12837
|
+
assertQueueSources(sources);
|
|
12838
|
+
const params = [limit];
|
|
12839
|
+
let cursorClause = "";
|
|
12840
|
+
if (after) {
|
|
12841
|
+
params.push(
|
|
12842
|
+
new Date(after.availableAt),
|
|
12843
|
+
new Date(after.createdAt),
|
|
12844
|
+
after.runId
|
|
12845
|
+
);
|
|
12846
|
+
cursorClause = `
|
|
12847
|
+
AND (r.available_at, r.created_at, r.run_id)
|
|
12848
|
+
> ($2::timestamptz, $3::timestamptz, $4::uuid)`;
|
|
12849
|
+
}
|
|
12850
|
+
params.push(maxRunning ?? null);
|
|
12851
|
+
const capIndex = params.length;
|
|
12852
|
+
params.push(sources);
|
|
12853
|
+
const sourcesIndex = params.length;
|
|
12854
|
+
const capClause = `
|
|
12855
|
+
AND ($${capIndex}::bigint IS NULL OR (
|
|
12856
|
+
SELECT count(*) FROM lattice_agent_runs AS c
|
|
12857
|
+
WHERE c.tenant_id = r.tenant_id
|
|
12858
|
+
AND c.source = ANY($${sourcesIndex}::text[])
|
|
12859
|
+
AND c.status = 'running'
|
|
12860
|
+
) < $${capIndex}::bigint)`;
|
|
12861
|
+
const filterClause = `WHERE r.source = ANY($${sourcesIndex}::text[]) AND r.status = 'pending'
|
|
12862
|
+
AND r.available_at <= now()
|
|
12863
|
+
AND NOT EXISTS (
|
|
12864
|
+
SELECT 1 FROM lattice_agent_runs AS o
|
|
12865
|
+
WHERE o.tenant_id = r.tenant_id
|
|
12866
|
+
AND o.thread_id = r.thread_id
|
|
12867
|
+
AND o.source IN ('run_api', 'project_task')
|
|
12868
|
+
AND o.status = 'running'
|
|
12869
|
+
)
|
|
12870
|
+
AND NOT EXISTS (
|
|
12871
|
+
SELECT 1 FROM lattice_agent_runs AS b
|
|
12872
|
+
WHERE b.tenant_id = r.tenant_id
|
|
12873
|
+
AND b.thread_id = r.thread_id
|
|
12874
|
+
AND b.source = 'run_api'
|
|
12875
|
+
AND b.rollback_requested = TRUE
|
|
12876
|
+
)${capClause}${cursorClause}`;
|
|
12877
|
+
if (fairTenants) {
|
|
12878
|
+
const ranked = await this.pool.query(
|
|
12879
|
+
`WITH ranked AS (
|
|
12880
|
+
SELECT r.*,
|
|
12881
|
+
row_number() OVER (
|
|
12882
|
+
PARTITION BY r.tenant_id
|
|
12883
|
+
ORDER BY r.available_at ASC, r.created_at ASC, r.run_id ASC
|
|
12884
|
+
) AS tenant_rank
|
|
12885
|
+
FROM lattice_agent_runs AS r
|
|
12886
|
+
${filterClause}
|
|
12887
|
+
)
|
|
12888
|
+
SELECT * FROM ranked
|
|
12889
|
+
ORDER BY tenant_rank ASC, available_at ASC, created_at ASC,
|
|
12890
|
+
run_id ASC, tenant_id ASC
|
|
12891
|
+
LIMIT $1`,
|
|
12892
|
+
params
|
|
12893
|
+
);
|
|
12894
|
+
return ranked.rows.map(rowToRecord);
|
|
12895
|
+
}
|
|
12896
|
+
const result = await this.pool.query(
|
|
12897
|
+
`SELECT r.* FROM lattice_agent_runs AS r
|
|
12898
|
+
${filterClause}
|
|
12899
|
+
ORDER BY r.available_at ASC, r.created_at ASC, r.run_id ASC
|
|
12900
|
+
LIMIT $1`,
|
|
12901
|
+
params
|
|
12902
|
+
);
|
|
12903
|
+
return result.rows.map(rowToRecord);
|
|
12904
|
+
}
|
|
12905
|
+
/** Atomically claims a pending run-queue row. */
|
|
12906
|
+
async claimRun(input) {
|
|
12907
|
+
const maxRunning = positiveLimit(input.maxRunningPerTenant);
|
|
12908
|
+
const sources = input.sources === void 0 ? ["run_api"] : [...input.sources];
|
|
12909
|
+
assertQueueSources(sources);
|
|
12910
|
+
if (maxRunning !== void 0) {
|
|
12911
|
+
return this.claimRunWithTenantCap(input, maxRunning, sources);
|
|
12912
|
+
}
|
|
12913
|
+
try {
|
|
12914
|
+
const result = await this.pool.query(
|
|
12915
|
+
CLAIM_RUN_SQL,
|
|
12916
|
+
[
|
|
12917
|
+
input.runId,
|
|
12918
|
+
input.owner,
|
|
12919
|
+
input.leaseMs,
|
|
12920
|
+
input.inputMessageIds ?? null,
|
|
12921
|
+
sources
|
|
12922
|
+
]
|
|
12923
|
+
);
|
|
12924
|
+
return result.rowCount === 0 ? void 0 : rowToRecord(result.rows[0]);
|
|
12925
|
+
} catch (error) {
|
|
12926
|
+
if (isActiveRunViolation(error)) return void 0;
|
|
12927
|
+
throw error;
|
|
12928
|
+
}
|
|
12929
|
+
}
|
|
12930
|
+
/**
|
|
12931
|
+
* Claims under a per-tenant running cap.
|
|
12932
|
+
*
|
|
12933
|
+
* @remarks Serializes claims for one tenant with a transaction-scoped
|
|
12934
|
+
* advisory lock, counts running rows, then runs the same claim statement.
|
|
12935
|
+
* The lock is shared with admission creates so cap checks cannot race.
|
|
12936
|
+
*/
|
|
12937
|
+
async claimRunWithTenantCap(input, maxRunning, sources) {
|
|
12938
|
+
const client = await this.pool.connect();
|
|
12939
|
+
try {
|
|
12940
|
+
await client.query("BEGIN");
|
|
12941
|
+
const candidate = await client.query(
|
|
12942
|
+
`SELECT tenant_id FROM lattice_agent_runs
|
|
12943
|
+
WHERE run_id = $1 AND source = ANY($2::text[]) AND status = 'pending'`,
|
|
12944
|
+
[input.runId, sources]
|
|
12945
|
+
);
|
|
12946
|
+
if (candidate.rowCount === 0) {
|
|
12947
|
+
await client.query("COMMIT");
|
|
12948
|
+
return void 0;
|
|
12949
|
+
}
|
|
12950
|
+
const tenantId = candidate.rows[0].tenant_id;
|
|
12951
|
+
await client.query("SELECT pg_advisory_xact_lock(hashtext($1))", [
|
|
12952
|
+
tenantId
|
|
12953
|
+
]);
|
|
12954
|
+
const running = await client.query(
|
|
12955
|
+
`SELECT count(*)::text AS count
|
|
12956
|
+
FROM lattice_agent_runs
|
|
12957
|
+
WHERE tenant_id = $1
|
|
12958
|
+
AND source = ANY($2::text[])
|
|
12959
|
+
AND status = 'running'`,
|
|
12960
|
+
[tenantId, sources]
|
|
12961
|
+
);
|
|
12962
|
+
if (Number(running.rows[0].count) >= maxRunning) {
|
|
12963
|
+
await client.query("COMMIT");
|
|
12964
|
+
return void 0;
|
|
12965
|
+
}
|
|
12966
|
+
const result = await client.query(
|
|
12967
|
+
CLAIM_RUN_SQL,
|
|
12968
|
+
[
|
|
12969
|
+
input.runId,
|
|
12970
|
+
input.owner,
|
|
12971
|
+
input.leaseMs,
|
|
12972
|
+
input.inputMessageIds ?? null,
|
|
12973
|
+
sources
|
|
12974
|
+
]
|
|
12975
|
+
);
|
|
12976
|
+
await client.query("COMMIT");
|
|
12977
|
+
return result.rowCount === 0 ? void 0 : rowToRecord(result.rows[0]);
|
|
12978
|
+
} catch (error) {
|
|
12979
|
+
await client.query("ROLLBACK");
|
|
12980
|
+
if (isActiveRunViolation(error)) return void 0;
|
|
12981
|
+
throw error;
|
|
12982
|
+
} finally {
|
|
12983
|
+
client.release();
|
|
12984
|
+
}
|
|
12985
|
+
}
|
|
12986
|
+
/** Captures the checkpoint a running run-queue row started from. */
|
|
12987
|
+
async setBaseCheckpoint(tenantId, runId, owner, baseCheckpointId) {
|
|
12988
|
+
const result = await this.pool.query(
|
|
12989
|
+
`UPDATE lattice_agent_runs
|
|
12990
|
+
SET base_checkpoint_id = $4,
|
|
12991
|
+
base_checkpoint_captured = TRUE,
|
|
12992
|
+
updated_at = now()
|
|
12993
|
+
WHERE tenant_id = $1 AND run_id = $2 AND owner = $3
|
|
12994
|
+
AND status = 'running'
|
|
12995
|
+
AND source IN ('run_api', 'project_task')
|
|
12996
|
+
RETURNING run_id`,
|
|
12997
|
+
[tenantId, runId, owner, baseCheckpointId]
|
|
12998
|
+
);
|
|
12999
|
+
return (result.rowCount ?? 0) > 0;
|
|
13000
|
+
}
|
|
13001
|
+
/** Records the resolved capability runtime revision for a running run. */
|
|
13002
|
+
async setCapabilityRevision(tenantId, runId, owner, revision) {
|
|
13003
|
+
const result = await this.pool.query(
|
|
13004
|
+
`UPDATE lattice_agent_runs
|
|
13005
|
+
SET capability_runtime_revision = $4, updated_at = now()
|
|
13006
|
+
WHERE tenant_id = $1 AND run_id = $2 AND owner = $3
|
|
13007
|
+
AND status = 'running'
|
|
13008
|
+
AND source IN ('run_api', 'project_task')
|
|
13009
|
+
RETURNING run_id`,
|
|
13010
|
+
[tenantId, runId, owner, revision]
|
|
13011
|
+
);
|
|
13012
|
+
return (result.rowCount ?? 0) > 0;
|
|
13013
|
+
}
|
|
13014
|
+
/** Deletes a run row immediately, optionally guarded by status/owner. */
|
|
13015
|
+
async deleteRun(tenantId, runId, expected) {
|
|
13016
|
+
const params = [tenantId, runId];
|
|
13017
|
+
let predicate = "tenant_id = $1 AND run_id = $2";
|
|
13018
|
+
if (expected?.status !== void 0) {
|
|
13019
|
+
params.push(expected.status);
|
|
13020
|
+
predicate += ` AND status = $${params.length}`;
|
|
13021
|
+
}
|
|
13022
|
+
if (expected?.owner !== void 0) {
|
|
13023
|
+
params.push(expected.owner);
|
|
13024
|
+
predicate += ` AND owner = $${params.length}`;
|
|
13025
|
+
}
|
|
13026
|
+
const result = await this.pool.query(
|
|
13027
|
+
`DELETE FROM lattice_agent_runs WHERE ${predicate}`,
|
|
13028
|
+
params
|
|
13029
|
+
);
|
|
13030
|
+
return (result.rowCount ?? 0) > 0;
|
|
13031
|
+
}
|
|
13032
|
+
/** Deletes terminal rows finished before `cutoffMs`, oldest first. */
|
|
13033
|
+
async deleteTerminalBefore(cutoffMs, limit) {
|
|
13034
|
+
if (!Number.isFinite(cutoffMs)) return 0;
|
|
13035
|
+
const max = Number.isFinite(limit) ? Math.floor(limit) : 0;
|
|
13036
|
+
if (max <= 0) return 0;
|
|
13037
|
+
const result = await this.pool.query(
|
|
13038
|
+
`DELETE FROM lattice_agent_runs
|
|
13039
|
+
WHERE run_id IN (
|
|
13040
|
+
SELECT run_id FROM lattice_agent_runs
|
|
13041
|
+
WHERE status IN ('success', 'error', 'interrupted', 'cancelled', 'timeout')
|
|
13042
|
+
AND finished_at < to_timestamp($1::double precision / 1000.0)
|
|
13043
|
+
ORDER BY finished_at ASC
|
|
13044
|
+
LIMIT $2
|
|
13045
|
+
)`,
|
|
13046
|
+
[cutoffMs, max]
|
|
13047
|
+
);
|
|
13048
|
+
return result.rowCount ?? 0;
|
|
13049
|
+
}
|
|
13050
|
+
/** Extends a running run's lease when `owner` still holds it. */
|
|
13051
|
+
async heartbeatRun(runId, owner, leaseMs) {
|
|
13052
|
+
const result = await this.pool.query(
|
|
13053
|
+
`UPDATE lattice_agent_runs
|
|
13054
|
+
SET lease_expires_at = now() + ($3::bigint || ' milliseconds')::interval,
|
|
13055
|
+
updated_at = now()
|
|
13056
|
+
WHERE run_id = $1 AND owner = $2 AND status = 'running'
|
|
13057
|
+
RETURNING run_id`,
|
|
13058
|
+
[runId, owner, leaseMs]
|
|
13059
|
+
);
|
|
13060
|
+
return (result.rowCount ?? 0) > 0;
|
|
13061
|
+
}
|
|
13062
|
+
/** Settles a running run when `owner` still holds it. */
|
|
13063
|
+
async settleRun(runId, owner, status, error) {
|
|
13064
|
+
const result = await this.pool.query(
|
|
13065
|
+
`UPDATE lattice_agent_runs
|
|
13066
|
+
SET status = $3, error = $4, finished_at = now(), updated_at = now()
|
|
13067
|
+
WHERE run_id = $1 AND owner = $2 AND status = 'running'
|
|
13068
|
+
RETURNING run_id`,
|
|
13069
|
+
[runId, owner, status, error ?? null]
|
|
13070
|
+
);
|
|
13071
|
+
return (result.rowCount ?? 0) > 0;
|
|
13072
|
+
}
|
|
13073
|
+
/** Settles a running run only when no rollback is pending. */
|
|
13074
|
+
async settleRunUnlessRollbackRequested(runId, owner, status, error) {
|
|
13075
|
+
const result = await this.pool.query(
|
|
13076
|
+
`UPDATE lattice_agent_runs
|
|
13077
|
+
SET status = $3, error = $4, finished_at = now(), updated_at = now()
|
|
13078
|
+
WHERE run_id = $1 AND owner = $2 AND status = 'running'
|
|
13079
|
+
AND rollback_requested = FALSE
|
|
13080
|
+
RETURNING run_id`,
|
|
13081
|
+
[runId, owner, status, error ?? null]
|
|
13082
|
+
);
|
|
13083
|
+
return (result.rowCount ?? 0) > 0;
|
|
13084
|
+
}
|
|
13085
|
+
/** Terminates an uncaptured rollback run with a repair signal. */
|
|
13086
|
+
async settleRollbackFailure(tenantId, runId, error) {
|
|
13087
|
+
const result = await this.pool.query(
|
|
13088
|
+
`UPDATE lattice_agent_runs
|
|
13089
|
+
SET status = 'error',
|
|
13090
|
+
error = $3,
|
|
13091
|
+
rollback_requested = FALSE,
|
|
13092
|
+
rollback_error = $3,
|
|
13093
|
+
finished_at = now(),
|
|
13094
|
+
updated_at = now()
|
|
13095
|
+
WHERE tenant_id = $1 AND run_id = $2
|
|
13096
|
+
AND source = 'run_api'
|
|
13097
|
+
AND rollback_requested = TRUE
|
|
13098
|
+
AND base_checkpoint_captured = FALSE
|
|
13099
|
+
AND status IN ('running', 'interrupted')
|
|
13100
|
+
RETURNING run_id`,
|
|
13101
|
+
[tenantId, runId, error]
|
|
13102
|
+
);
|
|
13103
|
+
return (result.rowCount ?? 0) > 0;
|
|
13104
|
+
}
|
|
13105
|
+
/** Records one bounded rollback failure and optionally converges. */
|
|
13106
|
+
async recordRollbackFailure(tenantId, runId, error, maxAttempts) {
|
|
13107
|
+
const result = await this.pool.query(
|
|
13108
|
+
`UPDATE lattice_agent_runs
|
|
13109
|
+
SET rollback_attempts = rollback_attempts + 1,
|
|
13110
|
+
rollback_error = $3,
|
|
13111
|
+
status = CASE
|
|
13112
|
+
WHEN rollback_attempts + 1 >= $4 THEN 'error'
|
|
13113
|
+
ELSE status
|
|
13114
|
+
END,
|
|
13115
|
+
error = CASE
|
|
13116
|
+
WHEN rollback_attempts + 1 >= $4 THEN $3
|
|
13117
|
+
ELSE error
|
|
13118
|
+
END,
|
|
13119
|
+
rollback_requested = CASE
|
|
13120
|
+
WHEN rollback_attempts + 1 >= $4 THEN FALSE
|
|
13121
|
+
ELSE rollback_requested
|
|
13122
|
+
END,
|
|
13123
|
+
finished_at = CASE
|
|
13124
|
+
WHEN rollback_attempts + 1 >= $4 THEN now()
|
|
13125
|
+
ELSE finished_at
|
|
13126
|
+
END,
|
|
13127
|
+
updated_at = now()
|
|
13128
|
+
WHERE tenant_id = $1
|
|
13129
|
+
AND run_id = $2
|
|
13130
|
+
AND source = 'run_api'
|
|
13131
|
+
AND rollback_requested = TRUE
|
|
13132
|
+
AND status IN ('running', 'interrupted')
|
|
13133
|
+
RETURNING rollback_attempts, status`,
|
|
13134
|
+
[tenantId, runId, error, maxAttempts]
|
|
13135
|
+
);
|
|
13136
|
+
if (result.rowCount === 0) return void 0;
|
|
13137
|
+
const row = result.rows[0];
|
|
13138
|
+
return {
|
|
13139
|
+
attempts: row.rollback_attempts,
|
|
13140
|
+
terminal: row.status === "error"
|
|
13141
|
+
};
|
|
13142
|
+
}
|
|
13143
|
+
/** Reads a run within a tenant. */
|
|
13144
|
+
async getRun(tenantId, runId) {
|
|
13145
|
+
const result = await this.pool.query(
|
|
13146
|
+
`SELECT * FROM lattice_agent_runs WHERE tenant_id = $1 AND run_id = $2`,
|
|
13147
|
+
[tenantId, runId]
|
|
13148
|
+
);
|
|
13149
|
+
return result.rowCount === 0 ? void 0 : rowToRecord(result.rows[0]);
|
|
13150
|
+
}
|
|
13151
|
+
/** Finds the active (pending/running) run for a ProjectTask. */
|
|
13152
|
+
async getActiveRunForTask(tenantId, taskId) {
|
|
13153
|
+
const result = await this.pool.query(
|
|
13154
|
+
`SELECT * FROM lattice_agent_runs
|
|
13155
|
+
WHERE tenant_id = $1 AND task_id = $2
|
|
13156
|
+
AND status IN ('pending', 'running')
|
|
13157
|
+
ORDER BY created_at DESC, run_id DESC
|
|
13158
|
+
LIMIT 1`,
|
|
13159
|
+
[tenantId, taskId]
|
|
13160
|
+
);
|
|
13161
|
+
return result.rowCount === 0 ? void 0 : rowToRecord(result.rows[0]);
|
|
13162
|
+
}
|
|
13163
|
+
/** Lists a thread's runs, newest first (including pending rows). */
|
|
13164
|
+
async listRuns(tenantId, threadId, options) {
|
|
13165
|
+
const limit = clampLimit(options?.limit);
|
|
13166
|
+
const offset = Math.max(0, Math.floor(options?.offset ?? 0));
|
|
13167
|
+
const result = await this.pool.query(
|
|
13168
|
+
`SELECT * FROM lattice_agent_runs
|
|
13169
|
+
WHERE tenant_id = $1 AND thread_id = $2
|
|
13170
|
+
ORDER BY created_at DESC
|
|
13171
|
+
LIMIT $3 OFFSET $4`,
|
|
13172
|
+
[tenantId, threadId, limit, offset]
|
|
13173
|
+
);
|
|
13174
|
+
return result.rows.map(rowToRecord);
|
|
13175
|
+
}
|
|
13176
|
+
/** Finds the thread's running run, if any. */
|
|
13177
|
+
async findRunning(tenantId, threadId) {
|
|
13178
|
+
const result = await this.pool.query(
|
|
13179
|
+
`SELECT * FROM lattice_agent_runs
|
|
13180
|
+
WHERE tenant_id = $1 AND thread_id = $2 AND status = 'running'
|
|
13181
|
+
LIMIT 1`,
|
|
13182
|
+
[tenantId, threadId]
|
|
13183
|
+
);
|
|
13184
|
+
return result.rowCount === 0 ? void 0 : rowToRecord(result.rows[0]);
|
|
13185
|
+
}
|
|
13186
|
+
/** Requests cancellation, settling pending/expired rows interrupted. */
|
|
13187
|
+
async requestCancel(tenantId, runId) {
|
|
13188
|
+
const client = await this.pool.connect();
|
|
13189
|
+
try {
|
|
13190
|
+
await client.query("BEGIN");
|
|
13191
|
+
const current = await client.query(
|
|
13192
|
+
`SELECT status,
|
|
13193
|
+
COALESCE(lease_expires_at < now(), TRUE) AS expired
|
|
13194
|
+
FROM lattice_agent_runs
|
|
13195
|
+
WHERE tenant_id = $1 AND run_id = $2
|
|
13196
|
+
FOR UPDATE`,
|
|
13197
|
+
[tenantId, runId]
|
|
13198
|
+
);
|
|
13199
|
+
if (current.rowCount === 0) {
|
|
13200
|
+
await client.query("COMMIT");
|
|
13201
|
+
return "not_found";
|
|
13202
|
+
}
|
|
13203
|
+
const row = current.rows[0];
|
|
13204
|
+
if (row.status === "pending") {
|
|
13205
|
+
await client.query(
|
|
13206
|
+
`UPDATE lattice_agent_runs
|
|
13207
|
+
SET status = 'interrupted', cancel_requested = TRUE,
|
|
13208
|
+
finished_at = now(), updated_at = now()
|
|
13209
|
+
WHERE tenant_id = $1 AND run_id = $2`,
|
|
13210
|
+
[tenantId, runId]
|
|
13211
|
+
);
|
|
13212
|
+
await client.query("COMMIT");
|
|
13213
|
+
return "cancelled";
|
|
13214
|
+
}
|
|
13215
|
+
if (row.status !== "running") {
|
|
13216
|
+
await client.query("COMMIT");
|
|
13217
|
+
return "terminal";
|
|
13218
|
+
}
|
|
13219
|
+
if (row.expired) {
|
|
13220
|
+
await client.query(
|
|
13221
|
+
`UPDATE lattice_agent_runs
|
|
13222
|
+
SET status = 'interrupted', cancel_requested = TRUE,
|
|
13223
|
+
finished_at = now(), updated_at = now()
|
|
13224
|
+
WHERE tenant_id = $1 AND run_id = $2`,
|
|
13225
|
+
[tenantId, runId]
|
|
13226
|
+
);
|
|
13227
|
+
await client.query("COMMIT");
|
|
13228
|
+
return "cancelled";
|
|
13229
|
+
}
|
|
13230
|
+
await client.query(
|
|
13231
|
+
`UPDATE lattice_agent_runs
|
|
13232
|
+
SET cancel_requested = TRUE, updated_at = now()
|
|
13233
|
+
WHERE tenant_id = $1 AND run_id = $2`,
|
|
13234
|
+
[tenantId, runId]
|
|
13235
|
+
);
|
|
13236
|
+
await client.query("COMMIT");
|
|
13237
|
+
return "requested";
|
|
13238
|
+
} catch (error) {
|
|
13239
|
+
await client.query("ROLLBACK");
|
|
13240
|
+
throw error;
|
|
13241
|
+
} finally {
|
|
13242
|
+
client.release();
|
|
13243
|
+
}
|
|
13244
|
+
}
|
|
13245
|
+
/** Deletes a pending run or flags a running run for rollback. */
|
|
13246
|
+
async requestRollback(tenantId, runId) {
|
|
13247
|
+
const client = await this.pool.connect();
|
|
13248
|
+
try {
|
|
13249
|
+
await client.query("BEGIN");
|
|
13250
|
+
const current = await client.query(
|
|
13251
|
+
`SELECT status, source FROM lattice_agent_runs
|
|
13252
|
+
WHERE tenant_id = $1 AND run_id = $2
|
|
13253
|
+
FOR UPDATE`,
|
|
13254
|
+
[tenantId, runId]
|
|
13255
|
+
);
|
|
13256
|
+
if (current.rowCount === 0) {
|
|
13257
|
+
await client.query("COMMIT");
|
|
13258
|
+
return "not_found";
|
|
13259
|
+
}
|
|
13260
|
+
const { status, source } = current.rows[0];
|
|
13261
|
+
if (source !== "run_api") {
|
|
13262
|
+
await client.query("COMMIT");
|
|
13263
|
+
return "terminal";
|
|
13264
|
+
}
|
|
13265
|
+
if (status === "pending") {
|
|
13266
|
+
await client.query(
|
|
13267
|
+
`DELETE FROM lattice_agent_runs WHERE tenant_id = $1 AND run_id = $2`,
|
|
13268
|
+
[tenantId, runId]
|
|
13269
|
+
);
|
|
13270
|
+
await client.query("COMMIT");
|
|
13271
|
+
return "deleted";
|
|
13272
|
+
}
|
|
13273
|
+
if (status !== "running") {
|
|
13274
|
+
await client.query("COMMIT");
|
|
13275
|
+
return "terminal";
|
|
13276
|
+
}
|
|
13277
|
+
await client.query(
|
|
13278
|
+
`UPDATE lattice_agent_runs
|
|
13279
|
+
SET rollback_requested = TRUE, cancel_requested = TRUE,
|
|
13280
|
+
updated_at = now()
|
|
13281
|
+
WHERE tenant_id = $1 AND run_id = $2`,
|
|
13282
|
+
[tenantId, runId]
|
|
13283
|
+
);
|
|
13284
|
+
await client.query("COMMIT");
|
|
13285
|
+
return "requested";
|
|
13286
|
+
} catch (error) {
|
|
13287
|
+
await client.query("ROLLBACK");
|
|
13288
|
+
throw error;
|
|
13289
|
+
} finally {
|
|
13290
|
+
client.release();
|
|
13291
|
+
}
|
|
13292
|
+
}
|
|
13293
|
+
/** Atomically reclaims an expired running row without requeueing it. */
|
|
13294
|
+
async recoverExpiredRun(runId, owner, leaseMs) {
|
|
13295
|
+
const result = await this.pool.query(
|
|
13296
|
+
`UPDATE lattice_agent_runs
|
|
13297
|
+
SET owner = $2,
|
|
13298
|
+
lease_expires_at = now() + ($3::bigint || ' milliseconds')::interval,
|
|
13299
|
+
attempts = attempts + 1,
|
|
13300
|
+
updated_at = now()
|
|
13301
|
+
WHERE run_id = $1
|
|
13302
|
+
AND source IN ('run_api', 'project_task')
|
|
13303
|
+
AND status = 'running'
|
|
13304
|
+
AND lease_expires_at < now()
|
|
13305
|
+
RETURNING *`,
|
|
13306
|
+
[runId, owner, leaseMs]
|
|
13307
|
+
);
|
|
13308
|
+
return result.rowCount === 0 ? void 0 : rowToRecord(result.rows[0]);
|
|
13309
|
+
}
|
|
13310
|
+
/** Atomically requeues or interrupts an expired run-queue row. */
|
|
13311
|
+
async settleExpiredRun(runId, outcome, options) {
|
|
13312
|
+
if (outcome === "pending") {
|
|
13313
|
+
const availableAt = options?.availableAt ?? Date.now();
|
|
13314
|
+
const result2 = await this.pool.query(
|
|
13315
|
+
`UPDATE lattice_agent_runs
|
|
13316
|
+
SET status = 'pending',
|
|
13317
|
+
owner = NULL,
|
|
13318
|
+
lease_expires_at = NULL,
|
|
13319
|
+
started_at = NULL,
|
|
13320
|
+
available_at = to_timestamp($2::double precision / 1000.0),
|
|
13321
|
+
finished_at = NULL,
|
|
13322
|
+
updated_at = now()
|
|
13323
|
+
WHERE run_id = $1
|
|
13324
|
+
AND source IN ('run_api', 'project_task')
|
|
13325
|
+
AND status = 'running'
|
|
13326
|
+
AND lease_expires_at < now()
|
|
13327
|
+
RETURNING run_id`,
|
|
13328
|
+
[runId, availableAt]
|
|
13329
|
+
);
|
|
13330
|
+
return (result2.rowCount ?? 0) > 0;
|
|
13331
|
+
}
|
|
13332
|
+
const result = await this.pool.query(
|
|
13333
|
+
`UPDATE lattice_agent_runs
|
|
13334
|
+
SET status = $3,
|
|
13335
|
+
error = COALESCE($2, error),
|
|
13336
|
+
finished_at = now(),
|
|
13337
|
+
updated_at = now()
|
|
13338
|
+
WHERE run_id = $1
|
|
13339
|
+
AND source IN ('run_api', 'project_task')
|
|
13340
|
+
AND status = 'running'
|
|
13341
|
+
AND lease_expires_at < now()
|
|
13342
|
+
RETURNING run_id`,
|
|
13343
|
+
[
|
|
13344
|
+
runId,
|
|
13345
|
+
options?.error ?? null,
|
|
13346
|
+
outcome === "timeout" ? "timeout" : "interrupted"
|
|
13347
|
+
]
|
|
13348
|
+
);
|
|
13349
|
+
return (result.rowCount ?? 0) > 0;
|
|
13350
|
+
}
|
|
13351
|
+
/** Counts runs grouped by lifecycle status. */
|
|
13352
|
+
async counts() {
|
|
13353
|
+
const counts = {
|
|
13354
|
+
pending: 0,
|
|
13355
|
+
running: 0,
|
|
13356
|
+
success: 0,
|
|
13357
|
+
error: 0,
|
|
13358
|
+
interrupted: 0,
|
|
13359
|
+
cancelled: 0,
|
|
13360
|
+
timeout: 0
|
|
13361
|
+
};
|
|
13362
|
+
const result = await this.pool.query(
|
|
13363
|
+
`SELECT status, count(*)::int AS count
|
|
13364
|
+
FROM lattice_agent_runs
|
|
13365
|
+
GROUP BY status`
|
|
13366
|
+
);
|
|
13367
|
+
for (const row of result.rows) {
|
|
13368
|
+
const status = parseStatus(row.status);
|
|
13369
|
+
if (status) counts[status] = row.count;
|
|
13370
|
+
}
|
|
13371
|
+
return counts;
|
|
13372
|
+
}
|
|
13373
|
+
/** Reports expired run-queue rows for the requested sources. */
|
|
13374
|
+
async reapExpired(sources = ["run_api"]) {
|
|
13375
|
+
assertQueueSources(sources);
|
|
13376
|
+
const result = await this.pool.query(
|
|
13377
|
+
`SELECT * FROM lattice_agent_runs
|
|
13378
|
+
WHERE source = ANY($1::text[]) AND status = 'running'
|
|
13379
|
+
AND lease_expires_at < now()
|
|
13380
|
+
ORDER BY lease_expires_at ASC`,
|
|
13381
|
+
[[...sources]]
|
|
13382
|
+
);
|
|
13383
|
+
return result.rows.map(reapedFromRow);
|
|
13384
|
+
}
|
|
13385
|
+
/** Lists rollback work that still needs a checkpoint rewind. */
|
|
13386
|
+
async listRollbackRuns() {
|
|
13387
|
+
const result = await this.pool.query(
|
|
13388
|
+
`SELECT * FROM lattice_agent_runs
|
|
13389
|
+
WHERE source = 'run_api'
|
|
13390
|
+
AND rollback_requested = TRUE
|
|
13391
|
+
AND (
|
|
13392
|
+
status = 'interrupted'
|
|
13393
|
+
OR (status = 'running' AND lease_expires_at < now())
|
|
13394
|
+
)
|
|
13395
|
+
ORDER BY updated_at ASC`
|
|
13396
|
+
);
|
|
13397
|
+
return result.rows.map(reapedFromRow);
|
|
13398
|
+
}
|
|
13399
|
+
/** Settles expired legacy rows, preserving the historical supervisor behavior. */
|
|
13400
|
+
async reapLegacyExpired() {
|
|
13401
|
+
const result = await this.pool.query(
|
|
13402
|
+
`UPDATE lattice_agent_runs
|
|
13403
|
+
SET status = 'interrupted',
|
|
13404
|
+
error = COALESCE(error, 'run owner lost'),
|
|
13405
|
+
finished_at = now(),
|
|
13406
|
+
updated_at = now()
|
|
13407
|
+
WHERE source = 'legacy' AND status = 'running'
|
|
13408
|
+
AND lease_expires_at < now()
|
|
13409
|
+
RETURNING run_id, tenant_id, assistant_id, thread_id, workspace_id, project_id`
|
|
13410
|
+
);
|
|
13411
|
+
return result.rows.map((row) => ({
|
|
13412
|
+
runId: row.run_id,
|
|
13413
|
+
tenantId: row.tenant_id,
|
|
13414
|
+
assistantId: row.assistant_id,
|
|
13415
|
+
threadId: row.thread_id,
|
|
13416
|
+
workspaceId: row.workspace_id ?? void 0,
|
|
13417
|
+
projectId: row.project_id ?? void 0
|
|
13418
|
+
}));
|
|
13419
|
+
}
|
|
13420
|
+
};
|
|
13421
|
+
function positiveLimit(limit) {
|
|
13422
|
+
return limit !== void 0 && Number.isSafeInteger(limit) && limit > 0 ? limit : void 0;
|
|
13423
|
+
}
|
|
13424
|
+
function clampLimit(limit) {
|
|
13425
|
+
if (limit === void 0) return DEFAULT_LIST_LIMIT;
|
|
13426
|
+
const floored = Math.floor(limit);
|
|
13427
|
+
if (!Number.isFinite(floored)) return DEFAULT_LIST_LIMIT;
|
|
13428
|
+
return Math.min(MAX_LIST_LIMIT, Math.max(1, floored));
|
|
13429
|
+
}
|
|
13430
|
+
function jsonParam(value) {
|
|
13431
|
+
return value === void 0 || value === null ? null : JSON.stringify(value);
|
|
13432
|
+
}
|
|
13433
|
+
function parseStatus(value) {
|
|
13434
|
+
switch (value) {
|
|
13435
|
+
case "pending":
|
|
13436
|
+
case "running":
|
|
13437
|
+
case "success":
|
|
13438
|
+
case "error":
|
|
13439
|
+
case "interrupted":
|
|
13440
|
+
case "cancelled":
|
|
13441
|
+
case "timeout":
|
|
13442
|
+
return value;
|
|
13443
|
+
default:
|
|
13444
|
+
return void 0;
|
|
13445
|
+
}
|
|
13446
|
+
}
|
|
13447
|
+
function parseSource(value) {
|
|
13448
|
+
switch (value) {
|
|
13449
|
+
case "run_api":
|
|
13450
|
+
return "run_api";
|
|
13451
|
+
case "project_task":
|
|
13452
|
+
return "project_task";
|
|
13453
|
+
default:
|
|
13454
|
+
return "legacy";
|
|
13455
|
+
}
|
|
13456
|
+
}
|
|
13457
|
+
function toJsonObject(value) {
|
|
13458
|
+
if (value === null || value === void 0) return null;
|
|
13459
|
+
if (typeof value !== "object" || Array.isArray(value)) return null;
|
|
13460
|
+
return value;
|
|
13461
|
+
}
|
|
13462
|
+
function rowToRecord(row) {
|
|
13463
|
+
return {
|
|
13464
|
+
runId: row.run_id,
|
|
13465
|
+
tenantId: row.tenant_id,
|
|
13466
|
+
workspaceId: row.workspace_id ?? void 0,
|
|
13467
|
+
projectId: row.project_id ?? void 0,
|
|
13468
|
+
assistantId: row.assistant_id,
|
|
13469
|
+
threadId: row.thread_id,
|
|
13470
|
+
status: parseStatus(row.status) ?? "error",
|
|
13471
|
+
source: parseSource(row.source),
|
|
13472
|
+
taskId: row.task_id ?? void 0,
|
|
13473
|
+
owner: row.owner,
|
|
13474
|
+
leaseExpiresAt: row.lease_expires_at === null ? null : row.lease_expires_at.getTime(),
|
|
13475
|
+
inputMessageIds: row.input_message_ids,
|
|
13476
|
+
attempts: row.attempts,
|
|
13477
|
+
error: row.error,
|
|
13478
|
+
cancelRequested: row.cancel_requested,
|
|
13479
|
+
rollbackRequested: row.rollback_requested,
|
|
13480
|
+
rollbackAttempts: row.rollback_attempts,
|
|
13481
|
+
rollbackError: row.rollback_error,
|
|
13482
|
+
input: toJsonObject(row.input),
|
|
13483
|
+
command: toJsonObject(row.command),
|
|
13484
|
+
config: toJsonObject(row.config),
|
|
13485
|
+
metadata: toJsonObject(row.metadata),
|
|
13486
|
+
baseCheckpointId: row.base_checkpoint_id,
|
|
13487
|
+
baseCheckpointCaptured: row.base_checkpoint_captured,
|
|
13488
|
+
capabilityRuntimeRevision: row.capability_runtime_revision,
|
|
13489
|
+
availableAt: row.available_at.getTime(),
|
|
13490
|
+
createdAt: row.created_at.getTime(),
|
|
13491
|
+
updatedAt: row.updated_at.getTime(),
|
|
13492
|
+
startedAt: row.started_at === null ? null : row.started_at.getTime(),
|
|
13493
|
+
finishedAt: row.finished_at === null ? null : row.finished_at.getTime()
|
|
13494
|
+
};
|
|
13495
|
+
}
|
|
13496
|
+
function reapedFromRow(row) {
|
|
13497
|
+
return {
|
|
13498
|
+
runId: row.run_id,
|
|
13499
|
+
tenantId: row.tenant_id,
|
|
13500
|
+
assistantId: row.assistant_id,
|
|
13501
|
+
threadId: row.thread_id,
|
|
13502
|
+
workspaceId: row.workspace_id ?? void 0,
|
|
13503
|
+
projectId: row.project_id ?? void 0,
|
|
13504
|
+
source: parseSource(row.source),
|
|
13505
|
+
taskId: row.task_id ?? void 0,
|
|
13506
|
+
attempts: row.attempts,
|
|
13507
|
+
cancelRequested: row.cancel_requested,
|
|
13508
|
+
rollbackRequested: row.rollback_requested,
|
|
13509
|
+
baseCheckpointId: row.base_checkpoint_id,
|
|
13510
|
+
createdAt: row.created_at.getTime(),
|
|
13511
|
+
startedAt: row.started_at === null ? null : row.started_at.getTime(),
|
|
13512
|
+
leaseExpiresAt: row.lease_expires_at === null ? null : row.lease_expires_at.getTime()
|
|
13513
|
+
};
|
|
13514
|
+
}
|
|
13515
|
+
function isActiveRunViolation(error) {
|
|
13516
|
+
if (typeof error !== "object" || error === null) return false;
|
|
13517
|
+
const { code, constraint } = error;
|
|
13518
|
+
return code === "23505" && constraint === "uq_lattice_agent_runs_one_running";
|
|
13519
|
+
}
|
|
13520
|
+
|
|
13521
|
+
// src/stores/PostgreSQLChannelReplyJobStore.ts
|
|
13522
|
+
import { Pool as Pool31 } from "pg";
|
|
13523
|
+
import {
|
|
13524
|
+
assertValidCreateChannelReplyJobInput,
|
|
13525
|
+
normalizeChannelReplyJobListLimit
|
|
13526
|
+
} from "@axiom-lattice/protocols";
|
|
13527
|
+
var DB_CLOCK_MS2 = "FLOOR(EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint";
|
|
13528
|
+
var COLUMNS = `run_id, tenant_id, assistant_id, thread_id, adapter_channel,
|
|
13529
|
+
channel_installation_id, reply_target, input_message_id, stream_cursor, status,
|
|
13530
|
+
attempts, available_at, lease_owner, lease_expires_at, last_error,
|
|
13531
|
+
output_message_id, created_at, updated_at, delivered_at`;
|
|
13532
|
+
function toNumber(value) {
|
|
13533
|
+
if (typeof value === "number") return value;
|
|
13534
|
+
if (typeof value === "string") return Number(value);
|
|
13535
|
+
throw new Error("Invalid channel reply job numeric column");
|
|
13536
|
+
}
|
|
13537
|
+
function toNullableNumber(value) {
|
|
13538
|
+
return value === null ? null : toNumber(value);
|
|
13539
|
+
}
|
|
13540
|
+
function toStatus(value) {
|
|
13541
|
+
if (value === "pending" || value === "delivered" || value === "failed") {
|
|
13542
|
+
return value;
|
|
13543
|
+
}
|
|
13544
|
+
throw new Error(`Invalid channel reply job status: ${value}`);
|
|
13545
|
+
}
|
|
13546
|
+
function mapRow9(row) {
|
|
13547
|
+
return {
|
|
13548
|
+
runId: row.run_id,
|
|
13549
|
+
tenantId: row.tenant_id,
|
|
13550
|
+
assistantId: row.assistant_id,
|
|
13551
|
+
threadId: row.thread_id,
|
|
13552
|
+
adapterChannel: row.adapter_channel,
|
|
13553
|
+
channelInstallationId: row.channel_installation_id,
|
|
13554
|
+
replyTarget: row.reply_target,
|
|
13555
|
+
inputMessageId: row.input_message_id,
|
|
13556
|
+
streamCursor: row.stream_cursor,
|
|
13557
|
+
status: toStatus(row.status),
|
|
13558
|
+
attempts: toNumber(row.attempts),
|
|
13559
|
+
availableAt: toNumber(row.available_at),
|
|
13560
|
+
leaseOwner: row.lease_owner,
|
|
13561
|
+
leaseExpiresAt: toNullableNumber(row.lease_expires_at),
|
|
13562
|
+
lastError: row.last_error,
|
|
13563
|
+
outputMessageId: row.output_message_id,
|
|
13564
|
+
createdAt: toNumber(row.created_at),
|
|
13565
|
+
updatedAt: toNumber(row.updated_at),
|
|
13566
|
+
deliveredAt: toNullableNumber(row.delivered_at)
|
|
13567
|
+
};
|
|
13568
|
+
}
|
|
13569
|
+
var PostgreSQLChannelReplyJobStore = class {
|
|
13570
|
+
/** @param options An externally managed pool, or a pool config/connection URL. */
|
|
13571
|
+
constructor(options) {
|
|
13572
|
+
this.ownsPool = true;
|
|
13573
|
+
this.initialized = false;
|
|
13574
|
+
this.initPromise = null;
|
|
13575
|
+
if (options.pool) {
|
|
13576
|
+
this.pool = options.pool;
|
|
13577
|
+
this.ownsPool = false;
|
|
13578
|
+
this.initialized = true;
|
|
13579
|
+
return;
|
|
13580
|
+
}
|
|
13581
|
+
if (typeof options.poolConfig === "string") {
|
|
13582
|
+
this.pool = new Pool31({ connectionString: options.poolConfig });
|
|
13583
|
+
} else if (options.poolConfig) {
|
|
13584
|
+
this.pool = new Pool31(options.poolConfig);
|
|
13585
|
+
} else {
|
|
13586
|
+
throw new Error("Either pool or poolConfig must be provided");
|
|
13587
|
+
}
|
|
13588
|
+
this.migrationManager = new MigrationManager(this.pool);
|
|
13589
|
+
this.migrationManager.register(createChannelReplyJobsTable);
|
|
13590
|
+
if (options.autoMigrate !== false) {
|
|
13591
|
+
this.initialize().catch((error) => {
|
|
13592
|
+
console.error("Failed to initialize PostgreSQLChannelReplyJobStore:", error);
|
|
13593
|
+
throw error;
|
|
13594
|
+
});
|
|
13595
|
+
}
|
|
13596
|
+
}
|
|
13597
|
+
/** Runs pending migrations once. */
|
|
13598
|
+
async initialize() {
|
|
13599
|
+
if (this.initialized) return;
|
|
13600
|
+
if (this.initPromise) return this.initPromise;
|
|
13601
|
+
this.initPromise = (async () => {
|
|
13602
|
+
try {
|
|
13603
|
+
await this.migrationManager.migrate();
|
|
13604
|
+
this.initialized = true;
|
|
13605
|
+
} finally {
|
|
13606
|
+
this.initPromise = null;
|
|
13607
|
+
}
|
|
13608
|
+
})();
|
|
13609
|
+
return this.initPromise;
|
|
13610
|
+
}
|
|
13611
|
+
/** Ends the pool when this store owns it. */
|
|
13612
|
+
async dispose() {
|
|
13613
|
+
if (this.ownsPool) await this.pool.end();
|
|
13614
|
+
}
|
|
13615
|
+
/** Creates a pending job with database-clock created/updated timestamps. */
|
|
13616
|
+
async create(input) {
|
|
13617
|
+
assertValidCreateChannelReplyJobInput(input);
|
|
13618
|
+
const result = await this.pool.query(
|
|
13619
|
+
`WITH clock AS (SELECT ${DB_CLOCK_MS2} AS now_ms)
|
|
13620
|
+
INSERT INTO lattice_channel_reply_jobs (
|
|
13621
|
+
run_id, tenant_id, assistant_id, thread_id, adapter_channel,
|
|
13622
|
+
channel_installation_id, reply_target, input_message_id, stream_cursor,
|
|
13623
|
+
status, attempts, available_at, lease_owner, lease_expires_at,
|
|
13624
|
+
last_error, output_message_id, created_at, updated_at, delivered_at
|
|
13625
|
+
)
|
|
13626
|
+
SELECT
|
|
13627
|
+
$1, $2, $3, $4, $5,
|
|
13628
|
+
$6, $7::jsonb, $8, $9,
|
|
13629
|
+
'pending', 0, $10, NULL, NULL,
|
|
13630
|
+
NULL, NULL, clock.now_ms, clock.now_ms, NULL
|
|
13631
|
+
FROM clock
|
|
13632
|
+
RETURNING ${COLUMNS}`,
|
|
13633
|
+
[
|
|
13634
|
+
input.runId,
|
|
13635
|
+
input.tenantId,
|
|
13636
|
+
input.assistantId,
|
|
13637
|
+
input.threadId,
|
|
13638
|
+
input.adapterChannel,
|
|
13639
|
+
input.channelInstallationId,
|
|
13640
|
+
JSON.stringify(input.replyTarget),
|
|
13641
|
+
input.inputMessageId,
|
|
13642
|
+
input.streamCursor ?? null,
|
|
13643
|
+
input.availableAt
|
|
13644
|
+
]
|
|
13645
|
+
);
|
|
13646
|
+
return mapRow9(result.rows[0]);
|
|
13647
|
+
}
|
|
13648
|
+
/** Reads one job within a tenant scope. */
|
|
13649
|
+
async get(tenantId, runId) {
|
|
13650
|
+
const result = await this.pool.query(
|
|
13651
|
+
`SELECT ${COLUMNS}
|
|
13652
|
+
FROM lattice_channel_reply_jobs
|
|
13653
|
+
WHERE tenant_id = $1 AND run_id = $2`,
|
|
13654
|
+
[tenantId, runId]
|
|
13655
|
+
);
|
|
13656
|
+
return result.rowCount === 0 ? null : mapRow9(result.rows[0]);
|
|
13657
|
+
}
|
|
13658
|
+
/** Lists claimable pending jobs that are due at `now`, oldest first. */
|
|
13659
|
+
async listPending(now, limit) {
|
|
13660
|
+
const result = await this.pool.query(
|
|
13661
|
+
`SELECT ${COLUMNS}
|
|
13662
|
+
FROM lattice_channel_reply_jobs
|
|
13663
|
+
WHERE status = 'pending'
|
|
13664
|
+
AND available_at <= $1
|
|
13665
|
+
AND (lease_owner IS NULL OR lease_expires_at IS NULL OR lease_expires_at <= $1)
|
|
13666
|
+
ORDER BY available_at ASC, created_at ASC, run_id ASC
|
|
13667
|
+
LIMIT $2`,
|
|
13668
|
+
[now, normalizeChannelReplyJobListLimit(limit)]
|
|
13669
|
+
);
|
|
13670
|
+
return result.rows.map(mapRow9);
|
|
13671
|
+
}
|
|
13672
|
+
/**
|
|
13673
|
+
* Atomically leases a claimable job.
|
|
13674
|
+
*
|
|
13675
|
+
* @param now Accepted for interface symmetry; PostgreSQL uses the database clock.
|
|
13676
|
+
*/
|
|
13677
|
+
async claim(tenantId, runId, owner, leaseMs, now) {
|
|
13678
|
+
void now;
|
|
13679
|
+
const result = await this.pool.query(
|
|
13680
|
+
`WITH clock AS (SELECT ${DB_CLOCK_MS2} AS now_ms)
|
|
13681
|
+
UPDATE lattice_channel_reply_jobs AS j
|
|
13682
|
+
SET lease_owner = $3,
|
|
13683
|
+
lease_expires_at = clock.now_ms + $4,
|
|
13684
|
+
updated_at = clock.now_ms
|
|
13685
|
+
FROM clock
|
|
13686
|
+
WHERE j.tenant_id = $1
|
|
13687
|
+
AND j.run_id = $2
|
|
13688
|
+
AND j.status = 'pending'
|
|
13689
|
+
AND j.available_at <= clock.now_ms
|
|
13690
|
+
AND (j.lease_owner IS NULL OR j.lease_expires_at IS NULL OR j.lease_expires_at <= clock.now_ms)
|
|
13691
|
+
RETURNING ${COLUMNS}`,
|
|
13692
|
+
[tenantId, runId, owner, leaseMs]
|
|
13693
|
+
);
|
|
13694
|
+
return result.rowCount === 0 ? null : mapRow9(result.rows[0]);
|
|
13695
|
+
}
|
|
13696
|
+
/**
|
|
13697
|
+
* Marks a job delivered when `owner` still holds the lease.
|
|
13698
|
+
*
|
|
13699
|
+
* @param now Accepted for interface symmetry; PostgreSQL uses the database clock.
|
|
13700
|
+
*/
|
|
13701
|
+
async markDelivered(tenantId, runId, owner, outputMessageId = null, now) {
|
|
13702
|
+
void now;
|
|
13703
|
+
const result = await this.pool.query(
|
|
13704
|
+
`WITH clock AS (SELECT ${DB_CLOCK_MS2} AS now_ms)
|
|
13705
|
+
UPDATE lattice_channel_reply_jobs AS j
|
|
13706
|
+
SET status = 'delivered',
|
|
13707
|
+
output_message_id = $4,
|
|
13708
|
+
lease_owner = NULL,
|
|
13709
|
+
lease_expires_at = NULL,
|
|
13710
|
+
delivered_at = clock.now_ms,
|
|
13711
|
+
updated_at = clock.now_ms
|
|
13712
|
+
FROM clock
|
|
13713
|
+
WHERE j.tenant_id = $1
|
|
13714
|
+
AND j.run_id = $2
|
|
13715
|
+
AND j.lease_owner = $3
|
|
13716
|
+
AND j.status = 'pending'
|
|
13717
|
+
RETURNING ${COLUMNS}`,
|
|
13718
|
+
[tenantId, runId, owner, outputMessageId]
|
|
13719
|
+
);
|
|
13720
|
+
return result.rowCount === 0 ? null : mapRow9(result.rows[0]);
|
|
13721
|
+
}
|
|
13722
|
+
/**
|
|
13723
|
+
* Returns a job to pending with an incremented attempt count.
|
|
13724
|
+
*
|
|
13725
|
+
* @param now Accepted for interface symmetry; PostgreSQL uses the database clock.
|
|
13726
|
+
*/
|
|
13727
|
+
async reschedule(tenantId, runId, owner, error, availableAt, now) {
|
|
13728
|
+
void now;
|
|
13729
|
+
const result = await this.pool.query(
|
|
13730
|
+
`WITH clock AS (SELECT ${DB_CLOCK_MS2} AS now_ms)
|
|
13731
|
+
UPDATE lattice_channel_reply_jobs AS j
|
|
13732
|
+
SET status = 'pending',
|
|
13733
|
+
attempts = attempts + 1,
|
|
13734
|
+
lease_owner = NULL,
|
|
13735
|
+
lease_expires_at = NULL,
|
|
13736
|
+
last_error = $4,
|
|
13737
|
+
available_at = $5,
|
|
13738
|
+
updated_at = clock.now_ms
|
|
13739
|
+
FROM clock
|
|
13740
|
+
WHERE j.tenant_id = $1
|
|
13741
|
+
AND j.run_id = $2
|
|
13742
|
+
AND j.lease_owner = $3
|
|
13743
|
+
AND j.status = 'pending'
|
|
13744
|
+
RETURNING ${COLUMNS}`,
|
|
13745
|
+
[tenantId, runId, owner, error, availableAt]
|
|
13746
|
+
);
|
|
13747
|
+
return result.rowCount === 0 ? null : mapRow9(result.rows[0]);
|
|
13748
|
+
}
|
|
13749
|
+
/**
|
|
13750
|
+
* Marks a job terminally failed when `owner` still holds the lease.
|
|
13751
|
+
*
|
|
13752
|
+
* @param now Accepted for interface symmetry; PostgreSQL uses the database clock.
|
|
13753
|
+
*/
|
|
13754
|
+
async markFailed(tenantId, runId, owner, error, now) {
|
|
13755
|
+
void now;
|
|
13756
|
+
const result = await this.pool.query(
|
|
13757
|
+
`WITH clock AS (SELECT ${DB_CLOCK_MS2} AS now_ms)
|
|
13758
|
+
UPDATE lattice_channel_reply_jobs AS j
|
|
13759
|
+
SET status = 'failed',
|
|
13760
|
+
last_error = $4,
|
|
13761
|
+
lease_owner = NULL,
|
|
13762
|
+
lease_expires_at = NULL,
|
|
13763
|
+
updated_at = clock.now_ms
|
|
13764
|
+
FROM clock
|
|
13765
|
+
WHERE j.tenant_id = $1
|
|
13766
|
+
AND j.run_id = $2
|
|
13767
|
+
AND j.lease_owner = $3
|
|
13768
|
+
AND j.status = 'pending'
|
|
13769
|
+
RETURNING ${COLUMNS}`,
|
|
13770
|
+
[tenantId, runId, owner, error]
|
|
13771
|
+
);
|
|
13772
|
+
return result.rowCount === 0 ? null : mapRow9(result.rows[0]);
|
|
13773
|
+
}
|
|
13774
|
+
/** Deletes one job. Returns whether a row was removed. */
|
|
13775
|
+
async delete(tenantId, runId) {
|
|
13776
|
+
const result = await this.pool.query(
|
|
13777
|
+
`DELETE FROM lattice_channel_reply_jobs WHERE tenant_id = $1 AND run_id = $2`,
|
|
13778
|
+
[tenantId, runId]
|
|
13779
|
+
);
|
|
13780
|
+
return (result.rowCount ?? 0) > 0;
|
|
13781
|
+
}
|
|
13782
|
+
/** Deletes terminal jobs whose last update is older than `before`. */
|
|
13783
|
+
async deleteTerminalBefore(before, limit) {
|
|
13784
|
+
const result = await this.pool.query(
|
|
13785
|
+
`DELETE FROM lattice_channel_reply_jobs
|
|
13786
|
+
WHERE run_id IN (
|
|
13787
|
+
SELECT run_id
|
|
13788
|
+
FROM lattice_channel_reply_jobs
|
|
13789
|
+
WHERE status IN ('delivered', 'failed')
|
|
13790
|
+
AND updated_at < $1
|
|
13791
|
+
ORDER BY updated_at ASC, run_id ASC
|
|
13792
|
+
LIMIT $2
|
|
13793
|
+
)`,
|
|
13794
|
+
[before, normalizeChannelReplyJobListLimit(limit)]
|
|
13795
|
+
);
|
|
13796
|
+
return result.rowCount ?? 0;
|
|
13797
|
+
}
|
|
13798
|
+
};
|
|
13799
|
+
|
|
13800
|
+
// src/stores/PostgreSQLCheckpointStorage.ts
|
|
13801
|
+
var CheckpointTargetNotEmptyError = class extends Error {
|
|
13802
|
+
/**
|
|
13803
|
+
* @param threadId - Non-empty target thread id.
|
|
13804
|
+
*/
|
|
13805
|
+
constructor(threadId) {
|
|
13806
|
+
super(`Checkpoint target thread ${threadId} is not empty`);
|
|
13807
|
+
/** Machine-readable conflict code. */
|
|
13808
|
+
this.code = "CHECKPOINT_TARGET_NOT_EMPTY";
|
|
13809
|
+
this.name = "CheckpointTargetNotEmptyError";
|
|
13810
|
+
}
|
|
13811
|
+
};
|
|
13812
|
+
var CheckpointBaseNotFoundError = class extends Error {
|
|
13813
|
+
/**
|
|
13814
|
+
* @param threadId - Thread owning the missing base checkpoint.
|
|
13815
|
+
* @param baseCheckpointId - Base checkpoint id that was not found.
|
|
13816
|
+
*/
|
|
13817
|
+
constructor(threadId, baseCheckpointId) {
|
|
13818
|
+
super(`Checkpoint ${baseCheckpointId} does not exist on thread ${threadId}`);
|
|
13819
|
+
/** Machine-readable failure code. */
|
|
13820
|
+
this.code = "CHECKPOINT_BASE_NOT_FOUND";
|
|
13821
|
+
this.name = "CheckpointBaseNotFoundError";
|
|
13822
|
+
this.threadId = threadId;
|
|
13823
|
+
this.baseCheckpointId = baseCheckpointId;
|
|
13824
|
+
}
|
|
13825
|
+
};
|
|
13826
|
+
async function rollbackQuietly(client) {
|
|
13827
|
+
try {
|
|
13828
|
+
await client.query("ROLLBACK");
|
|
13829
|
+
} catch {
|
|
13830
|
+
}
|
|
13831
|
+
}
|
|
13832
|
+
function isUniqueViolation3(error) {
|
|
13833
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "23505";
|
|
13834
|
+
}
|
|
13835
|
+
function isRecord4(value) {
|
|
13836
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
13837
|
+
}
|
|
13838
|
+
function checkpointKey(checkpointNs, checkpointId) {
|
|
13839
|
+
return `${checkpointNs}\0${checkpointId}`;
|
|
13840
|
+
}
|
|
13841
|
+
function blobKey(checkpointNs, channel, version) {
|
|
13842
|
+
return `${checkpointNs}\0${channel}\0${version}`;
|
|
13843
|
+
}
|
|
13844
|
+
function checkpointRecord(value) {
|
|
13845
|
+
if (typeof value === "string") {
|
|
13846
|
+
try {
|
|
13847
|
+
const parsed = JSON.parse(value);
|
|
13848
|
+
return isRecord4(parsed) ? parsed : void 0;
|
|
13849
|
+
} catch {
|
|
13850
|
+
return void 0;
|
|
13851
|
+
}
|
|
13852
|
+
}
|
|
13853
|
+
return isRecord4(value) ? value : void 0;
|
|
13854
|
+
}
|
|
13855
|
+
function channelVersions(value) {
|
|
13856
|
+
const checkpoint = checkpointRecord(value);
|
|
13857
|
+
const versions = checkpoint?.channel_versions;
|
|
13858
|
+
return isRecord4(versions) ? versions : void 0;
|
|
13859
|
+
}
|
|
13860
|
+
var DELETE_CHUNK_SIZE = 100;
|
|
13861
|
+
function chunk(items, size) {
|
|
13862
|
+
const chunks = [];
|
|
13863
|
+
for (let index = 0; index < items.length; index += size) {
|
|
13864
|
+
chunks.push(items.slice(index, index + size));
|
|
13865
|
+
}
|
|
13866
|
+
return chunks;
|
|
13867
|
+
}
|
|
13868
|
+
function toCount(value) {
|
|
13869
|
+
const parsed = Number(value ?? 0);
|
|
13870
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
13871
|
+
}
|
|
13872
|
+
var PostgreSQLCheckpointStorage = class {
|
|
13873
|
+
/**
|
|
13874
|
+
* @param pool - PostgreSQL pool shared with the rest of the store config.
|
|
13875
|
+
*/
|
|
13876
|
+
constructor(pool) {
|
|
13877
|
+
this.pool = pool;
|
|
13878
|
+
}
|
|
13879
|
+
/**
|
|
13880
|
+
* Measure the checkpoint storage used by one thread.
|
|
13881
|
+
*
|
|
13882
|
+
* @param threadId - Thread id whose checkpoint rows should be measured.
|
|
13883
|
+
* @returns Counts and byte sizes grouped by checkpoint storage category.
|
|
13884
|
+
*/
|
|
13885
|
+
async get(threadId) {
|
|
13886
|
+
const result = await this.pool.query(
|
|
13887
|
+
`SELECT
|
|
13888
|
+
(SELECT count(*)::bigint FROM checkpoints WHERE thread_id=$1) AS checkpoint_count,
|
|
13889
|
+
(SELECT coalesce(sum(pg_column_size(checkpoint) + pg_column_size(metadata)),0)::bigint FROM checkpoints WHERE thread_id=$1) AS checkpoint_bytes,
|
|
13890
|
+
(SELECT count(*)::bigint FROM checkpoint_blobs WHERE thread_id=$1) AS blob_count,
|
|
13891
|
+
(SELECT coalesce(sum(coalesce(octet_length(blob),0)),0)::bigint FROM checkpoint_blobs WHERE thread_id=$1) AS blob_bytes,
|
|
13892
|
+
(SELECT count(*)::bigint FROM checkpoint_writes WHERE thread_id=$1) AS write_count,
|
|
13893
|
+
(SELECT coalesce(sum(octet_length(blob)),0)::bigint FROM checkpoint_writes WHERE thread_id=$1) AS write_bytes`,
|
|
13894
|
+
[threadId]
|
|
13895
|
+
);
|
|
13896
|
+
const row = result.rows[0];
|
|
13897
|
+
const checkpointCount = toCount(row?.checkpoint_count);
|
|
13898
|
+
const checkpointBytes = toCount(row?.checkpoint_bytes);
|
|
13899
|
+
const blobCount = toCount(row?.blob_count);
|
|
13900
|
+
const blobBytes = toCount(row?.blob_bytes);
|
|
13901
|
+
const writeCount = toCount(row?.write_count);
|
|
13902
|
+
const writeBytes = toCount(row?.write_bytes);
|
|
13903
|
+
return {
|
|
13904
|
+
threadId,
|
|
13905
|
+
totalBytes: checkpointBytes + blobBytes + writeBytes,
|
|
13906
|
+
checkpoints: { count: checkpointCount, bytes: checkpointBytes },
|
|
13907
|
+
channelValues: { count: blobCount, bytes: blobBytes },
|
|
13908
|
+
pendingWrites: { count: writeCount, bytes: writeBytes }
|
|
13909
|
+
};
|
|
13910
|
+
}
|
|
13911
|
+
/**
|
|
13912
|
+
* Copy every checkpoint row from a source thread to an empty target thread.
|
|
13913
|
+
*
|
|
13914
|
+
* @param sourceThreadId - Thread whose rows are copied.
|
|
13915
|
+
* @param targetThreadId - Empty thread that receives the rows.
|
|
13916
|
+
* @returns Resolves when the copy commits; an empty source is a no-op.
|
|
13917
|
+
* @throws {CheckpointTargetNotEmptyError} When the target already owns rows.
|
|
13918
|
+
*/
|
|
13919
|
+
async copyThread(sourceThreadId, targetThreadId) {
|
|
13920
|
+
const client = await this.pool.connect();
|
|
13921
|
+
try {
|
|
13922
|
+
await client.query("BEGIN");
|
|
13923
|
+
const target = await client.query(
|
|
13924
|
+
`SELECT (
|
|
13925
|
+
EXISTS (SELECT 1 FROM checkpoints WHERE thread_id=$1)
|
|
13926
|
+
OR EXISTS (SELECT 1 FROM checkpoint_blobs WHERE thread_id=$1)
|
|
13927
|
+
OR EXISTS (SELECT 1 FROM checkpoint_writes WHERE thread_id=$1)
|
|
13928
|
+
) AS not_empty`,
|
|
13929
|
+
[targetThreadId]
|
|
13930
|
+
);
|
|
13931
|
+
if (target.rows[0]?.not_empty) {
|
|
13932
|
+
throw new CheckpointTargetNotEmptyError(targetThreadId);
|
|
13933
|
+
}
|
|
13934
|
+
await client.query(
|
|
13935
|
+
`INSERT INTO checkpoints (
|
|
13936
|
+
thread_id, checkpoint_ns, checkpoint_id, parent_checkpoint_id, type, checkpoint, metadata
|
|
13937
|
+
)
|
|
13938
|
+
SELECT $2, checkpoint_ns, checkpoint_id, parent_checkpoint_id, type, checkpoint,
|
|
13939
|
+
jsonb_set(coalesce(metadata, '{}'::jsonb), '{thread_id}', to_jsonb($2::text), true)
|
|
13940
|
+
FROM checkpoints WHERE thread_id=$1`,
|
|
13941
|
+
[sourceThreadId, targetThreadId]
|
|
13942
|
+
);
|
|
13943
|
+
await client.query(
|
|
13944
|
+
`INSERT INTO checkpoint_blobs (thread_id, checkpoint_ns, channel, version, type, blob)
|
|
13945
|
+
SELECT $2, checkpoint_ns, channel, version, type, blob
|
|
13946
|
+
FROM checkpoint_blobs WHERE thread_id=$1`,
|
|
13947
|
+
[sourceThreadId, targetThreadId]
|
|
13948
|
+
);
|
|
13949
|
+
await client.query(
|
|
13950
|
+
`INSERT INTO checkpoint_writes (
|
|
13951
|
+
thread_id, checkpoint_ns, checkpoint_id, task_id, idx, channel, type, blob
|
|
13952
|
+
)
|
|
13953
|
+
SELECT $2, checkpoint_ns, checkpoint_id, task_id, idx, channel, type, blob
|
|
13954
|
+
FROM checkpoint_writes WHERE thread_id=$1`,
|
|
13955
|
+
[sourceThreadId, targetThreadId]
|
|
13956
|
+
);
|
|
13957
|
+
await client.query("COMMIT");
|
|
13958
|
+
} catch (error) {
|
|
13959
|
+
await rollbackQuietly(client);
|
|
13960
|
+
if (isUniqueViolation3(error)) {
|
|
13961
|
+
throw new CheckpointTargetNotEmptyError(targetThreadId);
|
|
13962
|
+
}
|
|
13963
|
+
throw error;
|
|
13964
|
+
} finally {
|
|
13965
|
+
client.release();
|
|
13966
|
+
}
|
|
13967
|
+
}
|
|
13968
|
+
/**
|
|
13969
|
+
* Retain only the latest N checkpoints per checkpoint namespace.
|
|
13970
|
+
*
|
|
13971
|
+
* @param threadId - Thread whose checkpoint history is pruned.
|
|
13972
|
+
* @param options - Retention options.
|
|
13973
|
+
* @param options.keepLatest - Number of latest checkpoints to retain per namespace.
|
|
13974
|
+
* @returns Counts of deleted checkpoint, blob, and write rows.
|
|
13975
|
+
*/
|
|
13976
|
+
async pruneThread(threadId, options) {
|
|
13977
|
+
if (!Number.isSafeInteger(options.keepLatest) || options.keepLatest < 1) {
|
|
13978
|
+
throw new RangeError("keepLatest must be an integer >= 1");
|
|
13979
|
+
}
|
|
13980
|
+
const keepLatest = options.keepLatest;
|
|
13981
|
+
const client = await this.pool.connect();
|
|
13982
|
+
try {
|
|
13983
|
+
await client.query("BEGIN");
|
|
13984
|
+
const retainedResult = await client.query(
|
|
13985
|
+
`SELECT checkpoint_ns, checkpoint_id, checkpoint FROM (
|
|
13986
|
+
SELECT checkpoint_ns, checkpoint_id, checkpoint,
|
|
13987
|
+
row_number() OVER (
|
|
13988
|
+
PARTITION BY checkpoint_ns ORDER BY checkpoint_id DESC
|
|
13989
|
+
) AS rn
|
|
13990
|
+
FROM checkpoints WHERE thread_id=$1
|
|
13991
|
+
) t WHERE rn <= $2`,
|
|
13992
|
+
[threadId, keepLatest]
|
|
13993
|
+
);
|
|
13994
|
+
const retained = retainedResult.rows;
|
|
13995
|
+
const retainedCheckpoints = new Set(
|
|
13996
|
+
retained.map((row) => checkpointKey(row.checkpoint_ns, row.checkpoint_id))
|
|
13997
|
+
);
|
|
13998
|
+
const fallbackNamespaces = /* @__PURE__ */ new Set();
|
|
13999
|
+
const retainedBlobs = /* @__PURE__ */ new Set();
|
|
14000
|
+
for (const row of retained) {
|
|
14001
|
+
const versions = channelVersions(row.checkpoint);
|
|
14002
|
+
if (!versions) {
|
|
14003
|
+
fallbackNamespaces.add(row.checkpoint_ns);
|
|
14004
|
+
continue;
|
|
14005
|
+
}
|
|
14006
|
+
for (const [channel, version] of Object.entries(versions)) {
|
|
14007
|
+
retainedBlobs.add(blobKey(row.checkpoint_ns, channel, String(version)));
|
|
14008
|
+
}
|
|
14009
|
+
}
|
|
14010
|
+
const writeRows = await client.query(
|
|
14011
|
+
`SELECT checkpoint_ns, checkpoint_id, task_id, idx
|
|
14012
|
+
FROM checkpoint_writes WHERE thread_id=$1`,
|
|
14013
|
+
[threadId]
|
|
14014
|
+
);
|
|
14015
|
+
const staleWrites = writeRows.rows.filter(
|
|
14016
|
+
(row) => !retainedCheckpoints.has(checkpointKey(row.checkpoint_ns, row.checkpoint_id))
|
|
14017
|
+
);
|
|
14018
|
+
let writes = 0;
|
|
14019
|
+
for (const batch of chunk(staleWrites, DELETE_CHUNK_SIZE)) {
|
|
14020
|
+
const params = [threadId];
|
|
14021
|
+
const tuples = batch.map((row) => {
|
|
14022
|
+
params.push(row.checkpoint_ns, row.checkpoint_id, row.task_id, row.idx);
|
|
14023
|
+
return `($${params.length - 3}, $${params.length - 2}, $${params.length - 1}, $${params.length})`;
|
|
14024
|
+
});
|
|
14025
|
+
const deleted = await client.query(
|
|
14026
|
+
`DELETE FROM checkpoint_writes
|
|
14027
|
+
WHERE thread_id=$1
|
|
14028
|
+
AND (checkpoint_ns, checkpoint_id, task_id, idx) IN (${tuples.join(", ")})`,
|
|
14029
|
+
params
|
|
14030
|
+
);
|
|
14031
|
+
writes += deleted.rowCount ?? 0;
|
|
14032
|
+
}
|
|
14033
|
+
const checkpointRows = await client.query(
|
|
14034
|
+
`SELECT checkpoint_ns, checkpoint_id FROM checkpoints WHERE thread_id=$1`,
|
|
14035
|
+
[threadId]
|
|
14036
|
+
);
|
|
14037
|
+
const staleCheckpoints = checkpointRows.rows.filter(
|
|
14038
|
+
(row) => !retainedCheckpoints.has(checkpointKey(row.checkpoint_ns, row.checkpoint_id))
|
|
14039
|
+
);
|
|
14040
|
+
let checkpoints = 0;
|
|
14041
|
+
for (const batch of chunk(staleCheckpoints, DELETE_CHUNK_SIZE)) {
|
|
14042
|
+
const params = [threadId];
|
|
14043
|
+
const tuples = batch.map((row) => {
|
|
14044
|
+
params.push(row.checkpoint_ns, row.checkpoint_id);
|
|
14045
|
+
return `($${params.length - 1}, $${params.length})`;
|
|
14046
|
+
});
|
|
14047
|
+
const deleted = await client.query(
|
|
14048
|
+
`DELETE FROM checkpoints
|
|
14049
|
+
WHERE thread_id=$1
|
|
14050
|
+
AND (checkpoint_ns, checkpoint_id) IN (${tuples.join(", ")})`,
|
|
14051
|
+
params
|
|
14052
|
+
);
|
|
14053
|
+
checkpoints += deleted.rowCount ?? 0;
|
|
14054
|
+
}
|
|
14055
|
+
const blobRows = await client.query(
|
|
14056
|
+
`SELECT checkpoint_ns, channel, version FROM checkpoint_blobs WHERE thread_id=$1`,
|
|
14057
|
+
[threadId]
|
|
14058
|
+
);
|
|
14059
|
+
const staleBlobs = blobRows.rows.filter(
|
|
14060
|
+
(row) => !fallbackNamespaces.has(row.checkpoint_ns) && !retainedBlobs.has(blobKey(row.checkpoint_ns, row.channel, row.version))
|
|
14061
|
+
);
|
|
14062
|
+
let blobs = 0;
|
|
14063
|
+
for (const batch of chunk(staleBlobs, DELETE_CHUNK_SIZE)) {
|
|
14064
|
+
const params = [threadId];
|
|
14065
|
+
const tuples = batch.map((row) => {
|
|
14066
|
+
params.push(row.checkpoint_ns, row.channel, row.version);
|
|
14067
|
+
return `($${params.length - 2}, $${params.length - 1}, $${params.length})`;
|
|
14068
|
+
});
|
|
14069
|
+
const deleted = await client.query(
|
|
14070
|
+
`DELETE FROM checkpoint_blobs
|
|
14071
|
+
WHERE thread_id=$1
|
|
14072
|
+
AND (checkpoint_ns, channel, version) IN (${tuples.join(", ")})`,
|
|
14073
|
+
params
|
|
14074
|
+
);
|
|
14075
|
+
blobs += deleted.rowCount ?? 0;
|
|
14076
|
+
}
|
|
14077
|
+
await client.query("COMMIT");
|
|
14078
|
+
return { checkpoints, blobs, writes };
|
|
14079
|
+
} catch (error) {
|
|
14080
|
+
await rollbackQuietly(client);
|
|
14081
|
+
throw error;
|
|
14082
|
+
} finally {
|
|
14083
|
+
client.release();
|
|
14084
|
+
}
|
|
14085
|
+
}
|
|
14086
|
+
/**
|
|
14087
|
+
* Delete the checkpoints that descend from a base checkpoint.
|
|
14088
|
+
*
|
|
14089
|
+
* Descendants are discovered by following both same-namespace
|
|
14090
|
+
* `parent_checkpoint_id` edges and cross-namespace `metadata.parents` edges
|
|
14091
|
+
* (LangGraph's `{ "<parent checkpoint_ns>": "<parent checkpoint id>" }` map on
|
|
14092
|
+
* subgraph checkpoints), recursively across every namespace. Out-of-order or
|
|
14093
|
+
* clock-skewed ids do not affect the result. Writes keyed to the base
|
|
14094
|
+
* checkpoint belong to the rolled-back run's first superstep and are deleted
|
|
14095
|
+
* too. Blobs referenced only by deleted checkpoints are removed.
|
|
14096
|
+
*
|
|
14097
|
+
* @param threadId - Thread whose later checkpoint history is deleted.
|
|
14098
|
+
* @param baseCheckpointId - Checkpoint to retain as the new tip, or `null` to
|
|
14099
|
+
* delete every checkpoint for the thread.
|
|
14100
|
+
* @returns Resolves when the deletion commits; no descendants is a no-op.
|
|
14101
|
+
* @throws {RangeError} When `baseCheckpointId` is an empty string.
|
|
14102
|
+
* @throws {CheckpointBaseNotFoundError} When the base checkpoint does not
|
|
14103
|
+
* exist on the thread.
|
|
14104
|
+
*/
|
|
14105
|
+
async deleteAfter(threadId, baseCheckpointId) {
|
|
14106
|
+
if (baseCheckpointId !== null && baseCheckpointId.length === 0) {
|
|
14107
|
+
throw new RangeError("baseCheckpointId must be null or a non-empty checkpoint id");
|
|
14108
|
+
}
|
|
14109
|
+
const client = await this.pool.connect();
|
|
14110
|
+
try {
|
|
14111
|
+
await client.query("BEGIN");
|
|
14112
|
+
if (baseCheckpointId === null) {
|
|
14113
|
+
await client.query("DELETE FROM checkpoint_writes WHERE thread_id=$1", [threadId]);
|
|
14114
|
+
await client.query("DELETE FROM checkpoint_blobs WHERE thread_id=$1", [threadId]);
|
|
14115
|
+
await client.query("DELETE FROM checkpoints WHERE thread_id=$1", [threadId]);
|
|
14116
|
+
await client.query("COMMIT");
|
|
14117
|
+
return;
|
|
14118
|
+
}
|
|
14119
|
+
const base = await client.query(
|
|
14120
|
+
`SELECT checkpoint_ns
|
|
14121
|
+
FROM checkpoints
|
|
14122
|
+
WHERE thread_id=$1 AND checkpoint_id=$2`,
|
|
14123
|
+
[threadId, baseCheckpointId]
|
|
14124
|
+
);
|
|
14125
|
+
if (base.rows.length === 0) {
|
|
14126
|
+
throw new CheckpointBaseNotFoundError(threadId, baseCheckpointId);
|
|
14127
|
+
}
|
|
14128
|
+
const baseCheckpointNs = base.rows[0].checkpoint_ns;
|
|
14129
|
+
const descendantResult = await client.query(
|
|
14130
|
+
`WITH RECURSIVE lineage(checkpoint_ns, checkpoint_id) AS (
|
|
14131
|
+
SELECT $2::text, $3::text
|
|
14132
|
+
UNION
|
|
14133
|
+
SELECT c.checkpoint_ns, c.checkpoint_id
|
|
14134
|
+
FROM checkpoints c
|
|
14135
|
+
JOIN lineage l ON (
|
|
14136
|
+
(c.checkpoint_ns = l.checkpoint_ns
|
|
14137
|
+
AND c.parent_checkpoint_id = l.checkpoint_id)
|
|
14138
|
+
OR (c.metadata->'parents'->>l.checkpoint_ns = l.checkpoint_id)
|
|
14139
|
+
)
|
|
14140
|
+
WHERE c.thread_id=$1
|
|
14141
|
+
),
|
|
14142
|
+
roots AS (
|
|
14143
|
+
SELECT $2::text AS checkpoint_ns, $3::text AS checkpoint_id
|
|
14144
|
+
)
|
|
14145
|
+
SELECT l.checkpoint_ns, l.checkpoint_id
|
|
14146
|
+
FROM lineage l
|
|
14147
|
+
WHERE NOT EXISTS (
|
|
14148
|
+
SELECT 1 FROM roots r
|
|
14149
|
+
WHERE r.checkpoint_ns = l.checkpoint_ns
|
|
14150
|
+
AND r.checkpoint_id = l.checkpoint_id
|
|
14151
|
+
)`,
|
|
14152
|
+
[threadId, baseCheckpointNs, baseCheckpointId]
|
|
14153
|
+
);
|
|
14154
|
+
const descendants = descendantResult.rows;
|
|
14155
|
+
await client.query(
|
|
14156
|
+
`DELETE FROM checkpoint_writes
|
|
14157
|
+
WHERE thread_id=$1 AND checkpoint_ns=$2 AND checkpoint_id=$3`,
|
|
14158
|
+
[threadId, baseCheckpointNs, baseCheckpointId]
|
|
14159
|
+
);
|
|
14160
|
+
for (const batch of chunk(descendants, DELETE_CHUNK_SIZE)) {
|
|
14161
|
+
const params = [threadId];
|
|
14162
|
+
const tuples = batch.map((row) => {
|
|
14163
|
+
params.push(row.checkpoint_ns, row.checkpoint_id);
|
|
14164
|
+
return `($${params.length - 1}, $${params.length})`;
|
|
14165
|
+
});
|
|
14166
|
+
await client.query(
|
|
14167
|
+
`DELETE FROM checkpoint_writes
|
|
14168
|
+
WHERE thread_id=$1
|
|
14169
|
+
AND (checkpoint_ns, checkpoint_id) IN (${tuples.join(", ")})`,
|
|
14170
|
+
params
|
|
14171
|
+
);
|
|
14172
|
+
await client.query(
|
|
14173
|
+
`DELETE FROM checkpoints
|
|
14174
|
+
WHERE thread_id=$1
|
|
14175
|
+
AND (checkpoint_ns, checkpoint_id) IN (${tuples.join(", ")})`,
|
|
14176
|
+
params
|
|
14177
|
+
);
|
|
14178
|
+
}
|
|
14179
|
+
const retainedResult = await client.query(
|
|
14180
|
+
`SELECT checkpoint_ns, checkpoint_id, checkpoint
|
|
14181
|
+
FROM checkpoints WHERE thread_id=$1`,
|
|
14182
|
+
[threadId]
|
|
14183
|
+
);
|
|
14184
|
+
const fallbackNamespaces = /* @__PURE__ */ new Set();
|
|
14185
|
+
const retainedBlobs = /* @__PURE__ */ new Set();
|
|
14186
|
+
for (const row of retainedResult.rows) {
|
|
14187
|
+
const versions = channelVersions(row.checkpoint);
|
|
14188
|
+
if (!versions) {
|
|
14189
|
+
fallbackNamespaces.add(row.checkpoint_ns);
|
|
14190
|
+
continue;
|
|
14191
|
+
}
|
|
14192
|
+
for (const [channel, version] of Object.entries(versions)) {
|
|
14193
|
+
retainedBlobs.add(blobKey(row.checkpoint_ns, channel, String(version)));
|
|
14194
|
+
}
|
|
14195
|
+
}
|
|
14196
|
+
const blobRows = await client.query(
|
|
14197
|
+
`SELECT checkpoint_ns, channel, version FROM checkpoint_blobs WHERE thread_id=$1`,
|
|
14198
|
+
[threadId]
|
|
14199
|
+
);
|
|
14200
|
+
const staleBlobs = blobRows.rows.filter(
|
|
14201
|
+
(row) => !fallbackNamespaces.has(row.checkpoint_ns) && !retainedBlobs.has(blobKey(row.checkpoint_ns, row.channel, row.version))
|
|
14202
|
+
);
|
|
14203
|
+
for (const batch of chunk(staleBlobs, DELETE_CHUNK_SIZE)) {
|
|
14204
|
+
const params = [threadId];
|
|
14205
|
+
const tuples = batch.map((row) => {
|
|
14206
|
+
params.push(row.checkpoint_ns, row.channel, row.version);
|
|
14207
|
+
return `($${params.length - 2}, $${params.length - 1}, $${params.length})`;
|
|
14208
|
+
});
|
|
14209
|
+
await client.query(
|
|
14210
|
+
`DELETE FROM checkpoint_blobs
|
|
14211
|
+
WHERE thread_id=$1
|
|
14212
|
+
AND (checkpoint_ns, channel, version) IN (${tuples.join(", ")})`,
|
|
14213
|
+
params
|
|
14214
|
+
);
|
|
14215
|
+
}
|
|
14216
|
+
await client.query("COMMIT");
|
|
14217
|
+
} catch (error) {
|
|
14218
|
+
await rollbackQuietly(client);
|
|
14219
|
+
throw error;
|
|
14220
|
+
} finally {
|
|
14221
|
+
client.release();
|
|
14222
|
+
}
|
|
14223
|
+
}
|
|
14224
|
+
/**
|
|
14225
|
+
* Delete every checkpoint row for a thread.
|
|
14226
|
+
*
|
|
14227
|
+
* @param threadId - Thread whose checkpoint rows are deleted.
|
|
14228
|
+
* @returns Resolves when the deletion commits.
|
|
14229
|
+
*/
|
|
14230
|
+
async deleteThread(threadId) {
|
|
14231
|
+
const client = await this.pool.connect();
|
|
14232
|
+
try {
|
|
14233
|
+
await client.query("BEGIN");
|
|
14234
|
+
await client.query("DELETE FROM checkpoint_writes WHERE thread_id=$1", [threadId]);
|
|
14235
|
+
await client.query("DELETE FROM checkpoint_blobs WHERE thread_id=$1", [threadId]);
|
|
14236
|
+
await client.query("DELETE FROM checkpoints WHERE thread_id=$1", [threadId]);
|
|
14237
|
+
await client.query("COMMIT");
|
|
14238
|
+
} catch (error) {
|
|
14239
|
+
await rollbackQuietly(client);
|
|
14240
|
+
throw error;
|
|
14241
|
+
} finally {
|
|
14242
|
+
client.release();
|
|
14243
|
+
}
|
|
14244
|
+
}
|
|
14245
|
+
};
|
|
14246
|
+
|
|
14247
|
+
// src/stores/PostgreSQLStore.ts
|
|
14248
|
+
import {
|
|
14249
|
+
assertStoreIdentity,
|
|
14250
|
+
assertStoreNamespacePrefix,
|
|
14251
|
+
storeExpiresAt,
|
|
14252
|
+
storePaginationValue
|
|
14253
|
+
} from "@axiom-lattice/protocols";
|
|
14254
|
+
function mapRow10(row) {
|
|
14255
|
+
return {
|
|
14256
|
+
namespace: row.namespace,
|
|
14257
|
+
key: row.key,
|
|
14258
|
+
value: row.value ?? {},
|
|
14259
|
+
createdAt: row.created_at,
|
|
14260
|
+
updatedAt: row.updated_at,
|
|
14261
|
+
expiresAt: row.expires_at ?? void 0
|
|
14262
|
+
};
|
|
14263
|
+
}
|
|
14264
|
+
var PostgreSQLStore = class {
|
|
14265
|
+
/**
|
|
14266
|
+
* @param pool - Pool shared with the rest of the store config.
|
|
14267
|
+
*/
|
|
14268
|
+
constructor(pool) {
|
|
14269
|
+
this.pool = pool;
|
|
14270
|
+
}
|
|
14271
|
+
async put(tenantId, input) {
|
|
14272
|
+
assertStoreIdentity(input.namespace, input.key);
|
|
14273
|
+
const now = /* @__PURE__ */ new Date();
|
|
14274
|
+
const expiresAt = storeExpiresAt(input.ttlMinutes, now) ?? null;
|
|
14275
|
+
const result = await this.pool.query(
|
|
14276
|
+
`INSERT INTO lattice_store_items
|
|
14277
|
+
(tenant_id, namespace, key, value, created_at, updated_at, expires_at)
|
|
14278
|
+
VALUES ($1, $2, $3, $4::jsonb, $5, $5, $6)
|
|
14279
|
+
ON CONFLICT (tenant_id, namespace, key) DO UPDATE SET
|
|
14280
|
+
value = EXCLUDED.value,
|
|
14281
|
+
updated_at = EXCLUDED.updated_at,
|
|
14282
|
+
expires_at = EXCLUDED.expires_at
|
|
14283
|
+
RETURNING tenant_id, namespace, key, value, created_at, updated_at, expires_at`,
|
|
14284
|
+
[tenantId, input.namespace, input.key, JSON.stringify(input.value), now, expiresAt]
|
|
14285
|
+
);
|
|
14286
|
+
return mapRow10(result.rows[0]);
|
|
14287
|
+
}
|
|
14288
|
+
async get(tenantId, namespace, key) {
|
|
14289
|
+
assertStoreIdentity(namespace, key);
|
|
14290
|
+
const result = await this.pool.query(
|
|
14291
|
+
`SELECT tenant_id, namespace, key, value, created_at, updated_at, expires_at
|
|
14292
|
+
FROM lattice_store_items
|
|
14293
|
+
WHERE tenant_id = $1 AND namespace = $2 AND key = $3
|
|
14294
|
+
AND (expires_at IS NULL OR expires_at > $4)`,
|
|
14295
|
+
[tenantId, namespace, key, /* @__PURE__ */ new Date()]
|
|
14296
|
+
);
|
|
14297
|
+
return result.rows[0] ? mapRow10(result.rows[0]) : null;
|
|
14298
|
+
}
|
|
14299
|
+
async delete(tenantId, namespace, key) {
|
|
14300
|
+
assertStoreIdentity(namespace, key);
|
|
14301
|
+
const result = await this.pool.query(
|
|
14302
|
+
`DELETE FROM lattice_store_items
|
|
14303
|
+
WHERE tenant_id = $1 AND namespace = $2 AND key = $3`,
|
|
14304
|
+
[tenantId, namespace, key]
|
|
14305
|
+
);
|
|
14306
|
+
return (result.rowCount ?? 0) > 0;
|
|
14307
|
+
}
|
|
14308
|
+
async search(tenantId, input) {
|
|
14309
|
+
const conditions = [
|
|
14310
|
+
"tenant_id = $1",
|
|
14311
|
+
"(expires_at IS NULL OR expires_at > $2)"
|
|
14312
|
+
];
|
|
14313
|
+
const params = [tenantId, /* @__PURE__ */ new Date()];
|
|
14314
|
+
if (input.namespacePrefix) {
|
|
14315
|
+
assertStoreNamespacePrefix(input.namespacePrefix);
|
|
14316
|
+
}
|
|
14317
|
+
if (input.namespacePrefix && input.namespacePrefix.length > 0) {
|
|
14318
|
+
params.push(input.namespacePrefix);
|
|
14319
|
+
conditions.push(`namespace[1:array_length($${params.length}::text[], 1)] = $${params.length}`);
|
|
14320
|
+
}
|
|
14321
|
+
if (input.filter !== void 0) {
|
|
14322
|
+
params.push(JSON.stringify(input.filter));
|
|
14323
|
+
conditions.push(`value @> $${params.length}::jsonb`);
|
|
14324
|
+
}
|
|
14325
|
+
if (input.query !== void 0 && input.query !== "") {
|
|
14326
|
+
params.push(input.query.replace(/[\\%_]/g, "\\$&"));
|
|
14327
|
+
conditions.push(`value::text ILIKE '%' || $${params.length} || '%' ESCAPE '\\'`);
|
|
14328
|
+
}
|
|
14329
|
+
params.push(storePaginationValue(input.limit, 10));
|
|
14330
|
+
const limitParam = params.length;
|
|
14331
|
+
params.push(storePaginationValue(input.offset, 0));
|
|
14332
|
+
const offsetParam = params.length;
|
|
14333
|
+
const result = await this.pool.query(
|
|
14334
|
+
`SELECT tenant_id, namespace, key, value, created_at, updated_at, expires_at
|
|
14335
|
+
FROM lattice_store_items
|
|
14336
|
+
WHERE ${conditions.join(" AND ")}
|
|
14337
|
+
ORDER BY updated_at DESC, namespace, key
|
|
14338
|
+
LIMIT $${limitParam} OFFSET $${offsetParam}`,
|
|
14339
|
+
params
|
|
14340
|
+
);
|
|
14341
|
+
return result.rows.map(mapRow10);
|
|
14342
|
+
}
|
|
14343
|
+
async listNamespaces(tenantId, prefix = [], limit = 100) {
|
|
14344
|
+
assertStoreNamespacePrefix(prefix);
|
|
14345
|
+
const conditions = [
|
|
14346
|
+
"tenant_id = $1",
|
|
14347
|
+
"(expires_at IS NULL OR expires_at > $2)"
|
|
14348
|
+
];
|
|
14349
|
+
const params = [tenantId, /* @__PURE__ */ new Date()];
|
|
14350
|
+
if (prefix.length > 0) {
|
|
14351
|
+
params.push(prefix);
|
|
14352
|
+
conditions.push(`namespace[1:array_length($${params.length}::text[], 1)] = $${params.length}`);
|
|
14353
|
+
}
|
|
14354
|
+
params.push(storePaginationValue(limit, 100));
|
|
14355
|
+
const result = await this.pool.query(
|
|
14356
|
+
`SELECT DISTINCT namespace
|
|
14357
|
+
FROM lattice_store_items
|
|
14358
|
+
WHERE ${conditions.join(" AND ")}
|
|
14359
|
+
ORDER BY namespace
|
|
14360
|
+
LIMIT $${params.length}`,
|
|
14361
|
+
params
|
|
14362
|
+
);
|
|
14363
|
+
return result.rows.map((row) => row.namespace);
|
|
14364
|
+
}
|
|
14365
|
+
async deleteExpired(now, limit) {
|
|
14366
|
+
const bounded = storePaginationValue(limit, 0);
|
|
14367
|
+
const result = await this.pool.query(
|
|
14368
|
+
`WITH expired AS (
|
|
14369
|
+
SELECT ctid FROM lattice_store_items
|
|
14370
|
+
WHERE expires_at IS NOT NULL AND expires_at <= $1
|
|
14371
|
+
ORDER BY expires_at
|
|
14372
|
+
LIMIT $2
|
|
14373
|
+
)
|
|
14374
|
+
DELETE FROM lattice_store_items WHERE ctid IN (SELECT ctid FROM expired)`,
|
|
14375
|
+
[now, bounded]
|
|
14376
|
+
);
|
|
14377
|
+
return result.rowCount ?? 0;
|
|
14378
|
+
}
|
|
14379
|
+
};
|
|
14380
|
+
|
|
14381
|
+
// src/migrations/store_migrations.ts
|
|
14382
|
+
var createStoreItemsTable = {
|
|
14383
|
+
version: 195,
|
|
14384
|
+
name: "create_lattice_store_items_table",
|
|
14385
|
+
up: async (client) => {
|
|
14386
|
+
await client.query(`
|
|
14387
|
+
CREATE TABLE IF NOT EXISTS lattice_store_items (
|
|
14388
|
+
tenant_id TEXT NOT NULL,
|
|
14389
|
+
namespace TEXT[] NOT NULL,
|
|
14390
|
+
key TEXT NOT NULL,
|
|
14391
|
+
value JSONB NOT NULL,
|
|
14392
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
14393
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
14394
|
+
expires_at TIMESTAMPTZ,
|
|
14395
|
+
PRIMARY KEY (tenant_id, namespace, key)
|
|
14396
|
+
)
|
|
14397
|
+
`);
|
|
14398
|
+
await client.query(`
|
|
14399
|
+
CREATE INDEX IF NOT EXISTS idx_lattice_store_items_namespace
|
|
14400
|
+
ON lattice_store_items USING GIN (namespace)
|
|
14401
|
+
`);
|
|
14402
|
+
await client.query(`
|
|
14403
|
+
CREATE INDEX IF NOT EXISTS idx_lattice_store_items_value
|
|
14404
|
+
ON lattice_store_items USING GIN (value)
|
|
14405
|
+
`);
|
|
14406
|
+
await client.query(`
|
|
14407
|
+
CREATE INDEX IF NOT EXISTS idx_lattice_store_items_expires_at
|
|
14408
|
+
ON lattice_store_items(expires_at)
|
|
14409
|
+
WHERE expires_at IS NOT NULL
|
|
14410
|
+
`);
|
|
14411
|
+
},
|
|
14412
|
+
down: async (client) => {
|
|
14413
|
+
await client.query("DROP INDEX IF EXISTS idx_lattice_store_items_expires_at");
|
|
14414
|
+
await client.query("DROP INDEX IF EXISTS idx_lattice_store_items_value");
|
|
14415
|
+
await client.query("DROP INDEX IF EXISTS idx_lattice_store_items_namespace");
|
|
14416
|
+
await client.query("DROP TABLE IF EXISTS lattice_store_items");
|
|
14417
|
+
}
|
|
14418
|
+
};
|
|
14419
|
+
|
|
14420
|
+
// src/createPgStoreConfig.ts
|
|
14421
|
+
async function createPgStoreConfig(connectionString) {
|
|
14422
|
+
const pool = new Pool32({ connectionString });
|
|
14423
|
+
const mm = new MigrationManager(pool);
|
|
14424
|
+
mm.register(createThreadsTable);
|
|
14425
|
+
mm.register(createScheduledTasksTable);
|
|
14426
|
+
mm.register(createAssistantsTable);
|
|
14427
|
+
mm.register(createSkillsTable);
|
|
14428
|
+
mm.register(createWorkspacesTable);
|
|
14429
|
+
mm.register(createProjectsTable);
|
|
14430
|
+
mm.register(createUsersTable);
|
|
14431
|
+
mm.register(createTenantsTable);
|
|
14432
|
+
mm.register(addUserStatusColumn);
|
|
14433
|
+
mm.register(createUserTenantLinkTable);
|
|
14434
|
+
mm.register(migrateRemoveTenantIdFromUsers);
|
|
14435
|
+
mm.register(createMetricsConfigsTable);
|
|
14436
|
+
mm.register(createMcpServerConfigsTable);
|
|
14437
|
+
mm.register(addAssistantTenantId);
|
|
14438
|
+
mm.register(addSkillTenantId);
|
|
14439
|
+
mm.register(changeSkillPrimaryKey);
|
|
14440
|
+
mm.register(addScheduleTenantId);
|
|
14441
|
+
mm.register(changeThreadPrimaryKey);
|
|
14442
|
+
mm.register(createChannelIdentityMappingTables);
|
|
14443
|
+
mm.register(createChannelInstallationsTable);
|
|
14444
|
+
mm.register(addThreadTenantId);
|
|
14445
|
+
mm.register(addProjectConfigColumn);
|
|
14446
|
+
registerThreadMessageQueueMigrations(mm);
|
|
14447
|
+
mm.register(createWorkflowTrackingTables);
|
|
14448
|
+
for (const m of evalMigrations) {
|
|
14449
|
+
mm.register(m);
|
|
14450
|
+
}
|
|
14451
|
+
mm.register(alterChannelInstallationsTable);
|
|
14452
|
+
mm.register(createChannelBindingsTable);
|
|
14453
|
+
mm.register(createDatabaseConfigsTable);
|
|
14454
|
+
mm.register(changeAssistantPrimaryKey);
|
|
14455
|
+
mm.register(addStepThreadId);
|
|
14456
|
+
mm.register(createA2AApiKeysTable);
|
|
14457
|
+
mm.register(addAssistantOwnerUserId);
|
|
14458
|
+
for (const m of taskMigrations) {
|
|
14459
|
+
mm.register(m);
|
|
14460
|
+
}
|
|
14461
|
+
mm.register(createMenuItemsTable);
|
|
14462
|
+
mm.register(createSharedResourcesTable);
|
|
14463
|
+
mm.register(addFileContentType);
|
|
14464
|
+
mm.register(addMcpAppContentType);
|
|
14465
|
+
mm.register(createCollectionsTable);
|
|
14466
|
+
mm.register(createTaskWorkItemsMigration);
|
|
12257
14467
|
mm.register(addWorkItemProjectFieldsMigration);
|
|
12258
14468
|
mm.register(createConnectionConfigsTable);
|
|
12259
14469
|
mm.register(addWorkflowRunsTenantStatusUpdatedIndex);
|
|
@@ -12284,6 +14494,19 @@ async function createPgStoreConfig(connectionString) {
|
|
|
12284
14494
|
mm.register(dropPluginDeploymentAuthToken);
|
|
12285
14495
|
mm.register(addModelProviderModelExtra);
|
|
12286
14496
|
mm.register(addModelProviderModelDefault);
|
|
14497
|
+
mm.register(addModelProviderModelKind);
|
|
14498
|
+
mm.register(createAgentRunLedgerTable);
|
|
14499
|
+
mm.register(createAgentRunTaskQueueMigration);
|
|
14500
|
+
mm.register(createAgentRunCapabilityRevisionMigration);
|
|
14501
|
+
mm.register(createAgentRunBaseCheckpointCapturedMigration);
|
|
14502
|
+
mm.register(createAgentRunRollbackFailureMigration);
|
|
14503
|
+
mm.register(agentRunRunningIndexSourceMigration);
|
|
14504
|
+
mm.register(addThreadExpiresAt);
|
|
14505
|
+
mm.register(createStoreItemsTable);
|
|
14506
|
+
mm.register(createChannelReplyJobsTable);
|
|
14507
|
+
mm.register(createAgentRunRetentionMigration);
|
|
14508
|
+
mm.register(createAgentRunProjectTaskMigration);
|
|
14509
|
+
mm.register(agentRunProjectTaskRunningIndexMigration);
|
|
12287
14510
|
await mm.migrate();
|
|
12288
14511
|
const checkpoint = PostgresSaver.fromConnString(connectionString);
|
|
12289
14512
|
checkpoint.setup().catch((err) => {
|
|
@@ -12328,13 +14551,17 @@ async function createPgStoreConfig(connectionString) {
|
|
|
12328
14551
|
lease: new PostgreSQLLeaseStore(opts),
|
|
12329
14552
|
outbox: new PostgreSQLOutboxStore(opts),
|
|
12330
14553
|
pluginDeployment: new PostgreSQLPluginDeploymentStore(opts),
|
|
14554
|
+
agentRun: new PostgreSQLAgentRunLedger(opts),
|
|
14555
|
+
channelReplyJob: new PostgreSQLChannelReplyJobStore(opts),
|
|
12331
14556
|
vectorStoreProvider: new PGVectorStoreProvider(pool, connectionString),
|
|
12332
|
-
checkpoint
|
|
14557
|
+
checkpoint,
|
|
14558
|
+
checkpointStorage: new PostgreSQLCheckpointStorage(pool),
|
|
14559
|
+
store: new PostgreSQLStore(pool)
|
|
12333
14560
|
};
|
|
12334
14561
|
}
|
|
12335
14562
|
|
|
12336
14563
|
// src/stores/PostgreSQLSkillStore.ts
|
|
12337
|
-
import { Pool as
|
|
14564
|
+
import { Pool as Pool33 } from "pg";
|
|
12338
14565
|
var PostgreSQLSkillStore = class {
|
|
12339
14566
|
constructor(options) {
|
|
12340
14567
|
this.initialized = false;
|
|
@@ -12347,9 +14574,9 @@ var PostgreSQLSkillStore = class {
|
|
|
12347
14574
|
return;
|
|
12348
14575
|
}
|
|
12349
14576
|
if (typeof options.poolConfig === "string") {
|
|
12350
|
-
this.pool = new
|
|
14577
|
+
this.pool = new Pool33({ connectionString: options.poolConfig });
|
|
12351
14578
|
} else if (options.poolConfig) {
|
|
12352
|
-
this.pool = new
|
|
14579
|
+
this.pool = new Pool33(options.poolConfig);
|
|
12353
14580
|
} else {
|
|
12354
14581
|
throw new Error("Either pool or poolConfig must be provided");
|
|
12355
14582
|
}
|
|
@@ -12650,7 +14877,7 @@ var PostgreSQLSkillStore = class {
|
|
|
12650
14877
|
};
|
|
12651
14878
|
|
|
12652
14879
|
// src/stores/ChannelIdentityMappingStore.ts
|
|
12653
|
-
import { Pool as
|
|
14880
|
+
import { Pool as Pool34 } from "pg";
|
|
12654
14881
|
var ChannelIdentityMappingStore = class {
|
|
12655
14882
|
constructor(options) {
|
|
12656
14883
|
this.initialized = false;
|
|
@@ -12662,7 +14889,7 @@ var ChannelIdentityMappingStore = class {
|
|
|
12662
14889
|
this.initialized = true;
|
|
12663
14890
|
return;
|
|
12664
14891
|
}
|
|
12665
|
-
this.pool = typeof options.poolConfig === "string" ? new
|
|
14892
|
+
this.pool = typeof options.poolConfig === "string" ? new Pool34({ connectionString: options.poolConfig }) : options.poolConfig ? new Pool34(options.poolConfig) : (() => {
|
|
12666
14893
|
throw new Error("Either pool or poolConfig must be provided");
|
|
12667
14894
|
})();
|
|
12668
14895
|
this.migrationManager = new MigrationManager(this.pool);
|
|
@@ -12883,17 +15110,22 @@ function mapRowToChannelIdentityMapping(row) {
|
|
|
12883
15110
|
export {
|
|
12884
15111
|
ChannelBindingStore,
|
|
12885
15112
|
ChannelIdentityMappingStore,
|
|
15113
|
+
CheckpointBaseNotFoundError,
|
|
15114
|
+
CheckpointTargetNotEmptyError,
|
|
12886
15115
|
DuplicateProjectMembershipError,
|
|
12887
15116
|
DuplicateProjectRoomMessageIdempotencyKeyError,
|
|
12888
15117
|
MenuStore,
|
|
12889
15118
|
MigrationManager,
|
|
12890
15119
|
PGVectorStoreProvider,
|
|
12891
|
-
|
|
15120
|
+
Pool35 as Pool,
|
|
12892
15121
|
PostgreSQLA2AApiKeyStore,
|
|
15122
|
+
PostgreSQLAgentRunLedger,
|
|
12893
15123
|
PostgreSQLAgentWebAppStore,
|
|
12894
15124
|
PostgreSQLAssistantStore,
|
|
12895
15125
|
PostgreSQLCapabilityBundleStore,
|
|
12896
15126
|
PostgreSQLChannelInstallationStore,
|
|
15127
|
+
PostgreSQLChannelReplyJobStore,
|
|
15128
|
+
PostgreSQLCheckpointStorage,
|
|
12897
15129
|
PostgreSQLCollectionStore,
|
|
12898
15130
|
PostgreSQLConnectionStore,
|
|
12899
15131
|
PostgreSQLDatabaseConfigStore,
|
|
@@ -12911,6 +15143,7 @@ export {
|
|
|
12911
15143
|
PostgreSQLProjectStore,
|
|
12912
15144
|
PostgreSQLScheduleStorage,
|
|
12913
15145
|
PostgreSQLSkillStore,
|
|
15146
|
+
PostgreSQLStore,
|
|
12914
15147
|
PostgreSQLTaskWorkItemStore,
|
|
12915
15148
|
PostgreSQLTenantStore,
|
|
12916
15149
|
PostgreSQLThreadStore,
|
|
@@ -12935,6 +15168,7 @@ export {
|
|
|
12935
15168
|
addSkillTenantId,
|
|
12936
15169
|
addStepThreadId,
|
|
12937
15170
|
addTaskIdToEvalRuns,
|
|
15171
|
+
addThreadExpiresAt,
|
|
12938
15172
|
addThreadTenantId,
|
|
12939
15173
|
addWorkflowRunsTenantStatusUpdatedIndex,
|
|
12940
15174
|
alterChannelInstallationsTable,
|
|
@@ -12942,12 +15176,14 @@ export {
|
|
|
12942
15176
|
changeSkillPrimaryKey,
|
|
12943
15177
|
changeThreadPrimaryKey,
|
|
12944
15178
|
createA2AApiKeysTable,
|
|
15179
|
+
createAgentRunLedgerTable,
|
|
12945
15180
|
createAgentWebAppsTable,
|
|
12946
15181
|
createAssistantsTable,
|
|
12947
15182
|
createCapabilityBundlesTable,
|
|
12948
15183
|
createChannelBindingsTable,
|
|
12949
15184
|
createChannelIdentityMappingTables,
|
|
12950
15185
|
createChannelInstallationsTable,
|
|
15186
|
+
createChannelReplyJobsTable,
|
|
12951
15187
|
createCollectionsTable,
|
|
12952
15188
|
createDatabaseConfigsTable,
|
|
12953
15189
|
createEvalCasesTable,
|
|
@@ -12967,6 +15203,7 @@ export {
|
|
|
12967
15203
|
createScheduledTasksTable,
|
|
12968
15204
|
createSharedResourcesTable,
|
|
12969
15205
|
createSkillsTable,
|
|
15206
|
+
createStoreItemsTable,
|
|
12970
15207
|
createTenantsTable,
|
|
12971
15208
|
createThreadMessageQueueTable,
|
|
12972
15209
|
createThreadsTable,
|