@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.
Files changed (44) hide show
  1. package/.turbo/turbo-build.log +10 -10
  2. package/CHANGELOG.md +187 -0
  3. package/dist/index.d.mts +353 -3
  4. package/dist/index.d.ts +353 -3
  5. package/dist/index.js +2323 -93
  6. package/dist/index.js.map +1 -1
  7. package/dist/index.mjs +2329 -92
  8. package/dist/index.mjs.map +1 -1
  9. package/package.json +3 -3
  10. package/src/PGVectorStoreProvider.ts +19 -13
  11. package/src/__tests__/AgentRunRetention.migrations.test.ts +41 -0
  12. package/src/__tests__/PostgreSQLAgentRunLedger.test.ts +305 -0
  13. package/src/__tests__/PostgreSQLAgentRunRetention.test.ts +126 -0
  14. package/src/__tests__/PostgreSQLAgentRunTaskQueue.test.ts +1375 -0
  15. package/src/__tests__/PostgreSQLChannelReplyJobStore.test.ts +333 -0
  16. package/src/__tests__/PostgreSQLCheckpointStorage.deleteAfter.test.ts +473 -0
  17. package/src/__tests__/PostgreSQLCheckpointStorage.test.ts +538 -0
  18. package/src/__tests__/PostgreSQLStore.test.ts +173 -0
  19. package/src/__tests__/PostgreSQLThreadStore.test.ts +80 -0
  20. package/src/__tests__/ThreadMessageQueueStore.integration.test.ts +21 -0
  21. package/src/__tests__/ThreadMessageQueueStore.test.ts +8 -3
  22. package/src/__tests__/createPgStoreConfig.test.ts +38 -0
  23. package/src/createPgStoreConfig.ts +36 -1
  24. package/src/index.ts +7 -0
  25. package/src/migrations/agent_run_base_checkpoint_captured_migration.ts +34 -0
  26. package/src/migrations/agent_run_capability_revision_migration.ts +24 -0
  27. package/src/migrations/agent_run_ledger_migration.ts +51 -0
  28. package/src/migrations/agent_run_project_task_migration.ts +34 -0
  29. package/src/migrations/agent_run_project_task_running_index_migration.ts +33 -0
  30. package/src/migrations/agent_run_retention_migration.ts +25 -0
  31. package/src/migrations/agent_run_rollback_failure_migration.ts +28 -0
  32. package/src/migrations/agent_run_running_index_source_migration.ts +31 -0
  33. package/src/migrations/agent_run_task_queue_migration.ts +65 -0
  34. package/src/migrations/channel_reply_job_migration.ts +55 -0
  35. package/src/migrations/model_provider_migrations.ts +27 -0
  36. package/src/migrations/store_migrations.ts +50 -0
  37. package/src/migrations/thread_migrations.ts +25 -0
  38. package/src/stores/PostgreSQLAgentRunLedger.ts +1210 -0
  39. package/src/stores/PostgreSQLChannelReplyJobStore.ts +373 -0
  40. package/src/stores/PostgreSQLCheckpointStorage.ts +548 -0
  41. package/src/stores/PostgreSQLModelProviderStore.ts +25 -7
  42. package/src/stores/PostgreSQLStore.ts +167 -0
  43. package/src/stores/PostgreSQLThreadStore.ts +49 -10
  44. package/src/stores/ThreadMessageQueueStore.ts +10 -5
package/dist/index.js CHANGED
@@ -32,17 +32,22 @@ var index_exports = {};
32
32
  __export(index_exports, {
33
33
  ChannelBindingStore: () => ChannelBindingStore,
34
34
  ChannelIdentityMappingStore: () => ChannelIdentityMappingStore,
35
+ CheckpointBaseNotFoundError: () => CheckpointBaseNotFoundError,
36
+ CheckpointTargetNotEmptyError: () => CheckpointTargetNotEmptyError,
35
37
  DuplicateProjectMembershipError: () => DuplicateProjectMembershipError,
36
38
  DuplicateProjectRoomMessageIdempotencyKeyError: () => DuplicateProjectRoomMessageIdempotencyKeyError,
37
39
  MenuStore: () => MenuStore,
38
40
  MigrationManager: () => MigrationManager,
39
41
  PGVectorStoreProvider: () => PGVectorStoreProvider,
40
- Pool: () => import_pg33.Pool,
42
+ Pool: () => import_pg35.Pool,
41
43
  PostgreSQLA2AApiKeyStore: () => PostgreSQLA2AApiKeyStore,
44
+ PostgreSQLAgentRunLedger: () => PostgreSQLAgentRunLedger,
42
45
  PostgreSQLAgentWebAppStore: () => PostgreSQLAgentWebAppStore,
43
46
  PostgreSQLAssistantStore: () => PostgreSQLAssistantStore,
44
47
  PostgreSQLCapabilityBundleStore: () => PostgreSQLCapabilityBundleStore,
45
48
  PostgreSQLChannelInstallationStore: () => PostgreSQLChannelInstallationStore,
49
+ PostgreSQLChannelReplyJobStore: () => PostgreSQLChannelReplyJobStore,
50
+ PostgreSQLCheckpointStorage: () => PostgreSQLCheckpointStorage,
46
51
  PostgreSQLCollectionStore: () => PostgreSQLCollectionStore,
47
52
  PostgreSQLConnectionStore: () => PostgreSQLConnectionStore,
48
53
  PostgreSQLDatabaseConfigStore: () => PostgreSQLDatabaseConfigStore,
@@ -60,6 +65,7 @@ __export(index_exports, {
60
65
  PostgreSQLProjectStore: () => PostgreSQLProjectStore,
61
66
  PostgreSQLScheduleStorage: () => PostgreSQLScheduleStorage,
62
67
  PostgreSQLSkillStore: () => PostgreSQLSkillStore,
68
+ PostgreSQLStore: () => PostgreSQLStore,
63
69
  PostgreSQLTaskWorkItemStore: () => PostgreSQLTaskWorkItemStore,
64
70
  PostgreSQLTenantStore: () => PostgreSQLTenantStore,
65
71
  PostgreSQLThreadStore: () => PostgreSQLThreadStore,
@@ -84,6 +90,7 @@ __export(index_exports, {
84
90
  addSkillTenantId: () => addSkillTenantId,
85
91
  addStepThreadId: () => addStepThreadId,
86
92
  addTaskIdToEvalRuns: () => addTaskIdToEvalRuns,
93
+ addThreadExpiresAt: () => addThreadExpiresAt,
87
94
  addThreadTenantId: () => addThreadTenantId,
88
95
  addWorkflowRunsTenantStatusUpdatedIndex: () => addWorkflowRunsTenantStatusUpdatedIndex,
89
96
  alterChannelInstallationsTable: () => alterChannelInstallationsTable,
@@ -91,12 +98,14 @@ __export(index_exports, {
91
98
  changeSkillPrimaryKey: () => changeSkillPrimaryKey,
92
99
  changeThreadPrimaryKey: () => changeThreadPrimaryKey,
93
100
  createA2AApiKeysTable: () => createA2AApiKeysTable,
101
+ createAgentRunLedgerTable: () => createAgentRunLedgerTable,
94
102
  createAgentWebAppsTable: () => createAgentWebAppsTable,
95
103
  createAssistantsTable: () => createAssistantsTable,
96
104
  createCapabilityBundlesTable: () => createCapabilityBundlesTable,
97
105
  createChannelBindingsTable: () => createChannelBindingsTable,
98
106
  createChannelIdentityMappingTables: () => createChannelIdentityMappingTables,
99
107
  createChannelInstallationsTable: () => createChannelInstallationsTable,
108
+ createChannelReplyJobsTable: () => createChannelReplyJobsTable,
100
109
  createCollectionsTable: () => createCollectionsTable,
101
110
  createDatabaseConfigsTable: () => createDatabaseConfigsTable,
102
111
  createEvalCasesTable: () => createEvalCasesTable,
@@ -116,6 +125,7 @@ __export(index_exports, {
116
125
  createScheduledTasksTable: () => createScheduledTasksTable,
117
126
  createSharedResourcesTable: () => createSharedResourcesTable,
118
127
  createSkillsTable: () => createSkillsTable,
128
+ createStoreItemsTable: () => createStoreItemsTable,
119
129
  createTenantsTable: () => createTenantsTable,
120
130
  createThreadMessageQueueTable: () => createThreadMessageQueueTable,
121
131
  createThreadsTable: () => createThreadsTable,
@@ -130,10 +140,10 @@ __export(index_exports, {
130
140
  safeParse: () => safeParse
131
141
  });
132
142
  module.exports = __toCommonJS(index_exports);
133
- var import_pg33 = require("pg");
143
+ var import_pg35 = require("pg");
134
144
 
135
145
  // src/createPgStoreConfig.ts
136
- var import_pg30 = require("pg");
146
+ var import_pg32 = require("pg");
137
147
 
138
148
  // src/migrations/migration.ts
139
149
  var MigrationManager = class {
@@ -346,6 +356,25 @@ var createThreadsTable = {
346
356
  await client.query("DROP TABLE IF EXISTS lattice_threads");
347
357
  }
348
358
  };
359
+ var addThreadExpiresAt = {
360
+ version: 194,
361
+ name: "add_thread_expires_at",
362
+ up: async (client) => {
363
+ await client.query(`
364
+ ALTER TABLE lattice_threads
365
+ ADD COLUMN IF NOT EXISTS expires_at TIMESTAMPTZ
366
+ `);
367
+ await client.query(`
368
+ CREATE INDEX IF NOT EXISTS idx_lattice_threads_expires_at
369
+ ON lattice_threads(expires_at)
370
+ WHERE expires_at IS NOT NULL
371
+ `);
372
+ },
373
+ down: async (client) => {
374
+ await client.query("DROP INDEX IF EXISTS idx_lattice_threads_expires_at");
375
+ await client.query("ALTER TABLE lattice_threads DROP COLUMN IF EXISTS expires_at");
376
+ }
377
+ };
349
378
 
350
379
  // src/migrations/thread_tenant_migration.ts
351
380
  var addThreadTenantId = {
@@ -473,6 +502,7 @@ var PostgreSQLThreadStore = class {
473
502
  this.migrationManager.register(createThreadsTable);
474
503
  this.migrationManager.register(addThreadTenantId);
475
504
  this.migrationManager.register(changeThreadPrimaryKey);
505
+ this.migrationManager.register(addThreadExpiresAt);
476
506
  if (options.autoMigrate !== false) {
477
507
  this.initialize().catch((error) => {
478
508
  console.error("Failed to initialize PostgreSQLThreadStore:", error);
@@ -517,7 +547,7 @@ var PostgreSQLThreadStore = class {
517
547
  await this.ensureInitialized();
518
548
  const result = await this.pool.query(
519
549
  `
520
- SELECT id, tenant_id, assistant_id, metadata, created_at, updated_at
550
+ SELECT id, tenant_id, assistant_id, metadata, created_at, updated_at, expires_at
521
551
  FROM lattice_threads
522
552
  WHERE tenant_id = $1 AND assistant_id = $2
523
553
  ORDER BY created_at DESC
@@ -541,7 +571,7 @@ var PostgreSQLThreadStore = class {
541
571
  await this.ensureInitialized();
542
572
  const result = await this.pool.query(
543
573
  `
544
- SELECT id, tenant_id, assistant_id, metadata, created_at, updated_at
574
+ SELECT id, tenant_id, assistant_id, metadata, created_at, updated_at, expires_at
545
575
  FROM lattice_threads
546
576
  WHERE tenant_id = $1 AND id = $2
547
577
  `,
@@ -561,14 +591,15 @@ var PostgreSQLThreadStore = class {
561
591
  const metadata = data.metadata || {};
562
592
  await this.pool.query(
563
593
  `
564
- INSERT INTO lattice_threads (id, tenant_id, assistant_id, metadata, created_at, updated_at)
565
- VALUES ($1, $2, $3, $4, $5, $6)
594
+ INSERT INTO lattice_threads (id, tenant_id, assistant_id, metadata, created_at, updated_at, expires_at)
595
+ VALUES ($1, $2, $3, $4, $5, $6, $7)
566
596
  ON CONFLICT (id, tenant_id) DO UPDATE SET
567
597
  assistant_id = EXCLUDED.assistant_id,
568
598
  metadata = EXCLUDED.metadata,
569
- updated_at = EXCLUDED.updated_at
599
+ updated_at = EXCLUDED.updated_at,
600
+ expires_at = EXCLUDED.expires_at
570
601
  `,
571
- [threadId, tenantId, assistantId, JSON.stringify(metadata), now, now]
602
+ [threadId, tenantId, assistantId, JSON.stringify(metadata), now, now, data.expiresAt ?? null]
572
603
  );
573
604
  return {
574
605
  id: threadId,
@@ -576,7 +607,8 @@ var PostgreSQLThreadStore = class {
576
607
  assistantId,
577
608
  metadata,
578
609
  createdAt: now,
579
- updatedAt: now
610
+ updatedAt: now,
611
+ expiresAt: data.expiresAt
580
612
  };
581
613
  }
582
614
  /**
@@ -593,18 +625,20 @@ var PostgreSQLThreadStore = class {
593
625
  ...updates.metadata || {}
594
626
  };
595
627
  const now = /* @__PURE__ */ new Date();
628
+ const expiresAt = updates.expiresAt !== void 0 ? updates.expiresAt : existing.expiresAt ?? null;
596
629
  await this.pool.query(
597
630
  `
598
631
  UPDATE lattice_threads
599
- SET metadata = $1, updated_at = $2
600
- WHERE tenant_id = $3 AND id = $4
632
+ SET metadata = $1, updated_at = $2, expires_at = $3
633
+ WHERE tenant_id = $4 AND id = $5
601
634
  `,
602
- [JSON.stringify(updatedMetadata), now, tenantId, threadId]
635
+ [JSON.stringify(updatedMetadata), now, expiresAt, tenantId, threadId]
603
636
  );
604
637
  return {
605
638
  ...existing,
606
639
  metadata: updatedMetadata,
607
- updatedAt: now
640
+ updatedAt: now,
641
+ expiresAt: expiresAt ?? void 0
608
642
  };
609
643
  }
610
644
  /**
@@ -636,6 +670,23 @@ var PostgreSQLThreadStore = class {
636
670
  );
637
671
  return result.rows.length > 0;
638
672
  }
673
+ /**
674
+ * List expired threads across all tenants, oldest expiry first.
675
+ */
676
+ async listExpiredThreads(now, limit) {
677
+ await this.ensureInitialized();
678
+ const result = await this.pool.query(
679
+ `
680
+ SELECT id, tenant_id, assistant_id, metadata, created_at, updated_at, expires_at
681
+ FROM lattice_threads
682
+ WHERE expires_at IS NOT NULL AND expires_at <= $1
683
+ ORDER BY expires_at ASC
684
+ LIMIT $2
685
+ `,
686
+ [now, Math.max(0, limit)]
687
+ );
688
+ return result.rows.map(this.mapRowToThread);
689
+ }
639
690
  /**
640
691
  * Ensure store is initialized
641
692
  */
@@ -654,7 +705,8 @@ var PostgreSQLThreadStore = class {
654
705
  assistantId: row.assistant_id,
655
706
  metadata: typeof row.metadata === "string" ? JSON.parse(row.metadata) : row.metadata || {},
656
707
  createdAt: row.created_at,
657
- updatedAt: row.updated_at
708
+ updatedAt: row.updated_at,
709
+ expiresAt: row.expires_at ?? void 0
658
710
  };
659
711
  }
660
712
  };
@@ -5569,20 +5621,25 @@ var ThreadMessageQueueStore = class {
5569
5621
  }
5570
5622
  /**
5571
5623
  * Get all threads with pending or processing messages
5624
+ *
5625
+ * @returns One entry per full scope, with `hasPending` distinguishing
5626
+ * processing-only threads (no pending row) from threads with work to drain.
5572
5627
  */
5573
5628
  async getThreadsWithPendingMessages() {
5574
5629
  const result = await this.pool.query(
5575
- `SELECT DISTINCT ON (tenant_id, assistant_id, workspace_id, project_id, thread_id) tenant_id, assistant_id, thread_id, workspace_id, project_id
5576
- FROM lattice_thread_message_queue
5577
- WHERE status IN ('pending', 'processing')
5578
- ORDER BY tenant_id, assistant_id, workspace_id, project_id, thread_id`
5630
+ `SELECT tenant_id, assistant_id, thread_id, workspace_id, project_id,
5631
+ bool_or(status = 'pending') AS has_pending
5632
+ FROM lattice_thread_message_queue
5633
+ WHERE status IN ('pending', 'processing')
5634
+ GROUP BY tenant_id, assistant_id, thread_id, workspace_id, project_id`
5579
5635
  );
5580
5636
  return result.rows.map((row) => ({
5581
5637
  tenantId: row.tenant_id,
5582
5638
  assistantId: row.assistant_id,
5583
5639
  threadId: row.thread_id,
5584
5640
  workspaceId: row.workspace_id,
5585
- projectId: row.project_id
5641
+ projectId: row.project_id,
5642
+ hasPending: row.has_pending
5586
5643
  }));
5587
5644
  }
5588
5645
  /**
@@ -9600,19 +9657,18 @@ var PGVectorStoreProvider = class {
9600
9657
  }
9601
9658
  async create(params) {
9602
9659
  const { tableName, embeddingKey } = params;
9603
- let embeddings;
9604
- try {
9605
- embeddings = import_core7.embeddingsLatticeManager.getEmbeddingsClient(embeddingKey);
9606
- } catch {
9607
- try {
9608
- embeddings = import_core7.embeddingsLatticeManager.getEmbeddingsClient("default");
9609
- } catch {
9610
- throw new Error(
9611
- `No embedding model registered. Register one first:
9612
- import { registerEmbeddingsLattice } from "@axiom-lattice/core";
9613
- registerEmbeddingsLattice("default", new SomeEmbeddings(...));`
9614
- );
9615
- }
9660
+ const tenantId = params.tenantId ?? "default";
9661
+ const embeddings = await import_core7.embeddingsLatticeManager.getEmbeddingsClientWithTenant(
9662
+ tenantId,
9663
+ embeddingKey
9664
+ ) ?? await import_core7.embeddingsLatticeManager.getEmbeddingsClientWithTenant(
9665
+ tenantId,
9666
+ "default"
9667
+ );
9668
+ if (!embeddings) {
9669
+ throw new Error(
9670
+ `No embedding model "${embeddingKey}" registered for tenant "${tenantId}", and no deployment default. Configure one in the model provider panel.`
9671
+ );
9616
9672
  }
9617
9673
  return import_pgvector.PGVectorStore.initialize(embeddings, {
9618
9674
  postgresConnectionOptions: { connectionString: this.connectionString },
@@ -10013,6 +10069,299 @@ var createCollectionsTable = {
10013
10069
  }
10014
10070
  };
10015
10071
 
10072
+ // src/migrations/agent_run_ledger_migration.ts
10073
+ var createAgentRunLedgerTable = {
10074
+ version: 193,
10075
+ name: "create_lattice_agent_runs_table",
10076
+ up: async (client) => {
10077
+ await client.query(`
10078
+ CREATE TABLE IF NOT EXISTS lattice_agent_runs (
10079
+ run_id UUID PRIMARY KEY,
10080
+ tenant_id VARCHAR(255) NOT NULL,
10081
+ workspace_id VARCHAR(255),
10082
+ project_id VARCHAR(255),
10083
+ assistant_id VARCHAR(255) NOT NULL,
10084
+ thread_id VARCHAR(255) NOT NULL,
10085
+ status VARCHAR(32) NOT NULL,
10086
+ owner VARCHAR(255) NOT NULL,
10087
+ lease_expires_at TIMESTAMPTZ NOT NULL,
10088
+ input_message_ids TEXT[] NOT NULL DEFAULT '{}',
10089
+ attempts INTEGER NOT NULL DEFAULT 1,
10090
+ error TEXT,
10091
+ cancel_requested BOOLEAN NOT NULL DEFAULT FALSE,
10092
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
10093
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
10094
+ started_at TIMESTAMPTZ NOT NULL DEFAULT now(),
10095
+ finished_at TIMESTAMPTZ
10096
+ )
10097
+ `);
10098
+ await client.query(`
10099
+ CREATE UNIQUE INDEX IF NOT EXISTS uq_lattice_agent_runs_one_running
10100
+ ON lattice_agent_runs (tenant_id, thread_id) WHERE status = 'running'
10101
+ `);
10102
+ await client.query(`
10103
+ CREATE INDEX IF NOT EXISTS idx_lattice_agent_runs_history
10104
+ ON lattice_agent_runs (tenant_id, thread_id, created_at DESC)
10105
+ `);
10106
+ await client.query(`
10107
+ CREATE INDEX IF NOT EXISTS idx_lattice_agent_runs_reap
10108
+ ON lattice_agent_runs (status, lease_expires_at)
10109
+ `);
10110
+ },
10111
+ down: async (client) => {
10112
+ await client.query("DROP TABLE IF EXISTS lattice_agent_runs");
10113
+ }
10114
+ };
10115
+
10116
+ // src/migrations/channel_reply_job_migration.ts
10117
+ var DB_CLOCK_MS = "FLOOR(EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint";
10118
+ var createChannelReplyJobsTable = {
10119
+ version: 200,
10120
+ name: "create_lattice_channel_reply_jobs",
10121
+ up: async (client) => {
10122
+ await client.query(`
10123
+ CREATE TABLE IF NOT EXISTS lattice_channel_reply_jobs (
10124
+ run_id TEXT PRIMARY KEY,
10125
+ tenant_id VARCHAR(255) NOT NULL,
10126
+ assistant_id TEXT NOT NULL,
10127
+ thread_id TEXT NOT NULL,
10128
+ adapter_channel TEXT NOT NULL,
10129
+ channel_installation_id TEXT NOT NULL,
10130
+ reply_target JSONB NOT NULL,
10131
+ input_message_id TEXT NOT NULL,
10132
+ stream_cursor TEXT NULL,
10133
+ status TEXT NOT NULL,
10134
+ attempts INT NOT NULL DEFAULT 0,
10135
+ available_at BIGINT NOT NULL,
10136
+ lease_owner TEXT NULL,
10137
+ lease_expires_at BIGINT NULL,
10138
+ last_error TEXT NULL,
10139
+ output_message_id TEXT NULL,
10140
+ created_at BIGINT NOT NULL DEFAULT (${DB_CLOCK_MS}),
10141
+ updated_at BIGINT NOT NULL DEFAULT (${DB_CLOCK_MS}),
10142
+ delivered_at BIGINT NULL
10143
+ )
10144
+ `);
10145
+ await client.query(`
10146
+ CREATE INDEX IF NOT EXISTS idx_lattice_channel_reply_jobs_due
10147
+ ON lattice_channel_reply_jobs (status, available_at)
10148
+ `);
10149
+ await client.query(`
10150
+ CREATE INDEX IF NOT EXISTS idx_lattice_channel_reply_jobs_tenant_thread
10151
+ ON lattice_channel_reply_jobs (tenant_id, thread_id)
10152
+ `);
10153
+ },
10154
+ down: async (client) => {
10155
+ await client.query("DROP INDEX IF EXISTS idx_lattice_channel_reply_jobs_tenant_thread");
10156
+ await client.query("DROP INDEX IF EXISTS idx_lattice_channel_reply_jobs_due");
10157
+ await client.query("DROP TABLE IF EXISTS lattice_channel_reply_jobs");
10158
+ }
10159
+ };
10160
+
10161
+ // src/migrations/agent_run_task_queue_migration.ts
10162
+ var createAgentRunTaskQueueMigration = {
10163
+ version: 196,
10164
+ name: "expand_lattice_agent_runs_task_queue",
10165
+ up: async (client) => {
10166
+ await client.query(`
10167
+ ALTER TABLE lattice_agent_runs
10168
+ ALTER COLUMN owner DROP NOT NULL,
10169
+ ALTER COLUMN lease_expires_at DROP NOT NULL,
10170
+ ALTER COLUMN started_at DROP NOT NULL,
10171
+ ALTER COLUMN started_at DROP DEFAULT
10172
+ `);
10173
+ await client.query(`
10174
+ ALTER TABLE lattice_agent_runs
10175
+ ADD COLUMN IF NOT EXISTS source VARCHAR(32) NOT NULL DEFAULT 'legacy',
10176
+ ADD COLUMN IF NOT EXISTS input JSONB,
10177
+ ADD COLUMN IF NOT EXISTS command JSONB,
10178
+ ADD COLUMN IF NOT EXISTS config JSONB,
10179
+ ADD COLUMN IF NOT EXISTS metadata JSONB,
10180
+ ADD COLUMN IF NOT EXISTS base_checkpoint_id TEXT,
10181
+ ADD COLUMN IF NOT EXISTS available_at TIMESTAMPTZ NOT NULL DEFAULT now(),
10182
+ ADD COLUMN IF NOT EXISTS rollback_requested BOOLEAN NOT NULL DEFAULT FALSE
10183
+ `);
10184
+ await client.query(`
10185
+ ALTER TABLE lattice_agent_runs
10186
+ ALTER COLUMN source SET DEFAULT 'legacy'
10187
+ `);
10188
+ await client.query(`
10189
+ CREATE INDEX IF NOT EXISTS idx_lattice_agent_runs_run_queue
10190
+ ON lattice_agent_runs (source, status, available_at, created_at)
10191
+ `);
10192
+ },
10193
+ down: async (client) => {
10194
+ await client.query(`
10195
+ DROP INDEX IF EXISTS idx_lattice_agent_runs_run_queue
10196
+ `);
10197
+ await client.query(`
10198
+ ALTER TABLE lattice_agent_runs
10199
+ DROP COLUMN IF EXISTS rollback_requested,
10200
+ DROP COLUMN IF EXISTS available_at,
10201
+ DROP COLUMN IF EXISTS base_checkpoint_id,
10202
+ DROP COLUMN IF EXISTS metadata,
10203
+ DROP COLUMN IF EXISTS config,
10204
+ DROP COLUMN IF EXISTS command,
10205
+ DROP COLUMN IF EXISTS input,
10206
+ DROP COLUMN IF EXISTS source
10207
+ `);
10208
+ await client.query(`
10209
+ ALTER TABLE lattice_agent_runs
10210
+ ALTER COLUMN started_at SET DEFAULT now()
10211
+ `);
10212
+ }
10213
+ };
10214
+
10215
+ // src/migrations/agent_run_capability_revision_migration.ts
10216
+ var createAgentRunCapabilityRevisionMigration = {
10217
+ version: 197,
10218
+ name: "add_agent_run_capability_revision",
10219
+ up: async (client) => {
10220
+ await client.query(`
10221
+ ALTER TABLE lattice_agent_runs
10222
+ ADD COLUMN IF NOT EXISTS capability_runtime_revision TEXT
10223
+ `);
10224
+ },
10225
+ down: async (client) => {
10226
+ await client.query(`
10227
+ ALTER TABLE lattice_agent_runs
10228
+ DROP COLUMN IF EXISTS capability_runtime_revision
10229
+ `);
10230
+ }
10231
+ };
10232
+
10233
+ // src/migrations/agent_run_base_checkpoint_captured_migration.ts
10234
+ var createAgentRunBaseCheckpointCapturedMigration = {
10235
+ version: 198,
10236
+ name: "add_agent_run_base_checkpoint_captured",
10237
+ up: async (client) => {
10238
+ await client.query(`
10239
+ ALTER TABLE lattice_agent_runs
10240
+ ADD COLUMN IF NOT EXISTS base_checkpoint_captured BOOLEAN NOT NULL DEFAULT FALSE
10241
+ `);
10242
+ await client.query(`
10243
+ UPDATE lattice_agent_runs
10244
+ SET base_checkpoint_captured = TRUE
10245
+ WHERE base_checkpoint_id IS NOT NULL
10246
+ `);
10247
+ },
10248
+ down: async (client) => {
10249
+ await client.query(`
10250
+ ALTER TABLE lattice_agent_runs
10251
+ DROP COLUMN IF EXISTS base_checkpoint_captured
10252
+ `);
10253
+ }
10254
+ };
10255
+
10256
+ // src/migrations/agent_run_rollback_failure_migration.ts
10257
+ var createAgentRunRollbackFailureMigration = {
10258
+ version: 199,
10259
+ name: "add_agent_run_rollback_failure",
10260
+ up: async (client) => {
10261
+ await client.query(`
10262
+ ALTER TABLE lattice_agent_runs
10263
+ ADD COLUMN IF NOT EXISTS rollback_attempts INTEGER NOT NULL DEFAULT 0,
10264
+ ADD COLUMN IF NOT EXISTS rollback_error TEXT
10265
+ `);
10266
+ },
10267
+ down: async (client) => {
10268
+ await client.query(`
10269
+ ALTER TABLE lattice_agent_runs
10270
+ DROP COLUMN IF EXISTS rollback_error,
10271
+ DROP COLUMN IF EXISTS rollback_attempts
10272
+ `);
10273
+ }
10274
+ };
10275
+
10276
+ // src/migrations/agent_run_running_index_source_migration.ts
10277
+ var agentRunRunningIndexSourceMigration = {
10278
+ version: 202,
10279
+ name: "scope_agent_run_running_index_to_run_api",
10280
+ up: async (client) => {
10281
+ await client.query("DROP INDEX IF EXISTS uq_lattice_agent_runs_one_running");
10282
+ await client.query(`
10283
+ CREATE UNIQUE INDEX IF NOT EXISTS uq_lattice_agent_runs_one_running
10284
+ ON lattice_agent_runs (tenant_id, thread_id)
10285
+ WHERE status = 'running' AND source = 'run_api'
10286
+ `);
10287
+ },
10288
+ down: async (client) => {
10289
+ await client.query("DROP INDEX IF EXISTS uq_lattice_agent_runs_one_running");
10290
+ await client.query(`
10291
+ CREATE UNIQUE INDEX IF NOT EXISTS uq_lattice_agent_runs_one_running
10292
+ ON lattice_agent_runs (tenant_id, thread_id)
10293
+ WHERE status = 'running'
10294
+ `);
10295
+ }
10296
+ };
10297
+
10298
+ // src/migrations/agent_run_retention_migration.ts
10299
+ var createAgentRunRetentionMigration = {
10300
+ version: 201,
10301
+ name: "add_agent_run_retention_index",
10302
+ up: async (client) => {
10303
+ await client.query(`
10304
+ CREATE INDEX IF NOT EXISTS idx_lattice_agent_runs_terminal_finished_at
10305
+ ON lattice_agent_runs (finished_at)
10306
+ WHERE status IN ('success', 'error', 'interrupted', 'cancelled', 'timeout')
10307
+ `);
10308
+ },
10309
+ down: async (client) => {
10310
+ await client.query(`
10311
+ DROP INDEX IF EXISTS idx_lattice_agent_runs_terminal_finished_at
10312
+ `);
10313
+ }
10314
+ };
10315
+
10316
+ // src/migrations/agent_run_project_task_migration.ts
10317
+ var createAgentRunProjectTaskMigration = {
10318
+ version: 203,
10319
+ name: "add_agent_run_project_task",
10320
+ up: async (client) => {
10321
+ await client.query(`
10322
+ ALTER TABLE lattice_agent_runs
10323
+ ADD COLUMN IF NOT EXISTS task_id TEXT NULL
10324
+ `);
10325
+ await client.query(`
10326
+ CREATE INDEX IF NOT EXISTS idx_lattice_agent_runs_project_task
10327
+ ON lattice_agent_runs (tenant_id, task_id, status, created_at DESC)
10328
+ WHERE task_id IS NOT NULL
10329
+ `);
10330
+ },
10331
+ down: async (client) => {
10332
+ await client.query(`
10333
+ DROP INDEX IF EXISTS idx_lattice_agent_runs_project_task
10334
+ `);
10335
+ await client.query(`
10336
+ ALTER TABLE lattice_agent_runs
10337
+ DROP COLUMN IF EXISTS task_id
10338
+ `);
10339
+ }
10340
+ };
10341
+
10342
+ // src/migrations/agent_run_project_task_running_index_migration.ts
10343
+ var agentRunProjectTaskRunningIndexMigration = {
10344
+ version: 204,
10345
+ name: "scope_agent_run_running_index_to_queue_sources",
10346
+ up: async (client) => {
10347
+ await client.query("DROP INDEX IF EXISTS uq_lattice_agent_runs_one_running");
10348
+ await client.query(`
10349
+ CREATE UNIQUE INDEX IF NOT EXISTS uq_lattice_agent_runs_one_running
10350
+ ON lattice_agent_runs (tenant_id, thread_id)
10351
+ WHERE status = 'running'
10352
+ AND source IN ('run_api', 'project_task')
10353
+ `);
10354
+ },
10355
+ down: async (client) => {
10356
+ await client.query("DROP INDEX IF EXISTS uq_lattice_agent_runs_one_running");
10357
+ await client.query(`
10358
+ CREATE UNIQUE INDEX IF NOT EXISTS uq_lattice_agent_runs_one_running
10359
+ ON lattice_agent_runs (tenant_id, thread_id)
10360
+ WHERE status = 'running' AND source = 'run_api'
10361
+ `);
10362
+ }
10363
+ };
10364
+
10016
10365
  // src/migrations/capability_bundle_migration.ts
10017
10366
  var createCapabilityBundlesTable = {
10018
10367
  version: 170,
@@ -10472,6 +10821,21 @@ var addModelProviderModelDefault = {
10472
10821
  `);
10473
10822
  }
10474
10823
  };
10824
+ var addModelProviderModelKind = {
10825
+ version: 193,
10826
+ name: "add_model_provider_model_kind",
10827
+ up: async (client) => {
10828
+ await client.query(`
10829
+ ALTER TABLE lattice_model_provider_models
10830
+ ADD COLUMN IF NOT EXISTS kind VARCHAR(16) NOT NULL DEFAULT 'chat'
10831
+ `);
10832
+ },
10833
+ down: async (client) => {
10834
+ await client.query(`
10835
+ ALTER TABLE lattice_model_provider_models DROP COLUMN IF EXISTS kind
10836
+ `);
10837
+ }
10838
+ };
10475
10839
 
10476
10840
  // src/stores/PostgreSQLCapabilityBundleStore.ts
10477
10841
  var import_crypto4 = require("crypto");
@@ -11465,6 +11829,7 @@ var PostgreSQLModelProviderStore = class {
11465
11829
  this.migrationManager.register(addModelProviderModelStale);
11466
11830
  this.migrationManager.register(addModelProviderModelExtra);
11467
11831
  this.migrationManager.register(addModelProviderModelDefault);
11832
+ this.migrationManager.register(addModelProviderModelKind);
11468
11833
  if (options.autoMigrate !== false) {
11469
11834
  this.initialize().catch((error) => {
11470
11835
  console.error("Failed to initialize PostgreSQLModelProviderStore:", error);
@@ -11711,7 +12076,7 @@ var PostgreSQLModelProviderStore = class {
11711
12076
  const result = providerId ? await this.pool.query(
11712
12077
  `
11713
12078
  SELECT id, tenant_id, provider_id, model_id, display_name, owned_by,
11714
- upstream_created, enabled, stale, extra, is_default, discovered_at
12079
+ upstream_created, enabled, stale, extra, kind, is_default, discovered_at
11715
12080
  FROM lattice_model_provider_models
11716
12081
  WHERE tenant_id = $1 AND provider_id = $2
11717
12082
  ORDER BY model_id ASC
@@ -11720,7 +12085,7 @@ var PostgreSQLModelProviderStore = class {
11720
12085
  ) : await this.pool.query(
11721
12086
  `
11722
12087
  SELECT id, tenant_id, provider_id, model_id, display_name, owned_by,
11723
- upstream_created, enabled, stale, extra, is_default, discovered_at
12088
+ upstream_created, enabled, stale, extra, kind, is_default, discovered_at
11724
12089
  FROM lattice_model_provider_models
11725
12090
  WHERE tenant_id = $1
11726
12091
  ORDER BY model_id ASC
@@ -11739,7 +12104,7 @@ var PostgreSQLModelProviderStore = class {
11739
12104
  const result = await this.pool.query(
11740
12105
  `
11741
12106
  SELECT id, tenant_id, provider_id, model_id, display_name, owned_by,
11742
- upstream_created, enabled, stale, extra, is_default, discovered_at
12107
+ upstream_created, enabled, stale, extra, kind, is_default, discovered_at
11743
12108
  FROM lattice_model_provider_models
11744
12109
  WHERE tenant_id = $1 AND provider_id = $2 AND model_id = $3
11745
12110
  `,
@@ -11772,7 +12137,7 @@ var PostgreSQLModelProviderStore = class {
11772
12137
  await client.query("BEGIN");
11773
12138
  const current = await client.query(
11774
12139
  `
11775
- SELECT model_id, enabled, extra, is_default FROM lattice_model_provider_models
12140
+ SELECT model_id, enabled, extra, kind, is_default FROM lattice_model_provider_models
11776
12141
  WHERE tenant_id = $1 AND provider_id = $2
11777
12142
  `,
11778
12143
  [tenantId, providerId]
@@ -11783,6 +12148,9 @@ var PostgreSQLModelProviderStore = class {
11783
12148
  const currentExtra = new Map(
11784
12149
  current.rows.map((row) => [row.model_id, row.extra])
11785
12150
  );
12151
+ const currentKind = new Map(
12152
+ current.rows.map((row) => [row.model_id, row.kind])
12153
+ );
11786
12154
  const currentIsDefault = new Map(
11787
12155
  current.rows.map((row) => [row.model_id, row.is_default])
11788
12156
  );
@@ -11797,6 +12165,9 @@ var PostgreSQLModelProviderStore = class {
11797
12165
  const effectiveExtra = rows.map(
11798
12166
  (model) => model.extra ?? currentExtra.get(model.id) ?? {}
11799
12167
  );
12168
+ const effectiveKind = rows.map(
12169
+ (model) => model.kind ?? currentKind.get(model.id) ?? "chat"
12170
+ );
11800
12171
  const namesDefault = rows.some((model) => model.isDefault === true);
11801
12172
  if (namesDefault) {
11802
12173
  await client.query(
@@ -11833,15 +12204,15 @@ var PostgreSQLModelProviderStore = class {
11833
12204
  `
11834
12205
  INSERT INTO lattice_model_provider_models (
11835
12206
  id, tenant_id, provider_id, model_id, display_name, owned_by,
11836
- upstream_created, raw, enabled, stale, extra, is_default, discovered_at
12207
+ upstream_created, raw, enabled, stale, extra, kind, is_default, discovered_at
11837
12208
  )
11838
12209
  SELECT * FROM unnest(
11839
12210
  $1::text[], $2::text[], $3::text[], $4::text[], $5::text[],
11840
12211
  $6::text[], $7::timestamp[], $8::jsonb[], $9::boolean[], $10::boolean[],
11841
- $11::jsonb[], $12::boolean[], $13::timestamp[]
12212
+ $11::jsonb[], $12::text[], $13::boolean[], $14::timestamp[]
11842
12213
  ) AS t(
11843
12214
  id, tenant_id, provider_id, model_id, display_name, owned_by,
11844
- upstream_created, raw, enabled, stale, extra, is_default, discovered_at
12215
+ upstream_created, raw, enabled, stale, extra, kind, is_default, discovered_at
11845
12216
  )
11846
12217
  ON CONFLICT (tenant_id, provider_id, model_id) DO UPDATE SET
11847
12218
  display_name = EXCLUDED.display_name,
@@ -11851,6 +12222,7 @@ var PostgreSQLModelProviderStore = class {
11851
12222
  enabled = EXCLUDED.enabled,
11852
12223
  stale = EXCLUDED.stale,
11853
12224
  extra = EXCLUDED.extra,
12225
+ kind = EXCLUDED.kind,
11854
12226
  is_default = EXCLUDED.is_default,
11855
12227
  discovered_at = EXCLUDED.discovered_at
11856
12228
  `,
@@ -11870,6 +12242,7 @@ var PostgreSQLModelProviderStore = class {
11870
12242
  effectiveEnabled,
11871
12243
  rows.map((model) => model.stale ?? false),
11872
12244
  effectiveExtra.map((extra) => JSON.stringify(extra)),
12245
+ effectiveKind,
11873
12246
  effectiveIsDefault,
11874
12247
  rows.map(() => (/* @__PURE__ */ new Date()).toISOString())
11875
12248
  ]
@@ -11976,6 +12349,7 @@ var PostgreSQLModelProviderStore = class {
11976
12349
  enabled: row.enabled,
11977
12350
  stale: row.stale,
11978
12351
  extra: extraOrUndefined(row.extra),
12352
+ kind: row.kind || "chat",
11979
12353
  isDefault: row.is_default,
11980
12354
  discoveredAt: row.discovered_at
11981
12355
  };
@@ -12329,54 +12703,1883 @@ var PostgreSQLPluginDeploymentStore = class {
12329
12703
  }
12330
12704
  };
12331
12705
 
12332
- // src/createPgStoreConfig.ts
12333
- async function createPgStoreConfig(connectionString) {
12334
- const pool = new import_pg30.Pool({ connectionString });
12335
- const mm = new MigrationManager(pool);
12336
- mm.register(createThreadsTable);
12337
- mm.register(createScheduledTasksTable);
12338
- mm.register(createAssistantsTable);
12339
- mm.register(createSkillsTable);
12340
- mm.register(createWorkspacesTable);
12341
- mm.register(createProjectsTable);
12342
- mm.register(createUsersTable);
12343
- mm.register(createTenantsTable);
12344
- mm.register(addUserStatusColumn);
12345
- mm.register(createUserTenantLinkTable);
12346
- mm.register(migrateRemoveTenantIdFromUsers);
12347
- mm.register(createMetricsConfigsTable);
12348
- mm.register(createMcpServerConfigsTable);
12349
- mm.register(addAssistantTenantId);
12350
- mm.register(addSkillTenantId);
12351
- mm.register(changeSkillPrimaryKey);
12352
- mm.register(addScheduleTenantId);
12353
- mm.register(changeThreadPrimaryKey);
12354
- mm.register(createChannelIdentityMappingTables);
12355
- mm.register(createChannelInstallationsTable);
12356
- mm.register(addThreadTenantId);
12357
- mm.register(addProjectConfigColumn);
12358
- registerThreadMessageQueueMigrations(mm);
12359
- mm.register(createWorkflowTrackingTables);
12360
- for (const m of evalMigrations) {
12361
- mm.register(m);
12362
- }
12363
- mm.register(alterChannelInstallationsTable);
12364
- mm.register(createChannelBindingsTable);
12365
- mm.register(createDatabaseConfigsTable);
12366
- mm.register(changeAssistantPrimaryKey);
12367
- mm.register(addStepThreadId);
12368
- mm.register(createA2AApiKeysTable);
12369
- mm.register(addAssistantOwnerUserId);
12370
- for (const m of taskMigrations) {
12371
- mm.register(m);
12706
+ // src/stores/PostgreSQLAgentRunLedger.ts
12707
+ var import_pg30 = require("pg");
12708
+ var import_core10 = require("@axiom-lattice/core");
12709
+ var DEFAULT_LIST_LIMIT = 50;
12710
+ var MAX_LIST_LIMIT = 200;
12711
+ var CLAIM_RUN_SQL = `UPDATE lattice_agent_runs AS r
12712
+ SET status = 'running',
12713
+ owner = $2,
12714
+ lease_expires_at = now() + ($3::bigint || ' milliseconds')::interval,
12715
+ started_at = now(),
12716
+ attempts = r.attempts + 1,
12717
+ input_message_ids = COALESCE($4::text[], r.input_message_ids),
12718
+ updated_at = now()
12719
+ WHERE r.run_id = $1
12720
+ AND r.source = ANY($5::text[])
12721
+ AND r.status = 'pending'
12722
+ AND r.available_at <= now()
12723
+ AND NOT EXISTS (
12724
+ SELECT 1 FROM lattice_agent_runs AS o
12725
+ WHERE o.tenant_id = r.tenant_id
12726
+ AND o.thread_id = r.thread_id
12727
+ AND o.source IN ('run_api', 'project_task')
12728
+ AND o.status = 'running'
12729
+ )
12730
+ AND NOT EXISTS (
12731
+ SELECT 1 FROM lattice_agent_runs AS b
12732
+ WHERE b.tenant_id = r.tenant_id
12733
+ AND b.thread_id = r.thread_id
12734
+ AND b.source = 'run_api'
12735
+ AND b.rollback_requested = TRUE
12736
+ )
12737
+ RETURNING r.*`;
12738
+ var PostgreSQLAgentRunLedger = class {
12739
+ /**
12740
+ * @param options An externally managed pool, or a pool config/connection URL.
12741
+ */
12742
+ constructor(options) {
12743
+ this.ownsPool = true;
12744
+ this.initialized = false;
12745
+ this.initPromise = null;
12746
+ if (options.pool) {
12747
+ this.pool = options.pool;
12748
+ this.ownsPool = false;
12749
+ this.initialized = true;
12750
+ return;
12751
+ }
12752
+ if (typeof options.poolConfig === "string") {
12753
+ this.pool = new import_pg30.Pool({ connectionString: options.poolConfig });
12754
+ } else if (options.poolConfig) {
12755
+ this.pool = new import_pg30.Pool(options.poolConfig);
12756
+ } else {
12757
+ throw new Error("Either pool or poolConfig must be provided");
12758
+ }
12759
+ this.migrationManager = new MigrationManager(this.pool);
12760
+ this.migrationManager.register(createAgentRunLedgerTable);
12761
+ this.migrationManager.register(createAgentRunTaskQueueMigration);
12762
+ this.migrationManager.register(createAgentRunCapabilityRevisionMigration);
12763
+ this.migrationManager.register(createAgentRunBaseCheckpointCapturedMigration);
12764
+ this.migrationManager.register(createAgentRunRollbackFailureMigration);
12765
+ this.migrationManager.register(createAgentRunRetentionMigration);
12766
+ this.migrationManager.register(agentRunRunningIndexSourceMigration);
12767
+ this.migrationManager.register(createAgentRunProjectTaskMigration);
12768
+ this.migrationManager.register(agentRunProjectTaskRunningIndexMigration);
12769
+ if (options.autoMigrate !== false) {
12770
+ this.initialize().catch((error) => {
12771
+ console.error("Failed to initialize PostgreSQLAgentRunLedger:", error);
12772
+ throw error;
12773
+ });
12774
+ }
12372
12775
  }
12373
- mm.register(createMenuItemsTable);
12374
- mm.register(createSharedResourcesTable);
12375
- mm.register(addFileContentType);
12376
- mm.register(addMcpAppContentType);
12377
- mm.register(createCollectionsTable);
12378
- mm.register(createTaskWorkItemsMigration);
12379
- mm.register(addWorkItemProjectFieldsMigration);
12776
+ /** Runs pending migrations once. */
12777
+ async initialize() {
12778
+ if (this.initialized) return;
12779
+ if (this.initPromise) return this.initPromise;
12780
+ this.initPromise = (async () => {
12781
+ try {
12782
+ await this.migrationManager.migrate();
12783
+ this.initialized = true;
12784
+ } finally {
12785
+ this.initPromise = null;
12786
+ }
12787
+ })();
12788
+ return this.initPromise;
12789
+ }
12790
+ /** Ends the pool when this store owns it. */
12791
+ async dispose() {
12792
+ if (this.ownsPool && this.pool) {
12793
+ await this.pool.end();
12794
+ }
12795
+ }
12796
+ /**
12797
+ * Creates a running legacy run, reconciling any existing running row first.
12798
+ *
12799
+ * @remarks The caller holds the `thread-run` lease, so a surviving `running`
12800
+ * row is stale (its owner can no longer hold the lease) even when its ledger
12801
+ * lease has not expired yet.
12802
+ */
12803
+ async beginRun(input) {
12804
+ const client = await this.pool.connect();
12805
+ try {
12806
+ await client.query("BEGIN");
12807
+ await client.query(
12808
+ `UPDATE lattice_agent_runs
12809
+ SET status = 'interrupted',
12810
+ error = COALESCE(error, 'superseded by a new run on the same thread'),
12811
+ finished_at = now(),
12812
+ updated_at = now()
12813
+ WHERE tenant_id = $1 AND thread_id = $2
12814
+ AND status = 'running'`,
12815
+ [input.tenantId, input.threadId]
12816
+ );
12817
+ const result = await client.query(
12818
+ `INSERT INTO lattice_agent_runs
12819
+ (run_id, tenant_id, workspace_id, project_id, assistant_id, thread_id,
12820
+ status, source, owner, lease_expires_at, input_message_ids, attempts,
12821
+ started_at)
12822
+ VALUES ($1, $2, $3, $4, $5, $6, 'running', 'legacy', $7,
12823
+ now() + ($8::bigint || ' milliseconds')::interval, $9, 1, now())
12824
+ RETURNING *`,
12825
+ [
12826
+ input.runId,
12827
+ input.tenantId,
12828
+ input.workspaceId ?? null,
12829
+ input.projectId ?? null,
12830
+ input.assistantId,
12831
+ input.threadId,
12832
+ input.owner,
12833
+ input.leaseMs,
12834
+ input.inputMessageIds
12835
+ ]
12836
+ );
12837
+ await client.query("COMMIT");
12838
+ return rowToRecord(result.rows[0]);
12839
+ } catch (error) {
12840
+ await client.query("ROLLBACK");
12841
+ if (isActiveRunViolation(error)) {
12842
+ throw new import_core10.AgentRunAlreadyActiveError();
12843
+ }
12844
+ throw error;
12845
+ } finally {
12846
+ client.release();
12847
+ }
12848
+ }
12849
+ /** Creates an unowned pending run-queue row. */
12850
+ async createPendingRun(input) {
12851
+ const source = input.source ?? "run_api";
12852
+ if (source === "project_task") {
12853
+ if (input.taskId === void 0 || input.taskId.trim().length === 0) {
12854
+ throw new import_core10.AgentRunInvalidTaskIdError(
12855
+ "A project_task run requires a non-empty taskId"
12856
+ );
12857
+ }
12858
+ } else if (input.taskId !== void 0) {
12859
+ throw new import_core10.AgentRunInvalidTaskIdError(
12860
+ `A ${source} run cannot carry a taskId`
12861
+ );
12862
+ }
12863
+ if (input.input != null && input.command != null) {
12864
+ throw new import_core10.AgentRunInputCommandConflictError();
12865
+ }
12866
+ const inputMessageIds = input.inputMessageIds ? (0, import_core10.normalizeAgentRunMessageIds)(input.inputMessageIds) : (0, import_core10.extractAgentRunInputMessageIds)(input.input);
12867
+ const availableAt = input.availableAt ?? Date.now();
12868
+ const limit = input.maxActiveRunsForTenant;
12869
+ const client = await this.pool.connect();
12870
+ try {
12871
+ await client.query("BEGIN");
12872
+ if (limit !== void 0 && limit > 0) {
12873
+ await client.query("SELECT pg_advisory_xact_lock(hashtext($1))", [
12874
+ input.tenantId
12875
+ ]);
12876
+ }
12877
+ await client.query("SELECT pg_advisory_xact_lock(hashtext($1))", [
12878
+ `agent-run:${input.tenantId}:${input.threadId}`
12879
+ ]);
12880
+ if (inputMessageIds.length > 0) {
12881
+ const duplicate = await client.query(
12882
+ `SELECT input_message_ids
12883
+ FROM lattice_agent_runs
12884
+ WHERE tenant_id = $1 AND thread_id = $2
12885
+ AND status IN ('pending', 'running')
12886
+ AND input_message_ids && $3::text[]
12887
+ LIMIT 1`,
12888
+ [input.tenantId, input.threadId, inputMessageIds]
12889
+ );
12890
+ if ((duplicate.rowCount ?? 0) > 0) {
12891
+ const existing = duplicate.rows[0].input_message_ids;
12892
+ const messageId = existing.find((id) => inputMessageIds.includes(id)) ?? inputMessageIds[0];
12893
+ throw new import_core10.AgentRunDuplicateMessageIdError(messageId);
12894
+ }
12895
+ }
12896
+ if (limit !== void 0 && limit > 0) {
12897
+ const active = await client.query(
12898
+ `SELECT count(*)::text AS count
12899
+ FROM lattice_agent_runs
12900
+ WHERE tenant_id = $1
12901
+ AND source = 'run_api'
12902
+ AND status IN ('pending', 'running')`,
12903
+ [input.tenantId]
12904
+ );
12905
+ if (Number(active.rows[0].count) >= limit) {
12906
+ throw new import_core10.AgentRunTenantQuotaExceededError(input.tenantId, limit);
12907
+ }
12908
+ }
12909
+ const result = await client.query(
12910
+ `INSERT INTO lattice_agent_runs
12911
+ (run_id, tenant_id, workspace_id, project_id, assistant_id, thread_id,
12912
+ status, source, owner, lease_expires_at, input_message_ids, attempts,
12913
+ input, command, config, metadata, capability_runtime_revision,
12914
+ available_at, created_at, updated_at, task_id)
12915
+ VALUES ($1, $2, $3, $4, $5, $6, 'pending', $7, NULL, NULL, $8, 0,
12916
+ $9::jsonb, $10::jsonb, $11::jsonb, $12::jsonb, $13,
12917
+ to_timestamp($14::double precision / 1000.0),
12918
+ date_trunc('milliseconds', now()),
12919
+ date_trunc('milliseconds', now()), $15)
12920
+ RETURNING *`,
12921
+ [
12922
+ input.runId,
12923
+ input.tenantId,
12924
+ input.workspaceId ?? null,
12925
+ input.projectId ?? null,
12926
+ input.assistantId,
12927
+ input.threadId,
12928
+ source,
12929
+ inputMessageIds,
12930
+ jsonParam(input.input),
12931
+ jsonParam(input.command),
12932
+ jsonParam(input.config),
12933
+ jsonParam(input.metadata),
12934
+ input.capabilityRuntimeRevision ?? null,
12935
+ availableAt,
12936
+ input.taskId ?? null
12937
+ ]
12938
+ );
12939
+ await client.query("COMMIT");
12940
+ return rowToRecord(result.rows[0]);
12941
+ } catch (error) {
12942
+ await client.query("ROLLBACK");
12943
+ throw error;
12944
+ } finally {
12945
+ client.release();
12946
+ }
12947
+ }
12948
+ /** Lists claimable run-queue rows, oldest available first. */
12949
+ async listPendingRuns(options) {
12950
+ const limit = clampLimit(options?.limit);
12951
+ const after = options?.after;
12952
+ const fairTenants = options?.fairTenants === true;
12953
+ if (fairTenants && after !== void 0) {
12954
+ throw new RangeError(
12955
+ "listPendingRuns fairTenants does not support the after cursor"
12956
+ );
12957
+ }
12958
+ const maxRunning = positiveLimit(options?.maxRunningPerTenant);
12959
+ const sources = options?.sources === void 0 ? ["run_api"] : [...options.sources];
12960
+ (0, import_core10.assertQueueSources)(sources);
12961
+ const params = [limit];
12962
+ let cursorClause = "";
12963
+ if (after) {
12964
+ params.push(
12965
+ new Date(after.availableAt),
12966
+ new Date(after.createdAt),
12967
+ after.runId
12968
+ );
12969
+ cursorClause = `
12970
+ AND (r.available_at, r.created_at, r.run_id)
12971
+ > ($2::timestamptz, $3::timestamptz, $4::uuid)`;
12972
+ }
12973
+ params.push(maxRunning ?? null);
12974
+ const capIndex = params.length;
12975
+ params.push(sources);
12976
+ const sourcesIndex = params.length;
12977
+ const capClause = `
12978
+ AND ($${capIndex}::bigint IS NULL OR (
12979
+ SELECT count(*) FROM lattice_agent_runs AS c
12980
+ WHERE c.tenant_id = r.tenant_id
12981
+ AND c.source = ANY($${sourcesIndex}::text[])
12982
+ AND c.status = 'running'
12983
+ ) < $${capIndex}::bigint)`;
12984
+ const filterClause = `WHERE r.source = ANY($${sourcesIndex}::text[]) AND r.status = 'pending'
12985
+ AND r.available_at <= now()
12986
+ AND NOT EXISTS (
12987
+ SELECT 1 FROM lattice_agent_runs AS o
12988
+ WHERE o.tenant_id = r.tenant_id
12989
+ AND o.thread_id = r.thread_id
12990
+ AND o.source IN ('run_api', 'project_task')
12991
+ AND o.status = 'running'
12992
+ )
12993
+ AND NOT EXISTS (
12994
+ SELECT 1 FROM lattice_agent_runs AS b
12995
+ WHERE b.tenant_id = r.tenant_id
12996
+ AND b.thread_id = r.thread_id
12997
+ AND b.source = 'run_api'
12998
+ AND b.rollback_requested = TRUE
12999
+ )${capClause}${cursorClause}`;
13000
+ if (fairTenants) {
13001
+ const ranked = await this.pool.query(
13002
+ `WITH ranked AS (
13003
+ SELECT r.*,
13004
+ row_number() OVER (
13005
+ PARTITION BY r.tenant_id
13006
+ ORDER BY r.available_at ASC, r.created_at ASC, r.run_id ASC
13007
+ ) AS tenant_rank
13008
+ FROM lattice_agent_runs AS r
13009
+ ${filterClause}
13010
+ )
13011
+ SELECT * FROM ranked
13012
+ ORDER BY tenant_rank ASC, available_at ASC, created_at ASC,
13013
+ run_id ASC, tenant_id ASC
13014
+ LIMIT $1`,
13015
+ params
13016
+ );
13017
+ return ranked.rows.map(rowToRecord);
13018
+ }
13019
+ const result = await this.pool.query(
13020
+ `SELECT r.* FROM lattice_agent_runs AS r
13021
+ ${filterClause}
13022
+ ORDER BY r.available_at ASC, r.created_at ASC, r.run_id ASC
13023
+ LIMIT $1`,
13024
+ params
13025
+ );
13026
+ return result.rows.map(rowToRecord);
13027
+ }
13028
+ /** Atomically claims a pending run-queue row. */
13029
+ async claimRun(input) {
13030
+ const maxRunning = positiveLimit(input.maxRunningPerTenant);
13031
+ const sources = input.sources === void 0 ? ["run_api"] : [...input.sources];
13032
+ (0, import_core10.assertQueueSources)(sources);
13033
+ if (maxRunning !== void 0) {
13034
+ return this.claimRunWithTenantCap(input, maxRunning, sources);
13035
+ }
13036
+ try {
13037
+ const result = await this.pool.query(
13038
+ CLAIM_RUN_SQL,
13039
+ [
13040
+ input.runId,
13041
+ input.owner,
13042
+ input.leaseMs,
13043
+ input.inputMessageIds ?? null,
13044
+ sources
13045
+ ]
13046
+ );
13047
+ return result.rowCount === 0 ? void 0 : rowToRecord(result.rows[0]);
13048
+ } catch (error) {
13049
+ if (isActiveRunViolation(error)) return void 0;
13050
+ throw error;
13051
+ }
13052
+ }
13053
+ /**
13054
+ * Claims under a per-tenant running cap.
13055
+ *
13056
+ * @remarks Serializes claims for one tenant with a transaction-scoped
13057
+ * advisory lock, counts running rows, then runs the same claim statement.
13058
+ * The lock is shared with admission creates so cap checks cannot race.
13059
+ */
13060
+ async claimRunWithTenantCap(input, maxRunning, sources) {
13061
+ const client = await this.pool.connect();
13062
+ try {
13063
+ await client.query("BEGIN");
13064
+ const candidate = await client.query(
13065
+ `SELECT tenant_id FROM lattice_agent_runs
13066
+ WHERE run_id = $1 AND source = ANY($2::text[]) AND status = 'pending'`,
13067
+ [input.runId, sources]
13068
+ );
13069
+ if (candidate.rowCount === 0) {
13070
+ await client.query("COMMIT");
13071
+ return void 0;
13072
+ }
13073
+ const tenantId = candidate.rows[0].tenant_id;
13074
+ await client.query("SELECT pg_advisory_xact_lock(hashtext($1))", [
13075
+ tenantId
13076
+ ]);
13077
+ const running = await client.query(
13078
+ `SELECT count(*)::text AS count
13079
+ FROM lattice_agent_runs
13080
+ WHERE tenant_id = $1
13081
+ AND source = ANY($2::text[])
13082
+ AND status = 'running'`,
13083
+ [tenantId, sources]
13084
+ );
13085
+ if (Number(running.rows[0].count) >= maxRunning) {
13086
+ await client.query("COMMIT");
13087
+ return void 0;
13088
+ }
13089
+ const result = await client.query(
13090
+ CLAIM_RUN_SQL,
13091
+ [
13092
+ input.runId,
13093
+ input.owner,
13094
+ input.leaseMs,
13095
+ input.inputMessageIds ?? null,
13096
+ sources
13097
+ ]
13098
+ );
13099
+ await client.query("COMMIT");
13100
+ return result.rowCount === 0 ? void 0 : rowToRecord(result.rows[0]);
13101
+ } catch (error) {
13102
+ await client.query("ROLLBACK");
13103
+ if (isActiveRunViolation(error)) return void 0;
13104
+ throw error;
13105
+ } finally {
13106
+ client.release();
13107
+ }
13108
+ }
13109
+ /** Captures the checkpoint a running run-queue row started from. */
13110
+ async setBaseCheckpoint(tenantId, runId, owner, baseCheckpointId) {
13111
+ const result = await this.pool.query(
13112
+ `UPDATE lattice_agent_runs
13113
+ SET base_checkpoint_id = $4,
13114
+ base_checkpoint_captured = TRUE,
13115
+ updated_at = now()
13116
+ WHERE tenant_id = $1 AND run_id = $2 AND owner = $3
13117
+ AND status = 'running'
13118
+ AND source IN ('run_api', 'project_task')
13119
+ RETURNING run_id`,
13120
+ [tenantId, runId, owner, baseCheckpointId]
13121
+ );
13122
+ return (result.rowCount ?? 0) > 0;
13123
+ }
13124
+ /** Records the resolved capability runtime revision for a running run. */
13125
+ async setCapabilityRevision(tenantId, runId, owner, revision) {
13126
+ const result = await this.pool.query(
13127
+ `UPDATE lattice_agent_runs
13128
+ SET capability_runtime_revision = $4, updated_at = now()
13129
+ WHERE tenant_id = $1 AND run_id = $2 AND owner = $3
13130
+ AND status = 'running'
13131
+ AND source IN ('run_api', 'project_task')
13132
+ RETURNING run_id`,
13133
+ [tenantId, runId, owner, revision]
13134
+ );
13135
+ return (result.rowCount ?? 0) > 0;
13136
+ }
13137
+ /** Deletes a run row immediately, optionally guarded by status/owner. */
13138
+ async deleteRun(tenantId, runId, expected) {
13139
+ const params = [tenantId, runId];
13140
+ let predicate = "tenant_id = $1 AND run_id = $2";
13141
+ if (expected?.status !== void 0) {
13142
+ params.push(expected.status);
13143
+ predicate += ` AND status = $${params.length}`;
13144
+ }
13145
+ if (expected?.owner !== void 0) {
13146
+ params.push(expected.owner);
13147
+ predicate += ` AND owner = $${params.length}`;
13148
+ }
13149
+ const result = await this.pool.query(
13150
+ `DELETE FROM lattice_agent_runs WHERE ${predicate}`,
13151
+ params
13152
+ );
13153
+ return (result.rowCount ?? 0) > 0;
13154
+ }
13155
+ /** Deletes terminal rows finished before `cutoffMs`, oldest first. */
13156
+ async deleteTerminalBefore(cutoffMs, limit) {
13157
+ if (!Number.isFinite(cutoffMs)) return 0;
13158
+ const max = Number.isFinite(limit) ? Math.floor(limit) : 0;
13159
+ if (max <= 0) return 0;
13160
+ const result = await this.pool.query(
13161
+ `DELETE FROM lattice_agent_runs
13162
+ WHERE run_id IN (
13163
+ SELECT run_id FROM lattice_agent_runs
13164
+ WHERE status IN ('success', 'error', 'interrupted', 'cancelled', 'timeout')
13165
+ AND finished_at < to_timestamp($1::double precision / 1000.0)
13166
+ ORDER BY finished_at ASC
13167
+ LIMIT $2
13168
+ )`,
13169
+ [cutoffMs, max]
13170
+ );
13171
+ return result.rowCount ?? 0;
13172
+ }
13173
+ /** Extends a running run's lease when `owner` still holds it. */
13174
+ async heartbeatRun(runId, owner, leaseMs) {
13175
+ const result = await this.pool.query(
13176
+ `UPDATE lattice_agent_runs
13177
+ SET lease_expires_at = now() + ($3::bigint || ' milliseconds')::interval,
13178
+ updated_at = now()
13179
+ WHERE run_id = $1 AND owner = $2 AND status = 'running'
13180
+ RETURNING run_id`,
13181
+ [runId, owner, leaseMs]
13182
+ );
13183
+ return (result.rowCount ?? 0) > 0;
13184
+ }
13185
+ /** Settles a running run when `owner` still holds it. */
13186
+ async settleRun(runId, owner, status, error) {
13187
+ const result = await this.pool.query(
13188
+ `UPDATE lattice_agent_runs
13189
+ SET status = $3, error = $4, finished_at = now(), updated_at = now()
13190
+ WHERE run_id = $1 AND owner = $2 AND status = 'running'
13191
+ RETURNING run_id`,
13192
+ [runId, owner, status, error ?? null]
13193
+ );
13194
+ return (result.rowCount ?? 0) > 0;
13195
+ }
13196
+ /** Settles a running run only when no rollback is pending. */
13197
+ async settleRunUnlessRollbackRequested(runId, owner, status, error) {
13198
+ const result = await this.pool.query(
13199
+ `UPDATE lattice_agent_runs
13200
+ SET status = $3, error = $4, finished_at = now(), updated_at = now()
13201
+ WHERE run_id = $1 AND owner = $2 AND status = 'running'
13202
+ AND rollback_requested = FALSE
13203
+ RETURNING run_id`,
13204
+ [runId, owner, status, error ?? null]
13205
+ );
13206
+ return (result.rowCount ?? 0) > 0;
13207
+ }
13208
+ /** Terminates an uncaptured rollback run with a repair signal. */
13209
+ async settleRollbackFailure(tenantId, runId, error) {
13210
+ const result = await this.pool.query(
13211
+ `UPDATE lattice_agent_runs
13212
+ SET status = 'error',
13213
+ error = $3,
13214
+ rollback_requested = FALSE,
13215
+ rollback_error = $3,
13216
+ finished_at = now(),
13217
+ updated_at = now()
13218
+ WHERE tenant_id = $1 AND run_id = $2
13219
+ AND source = 'run_api'
13220
+ AND rollback_requested = TRUE
13221
+ AND base_checkpoint_captured = FALSE
13222
+ AND status IN ('running', 'interrupted')
13223
+ RETURNING run_id`,
13224
+ [tenantId, runId, error]
13225
+ );
13226
+ return (result.rowCount ?? 0) > 0;
13227
+ }
13228
+ /** Records one bounded rollback failure and optionally converges. */
13229
+ async recordRollbackFailure(tenantId, runId, error, maxAttempts) {
13230
+ const result = await this.pool.query(
13231
+ `UPDATE lattice_agent_runs
13232
+ SET rollback_attempts = rollback_attempts + 1,
13233
+ rollback_error = $3,
13234
+ status = CASE
13235
+ WHEN rollback_attempts + 1 >= $4 THEN 'error'
13236
+ ELSE status
13237
+ END,
13238
+ error = CASE
13239
+ WHEN rollback_attempts + 1 >= $4 THEN $3
13240
+ ELSE error
13241
+ END,
13242
+ rollback_requested = CASE
13243
+ WHEN rollback_attempts + 1 >= $4 THEN FALSE
13244
+ ELSE rollback_requested
13245
+ END,
13246
+ finished_at = CASE
13247
+ WHEN rollback_attempts + 1 >= $4 THEN now()
13248
+ ELSE finished_at
13249
+ END,
13250
+ updated_at = now()
13251
+ WHERE tenant_id = $1
13252
+ AND run_id = $2
13253
+ AND source = 'run_api'
13254
+ AND rollback_requested = TRUE
13255
+ AND status IN ('running', 'interrupted')
13256
+ RETURNING rollback_attempts, status`,
13257
+ [tenantId, runId, error, maxAttempts]
13258
+ );
13259
+ if (result.rowCount === 0) return void 0;
13260
+ const row = result.rows[0];
13261
+ return {
13262
+ attempts: row.rollback_attempts,
13263
+ terminal: row.status === "error"
13264
+ };
13265
+ }
13266
+ /** Reads a run within a tenant. */
13267
+ async getRun(tenantId, runId) {
13268
+ const result = await this.pool.query(
13269
+ `SELECT * FROM lattice_agent_runs WHERE tenant_id = $1 AND run_id = $2`,
13270
+ [tenantId, runId]
13271
+ );
13272
+ return result.rowCount === 0 ? void 0 : rowToRecord(result.rows[0]);
13273
+ }
13274
+ /** Finds the active (pending/running) run for a ProjectTask. */
13275
+ async getActiveRunForTask(tenantId, taskId) {
13276
+ const result = await this.pool.query(
13277
+ `SELECT * FROM lattice_agent_runs
13278
+ WHERE tenant_id = $1 AND task_id = $2
13279
+ AND status IN ('pending', 'running')
13280
+ ORDER BY created_at DESC, run_id DESC
13281
+ LIMIT 1`,
13282
+ [tenantId, taskId]
13283
+ );
13284
+ return result.rowCount === 0 ? void 0 : rowToRecord(result.rows[0]);
13285
+ }
13286
+ /** Lists a thread's runs, newest first (including pending rows). */
13287
+ async listRuns(tenantId, threadId, options) {
13288
+ const limit = clampLimit(options?.limit);
13289
+ const offset = Math.max(0, Math.floor(options?.offset ?? 0));
13290
+ const result = await this.pool.query(
13291
+ `SELECT * FROM lattice_agent_runs
13292
+ WHERE tenant_id = $1 AND thread_id = $2
13293
+ ORDER BY created_at DESC
13294
+ LIMIT $3 OFFSET $4`,
13295
+ [tenantId, threadId, limit, offset]
13296
+ );
13297
+ return result.rows.map(rowToRecord);
13298
+ }
13299
+ /** Finds the thread's running run, if any. */
13300
+ async findRunning(tenantId, threadId) {
13301
+ const result = await this.pool.query(
13302
+ `SELECT * FROM lattice_agent_runs
13303
+ WHERE tenant_id = $1 AND thread_id = $2 AND status = 'running'
13304
+ LIMIT 1`,
13305
+ [tenantId, threadId]
13306
+ );
13307
+ return result.rowCount === 0 ? void 0 : rowToRecord(result.rows[0]);
13308
+ }
13309
+ /** Requests cancellation, settling pending/expired rows interrupted. */
13310
+ async requestCancel(tenantId, runId) {
13311
+ const client = await this.pool.connect();
13312
+ try {
13313
+ await client.query("BEGIN");
13314
+ const current = await client.query(
13315
+ `SELECT status,
13316
+ COALESCE(lease_expires_at < now(), TRUE) AS expired
13317
+ FROM lattice_agent_runs
13318
+ WHERE tenant_id = $1 AND run_id = $2
13319
+ FOR UPDATE`,
13320
+ [tenantId, runId]
13321
+ );
13322
+ if (current.rowCount === 0) {
13323
+ await client.query("COMMIT");
13324
+ return "not_found";
13325
+ }
13326
+ const row = current.rows[0];
13327
+ if (row.status === "pending") {
13328
+ await client.query(
13329
+ `UPDATE lattice_agent_runs
13330
+ SET status = 'interrupted', cancel_requested = TRUE,
13331
+ finished_at = now(), updated_at = now()
13332
+ WHERE tenant_id = $1 AND run_id = $2`,
13333
+ [tenantId, runId]
13334
+ );
13335
+ await client.query("COMMIT");
13336
+ return "cancelled";
13337
+ }
13338
+ if (row.status !== "running") {
13339
+ await client.query("COMMIT");
13340
+ return "terminal";
13341
+ }
13342
+ if (row.expired) {
13343
+ await client.query(
13344
+ `UPDATE lattice_agent_runs
13345
+ SET status = 'interrupted', cancel_requested = TRUE,
13346
+ finished_at = now(), updated_at = now()
13347
+ WHERE tenant_id = $1 AND run_id = $2`,
13348
+ [tenantId, runId]
13349
+ );
13350
+ await client.query("COMMIT");
13351
+ return "cancelled";
13352
+ }
13353
+ await client.query(
13354
+ `UPDATE lattice_agent_runs
13355
+ SET cancel_requested = TRUE, updated_at = now()
13356
+ WHERE tenant_id = $1 AND run_id = $2`,
13357
+ [tenantId, runId]
13358
+ );
13359
+ await client.query("COMMIT");
13360
+ return "requested";
13361
+ } catch (error) {
13362
+ await client.query("ROLLBACK");
13363
+ throw error;
13364
+ } finally {
13365
+ client.release();
13366
+ }
13367
+ }
13368
+ /** Deletes a pending run or flags a running run for rollback. */
13369
+ async requestRollback(tenantId, runId) {
13370
+ const client = await this.pool.connect();
13371
+ try {
13372
+ await client.query("BEGIN");
13373
+ const current = await client.query(
13374
+ `SELECT status, source FROM lattice_agent_runs
13375
+ WHERE tenant_id = $1 AND run_id = $2
13376
+ FOR UPDATE`,
13377
+ [tenantId, runId]
13378
+ );
13379
+ if (current.rowCount === 0) {
13380
+ await client.query("COMMIT");
13381
+ return "not_found";
13382
+ }
13383
+ const { status, source } = current.rows[0];
13384
+ if (source !== "run_api") {
13385
+ await client.query("COMMIT");
13386
+ return "terminal";
13387
+ }
13388
+ if (status === "pending") {
13389
+ await client.query(
13390
+ `DELETE FROM lattice_agent_runs WHERE tenant_id = $1 AND run_id = $2`,
13391
+ [tenantId, runId]
13392
+ );
13393
+ await client.query("COMMIT");
13394
+ return "deleted";
13395
+ }
13396
+ if (status !== "running") {
13397
+ await client.query("COMMIT");
13398
+ return "terminal";
13399
+ }
13400
+ await client.query(
13401
+ `UPDATE lattice_agent_runs
13402
+ SET rollback_requested = TRUE, cancel_requested = TRUE,
13403
+ updated_at = now()
13404
+ WHERE tenant_id = $1 AND run_id = $2`,
13405
+ [tenantId, runId]
13406
+ );
13407
+ await client.query("COMMIT");
13408
+ return "requested";
13409
+ } catch (error) {
13410
+ await client.query("ROLLBACK");
13411
+ throw error;
13412
+ } finally {
13413
+ client.release();
13414
+ }
13415
+ }
13416
+ /** Atomically reclaims an expired running row without requeueing it. */
13417
+ async recoverExpiredRun(runId, owner, leaseMs) {
13418
+ const result = await this.pool.query(
13419
+ `UPDATE lattice_agent_runs
13420
+ SET owner = $2,
13421
+ lease_expires_at = now() + ($3::bigint || ' milliseconds')::interval,
13422
+ attempts = attempts + 1,
13423
+ updated_at = now()
13424
+ WHERE run_id = $1
13425
+ AND source IN ('run_api', 'project_task')
13426
+ AND status = 'running'
13427
+ AND lease_expires_at < now()
13428
+ RETURNING *`,
13429
+ [runId, owner, leaseMs]
13430
+ );
13431
+ return result.rowCount === 0 ? void 0 : rowToRecord(result.rows[0]);
13432
+ }
13433
+ /** Atomically requeues or interrupts an expired run-queue row. */
13434
+ async settleExpiredRun(runId, outcome, options) {
13435
+ if (outcome === "pending") {
13436
+ const availableAt = options?.availableAt ?? Date.now();
13437
+ const result2 = await this.pool.query(
13438
+ `UPDATE lattice_agent_runs
13439
+ SET status = 'pending',
13440
+ owner = NULL,
13441
+ lease_expires_at = NULL,
13442
+ started_at = NULL,
13443
+ available_at = to_timestamp($2::double precision / 1000.0),
13444
+ finished_at = NULL,
13445
+ updated_at = now()
13446
+ WHERE run_id = $1
13447
+ AND source IN ('run_api', 'project_task')
13448
+ AND status = 'running'
13449
+ AND lease_expires_at < now()
13450
+ RETURNING run_id`,
13451
+ [runId, availableAt]
13452
+ );
13453
+ return (result2.rowCount ?? 0) > 0;
13454
+ }
13455
+ const result = await this.pool.query(
13456
+ `UPDATE lattice_agent_runs
13457
+ SET status = $3,
13458
+ error = COALESCE($2, error),
13459
+ finished_at = now(),
13460
+ updated_at = now()
13461
+ WHERE run_id = $1
13462
+ AND source IN ('run_api', 'project_task')
13463
+ AND status = 'running'
13464
+ AND lease_expires_at < now()
13465
+ RETURNING run_id`,
13466
+ [
13467
+ runId,
13468
+ options?.error ?? null,
13469
+ outcome === "timeout" ? "timeout" : "interrupted"
13470
+ ]
13471
+ );
13472
+ return (result.rowCount ?? 0) > 0;
13473
+ }
13474
+ /** Counts runs grouped by lifecycle status. */
13475
+ async counts() {
13476
+ const counts = {
13477
+ pending: 0,
13478
+ running: 0,
13479
+ success: 0,
13480
+ error: 0,
13481
+ interrupted: 0,
13482
+ cancelled: 0,
13483
+ timeout: 0
13484
+ };
13485
+ const result = await this.pool.query(
13486
+ `SELECT status, count(*)::int AS count
13487
+ FROM lattice_agent_runs
13488
+ GROUP BY status`
13489
+ );
13490
+ for (const row of result.rows) {
13491
+ const status = parseStatus(row.status);
13492
+ if (status) counts[status] = row.count;
13493
+ }
13494
+ return counts;
13495
+ }
13496
+ /** Reports expired run-queue rows for the requested sources. */
13497
+ async reapExpired(sources = ["run_api"]) {
13498
+ (0, import_core10.assertQueueSources)(sources);
13499
+ const result = await this.pool.query(
13500
+ `SELECT * FROM lattice_agent_runs
13501
+ WHERE source = ANY($1::text[]) AND status = 'running'
13502
+ AND lease_expires_at < now()
13503
+ ORDER BY lease_expires_at ASC`,
13504
+ [[...sources]]
13505
+ );
13506
+ return result.rows.map(reapedFromRow);
13507
+ }
13508
+ /** Lists rollback work that still needs a checkpoint rewind. */
13509
+ async listRollbackRuns() {
13510
+ const result = await this.pool.query(
13511
+ `SELECT * FROM lattice_agent_runs
13512
+ WHERE source = 'run_api'
13513
+ AND rollback_requested = TRUE
13514
+ AND (
13515
+ status = 'interrupted'
13516
+ OR (status = 'running' AND lease_expires_at < now())
13517
+ )
13518
+ ORDER BY updated_at ASC`
13519
+ );
13520
+ return result.rows.map(reapedFromRow);
13521
+ }
13522
+ /** Settles expired legacy rows, preserving the historical supervisor behavior. */
13523
+ async reapLegacyExpired() {
13524
+ const result = await this.pool.query(
13525
+ `UPDATE lattice_agent_runs
13526
+ SET status = 'interrupted',
13527
+ error = COALESCE(error, 'run owner lost'),
13528
+ finished_at = now(),
13529
+ updated_at = now()
13530
+ WHERE source = 'legacy' AND status = 'running'
13531
+ AND lease_expires_at < now()
13532
+ RETURNING run_id, tenant_id, assistant_id, thread_id, workspace_id, project_id`
13533
+ );
13534
+ return result.rows.map((row) => ({
13535
+ runId: row.run_id,
13536
+ tenantId: row.tenant_id,
13537
+ assistantId: row.assistant_id,
13538
+ threadId: row.thread_id,
13539
+ workspaceId: row.workspace_id ?? void 0,
13540
+ projectId: row.project_id ?? void 0
13541
+ }));
13542
+ }
13543
+ };
13544
+ function positiveLimit(limit) {
13545
+ return limit !== void 0 && Number.isSafeInteger(limit) && limit > 0 ? limit : void 0;
13546
+ }
13547
+ function clampLimit(limit) {
13548
+ if (limit === void 0) return DEFAULT_LIST_LIMIT;
13549
+ const floored = Math.floor(limit);
13550
+ if (!Number.isFinite(floored)) return DEFAULT_LIST_LIMIT;
13551
+ return Math.min(MAX_LIST_LIMIT, Math.max(1, floored));
13552
+ }
13553
+ function jsonParam(value) {
13554
+ return value === void 0 || value === null ? null : JSON.stringify(value);
13555
+ }
13556
+ function parseStatus(value) {
13557
+ switch (value) {
13558
+ case "pending":
13559
+ case "running":
13560
+ case "success":
13561
+ case "error":
13562
+ case "interrupted":
13563
+ case "cancelled":
13564
+ case "timeout":
13565
+ return value;
13566
+ default:
13567
+ return void 0;
13568
+ }
13569
+ }
13570
+ function parseSource(value) {
13571
+ switch (value) {
13572
+ case "run_api":
13573
+ return "run_api";
13574
+ case "project_task":
13575
+ return "project_task";
13576
+ default:
13577
+ return "legacy";
13578
+ }
13579
+ }
13580
+ function toJsonObject(value) {
13581
+ if (value === null || value === void 0) return null;
13582
+ if (typeof value !== "object" || Array.isArray(value)) return null;
13583
+ return value;
13584
+ }
13585
+ function rowToRecord(row) {
13586
+ return {
13587
+ runId: row.run_id,
13588
+ tenantId: row.tenant_id,
13589
+ workspaceId: row.workspace_id ?? void 0,
13590
+ projectId: row.project_id ?? void 0,
13591
+ assistantId: row.assistant_id,
13592
+ threadId: row.thread_id,
13593
+ status: parseStatus(row.status) ?? "error",
13594
+ source: parseSource(row.source),
13595
+ taskId: row.task_id ?? void 0,
13596
+ owner: row.owner,
13597
+ leaseExpiresAt: row.lease_expires_at === null ? null : row.lease_expires_at.getTime(),
13598
+ inputMessageIds: row.input_message_ids,
13599
+ attempts: row.attempts,
13600
+ error: row.error,
13601
+ cancelRequested: row.cancel_requested,
13602
+ rollbackRequested: row.rollback_requested,
13603
+ rollbackAttempts: row.rollback_attempts,
13604
+ rollbackError: row.rollback_error,
13605
+ input: toJsonObject(row.input),
13606
+ command: toJsonObject(row.command),
13607
+ config: toJsonObject(row.config),
13608
+ metadata: toJsonObject(row.metadata),
13609
+ baseCheckpointId: row.base_checkpoint_id,
13610
+ baseCheckpointCaptured: row.base_checkpoint_captured,
13611
+ capabilityRuntimeRevision: row.capability_runtime_revision,
13612
+ availableAt: row.available_at.getTime(),
13613
+ createdAt: row.created_at.getTime(),
13614
+ updatedAt: row.updated_at.getTime(),
13615
+ startedAt: row.started_at === null ? null : row.started_at.getTime(),
13616
+ finishedAt: row.finished_at === null ? null : row.finished_at.getTime()
13617
+ };
13618
+ }
13619
+ function reapedFromRow(row) {
13620
+ return {
13621
+ runId: row.run_id,
13622
+ tenantId: row.tenant_id,
13623
+ assistantId: row.assistant_id,
13624
+ threadId: row.thread_id,
13625
+ workspaceId: row.workspace_id ?? void 0,
13626
+ projectId: row.project_id ?? void 0,
13627
+ source: parseSource(row.source),
13628
+ taskId: row.task_id ?? void 0,
13629
+ attempts: row.attempts,
13630
+ cancelRequested: row.cancel_requested,
13631
+ rollbackRequested: row.rollback_requested,
13632
+ baseCheckpointId: row.base_checkpoint_id,
13633
+ createdAt: row.created_at.getTime(),
13634
+ startedAt: row.started_at === null ? null : row.started_at.getTime(),
13635
+ leaseExpiresAt: row.lease_expires_at === null ? null : row.lease_expires_at.getTime()
13636
+ };
13637
+ }
13638
+ function isActiveRunViolation(error) {
13639
+ if (typeof error !== "object" || error === null) return false;
13640
+ const { code, constraint } = error;
13641
+ return code === "23505" && constraint === "uq_lattice_agent_runs_one_running";
13642
+ }
13643
+
13644
+ // src/stores/PostgreSQLChannelReplyJobStore.ts
13645
+ var import_pg31 = require("pg");
13646
+ var import_protocols6 = require("@axiom-lattice/protocols");
13647
+ var DB_CLOCK_MS2 = "FLOOR(EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint";
13648
+ var COLUMNS = `run_id, tenant_id, assistant_id, thread_id, adapter_channel,
13649
+ channel_installation_id, reply_target, input_message_id, stream_cursor, status,
13650
+ attempts, available_at, lease_owner, lease_expires_at, last_error,
13651
+ output_message_id, created_at, updated_at, delivered_at`;
13652
+ function toNumber(value) {
13653
+ if (typeof value === "number") return value;
13654
+ if (typeof value === "string") return Number(value);
13655
+ throw new Error("Invalid channel reply job numeric column");
13656
+ }
13657
+ function toNullableNumber(value) {
13658
+ return value === null ? null : toNumber(value);
13659
+ }
13660
+ function toStatus(value) {
13661
+ if (value === "pending" || value === "delivered" || value === "failed") {
13662
+ return value;
13663
+ }
13664
+ throw new Error(`Invalid channel reply job status: ${value}`);
13665
+ }
13666
+ function mapRow9(row) {
13667
+ return {
13668
+ runId: row.run_id,
13669
+ tenantId: row.tenant_id,
13670
+ assistantId: row.assistant_id,
13671
+ threadId: row.thread_id,
13672
+ adapterChannel: row.adapter_channel,
13673
+ channelInstallationId: row.channel_installation_id,
13674
+ replyTarget: row.reply_target,
13675
+ inputMessageId: row.input_message_id,
13676
+ streamCursor: row.stream_cursor,
13677
+ status: toStatus(row.status),
13678
+ attempts: toNumber(row.attempts),
13679
+ availableAt: toNumber(row.available_at),
13680
+ leaseOwner: row.lease_owner,
13681
+ leaseExpiresAt: toNullableNumber(row.lease_expires_at),
13682
+ lastError: row.last_error,
13683
+ outputMessageId: row.output_message_id,
13684
+ createdAt: toNumber(row.created_at),
13685
+ updatedAt: toNumber(row.updated_at),
13686
+ deliveredAt: toNullableNumber(row.delivered_at)
13687
+ };
13688
+ }
13689
+ var PostgreSQLChannelReplyJobStore = class {
13690
+ /** @param options An externally managed pool, or a pool config/connection URL. */
13691
+ constructor(options) {
13692
+ this.ownsPool = true;
13693
+ this.initialized = false;
13694
+ this.initPromise = null;
13695
+ if (options.pool) {
13696
+ this.pool = options.pool;
13697
+ this.ownsPool = false;
13698
+ this.initialized = true;
13699
+ return;
13700
+ }
13701
+ if (typeof options.poolConfig === "string") {
13702
+ this.pool = new import_pg31.Pool({ connectionString: options.poolConfig });
13703
+ } else if (options.poolConfig) {
13704
+ this.pool = new import_pg31.Pool(options.poolConfig);
13705
+ } else {
13706
+ throw new Error("Either pool or poolConfig must be provided");
13707
+ }
13708
+ this.migrationManager = new MigrationManager(this.pool);
13709
+ this.migrationManager.register(createChannelReplyJobsTable);
13710
+ if (options.autoMigrate !== false) {
13711
+ this.initialize().catch((error) => {
13712
+ console.error("Failed to initialize PostgreSQLChannelReplyJobStore:", error);
13713
+ throw error;
13714
+ });
13715
+ }
13716
+ }
13717
+ /** Runs pending migrations once. */
13718
+ async initialize() {
13719
+ if (this.initialized) return;
13720
+ if (this.initPromise) return this.initPromise;
13721
+ this.initPromise = (async () => {
13722
+ try {
13723
+ await this.migrationManager.migrate();
13724
+ this.initialized = true;
13725
+ } finally {
13726
+ this.initPromise = null;
13727
+ }
13728
+ })();
13729
+ return this.initPromise;
13730
+ }
13731
+ /** Ends the pool when this store owns it. */
13732
+ async dispose() {
13733
+ if (this.ownsPool) await this.pool.end();
13734
+ }
13735
+ /** Creates a pending job with database-clock created/updated timestamps. */
13736
+ async create(input) {
13737
+ (0, import_protocols6.assertValidCreateChannelReplyJobInput)(input);
13738
+ const result = await this.pool.query(
13739
+ `WITH clock AS (SELECT ${DB_CLOCK_MS2} AS now_ms)
13740
+ INSERT INTO lattice_channel_reply_jobs (
13741
+ run_id, tenant_id, assistant_id, thread_id, adapter_channel,
13742
+ channel_installation_id, reply_target, input_message_id, stream_cursor,
13743
+ status, attempts, available_at, lease_owner, lease_expires_at,
13744
+ last_error, output_message_id, created_at, updated_at, delivered_at
13745
+ )
13746
+ SELECT
13747
+ $1, $2, $3, $4, $5,
13748
+ $6, $7::jsonb, $8, $9,
13749
+ 'pending', 0, $10, NULL, NULL,
13750
+ NULL, NULL, clock.now_ms, clock.now_ms, NULL
13751
+ FROM clock
13752
+ RETURNING ${COLUMNS}`,
13753
+ [
13754
+ input.runId,
13755
+ input.tenantId,
13756
+ input.assistantId,
13757
+ input.threadId,
13758
+ input.adapterChannel,
13759
+ input.channelInstallationId,
13760
+ JSON.stringify(input.replyTarget),
13761
+ input.inputMessageId,
13762
+ input.streamCursor ?? null,
13763
+ input.availableAt
13764
+ ]
13765
+ );
13766
+ return mapRow9(result.rows[0]);
13767
+ }
13768
+ /** Reads one job within a tenant scope. */
13769
+ async get(tenantId, runId) {
13770
+ const result = await this.pool.query(
13771
+ `SELECT ${COLUMNS}
13772
+ FROM lattice_channel_reply_jobs
13773
+ WHERE tenant_id = $1 AND run_id = $2`,
13774
+ [tenantId, runId]
13775
+ );
13776
+ return result.rowCount === 0 ? null : mapRow9(result.rows[0]);
13777
+ }
13778
+ /** Lists claimable pending jobs that are due at `now`, oldest first. */
13779
+ async listPending(now, limit) {
13780
+ const result = await this.pool.query(
13781
+ `SELECT ${COLUMNS}
13782
+ FROM lattice_channel_reply_jobs
13783
+ WHERE status = 'pending'
13784
+ AND available_at <= $1
13785
+ AND (lease_owner IS NULL OR lease_expires_at IS NULL OR lease_expires_at <= $1)
13786
+ ORDER BY available_at ASC, created_at ASC, run_id ASC
13787
+ LIMIT $2`,
13788
+ [now, (0, import_protocols6.normalizeChannelReplyJobListLimit)(limit)]
13789
+ );
13790
+ return result.rows.map(mapRow9);
13791
+ }
13792
+ /**
13793
+ * Atomically leases a claimable job.
13794
+ *
13795
+ * @param now Accepted for interface symmetry; PostgreSQL uses the database clock.
13796
+ */
13797
+ async claim(tenantId, runId, owner, leaseMs, now) {
13798
+ void now;
13799
+ const result = await this.pool.query(
13800
+ `WITH clock AS (SELECT ${DB_CLOCK_MS2} AS now_ms)
13801
+ UPDATE lattice_channel_reply_jobs AS j
13802
+ SET lease_owner = $3,
13803
+ lease_expires_at = clock.now_ms + $4,
13804
+ updated_at = clock.now_ms
13805
+ FROM clock
13806
+ WHERE j.tenant_id = $1
13807
+ AND j.run_id = $2
13808
+ AND j.status = 'pending'
13809
+ AND j.available_at <= clock.now_ms
13810
+ AND (j.lease_owner IS NULL OR j.lease_expires_at IS NULL OR j.lease_expires_at <= clock.now_ms)
13811
+ RETURNING ${COLUMNS}`,
13812
+ [tenantId, runId, owner, leaseMs]
13813
+ );
13814
+ return result.rowCount === 0 ? null : mapRow9(result.rows[0]);
13815
+ }
13816
+ /**
13817
+ * Marks a job delivered when `owner` still holds the lease.
13818
+ *
13819
+ * @param now Accepted for interface symmetry; PostgreSQL uses the database clock.
13820
+ */
13821
+ async markDelivered(tenantId, runId, owner, outputMessageId = null, now) {
13822
+ void now;
13823
+ const result = await this.pool.query(
13824
+ `WITH clock AS (SELECT ${DB_CLOCK_MS2} AS now_ms)
13825
+ UPDATE lattice_channel_reply_jobs AS j
13826
+ SET status = 'delivered',
13827
+ output_message_id = $4,
13828
+ lease_owner = NULL,
13829
+ lease_expires_at = NULL,
13830
+ delivered_at = clock.now_ms,
13831
+ updated_at = clock.now_ms
13832
+ FROM clock
13833
+ WHERE j.tenant_id = $1
13834
+ AND j.run_id = $2
13835
+ AND j.lease_owner = $3
13836
+ AND j.status = 'pending'
13837
+ RETURNING ${COLUMNS}`,
13838
+ [tenantId, runId, owner, outputMessageId]
13839
+ );
13840
+ return result.rowCount === 0 ? null : mapRow9(result.rows[0]);
13841
+ }
13842
+ /**
13843
+ * Returns a job to pending with an incremented attempt count.
13844
+ *
13845
+ * @param now Accepted for interface symmetry; PostgreSQL uses the database clock.
13846
+ */
13847
+ async reschedule(tenantId, runId, owner, error, availableAt, now) {
13848
+ void now;
13849
+ const result = await this.pool.query(
13850
+ `WITH clock AS (SELECT ${DB_CLOCK_MS2} AS now_ms)
13851
+ UPDATE lattice_channel_reply_jobs AS j
13852
+ SET status = 'pending',
13853
+ attempts = attempts + 1,
13854
+ lease_owner = NULL,
13855
+ lease_expires_at = NULL,
13856
+ last_error = $4,
13857
+ available_at = $5,
13858
+ updated_at = clock.now_ms
13859
+ FROM clock
13860
+ WHERE j.tenant_id = $1
13861
+ AND j.run_id = $2
13862
+ AND j.lease_owner = $3
13863
+ AND j.status = 'pending'
13864
+ RETURNING ${COLUMNS}`,
13865
+ [tenantId, runId, owner, error, availableAt]
13866
+ );
13867
+ return result.rowCount === 0 ? null : mapRow9(result.rows[0]);
13868
+ }
13869
+ /**
13870
+ * Marks a job terminally failed when `owner` still holds the lease.
13871
+ *
13872
+ * @param now Accepted for interface symmetry; PostgreSQL uses the database clock.
13873
+ */
13874
+ async markFailed(tenantId, runId, owner, error, now) {
13875
+ void now;
13876
+ const result = await this.pool.query(
13877
+ `WITH clock AS (SELECT ${DB_CLOCK_MS2} AS now_ms)
13878
+ UPDATE lattice_channel_reply_jobs AS j
13879
+ SET status = 'failed',
13880
+ last_error = $4,
13881
+ lease_owner = NULL,
13882
+ lease_expires_at = NULL,
13883
+ updated_at = clock.now_ms
13884
+ FROM clock
13885
+ WHERE j.tenant_id = $1
13886
+ AND j.run_id = $2
13887
+ AND j.lease_owner = $3
13888
+ AND j.status = 'pending'
13889
+ RETURNING ${COLUMNS}`,
13890
+ [tenantId, runId, owner, error]
13891
+ );
13892
+ return result.rowCount === 0 ? null : mapRow9(result.rows[0]);
13893
+ }
13894
+ /** Deletes one job. Returns whether a row was removed. */
13895
+ async delete(tenantId, runId) {
13896
+ const result = await this.pool.query(
13897
+ `DELETE FROM lattice_channel_reply_jobs WHERE tenant_id = $1 AND run_id = $2`,
13898
+ [tenantId, runId]
13899
+ );
13900
+ return (result.rowCount ?? 0) > 0;
13901
+ }
13902
+ /** Deletes terminal jobs whose last update is older than `before`. */
13903
+ async deleteTerminalBefore(before, limit) {
13904
+ const result = await this.pool.query(
13905
+ `DELETE FROM lattice_channel_reply_jobs
13906
+ WHERE run_id IN (
13907
+ SELECT run_id
13908
+ FROM lattice_channel_reply_jobs
13909
+ WHERE status IN ('delivered', 'failed')
13910
+ AND updated_at < $1
13911
+ ORDER BY updated_at ASC, run_id ASC
13912
+ LIMIT $2
13913
+ )`,
13914
+ [before, (0, import_protocols6.normalizeChannelReplyJobListLimit)(limit)]
13915
+ );
13916
+ return result.rowCount ?? 0;
13917
+ }
13918
+ };
13919
+
13920
+ // src/stores/PostgreSQLCheckpointStorage.ts
13921
+ var CheckpointTargetNotEmptyError = class extends Error {
13922
+ /**
13923
+ * @param threadId - Non-empty target thread id.
13924
+ */
13925
+ constructor(threadId) {
13926
+ super(`Checkpoint target thread ${threadId} is not empty`);
13927
+ /** Machine-readable conflict code. */
13928
+ this.code = "CHECKPOINT_TARGET_NOT_EMPTY";
13929
+ this.name = "CheckpointTargetNotEmptyError";
13930
+ }
13931
+ };
13932
+ var CheckpointBaseNotFoundError = class extends Error {
13933
+ /**
13934
+ * @param threadId - Thread owning the missing base checkpoint.
13935
+ * @param baseCheckpointId - Base checkpoint id that was not found.
13936
+ */
13937
+ constructor(threadId, baseCheckpointId) {
13938
+ super(`Checkpoint ${baseCheckpointId} does not exist on thread ${threadId}`);
13939
+ /** Machine-readable failure code. */
13940
+ this.code = "CHECKPOINT_BASE_NOT_FOUND";
13941
+ this.name = "CheckpointBaseNotFoundError";
13942
+ this.threadId = threadId;
13943
+ this.baseCheckpointId = baseCheckpointId;
13944
+ }
13945
+ };
13946
+ async function rollbackQuietly(client) {
13947
+ try {
13948
+ await client.query("ROLLBACK");
13949
+ } catch {
13950
+ }
13951
+ }
13952
+ function isUniqueViolation3(error) {
13953
+ return typeof error === "object" && error !== null && "code" in error && error.code === "23505";
13954
+ }
13955
+ function isRecord4(value) {
13956
+ return typeof value === "object" && value !== null && !Array.isArray(value);
13957
+ }
13958
+ function checkpointKey(checkpointNs, checkpointId) {
13959
+ return `${checkpointNs}\0${checkpointId}`;
13960
+ }
13961
+ function blobKey(checkpointNs, channel, version) {
13962
+ return `${checkpointNs}\0${channel}\0${version}`;
13963
+ }
13964
+ function checkpointRecord(value) {
13965
+ if (typeof value === "string") {
13966
+ try {
13967
+ const parsed = JSON.parse(value);
13968
+ return isRecord4(parsed) ? parsed : void 0;
13969
+ } catch {
13970
+ return void 0;
13971
+ }
13972
+ }
13973
+ return isRecord4(value) ? value : void 0;
13974
+ }
13975
+ function channelVersions(value) {
13976
+ const checkpoint = checkpointRecord(value);
13977
+ const versions = checkpoint?.channel_versions;
13978
+ return isRecord4(versions) ? versions : void 0;
13979
+ }
13980
+ var DELETE_CHUNK_SIZE = 100;
13981
+ function chunk(items, size) {
13982
+ const chunks = [];
13983
+ for (let index = 0; index < items.length; index += size) {
13984
+ chunks.push(items.slice(index, index + size));
13985
+ }
13986
+ return chunks;
13987
+ }
13988
+ function toCount(value) {
13989
+ const parsed = Number(value ?? 0);
13990
+ return Number.isFinite(parsed) ? parsed : 0;
13991
+ }
13992
+ var PostgreSQLCheckpointStorage = class {
13993
+ /**
13994
+ * @param pool - PostgreSQL pool shared with the rest of the store config.
13995
+ */
13996
+ constructor(pool) {
13997
+ this.pool = pool;
13998
+ }
13999
+ /**
14000
+ * Measure the checkpoint storage used by one thread.
14001
+ *
14002
+ * @param threadId - Thread id whose checkpoint rows should be measured.
14003
+ * @returns Counts and byte sizes grouped by checkpoint storage category.
14004
+ */
14005
+ async get(threadId) {
14006
+ const result = await this.pool.query(
14007
+ `SELECT
14008
+ (SELECT count(*)::bigint FROM checkpoints WHERE thread_id=$1) AS checkpoint_count,
14009
+ (SELECT coalesce(sum(pg_column_size(checkpoint) + pg_column_size(metadata)),0)::bigint FROM checkpoints WHERE thread_id=$1) AS checkpoint_bytes,
14010
+ (SELECT count(*)::bigint FROM checkpoint_blobs WHERE thread_id=$1) AS blob_count,
14011
+ (SELECT coalesce(sum(coalesce(octet_length(blob),0)),0)::bigint FROM checkpoint_blobs WHERE thread_id=$1) AS blob_bytes,
14012
+ (SELECT count(*)::bigint FROM checkpoint_writes WHERE thread_id=$1) AS write_count,
14013
+ (SELECT coalesce(sum(octet_length(blob)),0)::bigint FROM checkpoint_writes WHERE thread_id=$1) AS write_bytes`,
14014
+ [threadId]
14015
+ );
14016
+ const row = result.rows[0];
14017
+ const checkpointCount = toCount(row?.checkpoint_count);
14018
+ const checkpointBytes = toCount(row?.checkpoint_bytes);
14019
+ const blobCount = toCount(row?.blob_count);
14020
+ const blobBytes = toCount(row?.blob_bytes);
14021
+ const writeCount = toCount(row?.write_count);
14022
+ const writeBytes = toCount(row?.write_bytes);
14023
+ return {
14024
+ threadId,
14025
+ totalBytes: checkpointBytes + blobBytes + writeBytes,
14026
+ checkpoints: { count: checkpointCount, bytes: checkpointBytes },
14027
+ channelValues: { count: blobCount, bytes: blobBytes },
14028
+ pendingWrites: { count: writeCount, bytes: writeBytes }
14029
+ };
14030
+ }
14031
+ /**
14032
+ * Copy every checkpoint row from a source thread to an empty target thread.
14033
+ *
14034
+ * @param sourceThreadId - Thread whose rows are copied.
14035
+ * @param targetThreadId - Empty thread that receives the rows.
14036
+ * @returns Resolves when the copy commits; an empty source is a no-op.
14037
+ * @throws {CheckpointTargetNotEmptyError} When the target already owns rows.
14038
+ */
14039
+ async copyThread(sourceThreadId, targetThreadId) {
14040
+ const client = await this.pool.connect();
14041
+ try {
14042
+ await client.query("BEGIN");
14043
+ const target = await client.query(
14044
+ `SELECT (
14045
+ EXISTS (SELECT 1 FROM checkpoints WHERE thread_id=$1)
14046
+ OR EXISTS (SELECT 1 FROM checkpoint_blobs WHERE thread_id=$1)
14047
+ OR EXISTS (SELECT 1 FROM checkpoint_writes WHERE thread_id=$1)
14048
+ ) AS not_empty`,
14049
+ [targetThreadId]
14050
+ );
14051
+ if (target.rows[0]?.not_empty) {
14052
+ throw new CheckpointTargetNotEmptyError(targetThreadId);
14053
+ }
14054
+ await client.query(
14055
+ `INSERT INTO checkpoints (
14056
+ thread_id, checkpoint_ns, checkpoint_id, parent_checkpoint_id, type, checkpoint, metadata
14057
+ )
14058
+ SELECT $2, checkpoint_ns, checkpoint_id, parent_checkpoint_id, type, checkpoint,
14059
+ jsonb_set(coalesce(metadata, '{}'::jsonb), '{thread_id}', to_jsonb($2::text), true)
14060
+ FROM checkpoints WHERE thread_id=$1`,
14061
+ [sourceThreadId, targetThreadId]
14062
+ );
14063
+ await client.query(
14064
+ `INSERT INTO checkpoint_blobs (thread_id, checkpoint_ns, channel, version, type, blob)
14065
+ SELECT $2, checkpoint_ns, channel, version, type, blob
14066
+ FROM checkpoint_blobs WHERE thread_id=$1`,
14067
+ [sourceThreadId, targetThreadId]
14068
+ );
14069
+ await client.query(
14070
+ `INSERT INTO checkpoint_writes (
14071
+ thread_id, checkpoint_ns, checkpoint_id, task_id, idx, channel, type, blob
14072
+ )
14073
+ SELECT $2, checkpoint_ns, checkpoint_id, task_id, idx, channel, type, blob
14074
+ FROM checkpoint_writes WHERE thread_id=$1`,
14075
+ [sourceThreadId, targetThreadId]
14076
+ );
14077
+ await client.query("COMMIT");
14078
+ } catch (error) {
14079
+ await rollbackQuietly(client);
14080
+ if (isUniqueViolation3(error)) {
14081
+ throw new CheckpointTargetNotEmptyError(targetThreadId);
14082
+ }
14083
+ throw error;
14084
+ } finally {
14085
+ client.release();
14086
+ }
14087
+ }
14088
+ /**
14089
+ * Retain only the latest N checkpoints per checkpoint namespace.
14090
+ *
14091
+ * @param threadId - Thread whose checkpoint history is pruned.
14092
+ * @param options - Retention options.
14093
+ * @param options.keepLatest - Number of latest checkpoints to retain per namespace.
14094
+ * @returns Counts of deleted checkpoint, blob, and write rows.
14095
+ */
14096
+ async pruneThread(threadId, options) {
14097
+ if (!Number.isSafeInteger(options.keepLatest) || options.keepLatest < 1) {
14098
+ throw new RangeError("keepLatest must be an integer >= 1");
14099
+ }
14100
+ const keepLatest = options.keepLatest;
14101
+ const client = await this.pool.connect();
14102
+ try {
14103
+ await client.query("BEGIN");
14104
+ const retainedResult = await client.query(
14105
+ `SELECT checkpoint_ns, checkpoint_id, checkpoint FROM (
14106
+ SELECT checkpoint_ns, checkpoint_id, checkpoint,
14107
+ row_number() OVER (
14108
+ PARTITION BY checkpoint_ns ORDER BY checkpoint_id DESC
14109
+ ) AS rn
14110
+ FROM checkpoints WHERE thread_id=$1
14111
+ ) t WHERE rn <= $2`,
14112
+ [threadId, keepLatest]
14113
+ );
14114
+ const retained = retainedResult.rows;
14115
+ const retainedCheckpoints = new Set(
14116
+ retained.map((row) => checkpointKey(row.checkpoint_ns, row.checkpoint_id))
14117
+ );
14118
+ const fallbackNamespaces = /* @__PURE__ */ new Set();
14119
+ const retainedBlobs = /* @__PURE__ */ new Set();
14120
+ for (const row of retained) {
14121
+ const versions = channelVersions(row.checkpoint);
14122
+ if (!versions) {
14123
+ fallbackNamespaces.add(row.checkpoint_ns);
14124
+ continue;
14125
+ }
14126
+ for (const [channel, version] of Object.entries(versions)) {
14127
+ retainedBlobs.add(blobKey(row.checkpoint_ns, channel, String(version)));
14128
+ }
14129
+ }
14130
+ const writeRows = await client.query(
14131
+ `SELECT checkpoint_ns, checkpoint_id, task_id, idx
14132
+ FROM checkpoint_writes WHERE thread_id=$1`,
14133
+ [threadId]
14134
+ );
14135
+ const staleWrites = writeRows.rows.filter(
14136
+ (row) => !retainedCheckpoints.has(checkpointKey(row.checkpoint_ns, row.checkpoint_id))
14137
+ );
14138
+ let writes = 0;
14139
+ for (const batch of chunk(staleWrites, DELETE_CHUNK_SIZE)) {
14140
+ const params = [threadId];
14141
+ const tuples = batch.map((row) => {
14142
+ params.push(row.checkpoint_ns, row.checkpoint_id, row.task_id, row.idx);
14143
+ return `($${params.length - 3}, $${params.length - 2}, $${params.length - 1}, $${params.length})`;
14144
+ });
14145
+ const deleted = await client.query(
14146
+ `DELETE FROM checkpoint_writes
14147
+ WHERE thread_id=$1
14148
+ AND (checkpoint_ns, checkpoint_id, task_id, idx) IN (${tuples.join(", ")})`,
14149
+ params
14150
+ );
14151
+ writes += deleted.rowCount ?? 0;
14152
+ }
14153
+ const checkpointRows = await client.query(
14154
+ `SELECT checkpoint_ns, checkpoint_id FROM checkpoints WHERE thread_id=$1`,
14155
+ [threadId]
14156
+ );
14157
+ const staleCheckpoints = checkpointRows.rows.filter(
14158
+ (row) => !retainedCheckpoints.has(checkpointKey(row.checkpoint_ns, row.checkpoint_id))
14159
+ );
14160
+ let checkpoints = 0;
14161
+ for (const batch of chunk(staleCheckpoints, DELETE_CHUNK_SIZE)) {
14162
+ const params = [threadId];
14163
+ const tuples = batch.map((row) => {
14164
+ params.push(row.checkpoint_ns, row.checkpoint_id);
14165
+ return `($${params.length - 1}, $${params.length})`;
14166
+ });
14167
+ const deleted = await client.query(
14168
+ `DELETE FROM checkpoints
14169
+ WHERE thread_id=$1
14170
+ AND (checkpoint_ns, checkpoint_id) IN (${tuples.join(", ")})`,
14171
+ params
14172
+ );
14173
+ checkpoints += deleted.rowCount ?? 0;
14174
+ }
14175
+ const blobRows = await client.query(
14176
+ `SELECT checkpoint_ns, channel, version FROM checkpoint_blobs WHERE thread_id=$1`,
14177
+ [threadId]
14178
+ );
14179
+ const staleBlobs = blobRows.rows.filter(
14180
+ (row) => !fallbackNamespaces.has(row.checkpoint_ns) && !retainedBlobs.has(blobKey(row.checkpoint_ns, row.channel, row.version))
14181
+ );
14182
+ let blobs = 0;
14183
+ for (const batch of chunk(staleBlobs, DELETE_CHUNK_SIZE)) {
14184
+ const params = [threadId];
14185
+ const tuples = batch.map((row) => {
14186
+ params.push(row.checkpoint_ns, row.channel, row.version);
14187
+ return `($${params.length - 2}, $${params.length - 1}, $${params.length})`;
14188
+ });
14189
+ const deleted = await client.query(
14190
+ `DELETE FROM checkpoint_blobs
14191
+ WHERE thread_id=$1
14192
+ AND (checkpoint_ns, channel, version) IN (${tuples.join(", ")})`,
14193
+ params
14194
+ );
14195
+ blobs += deleted.rowCount ?? 0;
14196
+ }
14197
+ await client.query("COMMIT");
14198
+ return { checkpoints, blobs, writes };
14199
+ } catch (error) {
14200
+ await rollbackQuietly(client);
14201
+ throw error;
14202
+ } finally {
14203
+ client.release();
14204
+ }
14205
+ }
14206
+ /**
14207
+ * Delete the checkpoints that descend from a base checkpoint.
14208
+ *
14209
+ * Descendants are discovered by following both same-namespace
14210
+ * `parent_checkpoint_id` edges and cross-namespace `metadata.parents` edges
14211
+ * (LangGraph's `{ "<parent checkpoint_ns>": "<parent checkpoint id>" }` map on
14212
+ * subgraph checkpoints), recursively across every namespace. Out-of-order or
14213
+ * clock-skewed ids do not affect the result. Writes keyed to the base
14214
+ * checkpoint belong to the rolled-back run's first superstep and are deleted
14215
+ * too. Blobs referenced only by deleted checkpoints are removed.
14216
+ *
14217
+ * @param threadId - Thread whose later checkpoint history is deleted.
14218
+ * @param baseCheckpointId - Checkpoint to retain as the new tip, or `null` to
14219
+ * delete every checkpoint for the thread.
14220
+ * @returns Resolves when the deletion commits; no descendants is a no-op.
14221
+ * @throws {RangeError} When `baseCheckpointId` is an empty string.
14222
+ * @throws {CheckpointBaseNotFoundError} When the base checkpoint does not
14223
+ * exist on the thread.
14224
+ */
14225
+ async deleteAfter(threadId, baseCheckpointId) {
14226
+ if (baseCheckpointId !== null && baseCheckpointId.length === 0) {
14227
+ throw new RangeError("baseCheckpointId must be null or a non-empty checkpoint id");
14228
+ }
14229
+ const client = await this.pool.connect();
14230
+ try {
14231
+ await client.query("BEGIN");
14232
+ if (baseCheckpointId === null) {
14233
+ await client.query("DELETE FROM checkpoint_writes WHERE thread_id=$1", [threadId]);
14234
+ await client.query("DELETE FROM checkpoint_blobs WHERE thread_id=$1", [threadId]);
14235
+ await client.query("DELETE FROM checkpoints WHERE thread_id=$1", [threadId]);
14236
+ await client.query("COMMIT");
14237
+ return;
14238
+ }
14239
+ const base = await client.query(
14240
+ `SELECT checkpoint_ns
14241
+ FROM checkpoints
14242
+ WHERE thread_id=$1 AND checkpoint_id=$2`,
14243
+ [threadId, baseCheckpointId]
14244
+ );
14245
+ if (base.rows.length === 0) {
14246
+ throw new CheckpointBaseNotFoundError(threadId, baseCheckpointId);
14247
+ }
14248
+ const baseCheckpointNs = base.rows[0].checkpoint_ns;
14249
+ const descendantResult = await client.query(
14250
+ `WITH RECURSIVE lineage(checkpoint_ns, checkpoint_id) AS (
14251
+ SELECT $2::text, $3::text
14252
+ UNION
14253
+ SELECT c.checkpoint_ns, c.checkpoint_id
14254
+ FROM checkpoints c
14255
+ JOIN lineage l ON (
14256
+ (c.checkpoint_ns = l.checkpoint_ns
14257
+ AND c.parent_checkpoint_id = l.checkpoint_id)
14258
+ OR (c.metadata->'parents'->>l.checkpoint_ns = l.checkpoint_id)
14259
+ )
14260
+ WHERE c.thread_id=$1
14261
+ ),
14262
+ roots AS (
14263
+ SELECT $2::text AS checkpoint_ns, $3::text AS checkpoint_id
14264
+ )
14265
+ SELECT l.checkpoint_ns, l.checkpoint_id
14266
+ FROM lineage l
14267
+ WHERE NOT EXISTS (
14268
+ SELECT 1 FROM roots r
14269
+ WHERE r.checkpoint_ns = l.checkpoint_ns
14270
+ AND r.checkpoint_id = l.checkpoint_id
14271
+ )`,
14272
+ [threadId, baseCheckpointNs, baseCheckpointId]
14273
+ );
14274
+ const descendants = descendantResult.rows;
14275
+ await client.query(
14276
+ `DELETE FROM checkpoint_writes
14277
+ WHERE thread_id=$1 AND checkpoint_ns=$2 AND checkpoint_id=$3`,
14278
+ [threadId, baseCheckpointNs, baseCheckpointId]
14279
+ );
14280
+ for (const batch of chunk(descendants, DELETE_CHUNK_SIZE)) {
14281
+ const params = [threadId];
14282
+ const tuples = batch.map((row) => {
14283
+ params.push(row.checkpoint_ns, row.checkpoint_id);
14284
+ return `($${params.length - 1}, $${params.length})`;
14285
+ });
14286
+ await client.query(
14287
+ `DELETE FROM checkpoint_writes
14288
+ WHERE thread_id=$1
14289
+ AND (checkpoint_ns, checkpoint_id) IN (${tuples.join(", ")})`,
14290
+ params
14291
+ );
14292
+ await client.query(
14293
+ `DELETE FROM checkpoints
14294
+ WHERE thread_id=$1
14295
+ AND (checkpoint_ns, checkpoint_id) IN (${tuples.join(", ")})`,
14296
+ params
14297
+ );
14298
+ }
14299
+ const retainedResult = await client.query(
14300
+ `SELECT checkpoint_ns, checkpoint_id, checkpoint
14301
+ FROM checkpoints WHERE thread_id=$1`,
14302
+ [threadId]
14303
+ );
14304
+ const fallbackNamespaces = /* @__PURE__ */ new Set();
14305
+ const retainedBlobs = /* @__PURE__ */ new Set();
14306
+ for (const row of retainedResult.rows) {
14307
+ const versions = channelVersions(row.checkpoint);
14308
+ if (!versions) {
14309
+ fallbackNamespaces.add(row.checkpoint_ns);
14310
+ continue;
14311
+ }
14312
+ for (const [channel, version] of Object.entries(versions)) {
14313
+ retainedBlobs.add(blobKey(row.checkpoint_ns, channel, String(version)));
14314
+ }
14315
+ }
14316
+ const blobRows = await client.query(
14317
+ `SELECT checkpoint_ns, channel, version FROM checkpoint_blobs WHERE thread_id=$1`,
14318
+ [threadId]
14319
+ );
14320
+ const staleBlobs = blobRows.rows.filter(
14321
+ (row) => !fallbackNamespaces.has(row.checkpoint_ns) && !retainedBlobs.has(blobKey(row.checkpoint_ns, row.channel, row.version))
14322
+ );
14323
+ for (const batch of chunk(staleBlobs, DELETE_CHUNK_SIZE)) {
14324
+ const params = [threadId];
14325
+ const tuples = batch.map((row) => {
14326
+ params.push(row.checkpoint_ns, row.channel, row.version);
14327
+ return `($${params.length - 2}, $${params.length - 1}, $${params.length})`;
14328
+ });
14329
+ await client.query(
14330
+ `DELETE FROM checkpoint_blobs
14331
+ WHERE thread_id=$1
14332
+ AND (checkpoint_ns, channel, version) IN (${tuples.join(", ")})`,
14333
+ params
14334
+ );
14335
+ }
14336
+ await client.query("COMMIT");
14337
+ } catch (error) {
14338
+ await rollbackQuietly(client);
14339
+ throw error;
14340
+ } finally {
14341
+ client.release();
14342
+ }
14343
+ }
14344
+ /**
14345
+ * Delete every checkpoint row for a thread.
14346
+ *
14347
+ * @param threadId - Thread whose checkpoint rows are deleted.
14348
+ * @returns Resolves when the deletion commits.
14349
+ */
14350
+ async deleteThread(threadId) {
14351
+ const client = await this.pool.connect();
14352
+ try {
14353
+ await client.query("BEGIN");
14354
+ await client.query("DELETE FROM checkpoint_writes WHERE thread_id=$1", [threadId]);
14355
+ await client.query("DELETE FROM checkpoint_blobs WHERE thread_id=$1", [threadId]);
14356
+ await client.query("DELETE FROM checkpoints WHERE thread_id=$1", [threadId]);
14357
+ await client.query("COMMIT");
14358
+ } catch (error) {
14359
+ await rollbackQuietly(client);
14360
+ throw error;
14361
+ } finally {
14362
+ client.release();
14363
+ }
14364
+ }
14365
+ };
14366
+
14367
+ // src/stores/PostgreSQLStore.ts
14368
+ var import_protocols7 = require("@axiom-lattice/protocols");
14369
+ function mapRow10(row) {
14370
+ return {
14371
+ namespace: row.namespace,
14372
+ key: row.key,
14373
+ value: row.value ?? {},
14374
+ createdAt: row.created_at,
14375
+ updatedAt: row.updated_at,
14376
+ expiresAt: row.expires_at ?? void 0
14377
+ };
14378
+ }
14379
+ var PostgreSQLStore = class {
14380
+ /**
14381
+ * @param pool - Pool shared with the rest of the store config.
14382
+ */
14383
+ constructor(pool) {
14384
+ this.pool = pool;
14385
+ }
14386
+ async put(tenantId, input) {
14387
+ (0, import_protocols7.assertStoreIdentity)(input.namespace, input.key);
14388
+ const now = /* @__PURE__ */ new Date();
14389
+ const expiresAt = (0, import_protocols7.storeExpiresAt)(input.ttlMinutes, now) ?? null;
14390
+ const result = await this.pool.query(
14391
+ `INSERT INTO lattice_store_items
14392
+ (tenant_id, namespace, key, value, created_at, updated_at, expires_at)
14393
+ VALUES ($1, $2, $3, $4::jsonb, $5, $5, $6)
14394
+ ON CONFLICT (tenant_id, namespace, key) DO UPDATE SET
14395
+ value = EXCLUDED.value,
14396
+ updated_at = EXCLUDED.updated_at,
14397
+ expires_at = EXCLUDED.expires_at
14398
+ RETURNING tenant_id, namespace, key, value, created_at, updated_at, expires_at`,
14399
+ [tenantId, input.namespace, input.key, JSON.stringify(input.value), now, expiresAt]
14400
+ );
14401
+ return mapRow10(result.rows[0]);
14402
+ }
14403
+ async get(tenantId, namespace, key) {
14404
+ (0, import_protocols7.assertStoreIdentity)(namespace, key);
14405
+ const result = await this.pool.query(
14406
+ `SELECT tenant_id, namespace, key, value, created_at, updated_at, expires_at
14407
+ FROM lattice_store_items
14408
+ WHERE tenant_id = $1 AND namespace = $2 AND key = $3
14409
+ AND (expires_at IS NULL OR expires_at > $4)`,
14410
+ [tenantId, namespace, key, /* @__PURE__ */ new Date()]
14411
+ );
14412
+ return result.rows[0] ? mapRow10(result.rows[0]) : null;
14413
+ }
14414
+ async delete(tenantId, namespace, key) {
14415
+ (0, import_protocols7.assertStoreIdentity)(namespace, key);
14416
+ const result = await this.pool.query(
14417
+ `DELETE FROM lattice_store_items
14418
+ WHERE tenant_id = $1 AND namespace = $2 AND key = $3`,
14419
+ [tenantId, namespace, key]
14420
+ );
14421
+ return (result.rowCount ?? 0) > 0;
14422
+ }
14423
+ async search(tenantId, input) {
14424
+ const conditions = [
14425
+ "tenant_id = $1",
14426
+ "(expires_at IS NULL OR expires_at > $2)"
14427
+ ];
14428
+ const params = [tenantId, /* @__PURE__ */ new Date()];
14429
+ if (input.namespacePrefix) {
14430
+ (0, import_protocols7.assertStoreNamespacePrefix)(input.namespacePrefix);
14431
+ }
14432
+ if (input.namespacePrefix && input.namespacePrefix.length > 0) {
14433
+ params.push(input.namespacePrefix);
14434
+ conditions.push(`namespace[1:array_length($${params.length}::text[], 1)] = $${params.length}`);
14435
+ }
14436
+ if (input.filter !== void 0) {
14437
+ params.push(JSON.stringify(input.filter));
14438
+ conditions.push(`value @> $${params.length}::jsonb`);
14439
+ }
14440
+ if (input.query !== void 0 && input.query !== "") {
14441
+ params.push(input.query.replace(/[\\%_]/g, "\\$&"));
14442
+ conditions.push(`value::text ILIKE '%' || $${params.length} || '%' ESCAPE '\\'`);
14443
+ }
14444
+ params.push((0, import_protocols7.storePaginationValue)(input.limit, 10));
14445
+ const limitParam = params.length;
14446
+ params.push((0, import_protocols7.storePaginationValue)(input.offset, 0));
14447
+ const offsetParam = params.length;
14448
+ const result = await this.pool.query(
14449
+ `SELECT tenant_id, namespace, key, value, created_at, updated_at, expires_at
14450
+ FROM lattice_store_items
14451
+ WHERE ${conditions.join(" AND ")}
14452
+ ORDER BY updated_at DESC, namespace, key
14453
+ LIMIT $${limitParam} OFFSET $${offsetParam}`,
14454
+ params
14455
+ );
14456
+ return result.rows.map(mapRow10);
14457
+ }
14458
+ async listNamespaces(tenantId, prefix = [], limit = 100) {
14459
+ (0, import_protocols7.assertStoreNamespacePrefix)(prefix);
14460
+ const conditions = [
14461
+ "tenant_id = $1",
14462
+ "(expires_at IS NULL OR expires_at > $2)"
14463
+ ];
14464
+ const params = [tenantId, /* @__PURE__ */ new Date()];
14465
+ if (prefix.length > 0) {
14466
+ params.push(prefix);
14467
+ conditions.push(`namespace[1:array_length($${params.length}::text[], 1)] = $${params.length}`);
14468
+ }
14469
+ params.push((0, import_protocols7.storePaginationValue)(limit, 100));
14470
+ const result = await this.pool.query(
14471
+ `SELECT DISTINCT namespace
14472
+ FROM lattice_store_items
14473
+ WHERE ${conditions.join(" AND ")}
14474
+ ORDER BY namespace
14475
+ LIMIT $${params.length}`,
14476
+ params
14477
+ );
14478
+ return result.rows.map((row) => row.namespace);
14479
+ }
14480
+ async deleteExpired(now, limit) {
14481
+ const bounded = (0, import_protocols7.storePaginationValue)(limit, 0);
14482
+ const result = await this.pool.query(
14483
+ `WITH expired AS (
14484
+ SELECT ctid FROM lattice_store_items
14485
+ WHERE expires_at IS NOT NULL AND expires_at <= $1
14486
+ ORDER BY expires_at
14487
+ LIMIT $2
14488
+ )
14489
+ DELETE FROM lattice_store_items WHERE ctid IN (SELECT ctid FROM expired)`,
14490
+ [now, bounded]
14491
+ );
14492
+ return result.rowCount ?? 0;
14493
+ }
14494
+ };
14495
+
14496
+ // src/migrations/store_migrations.ts
14497
+ var createStoreItemsTable = {
14498
+ version: 195,
14499
+ name: "create_lattice_store_items_table",
14500
+ up: async (client) => {
14501
+ await client.query(`
14502
+ CREATE TABLE IF NOT EXISTS lattice_store_items (
14503
+ tenant_id TEXT NOT NULL,
14504
+ namespace TEXT[] NOT NULL,
14505
+ key TEXT NOT NULL,
14506
+ value JSONB NOT NULL,
14507
+ created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
14508
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
14509
+ expires_at TIMESTAMPTZ,
14510
+ PRIMARY KEY (tenant_id, namespace, key)
14511
+ )
14512
+ `);
14513
+ await client.query(`
14514
+ CREATE INDEX IF NOT EXISTS idx_lattice_store_items_namespace
14515
+ ON lattice_store_items USING GIN (namespace)
14516
+ `);
14517
+ await client.query(`
14518
+ CREATE INDEX IF NOT EXISTS idx_lattice_store_items_value
14519
+ ON lattice_store_items USING GIN (value)
14520
+ `);
14521
+ await client.query(`
14522
+ CREATE INDEX IF NOT EXISTS idx_lattice_store_items_expires_at
14523
+ ON lattice_store_items(expires_at)
14524
+ WHERE expires_at IS NOT NULL
14525
+ `);
14526
+ },
14527
+ down: async (client) => {
14528
+ await client.query("DROP INDEX IF EXISTS idx_lattice_store_items_expires_at");
14529
+ await client.query("DROP INDEX IF EXISTS idx_lattice_store_items_value");
14530
+ await client.query("DROP INDEX IF EXISTS idx_lattice_store_items_namespace");
14531
+ await client.query("DROP TABLE IF EXISTS lattice_store_items");
14532
+ }
14533
+ };
14534
+
14535
+ // src/createPgStoreConfig.ts
14536
+ async function createPgStoreConfig(connectionString) {
14537
+ const pool = new import_pg32.Pool({ connectionString });
14538
+ const mm = new MigrationManager(pool);
14539
+ mm.register(createThreadsTable);
14540
+ mm.register(createScheduledTasksTable);
14541
+ mm.register(createAssistantsTable);
14542
+ mm.register(createSkillsTable);
14543
+ mm.register(createWorkspacesTable);
14544
+ mm.register(createProjectsTable);
14545
+ mm.register(createUsersTable);
14546
+ mm.register(createTenantsTable);
14547
+ mm.register(addUserStatusColumn);
14548
+ mm.register(createUserTenantLinkTable);
14549
+ mm.register(migrateRemoveTenantIdFromUsers);
14550
+ mm.register(createMetricsConfigsTable);
14551
+ mm.register(createMcpServerConfigsTable);
14552
+ mm.register(addAssistantTenantId);
14553
+ mm.register(addSkillTenantId);
14554
+ mm.register(changeSkillPrimaryKey);
14555
+ mm.register(addScheduleTenantId);
14556
+ mm.register(changeThreadPrimaryKey);
14557
+ mm.register(createChannelIdentityMappingTables);
14558
+ mm.register(createChannelInstallationsTable);
14559
+ mm.register(addThreadTenantId);
14560
+ mm.register(addProjectConfigColumn);
14561
+ registerThreadMessageQueueMigrations(mm);
14562
+ mm.register(createWorkflowTrackingTables);
14563
+ for (const m of evalMigrations) {
14564
+ mm.register(m);
14565
+ }
14566
+ mm.register(alterChannelInstallationsTable);
14567
+ mm.register(createChannelBindingsTable);
14568
+ mm.register(createDatabaseConfigsTable);
14569
+ mm.register(changeAssistantPrimaryKey);
14570
+ mm.register(addStepThreadId);
14571
+ mm.register(createA2AApiKeysTable);
14572
+ mm.register(addAssistantOwnerUserId);
14573
+ for (const m of taskMigrations) {
14574
+ mm.register(m);
14575
+ }
14576
+ mm.register(createMenuItemsTable);
14577
+ mm.register(createSharedResourcesTable);
14578
+ mm.register(addFileContentType);
14579
+ mm.register(addMcpAppContentType);
14580
+ mm.register(createCollectionsTable);
14581
+ mm.register(createTaskWorkItemsMigration);
14582
+ mm.register(addWorkItemProjectFieldsMigration);
12380
14583
  mm.register(createConnectionConfigsTable);
12381
14584
  mm.register(addWorkflowRunsTenantStatusUpdatedIndex);
12382
14585
  mm.register(addProjectKindColumn);
@@ -12406,6 +14609,19 @@ async function createPgStoreConfig(connectionString) {
12406
14609
  mm.register(dropPluginDeploymentAuthToken);
12407
14610
  mm.register(addModelProviderModelExtra);
12408
14611
  mm.register(addModelProviderModelDefault);
14612
+ mm.register(addModelProviderModelKind);
14613
+ mm.register(createAgentRunLedgerTable);
14614
+ mm.register(createAgentRunTaskQueueMigration);
14615
+ mm.register(createAgentRunCapabilityRevisionMigration);
14616
+ mm.register(createAgentRunBaseCheckpointCapturedMigration);
14617
+ mm.register(createAgentRunRollbackFailureMigration);
14618
+ mm.register(agentRunRunningIndexSourceMigration);
14619
+ mm.register(addThreadExpiresAt);
14620
+ mm.register(createStoreItemsTable);
14621
+ mm.register(createChannelReplyJobsTable);
14622
+ mm.register(createAgentRunRetentionMigration);
14623
+ mm.register(createAgentRunProjectTaskMigration);
14624
+ mm.register(agentRunProjectTaskRunningIndexMigration);
12409
14625
  await mm.migrate();
12410
14626
  const checkpoint = import_langgraph_checkpoint_postgres.PostgresSaver.fromConnString(connectionString);
12411
14627
  checkpoint.setup().catch((err) => {
@@ -12450,13 +14666,17 @@ async function createPgStoreConfig(connectionString) {
12450
14666
  lease: new PostgreSQLLeaseStore(opts),
12451
14667
  outbox: new PostgreSQLOutboxStore(opts),
12452
14668
  pluginDeployment: new PostgreSQLPluginDeploymentStore(opts),
14669
+ agentRun: new PostgreSQLAgentRunLedger(opts),
14670
+ channelReplyJob: new PostgreSQLChannelReplyJobStore(opts),
12453
14671
  vectorStoreProvider: new PGVectorStoreProvider(pool, connectionString),
12454
- checkpoint
14672
+ checkpoint,
14673
+ checkpointStorage: new PostgreSQLCheckpointStorage(pool),
14674
+ store: new PostgreSQLStore(pool)
12455
14675
  };
12456
14676
  }
12457
14677
 
12458
14678
  // src/stores/PostgreSQLSkillStore.ts
12459
- var import_pg31 = require("pg");
14679
+ var import_pg33 = require("pg");
12460
14680
  var PostgreSQLSkillStore = class {
12461
14681
  constructor(options) {
12462
14682
  this.initialized = false;
@@ -12469,9 +14689,9 @@ var PostgreSQLSkillStore = class {
12469
14689
  return;
12470
14690
  }
12471
14691
  if (typeof options.poolConfig === "string") {
12472
- this.pool = new import_pg31.Pool({ connectionString: options.poolConfig });
14692
+ this.pool = new import_pg33.Pool({ connectionString: options.poolConfig });
12473
14693
  } else if (options.poolConfig) {
12474
- this.pool = new import_pg31.Pool(options.poolConfig);
14694
+ this.pool = new import_pg33.Pool(options.poolConfig);
12475
14695
  } else {
12476
14696
  throw new Error("Either pool or poolConfig must be provided");
12477
14697
  }
@@ -12772,7 +14992,7 @@ var PostgreSQLSkillStore = class {
12772
14992
  };
12773
14993
 
12774
14994
  // src/stores/ChannelIdentityMappingStore.ts
12775
- var import_pg32 = require("pg");
14995
+ var import_pg34 = require("pg");
12776
14996
  var ChannelIdentityMappingStore = class {
12777
14997
  constructor(options) {
12778
14998
  this.initialized = false;
@@ -12784,7 +15004,7 @@ var ChannelIdentityMappingStore = class {
12784
15004
  this.initialized = true;
12785
15005
  return;
12786
15006
  }
12787
- this.pool = typeof options.poolConfig === "string" ? new import_pg32.Pool({ connectionString: options.poolConfig }) : options.poolConfig ? new import_pg32.Pool(options.poolConfig) : (() => {
15007
+ this.pool = typeof options.poolConfig === "string" ? new import_pg34.Pool({ connectionString: options.poolConfig }) : options.poolConfig ? new import_pg34.Pool(options.poolConfig) : (() => {
12788
15008
  throw new Error("Either pool or poolConfig must be provided");
12789
15009
  })();
12790
15010
  this.migrationManager = new MigrationManager(this.pool);
@@ -13006,6 +15226,8 @@ function mapRowToChannelIdentityMapping(row) {
13006
15226
  0 && (module.exports = {
13007
15227
  ChannelBindingStore,
13008
15228
  ChannelIdentityMappingStore,
15229
+ CheckpointBaseNotFoundError,
15230
+ CheckpointTargetNotEmptyError,
13009
15231
  DuplicateProjectMembershipError,
13010
15232
  DuplicateProjectRoomMessageIdempotencyKeyError,
13011
15233
  MenuStore,
@@ -13013,10 +15235,13 @@ function mapRowToChannelIdentityMapping(row) {
13013
15235
  PGVectorStoreProvider,
13014
15236
  Pool,
13015
15237
  PostgreSQLA2AApiKeyStore,
15238
+ PostgreSQLAgentRunLedger,
13016
15239
  PostgreSQLAgentWebAppStore,
13017
15240
  PostgreSQLAssistantStore,
13018
15241
  PostgreSQLCapabilityBundleStore,
13019
15242
  PostgreSQLChannelInstallationStore,
15243
+ PostgreSQLChannelReplyJobStore,
15244
+ PostgreSQLCheckpointStorage,
13020
15245
  PostgreSQLCollectionStore,
13021
15246
  PostgreSQLConnectionStore,
13022
15247
  PostgreSQLDatabaseConfigStore,
@@ -13034,6 +15259,7 @@ function mapRowToChannelIdentityMapping(row) {
13034
15259
  PostgreSQLProjectStore,
13035
15260
  PostgreSQLScheduleStorage,
13036
15261
  PostgreSQLSkillStore,
15262
+ PostgreSQLStore,
13037
15263
  PostgreSQLTaskWorkItemStore,
13038
15264
  PostgreSQLTenantStore,
13039
15265
  PostgreSQLThreadStore,
@@ -13058,6 +15284,7 @@ function mapRowToChannelIdentityMapping(row) {
13058
15284
  addSkillTenantId,
13059
15285
  addStepThreadId,
13060
15286
  addTaskIdToEvalRuns,
15287
+ addThreadExpiresAt,
13061
15288
  addThreadTenantId,
13062
15289
  addWorkflowRunsTenantStatusUpdatedIndex,
13063
15290
  alterChannelInstallationsTable,
@@ -13065,12 +15292,14 @@ function mapRowToChannelIdentityMapping(row) {
13065
15292
  changeSkillPrimaryKey,
13066
15293
  changeThreadPrimaryKey,
13067
15294
  createA2AApiKeysTable,
15295
+ createAgentRunLedgerTable,
13068
15296
  createAgentWebAppsTable,
13069
15297
  createAssistantsTable,
13070
15298
  createCapabilityBundlesTable,
13071
15299
  createChannelBindingsTable,
13072
15300
  createChannelIdentityMappingTables,
13073
15301
  createChannelInstallationsTable,
15302
+ createChannelReplyJobsTable,
13074
15303
  createCollectionsTable,
13075
15304
  createDatabaseConfigsTable,
13076
15305
  createEvalCasesTable,
@@ -13090,6 +15319,7 @@ function mapRowToChannelIdentityMapping(row) {
13090
15319
  createScheduledTasksTable,
13091
15320
  createSharedResourcesTable,
13092
15321
  createSkillsTable,
15322
+ createStoreItemsTable,
13093
15323
  createTenantsTable,
13094
15324
  createThreadMessageQueueTable,
13095
15325
  createThreadsTable,