@axiom-lattice/pg-stores 3.3.13 → 4.0.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 (46) hide show
  1. package/.turbo/turbo-build.log +10 -10
  2. package/CHANGELOG.md +208 -0
  3. package/dist/index.d.mts +357 -3
  4. package/dist/index.d.ts +357 -3
  5. package/dist/index.js +2337 -77
  6. package/dist/index.js.map +1 -1
  7. package/dist/index.mjs +2342 -76
  8. package/dist/index.mjs.map +1 -1
  9. package/package.json +3 -3
  10. package/src/__tests__/AgentRunRetention.migrations.test.ts +41 -0
  11. package/src/__tests__/PostgreSQLAgentRunLedger.test.ts +305 -0
  12. package/src/__tests__/PostgreSQLAgentRunRetention.test.ts +126 -0
  13. package/src/__tests__/PostgreSQLAgentRunTaskQueue.test.ts +1375 -0
  14. package/src/__tests__/PostgreSQLChannelReplyJobStore.test.ts +335 -0
  15. package/src/__tests__/PostgreSQLCheckpointStorage.deleteAfter.test.ts +473 -0
  16. package/src/__tests__/PostgreSQLCheckpointStorage.test.ts +538 -0
  17. package/src/__tests__/PostgreSQLStore.test.ts +173 -0
  18. package/src/__tests__/PostgreSQLThreadStore.test.ts +80 -0
  19. package/src/__tests__/ThreadMessageQueueStore.integration.test.ts +21 -0
  20. package/src/__tests__/ThreadMessageQueueStore.test.ts +8 -3
  21. package/src/__tests__/createPgStoreConfig.test.ts +38 -0
  22. package/src/__tests__/eval-task-id.test.ts +14 -3
  23. package/src/createPgStoreConfig.ts +35 -2
  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/eval_migrations.ts +19 -0
  36. package/src/migrations/store_migrations.ts +50 -0
  37. package/src/migrations/task_migration.ts +17 -0
  38. package/src/migrations/thread_migrations.ts +25 -0
  39. package/src/stores/PostgreSQLAgentRunLedger.ts +1221 -0
  40. package/src/stores/PostgreSQLChannelReplyJobStore.ts +373 -0
  41. package/src/stores/PostgreSQLCheckpointStorage.ts +548 -0
  42. package/src/stores/PostgreSQLEvalStore.ts +17 -4
  43. package/src/stores/PostgreSQLStore.ts +167 -0
  44. package/src/stores/PostgreSQLTaskStore.ts +4 -0
  45. package/src/stores/PostgreSQLThreadStore.ts +49 -10
  46. 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,
@@ -73,6 +79,7 @@ __export(index_exports, {
73
79
  ProjectRoomMessageIdConflictError: () => ProjectRoomMessageIdConflictError,
74
80
  ThreadMessageQueueStore: () => ThreadMessageQueueStore,
75
81
  addAssistantTenantId: () => addAssistantTenantId,
82
+ addCancelRequestedToEvalRuns: () => addCancelRequestedToEvalRuns,
76
83
  addEnvToEvalRuns: () => addEnvToEvalRuns,
77
84
  addFileContentType: () => addFileContentType,
78
85
  addHoldoutToEvalRuns: () => addHoldoutToEvalRuns,
@@ -84,6 +91,7 @@ __export(index_exports, {
84
91
  addSkillTenantId: () => addSkillTenantId,
85
92
  addStepThreadId: () => addStepThreadId,
86
93
  addTaskIdToEvalRuns: () => addTaskIdToEvalRuns,
94
+ addThreadExpiresAt: () => addThreadExpiresAt,
87
95
  addThreadTenantId: () => addThreadTenantId,
88
96
  addWorkflowRunsTenantStatusUpdatedIndex: () => addWorkflowRunsTenantStatusUpdatedIndex,
89
97
  alterChannelInstallationsTable: () => alterChannelInstallationsTable,
@@ -91,12 +99,14 @@ __export(index_exports, {
91
99
  changeSkillPrimaryKey: () => changeSkillPrimaryKey,
92
100
  changeThreadPrimaryKey: () => changeThreadPrimaryKey,
93
101
  createA2AApiKeysTable: () => createA2AApiKeysTable,
102
+ createAgentRunLedgerTable: () => createAgentRunLedgerTable,
94
103
  createAgentWebAppsTable: () => createAgentWebAppsTable,
95
104
  createAssistantsTable: () => createAssistantsTable,
96
105
  createCapabilityBundlesTable: () => createCapabilityBundlesTable,
97
106
  createChannelBindingsTable: () => createChannelBindingsTable,
98
107
  createChannelIdentityMappingTables: () => createChannelIdentityMappingTables,
99
108
  createChannelInstallationsTable: () => createChannelInstallationsTable,
109
+ createChannelReplyJobsTable: () => createChannelReplyJobsTable,
100
110
  createCollectionsTable: () => createCollectionsTable,
101
111
  createDatabaseConfigsTable: () => createDatabaseConfigsTable,
102
112
  createEvalCasesTable: () => createEvalCasesTable,
@@ -116,6 +126,7 @@ __export(index_exports, {
116
126
  createScheduledTasksTable: () => createScheduledTasksTable,
117
127
  createSharedResourcesTable: () => createSharedResourcesTable,
118
128
  createSkillsTable: () => createSkillsTable,
129
+ createStoreItemsTable: () => createStoreItemsTable,
119
130
  createTenantsTable: () => createTenantsTable,
120
131
  createThreadMessageQueueTable: () => createThreadMessageQueueTable,
121
132
  createThreadsTable: () => createThreadsTable,
@@ -130,10 +141,10 @@ __export(index_exports, {
130
141
  safeParse: () => safeParse
131
142
  });
132
143
  module.exports = __toCommonJS(index_exports);
133
- var import_pg33 = require("pg");
144
+ var import_pg35 = require("pg");
134
145
 
135
146
  // src/createPgStoreConfig.ts
136
- var import_pg30 = require("pg");
147
+ var import_pg32 = require("pg");
137
148
 
138
149
  // src/migrations/migration.ts
139
150
  var MigrationManager = class {
@@ -346,6 +357,25 @@ var createThreadsTable = {
346
357
  await client.query("DROP TABLE IF EXISTS lattice_threads");
347
358
  }
348
359
  };
360
+ var addThreadExpiresAt = {
361
+ version: 194,
362
+ name: "add_thread_expires_at",
363
+ up: async (client) => {
364
+ await client.query(`
365
+ ALTER TABLE lattice_threads
366
+ ADD COLUMN IF NOT EXISTS expires_at TIMESTAMPTZ
367
+ `);
368
+ await client.query(`
369
+ CREATE INDEX IF NOT EXISTS idx_lattice_threads_expires_at
370
+ ON lattice_threads(expires_at)
371
+ WHERE expires_at IS NOT NULL
372
+ `);
373
+ },
374
+ down: async (client) => {
375
+ await client.query("DROP INDEX IF EXISTS idx_lattice_threads_expires_at");
376
+ await client.query("ALTER TABLE lattice_threads DROP COLUMN IF EXISTS expires_at");
377
+ }
378
+ };
349
379
 
350
380
  // src/migrations/thread_tenant_migration.ts
351
381
  var addThreadTenantId = {
@@ -473,6 +503,7 @@ var PostgreSQLThreadStore = class {
473
503
  this.migrationManager.register(createThreadsTable);
474
504
  this.migrationManager.register(addThreadTenantId);
475
505
  this.migrationManager.register(changeThreadPrimaryKey);
506
+ this.migrationManager.register(addThreadExpiresAt);
476
507
  if (options.autoMigrate !== false) {
477
508
  this.initialize().catch((error) => {
478
509
  console.error("Failed to initialize PostgreSQLThreadStore:", error);
@@ -517,7 +548,7 @@ var PostgreSQLThreadStore = class {
517
548
  await this.ensureInitialized();
518
549
  const result = await this.pool.query(
519
550
  `
520
- SELECT id, tenant_id, assistant_id, metadata, created_at, updated_at
551
+ SELECT id, tenant_id, assistant_id, metadata, created_at, updated_at, expires_at
521
552
  FROM lattice_threads
522
553
  WHERE tenant_id = $1 AND assistant_id = $2
523
554
  ORDER BY created_at DESC
@@ -541,7 +572,7 @@ var PostgreSQLThreadStore = class {
541
572
  await this.ensureInitialized();
542
573
  const result = await this.pool.query(
543
574
  `
544
- SELECT id, tenant_id, assistant_id, metadata, created_at, updated_at
575
+ SELECT id, tenant_id, assistant_id, metadata, created_at, updated_at, expires_at
545
576
  FROM lattice_threads
546
577
  WHERE tenant_id = $1 AND id = $2
547
578
  `,
@@ -561,14 +592,15 @@ var PostgreSQLThreadStore = class {
561
592
  const metadata = data.metadata || {};
562
593
  await this.pool.query(
563
594
  `
564
- INSERT INTO lattice_threads (id, tenant_id, assistant_id, metadata, created_at, updated_at)
565
- VALUES ($1, $2, $3, $4, $5, $6)
595
+ INSERT INTO lattice_threads (id, tenant_id, assistant_id, metadata, created_at, updated_at, expires_at)
596
+ VALUES ($1, $2, $3, $4, $5, $6, $7)
566
597
  ON CONFLICT (id, tenant_id) DO UPDATE SET
567
598
  assistant_id = EXCLUDED.assistant_id,
568
599
  metadata = EXCLUDED.metadata,
569
- updated_at = EXCLUDED.updated_at
600
+ updated_at = EXCLUDED.updated_at,
601
+ expires_at = EXCLUDED.expires_at
570
602
  `,
571
- [threadId, tenantId, assistantId, JSON.stringify(metadata), now, now]
603
+ [threadId, tenantId, assistantId, JSON.stringify(metadata), now, now, data.expiresAt ?? null]
572
604
  );
573
605
  return {
574
606
  id: threadId,
@@ -576,7 +608,8 @@ var PostgreSQLThreadStore = class {
576
608
  assistantId,
577
609
  metadata,
578
610
  createdAt: now,
579
- updatedAt: now
611
+ updatedAt: now,
612
+ expiresAt: data.expiresAt
580
613
  };
581
614
  }
582
615
  /**
@@ -593,18 +626,20 @@ var PostgreSQLThreadStore = class {
593
626
  ...updates.metadata || {}
594
627
  };
595
628
  const now = /* @__PURE__ */ new Date();
629
+ const expiresAt = updates.expiresAt !== void 0 ? updates.expiresAt : existing.expiresAt ?? null;
596
630
  await this.pool.query(
597
631
  `
598
632
  UPDATE lattice_threads
599
- SET metadata = $1, updated_at = $2
600
- WHERE tenant_id = $3 AND id = $4
633
+ SET metadata = $1, updated_at = $2, expires_at = $3
634
+ WHERE tenant_id = $4 AND id = $5
601
635
  `,
602
- [JSON.stringify(updatedMetadata), now, tenantId, threadId]
636
+ [JSON.stringify(updatedMetadata), now, expiresAt, tenantId, threadId]
603
637
  );
604
638
  return {
605
639
  ...existing,
606
640
  metadata: updatedMetadata,
607
- updatedAt: now
641
+ updatedAt: now,
642
+ expiresAt: expiresAt ?? void 0
608
643
  };
609
644
  }
610
645
  /**
@@ -636,6 +671,23 @@ var PostgreSQLThreadStore = class {
636
671
  );
637
672
  return result.rows.length > 0;
638
673
  }
674
+ /**
675
+ * List expired threads across all tenants, oldest expiry first.
676
+ */
677
+ async listExpiredThreads(now, limit) {
678
+ await this.ensureInitialized();
679
+ const result = await this.pool.query(
680
+ `
681
+ SELECT id, tenant_id, assistant_id, metadata, created_at, updated_at, expires_at
682
+ FROM lattice_threads
683
+ WHERE expires_at IS NOT NULL AND expires_at <= $1
684
+ ORDER BY expires_at ASC
685
+ LIMIT $2
686
+ `,
687
+ [now, Math.max(0, limit)]
688
+ );
689
+ return result.rows.map(this.mapRowToThread);
690
+ }
639
691
  /**
640
692
  * Ensure store is initialized
641
693
  */
@@ -654,7 +706,8 @@ var PostgreSQLThreadStore = class {
654
706
  assistantId: row.assistant_id,
655
707
  metadata: typeof row.metadata === "string" ? JSON.parse(row.metadata) : row.metadata || {},
656
708
  createdAt: row.created_at,
657
- updatedAt: row.updated_at
709
+ updatedAt: row.updated_at,
710
+ expiresAt: row.expires_at ?? void 0
658
711
  };
659
712
  }
660
713
  };
@@ -4378,6 +4431,21 @@ var addTaskIdToEvalRuns = {
4378
4431
  `);
4379
4432
  }
4380
4433
  };
4434
+ var addCancelRequestedToEvalRuns = {
4435
+ version: 205,
4436
+ name: "add_cancel_requested_to_eval_runs",
4437
+ up: async (client) => {
4438
+ await client.query(`
4439
+ ALTER TABLE lattice_eval_runs
4440
+ ADD COLUMN IF NOT EXISTS cancel_requested BOOLEAN NOT NULL DEFAULT FALSE
4441
+ `);
4442
+ },
4443
+ down: async (client) => {
4444
+ await client.query(`
4445
+ ALTER TABLE lattice_eval_runs DROP COLUMN IF EXISTS cancel_requested
4446
+ `);
4447
+ }
4448
+ };
4381
4449
  var evalMigrations = [
4382
4450
  createEvalProjectsTable,
4383
4451
  createEvalSuitesTable,
@@ -4389,7 +4457,8 @@ var evalMigrations = [
4389
4457
  addInterruptColumnsToEval,
4390
4458
  addInterruptPolicyToEvalCases,
4391
4459
  enforceUniqueEvalProjectName,
4392
- addTaskIdToEvalRuns
4460
+ addTaskIdToEvalRuns,
4461
+ addCancelRequestedToEvalRuns
4393
4462
  ];
4394
4463
 
4395
4464
  // src/stores/PostgreSQLEvalStore.ts
@@ -4514,6 +4583,7 @@ var PostgreSQLEvalStore = class {
4514
4583
  interruptedCases: row.interrupted_cases ?? 0,
4515
4584
  avgScore: row.avg_score ?? 0,
4516
4585
  error: row.error,
4586
+ cancelRequested: row.cancel_requested ?? false,
4517
4587
  holdout: row.holdout,
4518
4588
  envProjectId: row.env_project_id,
4519
4589
  envWorkspaceId: row.env_workspace_id,
@@ -4885,7 +4955,7 @@ var PostgreSQLEvalStore = class {
4885
4955
  `SELECT id, project_id, tenant_id,
4886
4956
  status, concurrency, total_cases,
4887
4957
  passed_cases, failed_cases, interrupted_cases, avg_score,
4888
- error, holdout, env_project_id, env_workspace_id, task_id, created_at, started_at, completed_at
4958
+ error, holdout, env_project_id, env_workspace_id, task_id, cancel_requested, created_at, started_at, completed_at
4889
4959
  FROM lattice_eval_runs
4890
4960
  WHERE ${conditions.join(" AND ")}
4891
4961
  ORDER BY created_at DESC`,
@@ -4900,7 +4970,7 @@ var PostgreSQLEvalStore = class {
4900
4970
  `SELECT id, project_id, tenant_id,
4901
4971
  status, concurrency, total_cases,
4902
4972
  passed_cases, failed_cases, interrupted_cases, avg_score,
4903
- error, holdout, env_project_id, env_workspace_id, task_id, created_at, started_at, completed_at
4973
+ error, holdout, env_project_id, env_workspace_id, task_id, cancel_requested, created_at, started_at, completed_at
4904
4974
  FROM lattice_eval_runs
4905
4975
  WHERE id = $1 AND tenant_id = $2`,
4906
4976
  [id, tenantId]
@@ -4918,7 +4988,7 @@ var PostgreSQLEvalStore = class {
4918
4988
  RETURNING id, project_id, tenant_id,
4919
4989
  status, concurrency, total_cases,
4920
4990
  passed_cases, failed_cases, interrupted_cases, avg_score,
4921
- error, holdout, env_project_id, env_workspace_id, task_id, created_at, started_at, completed_at`,
4991
+ error, holdout, env_project_id, env_workspace_id, task_id, cancel_requested, created_at, started_at, completed_at`,
4922
4992
  [
4923
4993
  actualId,
4924
4994
  projectId,
@@ -4980,11 +5050,22 @@ var PostgreSQLEvalStore = class {
4980
5050
  RETURNING id, project_id, tenant_id,
4981
5051
  status, concurrency, total_cases,
4982
5052
  passed_cases, failed_cases, interrupted_cases, avg_score,
4983
- error, holdout, env_project_id, env_workspace_id, task_id, created_at, started_at, completed_at`,
5053
+ error, holdout, env_project_id, env_workspace_id, task_id, cancel_requested, created_at, started_at, completed_at`,
4984
5054
  vals
4985
5055
  );
4986
5056
  return rows.length ? this.mapRowToRun(rows[0]) : null;
4987
5057
  }
5058
+ /** Request cooperative cancellation for a running evaluation. */
5059
+ async requestCancel(tenantId, id) {
5060
+ await this.ensureInitialized();
5061
+ const { rowCount } = await this.pool.query(
5062
+ `UPDATE lattice_eval_runs
5063
+ SET cancel_requested = TRUE
5064
+ WHERE tenant_id = $1 AND id = $2 AND status = 'running'`,
5065
+ [tenantId, id]
5066
+ );
5067
+ return (rowCount ?? 0) > 0;
5068
+ }
4988
5069
  /** Delete a run and its results */
4989
5070
  async deleteRun(tenantId, id) {
4990
5071
  await this.ensureInitialized();
@@ -5569,20 +5650,25 @@ var ThreadMessageQueueStore = class {
5569
5650
  }
5570
5651
  /**
5571
5652
  * Get all threads with pending or processing messages
5653
+ *
5654
+ * @returns One entry per full scope, with `hasPending` distinguishing
5655
+ * processing-only threads (no pending row) from threads with work to drain.
5572
5656
  */
5573
5657
  async getThreadsWithPendingMessages() {
5574
5658
  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`
5659
+ `SELECT tenant_id, assistant_id, thread_id, workspace_id, project_id,
5660
+ bool_or(status = 'pending') AS has_pending
5661
+ FROM lattice_thread_message_queue
5662
+ WHERE status IN ('pending', 'processing')
5663
+ GROUP BY tenant_id, assistant_id, thread_id, workspace_id, project_id`
5579
5664
  );
5580
5665
  return result.rows.map((row) => ({
5581
5666
  tenantId: row.tenant_id,
5582
5667
  assistantId: row.assistant_id,
5583
5668
  threadId: row.thread_id,
5584
5669
  workspaceId: row.workspace_id,
5585
- projectId: row.project_id
5670
+ projectId: row.project_id,
5671
+ hasPending: row.has_pending
5586
5672
  }));
5587
5673
  }
5588
5674
  /**
@@ -7725,12 +7811,27 @@ var addTaskDependenciesGinIndex = {
7725
7811
  await client.query("DROP INDEX IF EXISTS idx_lattice_tasks_dependencies_gin");
7726
7812
  }
7727
7813
  };
7814
+ var addTaskContextThreadIndex = {
7815
+ version: 206,
7816
+ name: "add_task_context_thread_index",
7817
+ up: async (client) => {
7818
+ await client.query(`
7819
+ CREATE INDEX IF NOT EXISTS idx_lattice_tasks_context_thread
7820
+ ON lattice_tasks (tenant_id, (context->>'thread_id'))
7821
+ WHERE context->>'thread_id' IS NOT NULL
7822
+ `);
7823
+ },
7824
+ down: async (client) => {
7825
+ await client.query("DROP INDEX IF EXISTS idx_lattice_tasks_context_thread");
7826
+ }
7827
+ };
7728
7828
  var taskMigrations = [
7729
7829
  createTasksTable,
7730
7830
  addTaskFieldsMigration,
7731
7831
  addTaskProjectFieldsMigration,
7732
7832
  addFilesToTasks,
7733
- addTaskDependenciesGinIndex
7833
+ addTaskDependenciesGinIndex,
7834
+ addTaskContextThreadIndex
7734
7835
  ];
7735
7836
 
7736
7837
  // src/stores/PostgreSQLTaskStore.ts
@@ -7941,6 +8042,10 @@ var PostgreSQLTaskStore = class {
7941
8042
  conditions.push(`priority = $${paramIndex++}`);
7942
8043
  params.push(filter.priority);
7943
8044
  }
8045
+ if (filter.threadId) {
8046
+ conditions.push(`context->>'thread_id' = $${paramIndex++}`);
8047
+ params.push(filter.threadId);
8048
+ }
7944
8049
  if (filter.workspaceId) {
7945
8050
  conditions.push(`workspace_id = $${paramIndex++}`);
7946
8051
  params.push(filter.workspaceId);
@@ -10012,6 +10117,299 @@ var createCollectionsTable = {
10012
10117
  }
10013
10118
  };
10014
10119
 
10120
+ // src/migrations/agent_run_ledger_migration.ts
10121
+ var createAgentRunLedgerTable = {
10122
+ version: 193,
10123
+ name: "create_lattice_agent_runs_table",
10124
+ up: async (client) => {
10125
+ await client.query(`
10126
+ CREATE TABLE IF NOT EXISTS lattice_agent_runs (
10127
+ run_id UUID PRIMARY KEY,
10128
+ tenant_id VARCHAR(255) NOT NULL,
10129
+ workspace_id VARCHAR(255),
10130
+ project_id VARCHAR(255),
10131
+ assistant_id VARCHAR(255) NOT NULL,
10132
+ thread_id VARCHAR(255) NOT NULL,
10133
+ status VARCHAR(32) NOT NULL,
10134
+ owner VARCHAR(255) NOT NULL,
10135
+ lease_expires_at TIMESTAMPTZ NOT NULL,
10136
+ input_message_ids TEXT[] NOT NULL DEFAULT '{}',
10137
+ attempts INTEGER NOT NULL DEFAULT 1,
10138
+ error TEXT,
10139
+ cancel_requested BOOLEAN NOT NULL DEFAULT FALSE,
10140
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
10141
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
10142
+ started_at TIMESTAMPTZ NOT NULL DEFAULT now(),
10143
+ finished_at TIMESTAMPTZ
10144
+ )
10145
+ `);
10146
+ await client.query(`
10147
+ CREATE UNIQUE INDEX IF NOT EXISTS uq_lattice_agent_runs_one_running
10148
+ ON lattice_agent_runs (tenant_id, thread_id) WHERE status = 'running'
10149
+ `);
10150
+ await client.query(`
10151
+ CREATE INDEX IF NOT EXISTS idx_lattice_agent_runs_history
10152
+ ON lattice_agent_runs (tenant_id, thread_id, created_at DESC)
10153
+ `);
10154
+ await client.query(`
10155
+ CREATE INDEX IF NOT EXISTS idx_lattice_agent_runs_reap
10156
+ ON lattice_agent_runs (status, lease_expires_at)
10157
+ `);
10158
+ },
10159
+ down: async (client) => {
10160
+ await client.query("DROP TABLE IF EXISTS lattice_agent_runs");
10161
+ }
10162
+ };
10163
+
10164
+ // src/migrations/channel_reply_job_migration.ts
10165
+ var DB_CLOCK_MS = "FLOOR(EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint";
10166
+ var createChannelReplyJobsTable = {
10167
+ version: 200,
10168
+ name: "create_lattice_channel_reply_jobs",
10169
+ up: async (client) => {
10170
+ await client.query(`
10171
+ CREATE TABLE IF NOT EXISTS lattice_channel_reply_jobs (
10172
+ run_id TEXT PRIMARY KEY,
10173
+ tenant_id VARCHAR(255) NOT NULL,
10174
+ assistant_id TEXT NOT NULL,
10175
+ thread_id TEXT NOT NULL,
10176
+ adapter_channel TEXT NOT NULL,
10177
+ channel_installation_id TEXT NOT NULL,
10178
+ reply_target JSONB NOT NULL,
10179
+ input_message_id TEXT NOT NULL,
10180
+ stream_cursor TEXT NULL,
10181
+ status TEXT NOT NULL,
10182
+ attempts INT NOT NULL DEFAULT 0,
10183
+ available_at BIGINT NOT NULL,
10184
+ lease_owner TEXT NULL,
10185
+ lease_expires_at BIGINT NULL,
10186
+ last_error TEXT NULL,
10187
+ output_message_id TEXT NULL,
10188
+ created_at BIGINT NOT NULL DEFAULT (${DB_CLOCK_MS}),
10189
+ updated_at BIGINT NOT NULL DEFAULT (${DB_CLOCK_MS}),
10190
+ delivered_at BIGINT NULL
10191
+ )
10192
+ `);
10193
+ await client.query(`
10194
+ CREATE INDEX IF NOT EXISTS idx_lattice_channel_reply_jobs_due
10195
+ ON lattice_channel_reply_jobs (status, available_at)
10196
+ `);
10197
+ await client.query(`
10198
+ CREATE INDEX IF NOT EXISTS idx_lattice_channel_reply_jobs_tenant_thread
10199
+ ON lattice_channel_reply_jobs (tenant_id, thread_id)
10200
+ `);
10201
+ },
10202
+ down: async (client) => {
10203
+ await client.query("DROP INDEX IF EXISTS idx_lattice_channel_reply_jobs_tenant_thread");
10204
+ await client.query("DROP INDEX IF EXISTS idx_lattice_channel_reply_jobs_due");
10205
+ await client.query("DROP TABLE IF EXISTS lattice_channel_reply_jobs");
10206
+ }
10207
+ };
10208
+
10209
+ // src/migrations/agent_run_task_queue_migration.ts
10210
+ var createAgentRunTaskQueueMigration = {
10211
+ version: 196,
10212
+ name: "expand_lattice_agent_runs_task_queue",
10213
+ up: async (client) => {
10214
+ await client.query(`
10215
+ ALTER TABLE lattice_agent_runs
10216
+ ALTER COLUMN owner DROP NOT NULL,
10217
+ ALTER COLUMN lease_expires_at DROP NOT NULL,
10218
+ ALTER COLUMN started_at DROP NOT NULL,
10219
+ ALTER COLUMN started_at DROP DEFAULT
10220
+ `);
10221
+ await client.query(`
10222
+ ALTER TABLE lattice_agent_runs
10223
+ ADD COLUMN IF NOT EXISTS source VARCHAR(32) NOT NULL DEFAULT 'legacy',
10224
+ ADD COLUMN IF NOT EXISTS input JSONB,
10225
+ ADD COLUMN IF NOT EXISTS command JSONB,
10226
+ ADD COLUMN IF NOT EXISTS config JSONB,
10227
+ ADD COLUMN IF NOT EXISTS metadata JSONB,
10228
+ ADD COLUMN IF NOT EXISTS base_checkpoint_id TEXT,
10229
+ ADD COLUMN IF NOT EXISTS available_at TIMESTAMPTZ NOT NULL DEFAULT now(),
10230
+ ADD COLUMN IF NOT EXISTS rollback_requested BOOLEAN NOT NULL DEFAULT FALSE
10231
+ `);
10232
+ await client.query(`
10233
+ ALTER TABLE lattice_agent_runs
10234
+ ALTER COLUMN source SET DEFAULT 'legacy'
10235
+ `);
10236
+ await client.query(`
10237
+ CREATE INDEX IF NOT EXISTS idx_lattice_agent_runs_run_queue
10238
+ ON lattice_agent_runs (source, status, available_at, created_at)
10239
+ `);
10240
+ },
10241
+ down: async (client) => {
10242
+ await client.query(`
10243
+ DROP INDEX IF EXISTS idx_lattice_agent_runs_run_queue
10244
+ `);
10245
+ await client.query(`
10246
+ ALTER TABLE lattice_agent_runs
10247
+ DROP COLUMN IF EXISTS rollback_requested,
10248
+ DROP COLUMN IF EXISTS available_at,
10249
+ DROP COLUMN IF EXISTS base_checkpoint_id,
10250
+ DROP COLUMN IF EXISTS metadata,
10251
+ DROP COLUMN IF EXISTS config,
10252
+ DROP COLUMN IF EXISTS command,
10253
+ DROP COLUMN IF EXISTS input,
10254
+ DROP COLUMN IF EXISTS source
10255
+ `);
10256
+ await client.query(`
10257
+ ALTER TABLE lattice_agent_runs
10258
+ ALTER COLUMN started_at SET DEFAULT now()
10259
+ `);
10260
+ }
10261
+ };
10262
+
10263
+ // src/migrations/agent_run_capability_revision_migration.ts
10264
+ var createAgentRunCapabilityRevisionMigration = {
10265
+ version: 197,
10266
+ name: "add_agent_run_capability_revision",
10267
+ up: async (client) => {
10268
+ await client.query(`
10269
+ ALTER TABLE lattice_agent_runs
10270
+ ADD COLUMN IF NOT EXISTS capability_runtime_revision TEXT
10271
+ `);
10272
+ },
10273
+ down: async (client) => {
10274
+ await client.query(`
10275
+ ALTER TABLE lattice_agent_runs
10276
+ DROP COLUMN IF EXISTS capability_runtime_revision
10277
+ `);
10278
+ }
10279
+ };
10280
+
10281
+ // src/migrations/agent_run_base_checkpoint_captured_migration.ts
10282
+ var createAgentRunBaseCheckpointCapturedMigration = {
10283
+ version: 198,
10284
+ name: "add_agent_run_base_checkpoint_captured",
10285
+ up: async (client) => {
10286
+ await client.query(`
10287
+ ALTER TABLE lattice_agent_runs
10288
+ ADD COLUMN IF NOT EXISTS base_checkpoint_captured BOOLEAN NOT NULL DEFAULT FALSE
10289
+ `);
10290
+ await client.query(`
10291
+ UPDATE lattice_agent_runs
10292
+ SET base_checkpoint_captured = TRUE
10293
+ WHERE base_checkpoint_id IS NOT NULL
10294
+ `);
10295
+ },
10296
+ down: async (client) => {
10297
+ await client.query(`
10298
+ ALTER TABLE lattice_agent_runs
10299
+ DROP COLUMN IF EXISTS base_checkpoint_captured
10300
+ `);
10301
+ }
10302
+ };
10303
+
10304
+ // src/migrations/agent_run_rollback_failure_migration.ts
10305
+ var createAgentRunRollbackFailureMigration = {
10306
+ version: 199,
10307
+ name: "add_agent_run_rollback_failure",
10308
+ up: async (client) => {
10309
+ await client.query(`
10310
+ ALTER TABLE lattice_agent_runs
10311
+ ADD COLUMN IF NOT EXISTS rollback_attempts INTEGER NOT NULL DEFAULT 0,
10312
+ ADD COLUMN IF NOT EXISTS rollback_error TEXT
10313
+ `);
10314
+ },
10315
+ down: async (client) => {
10316
+ await client.query(`
10317
+ ALTER TABLE lattice_agent_runs
10318
+ DROP COLUMN IF EXISTS rollback_error,
10319
+ DROP COLUMN IF EXISTS rollback_attempts
10320
+ `);
10321
+ }
10322
+ };
10323
+
10324
+ // src/migrations/agent_run_running_index_source_migration.ts
10325
+ var agentRunRunningIndexSourceMigration = {
10326
+ version: 202,
10327
+ name: "scope_agent_run_running_index_to_run_api",
10328
+ up: async (client) => {
10329
+ await client.query("DROP INDEX IF EXISTS uq_lattice_agent_runs_one_running");
10330
+ await client.query(`
10331
+ CREATE UNIQUE INDEX IF NOT EXISTS uq_lattice_agent_runs_one_running
10332
+ ON lattice_agent_runs (tenant_id, thread_id)
10333
+ WHERE status = 'running' AND source = 'run_api'
10334
+ `);
10335
+ },
10336
+ down: async (client) => {
10337
+ await client.query("DROP INDEX IF EXISTS uq_lattice_agent_runs_one_running");
10338
+ await client.query(`
10339
+ CREATE UNIQUE INDEX IF NOT EXISTS uq_lattice_agent_runs_one_running
10340
+ ON lattice_agent_runs (tenant_id, thread_id)
10341
+ WHERE status = 'running'
10342
+ `);
10343
+ }
10344
+ };
10345
+
10346
+ // src/migrations/agent_run_retention_migration.ts
10347
+ var createAgentRunRetentionMigration = {
10348
+ version: 201,
10349
+ name: "add_agent_run_retention_index",
10350
+ up: async (client) => {
10351
+ await client.query(`
10352
+ CREATE INDEX IF NOT EXISTS idx_lattice_agent_runs_terminal_finished_at
10353
+ ON lattice_agent_runs (finished_at)
10354
+ WHERE status IN ('success', 'error', 'interrupted', 'cancelled', 'timeout')
10355
+ `);
10356
+ },
10357
+ down: async (client) => {
10358
+ await client.query(`
10359
+ DROP INDEX IF EXISTS idx_lattice_agent_runs_terminal_finished_at
10360
+ `);
10361
+ }
10362
+ };
10363
+
10364
+ // src/migrations/agent_run_project_task_migration.ts
10365
+ var createAgentRunProjectTaskMigration = {
10366
+ version: 203,
10367
+ name: "add_agent_run_project_task",
10368
+ up: async (client) => {
10369
+ await client.query(`
10370
+ ALTER TABLE lattice_agent_runs
10371
+ ADD COLUMN IF NOT EXISTS task_id TEXT NULL
10372
+ `);
10373
+ await client.query(`
10374
+ CREATE INDEX IF NOT EXISTS idx_lattice_agent_runs_project_task
10375
+ ON lattice_agent_runs (tenant_id, task_id, status, created_at DESC)
10376
+ WHERE task_id IS NOT NULL
10377
+ `);
10378
+ },
10379
+ down: async (client) => {
10380
+ await client.query(`
10381
+ DROP INDEX IF EXISTS idx_lattice_agent_runs_project_task
10382
+ `);
10383
+ await client.query(`
10384
+ ALTER TABLE lattice_agent_runs
10385
+ DROP COLUMN IF EXISTS task_id
10386
+ `);
10387
+ }
10388
+ };
10389
+
10390
+ // src/migrations/agent_run_project_task_running_index_migration.ts
10391
+ var agentRunProjectTaskRunningIndexMigration = {
10392
+ version: 204,
10393
+ name: "scope_agent_run_running_index_to_queue_sources",
10394
+ up: async (client) => {
10395
+ await client.query("DROP INDEX IF EXISTS uq_lattice_agent_runs_one_running");
10396
+ await client.query(`
10397
+ CREATE UNIQUE INDEX IF NOT EXISTS uq_lattice_agent_runs_one_running
10398
+ ON lattice_agent_runs (tenant_id, thread_id)
10399
+ WHERE status = 'running'
10400
+ AND source IN ('run_api', 'project_task')
10401
+ `);
10402
+ },
10403
+ down: async (client) => {
10404
+ await client.query("DROP INDEX IF EXISTS uq_lattice_agent_runs_one_running");
10405
+ await client.query(`
10406
+ CREATE UNIQUE INDEX IF NOT EXISTS uq_lattice_agent_runs_one_running
10407
+ ON lattice_agent_runs (tenant_id, thread_id)
10408
+ WHERE status = 'running' AND source = 'run_api'
10409
+ `);
10410
+ }
10411
+ };
10412
+
10015
10413
  // src/migrations/capability_bundle_migration.ts
10016
10414
  var createCapabilityBundlesTable = {
10017
10415
  version: 170,
@@ -12353,52 +12751,1887 @@ var PostgreSQLPluginDeploymentStore = class {
12353
12751
  }
12354
12752
  };
12355
12753
 
12356
- // src/createPgStoreConfig.ts
12357
- async function createPgStoreConfig(connectionString) {
12358
- const pool = new import_pg30.Pool({ connectionString });
12359
- const mm = new MigrationManager(pool);
12360
- mm.register(createThreadsTable);
12361
- mm.register(createScheduledTasksTable);
12362
- mm.register(createAssistantsTable);
12363
- mm.register(createSkillsTable);
12364
- mm.register(createWorkspacesTable);
12365
- mm.register(createProjectsTable);
12366
- mm.register(createUsersTable);
12367
- mm.register(createTenantsTable);
12368
- mm.register(addUserStatusColumn);
12369
- mm.register(createUserTenantLinkTable);
12370
- mm.register(migrateRemoveTenantIdFromUsers);
12371
- mm.register(createMetricsConfigsTable);
12372
- mm.register(createMcpServerConfigsTable);
12373
- mm.register(addAssistantTenantId);
12374
- mm.register(addSkillTenantId);
12375
- mm.register(changeSkillPrimaryKey);
12376
- mm.register(addScheduleTenantId);
12377
- mm.register(changeThreadPrimaryKey);
12378
- mm.register(createChannelIdentityMappingTables);
12379
- mm.register(createChannelInstallationsTable);
12380
- mm.register(addThreadTenantId);
12381
- mm.register(addProjectConfigColumn);
12382
- registerThreadMessageQueueMigrations(mm);
12383
- mm.register(createWorkflowTrackingTables);
12384
- for (const m of evalMigrations) {
12385
- mm.register(m);
12386
- }
12387
- mm.register(alterChannelInstallationsTable);
12388
- mm.register(createChannelBindingsTable);
12389
- mm.register(createDatabaseConfigsTable);
12390
- mm.register(changeAssistantPrimaryKey);
12391
- mm.register(addStepThreadId);
12392
- mm.register(createA2AApiKeysTable);
12393
- mm.register(addAssistantOwnerUserId);
12394
- for (const m of taskMigrations) {
12395
- mm.register(m);
12754
+ // src/stores/PostgreSQLAgentRunLedger.ts
12755
+ var import_pg30 = require("pg");
12756
+ var import_core10 = require("@axiom-lattice/core");
12757
+ var DEFAULT_LIST_LIMIT = 50;
12758
+ var MAX_LIST_LIMIT = 200;
12759
+ var CLAIM_RUN_SQL = `UPDATE lattice_agent_runs AS r
12760
+ SET status = 'running',
12761
+ owner = $2,
12762
+ lease_expires_at = now() + ($3::bigint || ' milliseconds')::interval,
12763
+ started_at = now(),
12764
+ attempts = r.attempts + 1,
12765
+ input_message_ids = COALESCE($4::text[], r.input_message_ids),
12766
+ updated_at = now()
12767
+ WHERE r.run_id = $1
12768
+ AND r.source = ANY($5::text[])
12769
+ AND r.status = 'pending'
12770
+ AND r.available_at <= now()
12771
+ AND NOT EXISTS (
12772
+ SELECT 1 FROM lattice_agent_runs AS o
12773
+ WHERE o.tenant_id = r.tenant_id
12774
+ AND o.thread_id = r.thread_id
12775
+ AND o.source IN ('run_api', 'project_task')
12776
+ AND o.status = 'running'
12777
+ )
12778
+ AND NOT EXISTS (
12779
+ SELECT 1 FROM lattice_agent_runs AS b
12780
+ WHERE b.tenant_id = r.tenant_id
12781
+ AND b.thread_id = r.thread_id
12782
+ AND b.source = 'run_api'
12783
+ AND b.rollback_requested = TRUE
12784
+ )
12785
+ RETURNING r.*`;
12786
+ var PostgreSQLAgentRunLedger = class {
12787
+ /**
12788
+ * @param options An externally managed pool, or a pool config/connection URL.
12789
+ */
12790
+ constructor(options) {
12791
+ this.ownsPool = true;
12792
+ this.initialized = false;
12793
+ this.initPromise = null;
12794
+ if (options.pool) {
12795
+ this.pool = options.pool;
12796
+ this.ownsPool = false;
12797
+ this.initialized = true;
12798
+ return;
12799
+ }
12800
+ if (typeof options.poolConfig === "string") {
12801
+ this.pool = new import_pg30.Pool({ connectionString: options.poolConfig });
12802
+ } else if (options.poolConfig) {
12803
+ this.pool = new import_pg30.Pool(options.poolConfig);
12804
+ } else {
12805
+ throw new Error("Either pool or poolConfig must be provided");
12806
+ }
12807
+ this.migrationManager = new MigrationManager(this.pool);
12808
+ this.migrationManager.register(createAgentRunLedgerTable);
12809
+ this.migrationManager.register(createAgentRunTaskQueueMigration);
12810
+ this.migrationManager.register(createAgentRunCapabilityRevisionMigration);
12811
+ this.migrationManager.register(createAgentRunBaseCheckpointCapturedMigration);
12812
+ this.migrationManager.register(createAgentRunRollbackFailureMigration);
12813
+ this.migrationManager.register(createAgentRunRetentionMigration);
12814
+ this.migrationManager.register(agentRunRunningIndexSourceMigration);
12815
+ this.migrationManager.register(createAgentRunProjectTaskMigration);
12816
+ this.migrationManager.register(agentRunProjectTaskRunningIndexMigration);
12817
+ if (options.autoMigrate !== false) {
12818
+ this.initialize().catch((error) => {
12819
+ console.error("Failed to initialize PostgreSQLAgentRunLedger:", error);
12820
+ throw error;
12821
+ });
12822
+ }
12396
12823
  }
12397
- mm.register(createMenuItemsTable);
12398
- mm.register(createSharedResourcesTable);
12399
- mm.register(addFileContentType);
12400
- mm.register(addMcpAppContentType);
12401
- mm.register(createCollectionsTable);
12824
+ /** Runs pending migrations once. */
12825
+ async initialize() {
12826
+ if (this.initialized) return;
12827
+ if (this.initPromise) return this.initPromise;
12828
+ this.initPromise = (async () => {
12829
+ try {
12830
+ await this.migrationManager.migrate();
12831
+ this.initialized = true;
12832
+ } finally {
12833
+ this.initPromise = null;
12834
+ }
12835
+ })();
12836
+ return this.initPromise;
12837
+ }
12838
+ /** Ends the pool when this store owns it. */
12839
+ async dispose() {
12840
+ if (this.ownsPool && this.pool) {
12841
+ await this.pool.end();
12842
+ }
12843
+ }
12844
+ /**
12845
+ * Creates a running legacy run, reconciling any existing running row first.
12846
+ *
12847
+ * @remarks The caller holds the `thread-run` lease, so a surviving `running`
12848
+ * row is stale (its owner can no longer hold the lease) even when its ledger
12849
+ * lease has not expired yet.
12850
+ */
12851
+ async beginRun(input) {
12852
+ const client = await this.pool.connect();
12853
+ try {
12854
+ await client.query("BEGIN");
12855
+ await client.query(
12856
+ `UPDATE lattice_agent_runs
12857
+ SET status = 'interrupted',
12858
+ error = COALESCE(error, 'superseded by a new run on the same thread'),
12859
+ finished_at = now(),
12860
+ updated_at = now()
12861
+ WHERE tenant_id = $1 AND thread_id = $2
12862
+ AND status = 'running'`,
12863
+ [input.tenantId, input.threadId]
12864
+ );
12865
+ const result = await client.query(
12866
+ `INSERT INTO lattice_agent_runs
12867
+ (run_id, tenant_id, workspace_id, project_id, assistant_id, thread_id,
12868
+ status, source, owner, lease_expires_at, input_message_ids, attempts,
12869
+ started_at)
12870
+ VALUES ($1, $2, $3, $4, $5, $6, 'running', 'legacy', $7,
12871
+ now() + ($8::bigint || ' milliseconds')::interval, $9, 1, now())
12872
+ RETURNING *`,
12873
+ [
12874
+ input.runId,
12875
+ input.tenantId,
12876
+ input.workspaceId ?? null,
12877
+ input.projectId ?? null,
12878
+ input.assistantId,
12879
+ input.threadId,
12880
+ input.owner,
12881
+ input.leaseMs,
12882
+ input.inputMessageIds
12883
+ ]
12884
+ );
12885
+ await client.query("COMMIT");
12886
+ return rowToRecord(result.rows[0]);
12887
+ } catch (error) {
12888
+ await client.query("ROLLBACK");
12889
+ if (isActiveRunViolation(error)) {
12890
+ throw new import_core10.AgentRunAlreadyActiveError();
12891
+ }
12892
+ throw error;
12893
+ } finally {
12894
+ client.release();
12895
+ }
12896
+ }
12897
+ /** Creates an unowned pending run-queue row. */
12898
+ async createPendingRun(input) {
12899
+ const source = input.source ?? "run_api";
12900
+ if (source === "project_task") {
12901
+ if (input.taskId === void 0 || input.taskId.trim().length === 0) {
12902
+ throw new import_core10.AgentRunInvalidTaskIdError(
12903
+ "A project_task run requires a non-empty taskId"
12904
+ );
12905
+ }
12906
+ } else if (input.taskId !== void 0) {
12907
+ throw new import_core10.AgentRunInvalidTaskIdError(
12908
+ `A ${source} run cannot carry a taskId`
12909
+ );
12910
+ }
12911
+ if (input.input != null && input.command != null) {
12912
+ throw new import_core10.AgentRunInputCommandConflictError();
12913
+ }
12914
+ const inputMessageIds = input.inputMessageIds ? (0, import_core10.normalizeAgentRunMessageIds)(input.inputMessageIds) : (0, import_core10.extractAgentRunInputMessageIds)(input.input);
12915
+ const availableAt = input.availableAt ?? null;
12916
+ const limit = input.maxActiveRunsForTenant;
12917
+ const client = await this.pool.connect();
12918
+ try {
12919
+ await client.query("BEGIN");
12920
+ if (limit !== void 0 && limit > 0) {
12921
+ await client.query("SELECT pg_advisory_xact_lock(hashtext($1))", [
12922
+ input.tenantId
12923
+ ]);
12924
+ }
12925
+ await client.query("SELECT pg_advisory_xact_lock(hashtext($1))", [
12926
+ `agent-run:${input.tenantId}:${input.threadId}`
12927
+ ]);
12928
+ if (inputMessageIds.length > 0) {
12929
+ const duplicate = await client.query(
12930
+ `SELECT input_message_ids
12931
+ FROM lattice_agent_runs
12932
+ WHERE tenant_id = $1 AND thread_id = $2
12933
+ AND status IN ('pending', 'running')
12934
+ AND input_message_ids && $3::text[]
12935
+ LIMIT 1`,
12936
+ [input.tenantId, input.threadId, inputMessageIds]
12937
+ );
12938
+ if ((duplicate.rowCount ?? 0) > 0) {
12939
+ const existing = duplicate.rows[0].input_message_ids;
12940
+ const messageId = existing.find((id) => inputMessageIds.includes(id)) ?? inputMessageIds[0];
12941
+ throw new import_core10.AgentRunDuplicateMessageIdError(messageId);
12942
+ }
12943
+ }
12944
+ if (limit !== void 0 && limit > 0) {
12945
+ const active = await client.query(
12946
+ `SELECT count(*)::text AS count
12947
+ FROM lattice_agent_runs
12948
+ WHERE tenant_id = $1
12949
+ AND source = 'run_api'
12950
+ AND status IN ('pending', 'running')`,
12951
+ [input.tenantId]
12952
+ );
12953
+ if (Number(active.rows[0].count) >= limit) {
12954
+ throw new import_core10.AgentRunTenantQuotaExceededError(input.tenantId, limit);
12955
+ }
12956
+ }
12957
+ const result = await client.query(
12958
+ `INSERT INTO lattice_agent_runs
12959
+ (run_id, tenant_id, workspace_id, project_id, assistant_id, thread_id,
12960
+ status, source, owner, lease_expires_at, input_message_ids, attempts,
12961
+ input, command, config, metadata, capability_runtime_revision,
12962
+ available_at, created_at, updated_at, task_id)
12963
+ VALUES ($1, $2, $3, $4, $5, $6, 'pending', $7, NULL, NULL, $8, 0,
12964
+ $9::jsonb, $10::jsonb, $11::jsonb, $12::jsonb, $13,
12965
+ COALESCE(
12966
+ to_timestamp($14::double precision / 1000.0),
12967
+ date_trunc('milliseconds', now())
12968
+ ),
12969
+ date_trunc('milliseconds', now()),
12970
+ date_trunc('milliseconds', now()), $15)
12971
+ RETURNING *`,
12972
+ [
12973
+ input.runId,
12974
+ input.tenantId,
12975
+ input.workspaceId ?? null,
12976
+ input.projectId ?? null,
12977
+ input.assistantId,
12978
+ input.threadId,
12979
+ source,
12980
+ inputMessageIds,
12981
+ jsonParam(input.input),
12982
+ jsonParam(input.command),
12983
+ jsonParam(input.config),
12984
+ jsonParam(input.metadata),
12985
+ input.capabilityRuntimeRevision ?? null,
12986
+ availableAt,
12987
+ input.taskId ?? null
12988
+ ]
12989
+ );
12990
+ await client.query("COMMIT");
12991
+ return rowToRecord(result.rows[0]);
12992
+ } catch (error) {
12993
+ await client.query("ROLLBACK");
12994
+ throw error;
12995
+ } finally {
12996
+ client.release();
12997
+ }
12998
+ }
12999
+ /** Lists claimable run-queue rows, oldest available first. */
13000
+ async listPendingRuns(options) {
13001
+ const limit = clampLimit(options?.limit);
13002
+ const after = options?.after;
13003
+ const fairTenants = options?.fairTenants === true;
13004
+ if (fairTenants && after !== void 0) {
13005
+ throw new RangeError(
13006
+ "listPendingRuns fairTenants does not support the after cursor"
13007
+ );
13008
+ }
13009
+ const maxRunning = positiveLimit(options?.maxRunningPerTenant);
13010
+ const sources = options?.sources === void 0 ? ["run_api"] : [...options.sources];
13011
+ (0, import_core10.assertQueueSources)(sources);
13012
+ const params = [limit];
13013
+ let cursorClause = "";
13014
+ if (after) {
13015
+ params.push(
13016
+ new Date(after.availableAt),
13017
+ new Date(after.createdAt),
13018
+ after.runId
13019
+ );
13020
+ cursorClause = `
13021
+ AND (r.available_at, r.created_at, r.run_id)
13022
+ > ($2::timestamptz, $3::timestamptz, $4::uuid)`;
13023
+ }
13024
+ params.push(maxRunning ?? null);
13025
+ const capIndex = params.length;
13026
+ params.push(sources);
13027
+ const sourcesIndex = params.length;
13028
+ const capClause = `
13029
+ AND ($${capIndex}::bigint IS NULL OR (
13030
+ SELECT count(*) FROM lattice_agent_runs AS c
13031
+ WHERE c.tenant_id = r.tenant_id
13032
+ AND c.source = ANY($${sourcesIndex}::text[])
13033
+ AND c.status = 'running'
13034
+ ) < $${capIndex}::bigint)`;
13035
+ const filterClause = `WHERE r.source = ANY($${sourcesIndex}::text[]) AND r.status = 'pending'
13036
+ AND r.available_at <= now()
13037
+ AND NOT EXISTS (
13038
+ SELECT 1 FROM lattice_agent_runs AS o
13039
+ WHERE o.tenant_id = r.tenant_id
13040
+ AND o.thread_id = r.thread_id
13041
+ AND o.source IN ('run_api', 'project_task')
13042
+ AND o.status = 'running'
13043
+ )
13044
+ AND NOT EXISTS (
13045
+ SELECT 1 FROM lattice_agent_runs AS b
13046
+ WHERE b.tenant_id = r.tenant_id
13047
+ AND b.thread_id = r.thread_id
13048
+ AND b.source = 'run_api'
13049
+ AND b.rollback_requested = TRUE
13050
+ )${capClause}${cursorClause}`;
13051
+ if (fairTenants) {
13052
+ const ranked = await this.pool.query(
13053
+ `WITH ranked AS (
13054
+ SELECT r.*,
13055
+ row_number() OVER (
13056
+ PARTITION BY r.tenant_id
13057
+ ORDER BY r.available_at ASC, r.created_at ASC, r.run_id ASC
13058
+ ) AS tenant_rank
13059
+ FROM lattice_agent_runs AS r
13060
+ ${filterClause}
13061
+ )
13062
+ SELECT * FROM ranked
13063
+ ORDER BY tenant_rank ASC, available_at ASC, created_at ASC,
13064
+ run_id ASC, tenant_id ASC
13065
+ LIMIT $1`,
13066
+ params
13067
+ );
13068
+ return ranked.rows.map(rowToRecord);
13069
+ }
13070
+ const result = await this.pool.query(
13071
+ `SELECT r.* FROM lattice_agent_runs AS r
13072
+ ${filterClause}
13073
+ ORDER BY r.available_at ASC, r.created_at ASC, r.run_id ASC
13074
+ LIMIT $1`,
13075
+ params
13076
+ );
13077
+ return result.rows.map(rowToRecord);
13078
+ }
13079
+ /** Atomically claims a pending run-queue row. */
13080
+ async claimRun(input) {
13081
+ const maxRunning = positiveLimit(input.maxRunningPerTenant);
13082
+ const sources = input.sources === void 0 ? ["run_api"] : [...input.sources];
13083
+ (0, import_core10.assertQueueSources)(sources);
13084
+ if (maxRunning !== void 0) {
13085
+ return this.claimRunWithTenantCap(input, maxRunning, sources);
13086
+ }
13087
+ try {
13088
+ const result = await this.pool.query(
13089
+ CLAIM_RUN_SQL,
13090
+ [
13091
+ input.runId,
13092
+ input.owner,
13093
+ input.leaseMs,
13094
+ input.inputMessageIds ?? null,
13095
+ sources
13096
+ ]
13097
+ );
13098
+ return result.rowCount === 0 ? void 0 : rowToRecord(result.rows[0]);
13099
+ } catch (error) {
13100
+ if (isActiveRunViolation(error)) return void 0;
13101
+ throw error;
13102
+ }
13103
+ }
13104
+ /**
13105
+ * Claims under a per-tenant running cap.
13106
+ *
13107
+ * @remarks Serializes claims for one tenant with a transaction-scoped
13108
+ * advisory lock, counts running rows, then runs the same claim statement.
13109
+ * The lock is shared with admission creates so cap checks cannot race.
13110
+ */
13111
+ async claimRunWithTenantCap(input, maxRunning, sources) {
13112
+ const client = await this.pool.connect();
13113
+ try {
13114
+ await client.query("BEGIN");
13115
+ const candidate = await client.query(
13116
+ `SELECT tenant_id FROM lattice_agent_runs
13117
+ WHERE run_id = $1 AND source = ANY($2::text[]) AND status = 'pending'`,
13118
+ [input.runId, sources]
13119
+ );
13120
+ if (candidate.rowCount === 0) {
13121
+ await client.query("COMMIT");
13122
+ return void 0;
13123
+ }
13124
+ const tenantId = candidate.rows[0].tenant_id;
13125
+ await client.query("SELECT pg_advisory_xact_lock(hashtext($1))", [
13126
+ tenantId
13127
+ ]);
13128
+ const running = await client.query(
13129
+ `SELECT count(*)::text AS count
13130
+ FROM lattice_agent_runs
13131
+ WHERE tenant_id = $1
13132
+ AND source = ANY($2::text[])
13133
+ AND status = 'running'`,
13134
+ [tenantId, sources]
13135
+ );
13136
+ if (Number(running.rows[0].count) >= maxRunning) {
13137
+ await client.query("COMMIT");
13138
+ return void 0;
13139
+ }
13140
+ const result = await client.query(
13141
+ CLAIM_RUN_SQL,
13142
+ [
13143
+ input.runId,
13144
+ input.owner,
13145
+ input.leaseMs,
13146
+ input.inputMessageIds ?? null,
13147
+ sources
13148
+ ]
13149
+ );
13150
+ await client.query("COMMIT");
13151
+ return result.rowCount === 0 ? void 0 : rowToRecord(result.rows[0]);
13152
+ } catch (error) {
13153
+ await client.query("ROLLBACK");
13154
+ if (isActiveRunViolation(error)) return void 0;
13155
+ throw error;
13156
+ } finally {
13157
+ client.release();
13158
+ }
13159
+ }
13160
+ /** Captures the checkpoint a running run-queue row started from. */
13161
+ async setBaseCheckpoint(tenantId, runId, owner, baseCheckpointId) {
13162
+ const result = await this.pool.query(
13163
+ `UPDATE lattice_agent_runs
13164
+ SET base_checkpoint_id = $4,
13165
+ base_checkpoint_captured = TRUE,
13166
+ updated_at = now()
13167
+ WHERE tenant_id = $1 AND run_id = $2 AND owner = $3
13168
+ AND status = 'running'
13169
+ AND source IN ('run_api', 'project_task')
13170
+ RETURNING run_id`,
13171
+ [tenantId, runId, owner, baseCheckpointId]
13172
+ );
13173
+ return (result.rowCount ?? 0) > 0;
13174
+ }
13175
+ /** Records the resolved capability runtime revision for a running run. */
13176
+ async setCapabilityRevision(tenantId, runId, owner, revision) {
13177
+ const result = await this.pool.query(
13178
+ `UPDATE lattice_agent_runs
13179
+ SET capability_runtime_revision = $4, updated_at = now()
13180
+ WHERE tenant_id = $1 AND run_id = $2 AND owner = $3
13181
+ AND status = 'running'
13182
+ AND source IN ('run_api', 'project_task')
13183
+ RETURNING run_id`,
13184
+ [tenantId, runId, owner, revision]
13185
+ );
13186
+ return (result.rowCount ?? 0) > 0;
13187
+ }
13188
+ /** Deletes a run row immediately, optionally guarded by status/owner. */
13189
+ async deleteRun(tenantId, runId, expected) {
13190
+ const params = [tenantId, runId];
13191
+ let predicate = "tenant_id = $1 AND run_id = $2";
13192
+ if (expected?.status !== void 0) {
13193
+ params.push(expected.status);
13194
+ predicate += ` AND status = $${params.length}`;
13195
+ }
13196
+ if (expected?.owner !== void 0) {
13197
+ params.push(expected.owner);
13198
+ predicate += ` AND owner = $${params.length}`;
13199
+ }
13200
+ const result = await this.pool.query(
13201
+ `DELETE FROM lattice_agent_runs WHERE ${predicate}`,
13202
+ params
13203
+ );
13204
+ return (result.rowCount ?? 0) > 0;
13205
+ }
13206
+ /** Deletes terminal rows finished before `cutoffMs`, oldest first. */
13207
+ async deleteTerminalBefore(cutoffMs, limit) {
13208
+ if (!Number.isFinite(cutoffMs)) return 0;
13209
+ const max = Number.isFinite(limit) ? Math.floor(limit) : 0;
13210
+ if (max <= 0) return 0;
13211
+ const result = await this.pool.query(
13212
+ `DELETE FROM lattice_agent_runs
13213
+ WHERE run_id IN (
13214
+ SELECT run_id FROM lattice_agent_runs
13215
+ WHERE status IN ('success', 'error', 'interrupted', 'cancelled', 'timeout')
13216
+ AND finished_at < to_timestamp($1::double precision / 1000.0)
13217
+ ORDER BY finished_at ASC
13218
+ LIMIT $2
13219
+ )`,
13220
+ [cutoffMs, max]
13221
+ );
13222
+ return result.rowCount ?? 0;
13223
+ }
13224
+ /** Extends a running run's lease when `owner` still holds it. */
13225
+ async heartbeatRun(runId, owner, leaseMs) {
13226
+ const result = await this.pool.query(
13227
+ `UPDATE lattice_agent_runs
13228
+ SET lease_expires_at = now() + ($3::bigint || ' milliseconds')::interval,
13229
+ updated_at = now()
13230
+ WHERE run_id = $1 AND owner = $2 AND status = 'running'
13231
+ RETURNING run_id`,
13232
+ [runId, owner, leaseMs]
13233
+ );
13234
+ return (result.rowCount ?? 0) > 0;
13235
+ }
13236
+ /** Settles a running run when `owner` still holds it. */
13237
+ async settleRun(runId, owner, status, error) {
13238
+ const result = await this.pool.query(
13239
+ `UPDATE lattice_agent_runs
13240
+ SET status = $3, error = $4, finished_at = now(), updated_at = now()
13241
+ WHERE run_id = $1 AND owner = $2 AND status = 'running'
13242
+ RETURNING run_id`,
13243
+ [runId, owner, status, error ?? null]
13244
+ );
13245
+ return (result.rowCount ?? 0) > 0;
13246
+ }
13247
+ /** Settles a running run only when no rollback is pending. */
13248
+ async settleRunUnlessRollbackRequested(runId, owner, status, error) {
13249
+ const result = await this.pool.query(
13250
+ `UPDATE lattice_agent_runs
13251
+ SET status = $3, error = $4, finished_at = now(), updated_at = now()
13252
+ WHERE run_id = $1 AND owner = $2 AND status = 'running'
13253
+ AND rollback_requested = FALSE
13254
+ RETURNING run_id`,
13255
+ [runId, owner, status, error ?? null]
13256
+ );
13257
+ return (result.rowCount ?? 0) > 0;
13258
+ }
13259
+ /** Terminates an uncaptured rollback run with a repair signal. */
13260
+ async settleRollbackFailure(tenantId, runId, error) {
13261
+ const result = await this.pool.query(
13262
+ `UPDATE lattice_agent_runs
13263
+ SET status = 'error',
13264
+ error = $3,
13265
+ rollback_requested = FALSE,
13266
+ rollback_error = $3,
13267
+ finished_at = now(),
13268
+ updated_at = now()
13269
+ WHERE tenant_id = $1 AND run_id = $2
13270
+ AND source = 'run_api'
13271
+ AND rollback_requested = TRUE
13272
+ AND base_checkpoint_captured = FALSE
13273
+ AND status IN ('running', 'interrupted')
13274
+ RETURNING run_id`,
13275
+ [tenantId, runId, error]
13276
+ );
13277
+ return (result.rowCount ?? 0) > 0;
13278
+ }
13279
+ /** Records one bounded rollback failure and optionally converges. */
13280
+ async recordRollbackFailure(tenantId, runId, error, maxAttempts) {
13281
+ const result = await this.pool.query(
13282
+ `UPDATE lattice_agent_runs
13283
+ SET rollback_attempts = rollback_attempts + 1,
13284
+ rollback_error = $3,
13285
+ status = CASE
13286
+ WHEN rollback_attempts + 1 >= $4 THEN 'error'
13287
+ ELSE status
13288
+ END,
13289
+ error = CASE
13290
+ WHEN rollback_attempts + 1 >= $4 THEN $3
13291
+ ELSE error
13292
+ END,
13293
+ rollback_requested = CASE
13294
+ WHEN rollback_attempts + 1 >= $4 THEN FALSE
13295
+ ELSE rollback_requested
13296
+ END,
13297
+ finished_at = CASE
13298
+ WHEN rollback_attempts + 1 >= $4 THEN now()
13299
+ ELSE finished_at
13300
+ END,
13301
+ updated_at = now()
13302
+ WHERE tenant_id = $1
13303
+ AND run_id = $2
13304
+ AND source = 'run_api'
13305
+ AND rollback_requested = TRUE
13306
+ AND status IN ('running', 'interrupted')
13307
+ RETURNING rollback_attempts, status`,
13308
+ [tenantId, runId, error, maxAttempts]
13309
+ );
13310
+ if (result.rowCount === 0) return void 0;
13311
+ const row = result.rows[0];
13312
+ return {
13313
+ attempts: row.rollback_attempts,
13314
+ terminal: row.status === "error"
13315
+ };
13316
+ }
13317
+ /** Reads a run within a tenant. */
13318
+ async getRun(tenantId, runId) {
13319
+ const result = await this.pool.query(
13320
+ `SELECT * FROM lattice_agent_runs WHERE tenant_id = $1 AND run_id = $2`,
13321
+ [tenantId, runId]
13322
+ );
13323
+ return result.rowCount === 0 ? void 0 : rowToRecord(result.rows[0]);
13324
+ }
13325
+ /** Finds the active (pending/running) run for a ProjectTask. */
13326
+ async getActiveRunForTask(tenantId, taskId) {
13327
+ const result = await this.pool.query(
13328
+ `SELECT * FROM lattice_agent_runs
13329
+ WHERE tenant_id = $1 AND task_id = $2
13330
+ AND status IN ('pending', 'running')
13331
+ ORDER BY created_at DESC, run_id DESC
13332
+ LIMIT 1`,
13333
+ [tenantId, taskId]
13334
+ );
13335
+ return result.rowCount === 0 ? void 0 : rowToRecord(result.rows[0]);
13336
+ }
13337
+ /** Lists a thread's runs, newest first (including pending rows). */
13338
+ async listRuns(tenantId, threadId, options) {
13339
+ const limit = clampLimit(options?.limit);
13340
+ const offset = Math.max(0, Math.floor(options?.offset ?? 0));
13341
+ const result = await this.pool.query(
13342
+ `SELECT * FROM lattice_agent_runs
13343
+ WHERE tenant_id = $1 AND thread_id = $2
13344
+ ORDER BY created_at DESC
13345
+ LIMIT $3 OFFSET $4`,
13346
+ [tenantId, threadId, limit, offset]
13347
+ );
13348
+ return result.rows.map(rowToRecord);
13349
+ }
13350
+ /** Finds the thread's running run, if any. */
13351
+ async findRunning(tenantId, threadId) {
13352
+ const result = await this.pool.query(
13353
+ `SELECT * FROM lattice_agent_runs
13354
+ WHERE tenant_id = $1 AND thread_id = $2 AND status = 'running'
13355
+ LIMIT 1`,
13356
+ [tenantId, threadId]
13357
+ );
13358
+ return result.rowCount === 0 ? void 0 : rowToRecord(result.rows[0]);
13359
+ }
13360
+ /** Requests cancellation, settling pending/expired rows interrupted. */
13361
+ async requestCancel(tenantId, runId) {
13362
+ const client = await this.pool.connect();
13363
+ try {
13364
+ await client.query("BEGIN");
13365
+ const current = await client.query(
13366
+ `SELECT status,
13367
+ COALESCE(lease_expires_at < now(), TRUE) AS expired
13368
+ FROM lattice_agent_runs
13369
+ WHERE tenant_id = $1 AND run_id = $2
13370
+ FOR UPDATE`,
13371
+ [tenantId, runId]
13372
+ );
13373
+ if (current.rowCount === 0) {
13374
+ await client.query("COMMIT");
13375
+ return "not_found";
13376
+ }
13377
+ const row = current.rows[0];
13378
+ if (row.status === "pending") {
13379
+ await client.query(
13380
+ `UPDATE lattice_agent_runs
13381
+ SET status = 'interrupted', cancel_requested = TRUE,
13382
+ finished_at = now(), updated_at = now()
13383
+ WHERE tenant_id = $1 AND run_id = $2`,
13384
+ [tenantId, runId]
13385
+ );
13386
+ await client.query("COMMIT");
13387
+ return "cancelled";
13388
+ }
13389
+ if (row.status !== "running") {
13390
+ await client.query("COMMIT");
13391
+ return "terminal";
13392
+ }
13393
+ if (row.expired) {
13394
+ await client.query(
13395
+ `UPDATE lattice_agent_runs
13396
+ SET status = 'interrupted', cancel_requested = TRUE,
13397
+ finished_at = now(), updated_at = now()
13398
+ WHERE tenant_id = $1 AND run_id = $2`,
13399
+ [tenantId, runId]
13400
+ );
13401
+ await client.query("COMMIT");
13402
+ return "cancelled";
13403
+ }
13404
+ await client.query(
13405
+ `UPDATE lattice_agent_runs
13406
+ SET cancel_requested = TRUE, updated_at = now()
13407
+ WHERE tenant_id = $1 AND run_id = $2`,
13408
+ [tenantId, runId]
13409
+ );
13410
+ await client.query("COMMIT");
13411
+ return "requested";
13412
+ } catch (error) {
13413
+ await client.query("ROLLBACK");
13414
+ throw error;
13415
+ } finally {
13416
+ client.release();
13417
+ }
13418
+ }
13419
+ /** Deletes a pending run or flags a running run for rollback. */
13420
+ async requestRollback(tenantId, runId) {
13421
+ const client = await this.pool.connect();
13422
+ try {
13423
+ await client.query("BEGIN");
13424
+ const current = await client.query(
13425
+ `SELECT status, source FROM lattice_agent_runs
13426
+ WHERE tenant_id = $1 AND run_id = $2
13427
+ FOR UPDATE`,
13428
+ [tenantId, runId]
13429
+ );
13430
+ if (current.rowCount === 0) {
13431
+ await client.query("COMMIT");
13432
+ return "not_found";
13433
+ }
13434
+ const { status, source } = current.rows[0];
13435
+ if (source !== "run_api") {
13436
+ await client.query("COMMIT");
13437
+ return "terminal";
13438
+ }
13439
+ if (status === "pending") {
13440
+ await client.query(
13441
+ `DELETE FROM lattice_agent_runs WHERE tenant_id = $1 AND run_id = $2`,
13442
+ [tenantId, runId]
13443
+ );
13444
+ await client.query("COMMIT");
13445
+ return "deleted";
13446
+ }
13447
+ if (status !== "running") {
13448
+ await client.query("COMMIT");
13449
+ return "terminal";
13450
+ }
13451
+ await client.query(
13452
+ `UPDATE lattice_agent_runs
13453
+ SET rollback_requested = TRUE, cancel_requested = TRUE,
13454
+ updated_at = now()
13455
+ WHERE tenant_id = $1 AND run_id = $2`,
13456
+ [tenantId, runId]
13457
+ );
13458
+ await client.query("COMMIT");
13459
+ return "requested";
13460
+ } catch (error) {
13461
+ await client.query("ROLLBACK");
13462
+ throw error;
13463
+ } finally {
13464
+ client.release();
13465
+ }
13466
+ }
13467
+ /** Atomically reclaims an expired running row without requeueing it. */
13468
+ async recoverExpiredRun(runId, owner, leaseMs) {
13469
+ const result = await this.pool.query(
13470
+ `UPDATE lattice_agent_runs
13471
+ SET owner = $2,
13472
+ lease_expires_at = now() + ($3::bigint || ' milliseconds')::interval,
13473
+ attempts = attempts + 1,
13474
+ updated_at = now()
13475
+ WHERE run_id = $1
13476
+ AND source IN ('run_api', 'project_task')
13477
+ AND status = 'running'
13478
+ AND lease_expires_at < now()
13479
+ RETURNING *`,
13480
+ [runId, owner, leaseMs]
13481
+ );
13482
+ return result.rowCount === 0 ? void 0 : rowToRecord(result.rows[0]);
13483
+ }
13484
+ /** Atomically requeues or interrupts an expired run-queue row. */
13485
+ async settleExpiredRun(runId, outcome, options) {
13486
+ if (outcome === "pending") {
13487
+ const availableAt = options?.availableAt ?? null;
13488
+ const result2 = await this.pool.query(
13489
+ `UPDATE lattice_agent_runs
13490
+ SET status = 'pending',
13491
+ owner = NULL,
13492
+ lease_expires_at = NULL,
13493
+ started_at = NULL,
13494
+ available_at = COALESCE(
13495
+ to_timestamp($2::double precision / 1000.0),
13496
+ date_trunc('milliseconds', now())
13497
+ ),
13498
+ finished_at = NULL,
13499
+ updated_at = now()
13500
+ WHERE run_id = $1
13501
+ AND source IN ('run_api', 'project_task')
13502
+ AND status = 'running'
13503
+ AND lease_expires_at < now()
13504
+ RETURNING run_id`,
13505
+ [runId, availableAt]
13506
+ );
13507
+ return (result2.rowCount ?? 0) > 0;
13508
+ }
13509
+ const result = await this.pool.query(
13510
+ `UPDATE lattice_agent_runs
13511
+ SET status = $3,
13512
+ error = COALESCE($2, error),
13513
+ finished_at = now(),
13514
+ updated_at = now()
13515
+ WHERE run_id = $1
13516
+ AND source IN ('run_api', 'project_task')
13517
+ AND status = 'running'
13518
+ AND lease_expires_at < now()
13519
+ RETURNING run_id`,
13520
+ [
13521
+ runId,
13522
+ options?.error ?? null,
13523
+ outcome === "timeout" ? "timeout" : "interrupted"
13524
+ ]
13525
+ );
13526
+ return (result.rowCount ?? 0) > 0;
13527
+ }
13528
+ /** Counts runs grouped by lifecycle status. */
13529
+ async counts() {
13530
+ const counts = {
13531
+ pending: 0,
13532
+ running: 0,
13533
+ success: 0,
13534
+ error: 0,
13535
+ interrupted: 0,
13536
+ cancelled: 0,
13537
+ timeout: 0
13538
+ };
13539
+ const result = await this.pool.query(
13540
+ `SELECT status, count(*)::int AS count
13541
+ FROM lattice_agent_runs
13542
+ GROUP BY status`
13543
+ );
13544
+ for (const row of result.rows) {
13545
+ const status = parseStatus(row.status);
13546
+ if (status) counts[status] = row.count;
13547
+ }
13548
+ return counts;
13549
+ }
13550
+ /** Reports expired run-queue rows for the requested sources. */
13551
+ async reapExpired(sources = ["run_api"]) {
13552
+ (0, import_core10.assertQueueSources)(sources);
13553
+ const result = await this.pool.query(
13554
+ `SELECT * FROM lattice_agent_runs
13555
+ WHERE source = ANY($1::text[]) AND status = 'running'
13556
+ AND lease_expires_at < now()
13557
+ ORDER BY lease_expires_at ASC`,
13558
+ [[...sources]]
13559
+ );
13560
+ return result.rows.map(reapedFromRow);
13561
+ }
13562
+ /** Lists rollback work that still needs a checkpoint rewind. */
13563
+ async listRollbackRuns() {
13564
+ const result = await this.pool.query(
13565
+ `SELECT * FROM lattice_agent_runs
13566
+ WHERE source = 'run_api'
13567
+ AND rollback_requested = TRUE
13568
+ AND (
13569
+ status = 'interrupted'
13570
+ OR (status = 'running' AND lease_expires_at < now())
13571
+ )
13572
+ ORDER BY updated_at ASC`
13573
+ );
13574
+ return result.rows.map(reapedFromRow);
13575
+ }
13576
+ /** Settles expired legacy rows, preserving the historical supervisor behavior. */
13577
+ async reapLegacyExpired() {
13578
+ const result = await this.pool.query(
13579
+ `UPDATE lattice_agent_runs
13580
+ SET status = 'interrupted',
13581
+ error = COALESCE(error, 'run owner lost'),
13582
+ finished_at = now(),
13583
+ updated_at = now()
13584
+ WHERE source = 'legacy' AND status = 'running'
13585
+ AND lease_expires_at < now()
13586
+ RETURNING run_id, tenant_id, assistant_id, thread_id, workspace_id, project_id`
13587
+ );
13588
+ return result.rows.map((row) => ({
13589
+ runId: row.run_id,
13590
+ tenantId: row.tenant_id,
13591
+ assistantId: row.assistant_id,
13592
+ threadId: row.thread_id,
13593
+ workspaceId: row.workspace_id ?? void 0,
13594
+ projectId: row.project_id ?? void 0
13595
+ }));
13596
+ }
13597
+ };
13598
+ function positiveLimit(limit) {
13599
+ return limit !== void 0 && Number.isSafeInteger(limit) && limit > 0 ? limit : void 0;
13600
+ }
13601
+ function clampLimit(limit) {
13602
+ if (limit === void 0) return DEFAULT_LIST_LIMIT;
13603
+ const floored = Math.floor(limit);
13604
+ if (!Number.isFinite(floored)) return DEFAULT_LIST_LIMIT;
13605
+ return Math.min(MAX_LIST_LIMIT, Math.max(1, floored));
13606
+ }
13607
+ function jsonParam(value) {
13608
+ return value === void 0 || value === null ? null : JSON.stringify(value);
13609
+ }
13610
+ function parseStatus(value) {
13611
+ switch (value) {
13612
+ case "pending":
13613
+ case "running":
13614
+ case "success":
13615
+ case "error":
13616
+ case "interrupted":
13617
+ case "cancelled":
13618
+ case "timeout":
13619
+ return value;
13620
+ default:
13621
+ return void 0;
13622
+ }
13623
+ }
13624
+ function parseSource(value) {
13625
+ switch (value) {
13626
+ case "run_api":
13627
+ return "run_api";
13628
+ case "project_task":
13629
+ return "project_task";
13630
+ default:
13631
+ return "legacy";
13632
+ }
13633
+ }
13634
+ function toJsonObject(value) {
13635
+ if (value === null || value === void 0) return null;
13636
+ if (typeof value !== "object" || Array.isArray(value)) return null;
13637
+ return value;
13638
+ }
13639
+ function rowToRecord(row) {
13640
+ return {
13641
+ runId: row.run_id,
13642
+ tenantId: row.tenant_id,
13643
+ workspaceId: row.workspace_id ?? void 0,
13644
+ projectId: row.project_id ?? void 0,
13645
+ assistantId: row.assistant_id,
13646
+ threadId: row.thread_id,
13647
+ status: parseStatus(row.status) ?? "error",
13648
+ source: parseSource(row.source),
13649
+ taskId: row.task_id ?? void 0,
13650
+ owner: row.owner,
13651
+ leaseExpiresAt: row.lease_expires_at === null ? null : row.lease_expires_at.getTime(),
13652
+ inputMessageIds: row.input_message_ids,
13653
+ attempts: row.attempts,
13654
+ error: row.error,
13655
+ cancelRequested: row.cancel_requested,
13656
+ rollbackRequested: row.rollback_requested,
13657
+ rollbackAttempts: row.rollback_attempts,
13658
+ rollbackError: row.rollback_error,
13659
+ input: toJsonObject(row.input),
13660
+ command: toJsonObject(row.command),
13661
+ config: toJsonObject(row.config),
13662
+ metadata: toJsonObject(row.metadata),
13663
+ baseCheckpointId: row.base_checkpoint_id,
13664
+ baseCheckpointCaptured: row.base_checkpoint_captured,
13665
+ capabilityRuntimeRevision: row.capability_runtime_revision,
13666
+ availableAt: row.available_at.getTime(),
13667
+ createdAt: row.created_at.getTime(),
13668
+ updatedAt: row.updated_at.getTime(),
13669
+ startedAt: row.started_at === null ? null : row.started_at.getTime(),
13670
+ finishedAt: row.finished_at === null ? null : row.finished_at.getTime()
13671
+ };
13672
+ }
13673
+ function reapedFromRow(row) {
13674
+ return {
13675
+ runId: row.run_id,
13676
+ tenantId: row.tenant_id,
13677
+ assistantId: row.assistant_id,
13678
+ threadId: row.thread_id,
13679
+ workspaceId: row.workspace_id ?? void 0,
13680
+ projectId: row.project_id ?? void 0,
13681
+ source: parseSource(row.source),
13682
+ taskId: row.task_id ?? void 0,
13683
+ attempts: row.attempts,
13684
+ cancelRequested: row.cancel_requested,
13685
+ rollbackRequested: row.rollback_requested,
13686
+ baseCheckpointId: row.base_checkpoint_id,
13687
+ createdAt: row.created_at.getTime(),
13688
+ startedAt: row.started_at === null ? null : row.started_at.getTime(),
13689
+ leaseExpiresAt: row.lease_expires_at === null ? null : row.lease_expires_at.getTime()
13690
+ };
13691
+ }
13692
+ function isActiveRunViolation(error) {
13693
+ if (typeof error !== "object" || error === null) return false;
13694
+ const { code, constraint } = error;
13695
+ return code === "23505" && constraint === "uq_lattice_agent_runs_one_running";
13696
+ }
13697
+
13698
+ // src/stores/PostgreSQLChannelReplyJobStore.ts
13699
+ var import_pg31 = require("pg");
13700
+ var import_protocols6 = require("@axiom-lattice/protocols");
13701
+ var DB_CLOCK_MS2 = "FLOOR(EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint";
13702
+ var COLUMNS = `run_id, tenant_id, assistant_id, thread_id, adapter_channel,
13703
+ channel_installation_id, reply_target, input_message_id, stream_cursor, status,
13704
+ attempts, available_at, lease_owner, lease_expires_at, last_error,
13705
+ output_message_id, created_at, updated_at, delivered_at`;
13706
+ function toNumber(value) {
13707
+ if (typeof value === "number") return value;
13708
+ if (typeof value === "string") return Number(value);
13709
+ throw new Error("Invalid channel reply job numeric column");
13710
+ }
13711
+ function toNullableNumber(value) {
13712
+ return value === null ? null : toNumber(value);
13713
+ }
13714
+ function toStatus(value) {
13715
+ if (value === "pending" || value === "delivered" || value === "failed") {
13716
+ return value;
13717
+ }
13718
+ throw new Error(`Invalid channel reply job status: ${value}`);
13719
+ }
13720
+ function mapRow9(row) {
13721
+ return {
13722
+ runId: row.run_id,
13723
+ tenantId: row.tenant_id,
13724
+ assistantId: row.assistant_id,
13725
+ threadId: row.thread_id,
13726
+ adapterChannel: row.adapter_channel,
13727
+ channelInstallationId: row.channel_installation_id,
13728
+ replyTarget: row.reply_target,
13729
+ inputMessageId: row.input_message_id,
13730
+ streamCursor: row.stream_cursor,
13731
+ status: toStatus(row.status),
13732
+ attempts: toNumber(row.attempts),
13733
+ availableAt: toNumber(row.available_at),
13734
+ leaseOwner: row.lease_owner,
13735
+ leaseExpiresAt: toNullableNumber(row.lease_expires_at),
13736
+ lastError: row.last_error,
13737
+ outputMessageId: row.output_message_id,
13738
+ createdAt: toNumber(row.created_at),
13739
+ updatedAt: toNumber(row.updated_at),
13740
+ deliveredAt: toNullableNumber(row.delivered_at)
13741
+ };
13742
+ }
13743
+ var PostgreSQLChannelReplyJobStore = class {
13744
+ /** @param options An externally managed pool, or a pool config/connection URL. */
13745
+ constructor(options) {
13746
+ this.ownsPool = true;
13747
+ this.initialized = false;
13748
+ this.initPromise = null;
13749
+ if (options.pool) {
13750
+ this.pool = options.pool;
13751
+ this.ownsPool = false;
13752
+ this.initialized = true;
13753
+ return;
13754
+ }
13755
+ if (typeof options.poolConfig === "string") {
13756
+ this.pool = new import_pg31.Pool({ connectionString: options.poolConfig });
13757
+ } else if (options.poolConfig) {
13758
+ this.pool = new import_pg31.Pool(options.poolConfig);
13759
+ } else {
13760
+ throw new Error("Either pool or poolConfig must be provided");
13761
+ }
13762
+ this.migrationManager = new MigrationManager(this.pool);
13763
+ this.migrationManager.register(createChannelReplyJobsTable);
13764
+ if (options.autoMigrate !== false) {
13765
+ this.initialize().catch((error) => {
13766
+ console.error("Failed to initialize PostgreSQLChannelReplyJobStore:", error);
13767
+ throw error;
13768
+ });
13769
+ }
13770
+ }
13771
+ /** Runs pending migrations once. */
13772
+ async initialize() {
13773
+ if (this.initialized) return;
13774
+ if (this.initPromise) return this.initPromise;
13775
+ this.initPromise = (async () => {
13776
+ try {
13777
+ await this.migrationManager.migrate();
13778
+ this.initialized = true;
13779
+ } finally {
13780
+ this.initPromise = null;
13781
+ }
13782
+ })();
13783
+ return this.initPromise;
13784
+ }
13785
+ /** Ends the pool when this store owns it. */
13786
+ async dispose() {
13787
+ if (this.ownsPool) await this.pool.end();
13788
+ }
13789
+ /** Creates a pending job with database-clock created/updated timestamps. */
13790
+ async create(input) {
13791
+ (0, import_protocols6.assertValidCreateChannelReplyJobInput)(input);
13792
+ const result = await this.pool.query(
13793
+ `WITH clock AS (SELECT ${DB_CLOCK_MS2} AS now_ms)
13794
+ INSERT INTO lattice_channel_reply_jobs (
13795
+ run_id, tenant_id, assistant_id, thread_id, adapter_channel,
13796
+ channel_installation_id, reply_target, input_message_id, stream_cursor,
13797
+ status, attempts, available_at, lease_owner, lease_expires_at,
13798
+ last_error, output_message_id, created_at, updated_at, delivered_at
13799
+ )
13800
+ SELECT
13801
+ $1, $2, $3, $4, $5,
13802
+ $6, $7::jsonb, $8, $9,
13803
+ 'pending', 0, $10, NULL, NULL,
13804
+ NULL, NULL, clock.now_ms, clock.now_ms, NULL
13805
+ FROM clock
13806
+ RETURNING ${COLUMNS}`,
13807
+ [
13808
+ input.runId,
13809
+ input.tenantId,
13810
+ input.assistantId,
13811
+ input.threadId,
13812
+ input.adapterChannel,
13813
+ input.channelInstallationId,
13814
+ JSON.stringify(input.replyTarget),
13815
+ input.inputMessageId,
13816
+ input.streamCursor ?? null,
13817
+ input.availableAt
13818
+ ]
13819
+ );
13820
+ return mapRow9(result.rows[0]);
13821
+ }
13822
+ /** Reads one job within a tenant scope. */
13823
+ async get(tenantId, runId) {
13824
+ const result = await this.pool.query(
13825
+ `SELECT ${COLUMNS}
13826
+ FROM lattice_channel_reply_jobs
13827
+ WHERE tenant_id = $1 AND run_id = $2`,
13828
+ [tenantId, runId]
13829
+ );
13830
+ return result.rowCount === 0 ? null : mapRow9(result.rows[0]);
13831
+ }
13832
+ /** Lists claimable pending jobs that are due at `now`, oldest first. */
13833
+ async listPending(now, limit) {
13834
+ const result = await this.pool.query(
13835
+ `SELECT ${COLUMNS}
13836
+ FROM lattice_channel_reply_jobs
13837
+ WHERE status = 'pending'
13838
+ AND available_at <= $1
13839
+ AND (lease_owner IS NULL OR lease_expires_at IS NULL OR lease_expires_at <= $1)
13840
+ ORDER BY available_at ASC, created_at ASC, run_id ASC
13841
+ LIMIT $2`,
13842
+ [now, (0, import_protocols6.normalizeChannelReplyJobListLimit)(limit)]
13843
+ );
13844
+ return result.rows.map(mapRow9);
13845
+ }
13846
+ /**
13847
+ * Atomically leases a claimable job.
13848
+ *
13849
+ * @param now Accepted for interface symmetry; PostgreSQL uses the database clock.
13850
+ */
13851
+ async claim(tenantId, runId, owner, leaseMs, now) {
13852
+ void now;
13853
+ const result = await this.pool.query(
13854
+ `WITH clock AS (SELECT ${DB_CLOCK_MS2} AS now_ms)
13855
+ UPDATE lattice_channel_reply_jobs AS j
13856
+ SET lease_owner = $3,
13857
+ lease_expires_at = clock.now_ms + $4,
13858
+ updated_at = clock.now_ms
13859
+ FROM clock
13860
+ WHERE j.tenant_id = $1
13861
+ AND j.run_id = $2
13862
+ AND j.status = 'pending'
13863
+ AND j.available_at <= clock.now_ms
13864
+ AND (j.lease_owner IS NULL OR j.lease_expires_at IS NULL OR j.lease_expires_at <= clock.now_ms)
13865
+ RETURNING ${COLUMNS}`,
13866
+ [tenantId, runId, owner, leaseMs]
13867
+ );
13868
+ return result.rowCount === 0 ? null : mapRow9(result.rows[0]);
13869
+ }
13870
+ /**
13871
+ * Marks a job delivered when `owner` still holds the lease.
13872
+ *
13873
+ * @param now Accepted for interface symmetry; PostgreSQL uses the database clock.
13874
+ */
13875
+ async markDelivered(tenantId, runId, owner, outputMessageId = null, now) {
13876
+ void now;
13877
+ const result = await this.pool.query(
13878
+ `WITH clock AS (SELECT ${DB_CLOCK_MS2} AS now_ms)
13879
+ UPDATE lattice_channel_reply_jobs AS j
13880
+ SET status = 'delivered',
13881
+ output_message_id = $4,
13882
+ lease_owner = NULL,
13883
+ lease_expires_at = NULL,
13884
+ delivered_at = clock.now_ms,
13885
+ updated_at = clock.now_ms
13886
+ FROM clock
13887
+ WHERE j.tenant_id = $1
13888
+ AND j.run_id = $2
13889
+ AND j.lease_owner = $3
13890
+ AND j.status = 'pending'
13891
+ RETURNING ${COLUMNS}`,
13892
+ [tenantId, runId, owner, outputMessageId]
13893
+ );
13894
+ return result.rowCount === 0 ? null : mapRow9(result.rows[0]);
13895
+ }
13896
+ /**
13897
+ * Returns a job to pending with an incremented attempt count.
13898
+ *
13899
+ * @param now Accepted for interface symmetry; PostgreSQL uses the database clock.
13900
+ */
13901
+ async reschedule(tenantId, runId, owner, error, availableAt, now) {
13902
+ void now;
13903
+ const result = await this.pool.query(
13904
+ `WITH clock AS (SELECT ${DB_CLOCK_MS2} AS now_ms)
13905
+ UPDATE lattice_channel_reply_jobs AS j
13906
+ SET status = 'pending',
13907
+ attempts = attempts + 1,
13908
+ lease_owner = NULL,
13909
+ lease_expires_at = NULL,
13910
+ last_error = $4,
13911
+ available_at = $5,
13912
+ updated_at = clock.now_ms
13913
+ FROM clock
13914
+ WHERE j.tenant_id = $1
13915
+ AND j.run_id = $2
13916
+ AND j.lease_owner = $3
13917
+ AND j.status = 'pending'
13918
+ RETURNING ${COLUMNS}`,
13919
+ [tenantId, runId, owner, error, availableAt]
13920
+ );
13921
+ return result.rowCount === 0 ? null : mapRow9(result.rows[0]);
13922
+ }
13923
+ /**
13924
+ * Marks a job terminally failed when `owner` still holds the lease.
13925
+ *
13926
+ * @param now Accepted for interface symmetry; PostgreSQL uses the database clock.
13927
+ */
13928
+ async markFailed(tenantId, runId, owner, error, now) {
13929
+ void now;
13930
+ const result = await this.pool.query(
13931
+ `WITH clock AS (SELECT ${DB_CLOCK_MS2} AS now_ms)
13932
+ UPDATE lattice_channel_reply_jobs AS j
13933
+ SET status = 'failed',
13934
+ last_error = $4,
13935
+ lease_owner = NULL,
13936
+ lease_expires_at = NULL,
13937
+ updated_at = clock.now_ms
13938
+ FROM clock
13939
+ WHERE j.tenant_id = $1
13940
+ AND j.run_id = $2
13941
+ AND j.lease_owner = $3
13942
+ AND j.status = 'pending'
13943
+ RETURNING ${COLUMNS}`,
13944
+ [tenantId, runId, owner, error]
13945
+ );
13946
+ return result.rowCount === 0 ? null : mapRow9(result.rows[0]);
13947
+ }
13948
+ /** Deletes one job. Returns whether a row was removed. */
13949
+ async delete(tenantId, runId) {
13950
+ const result = await this.pool.query(
13951
+ `DELETE FROM lattice_channel_reply_jobs WHERE tenant_id = $1 AND run_id = $2`,
13952
+ [tenantId, runId]
13953
+ );
13954
+ return (result.rowCount ?? 0) > 0;
13955
+ }
13956
+ /** Deletes terminal jobs whose last update is older than `before`. */
13957
+ async deleteTerminalBefore(before, limit) {
13958
+ const result = await this.pool.query(
13959
+ `DELETE FROM lattice_channel_reply_jobs
13960
+ WHERE run_id IN (
13961
+ SELECT run_id
13962
+ FROM lattice_channel_reply_jobs
13963
+ WHERE status IN ('delivered', 'failed')
13964
+ AND updated_at < $1
13965
+ ORDER BY updated_at ASC, run_id ASC
13966
+ LIMIT $2
13967
+ )`,
13968
+ [before, (0, import_protocols6.normalizeChannelReplyJobListLimit)(limit)]
13969
+ );
13970
+ return result.rowCount ?? 0;
13971
+ }
13972
+ };
13973
+
13974
+ // src/stores/PostgreSQLCheckpointStorage.ts
13975
+ var CheckpointTargetNotEmptyError = class extends Error {
13976
+ /**
13977
+ * @param threadId - Non-empty target thread id.
13978
+ */
13979
+ constructor(threadId) {
13980
+ super(`Checkpoint target thread ${threadId} is not empty`);
13981
+ /** Machine-readable conflict code. */
13982
+ this.code = "CHECKPOINT_TARGET_NOT_EMPTY";
13983
+ this.name = "CheckpointTargetNotEmptyError";
13984
+ }
13985
+ };
13986
+ var CheckpointBaseNotFoundError = class extends Error {
13987
+ /**
13988
+ * @param threadId - Thread owning the missing base checkpoint.
13989
+ * @param baseCheckpointId - Base checkpoint id that was not found.
13990
+ */
13991
+ constructor(threadId, baseCheckpointId) {
13992
+ super(`Checkpoint ${baseCheckpointId} does not exist on thread ${threadId}`);
13993
+ /** Machine-readable failure code. */
13994
+ this.code = "CHECKPOINT_BASE_NOT_FOUND";
13995
+ this.name = "CheckpointBaseNotFoundError";
13996
+ this.threadId = threadId;
13997
+ this.baseCheckpointId = baseCheckpointId;
13998
+ }
13999
+ };
14000
+ async function rollbackQuietly(client) {
14001
+ try {
14002
+ await client.query("ROLLBACK");
14003
+ } catch {
14004
+ }
14005
+ }
14006
+ function isUniqueViolation3(error) {
14007
+ return typeof error === "object" && error !== null && "code" in error && error.code === "23505";
14008
+ }
14009
+ function isRecord4(value) {
14010
+ return typeof value === "object" && value !== null && !Array.isArray(value);
14011
+ }
14012
+ function checkpointKey(checkpointNs, checkpointId) {
14013
+ return `${checkpointNs}\0${checkpointId}`;
14014
+ }
14015
+ function blobKey(checkpointNs, channel, version) {
14016
+ return `${checkpointNs}\0${channel}\0${version}`;
14017
+ }
14018
+ function checkpointRecord(value) {
14019
+ if (typeof value === "string") {
14020
+ try {
14021
+ const parsed = JSON.parse(value);
14022
+ return isRecord4(parsed) ? parsed : void 0;
14023
+ } catch {
14024
+ return void 0;
14025
+ }
14026
+ }
14027
+ return isRecord4(value) ? value : void 0;
14028
+ }
14029
+ function channelVersions(value) {
14030
+ const checkpoint = checkpointRecord(value);
14031
+ const versions = checkpoint?.channel_versions;
14032
+ return isRecord4(versions) ? versions : void 0;
14033
+ }
14034
+ var DELETE_CHUNK_SIZE = 100;
14035
+ function chunk(items, size) {
14036
+ const chunks = [];
14037
+ for (let index = 0; index < items.length; index += size) {
14038
+ chunks.push(items.slice(index, index + size));
14039
+ }
14040
+ return chunks;
14041
+ }
14042
+ function toCount(value) {
14043
+ const parsed = Number(value ?? 0);
14044
+ return Number.isFinite(parsed) ? parsed : 0;
14045
+ }
14046
+ var PostgreSQLCheckpointStorage = class {
14047
+ /**
14048
+ * @param pool - PostgreSQL pool shared with the rest of the store config.
14049
+ */
14050
+ constructor(pool) {
14051
+ this.pool = pool;
14052
+ }
14053
+ /**
14054
+ * Measure the checkpoint storage used by one thread.
14055
+ *
14056
+ * @param threadId - Thread id whose checkpoint rows should be measured.
14057
+ * @returns Counts and byte sizes grouped by checkpoint storage category.
14058
+ */
14059
+ async get(threadId) {
14060
+ const result = await this.pool.query(
14061
+ `SELECT
14062
+ (SELECT count(*)::bigint FROM checkpoints WHERE thread_id=$1) AS checkpoint_count,
14063
+ (SELECT coalesce(sum(pg_column_size(checkpoint) + pg_column_size(metadata)),0)::bigint FROM checkpoints WHERE thread_id=$1) AS checkpoint_bytes,
14064
+ (SELECT count(*)::bigint FROM checkpoint_blobs WHERE thread_id=$1) AS blob_count,
14065
+ (SELECT coalesce(sum(coalesce(octet_length(blob),0)),0)::bigint FROM checkpoint_blobs WHERE thread_id=$1) AS blob_bytes,
14066
+ (SELECT count(*)::bigint FROM checkpoint_writes WHERE thread_id=$1) AS write_count,
14067
+ (SELECT coalesce(sum(octet_length(blob)),0)::bigint FROM checkpoint_writes WHERE thread_id=$1) AS write_bytes`,
14068
+ [threadId]
14069
+ );
14070
+ const row = result.rows[0];
14071
+ const checkpointCount = toCount(row?.checkpoint_count);
14072
+ const checkpointBytes = toCount(row?.checkpoint_bytes);
14073
+ const blobCount = toCount(row?.blob_count);
14074
+ const blobBytes = toCount(row?.blob_bytes);
14075
+ const writeCount = toCount(row?.write_count);
14076
+ const writeBytes = toCount(row?.write_bytes);
14077
+ return {
14078
+ threadId,
14079
+ totalBytes: checkpointBytes + blobBytes + writeBytes,
14080
+ checkpoints: { count: checkpointCount, bytes: checkpointBytes },
14081
+ channelValues: { count: blobCount, bytes: blobBytes },
14082
+ pendingWrites: { count: writeCount, bytes: writeBytes }
14083
+ };
14084
+ }
14085
+ /**
14086
+ * Copy every checkpoint row from a source thread to an empty target thread.
14087
+ *
14088
+ * @param sourceThreadId - Thread whose rows are copied.
14089
+ * @param targetThreadId - Empty thread that receives the rows.
14090
+ * @returns Resolves when the copy commits; an empty source is a no-op.
14091
+ * @throws {CheckpointTargetNotEmptyError} When the target already owns rows.
14092
+ */
14093
+ async copyThread(sourceThreadId, targetThreadId) {
14094
+ const client = await this.pool.connect();
14095
+ try {
14096
+ await client.query("BEGIN");
14097
+ const target = await client.query(
14098
+ `SELECT (
14099
+ EXISTS (SELECT 1 FROM checkpoints WHERE thread_id=$1)
14100
+ OR EXISTS (SELECT 1 FROM checkpoint_blobs WHERE thread_id=$1)
14101
+ OR EXISTS (SELECT 1 FROM checkpoint_writes WHERE thread_id=$1)
14102
+ ) AS not_empty`,
14103
+ [targetThreadId]
14104
+ );
14105
+ if (target.rows[0]?.not_empty) {
14106
+ throw new CheckpointTargetNotEmptyError(targetThreadId);
14107
+ }
14108
+ await client.query(
14109
+ `INSERT INTO checkpoints (
14110
+ thread_id, checkpoint_ns, checkpoint_id, parent_checkpoint_id, type, checkpoint, metadata
14111
+ )
14112
+ SELECT $2, checkpoint_ns, checkpoint_id, parent_checkpoint_id, type, checkpoint,
14113
+ jsonb_set(coalesce(metadata, '{}'::jsonb), '{thread_id}', to_jsonb($2::text), true)
14114
+ FROM checkpoints WHERE thread_id=$1`,
14115
+ [sourceThreadId, targetThreadId]
14116
+ );
14117
+ await client.query(
14118
+ `INSERT INTO checkpoint_blobs (thread_id, checkpoint_ns, channel, version, type, blob)
14119
+ SELECT $2, checkpoint_ns, channel, version, type, blob
14120
+ FROM checkpoint_blobs WHERE thread_id=$1`,
14121
+ [sourceThreadId, targetThreadId]
14122
+ );
14123
+ await client.query(
14124
+ `INSERT INTO checkpoint_writes (
14125
+ thread_id, checkpoint_ns, checkpoint_id, task_id, idx, channel, type, blob
14126
+ )
14127
+ SELECT $2, checkpoint_ns, checkpoint_id, task_id, idx, channel, type, blob
14128
+ FROM checkpoint_writes WHERE thread_id=$1`,
14129
+ [sourceThreadId, targetThreadId]
14130
+ );
14131
+ await client.query("COMMIT");
14132
+ } catch (error) {
14133
+ await rollbackQuietly(client);
14134
+ if (isUniqueViolation3(error)) {
14135
+ throw new CheckpointTargetNotEmptyError(targetThreadId);
14136
+ }
14137
+ throw error;
14138
+ } finally {
14139
+ client.release();
14140
+ }
14141
+ }
14142
+ /**
14143
+ * Retain only the latest N checkpoints per checkpoint namespace.
14144
+ *
14145
+ * @param threadId - Thread whose checkpoint history is pruned.
14146
+ * @param options - Retention options.
14147
+ * @param options.keepLatest - Number of latest checkpoints to retain per namespace.
14148
+ * @returns Counts of deleted checkpoint, blob, and write rows.
14149
+ */
14150
+ async pruneThread(threadId, options) {
14151
+ if (!Number.isSafeInteger(options.keepLatest) || options.keepLatest < 1) {
14152
+ throw new RangeError("keepLatest must be an integer >= 1");
14153
+ }
14154
+ const keepLatest = options.keepLatest;
14155
+ const client = await this.pool.connect();
14156
+ try {
14157
+ await client.query("BEGIN");
14158
+ const retainedResult = await client.query(
14159
+ `SELECT checkpoint_ns, checkpoint_id, checkpoint FROM (
14160
+ SELECT checkpoint_ns, checkpoint_id, checkpoint,
14161
+ row_number() OVER (
14162
+ PARTITION BY checkpoint_ns ORDER BY checkpoint_id DESC
14163
+ ) AS rn
14164
+ FROM checkpoints WHERE thread_id=$1
14165
+ ) t WHERE rn <= $2`,
14166
+ [threadId, keepLatest]
14167
+ );
14168
+ const retained = retainedResult.rows;
14169
+ const retainedCheckpoints = new Set(
14170
+ retained.map((row) => checkpointKey(row.checkpoint_ns, row.checkpoint_id))
14171
+ );
14172
+ const fallbackNamespaces = /* @__PURE__ */ new Set();
14173
+ const retainedBlobs = /* @__PURE__ */ new Set();
14174
+ for (const row of retained) {
14175
+ const versions = channelVersions(row.checkpoint);
14176
+ if (!versions) {
14177
+ fallbackNamespaces.add(row.checkpoint_ns);
14178
+ continue;
14179
+ }
14180
+ for (const [channel, version] of Object.entries(versions)) {
14181
+ retainedBlobs.add(blobKey(row.checkpoint_ns, channel, String(version)));
14182
+ }
14183
+ }
14184
+ const writeRows = await client.query(
14185
+ `SELECT checkpoint_ns, checkpoint_id, task_id, idx
14186
+ FROM checkpoint_writes WHERE thread_id=$1`,
14187
+ [threadId]
14188
+ );
14189
+ const staleWrites = writeRows.rows.filter(
14190
+ (row) => !retainedCheckpoints.has(checkpointKey(row.checkpoint_ns, row.checkpoint_id))
14191
+ );
14192
+ let writes = 0;
14193
+ for (const batch of chunk(staleWrites, DELETE_CHUNK_SIZE)) {
14194
+ const params = [threadId];
14195
+ const tuples = batch.map((row) => {
14196
+ params.push(row.checkpoint_ns, row.checkpoint_id, row.task_id, row.idx);
14197
+ return `($${params.length - 3}, $${params.length - 2}, $${params.length - 1}, $${params.length})`;
14198
+ });
14199
+ const deleted = await client.query(
14200
+ `DELETE FROM checkpoint_writes
14201
+ WHERE thread_id=$1
14202
+ AND (checkpoint_ns, checkpoint_id, task_id, idx) IN (${tuples.join(", ")})`,
14203
+ params
14204
+ );
14205
+ writes += deleted.rowCount ?? 0;
14206
+ }
14207
+ const checkpointRows = await client.query(
14208
+ `SELECT checkpoint_ns, checkpoint_id FROM checkpoints WHERE thread_id=$1`,
14209
+ [threadId]
14210
+ );
14211
+ const staleCheckpoints = checkpointRows.rows.filter(
14212
+ (row) => !retainedCheckpoints.has(checkpointKey(row.checkpoint_ns, row.checkpoint_id))
14213
+ );
14214
+ let checkpoints = 0;
14215
+ for (const batch of chunk(staleCheckpoints, DELETE_CHUNK_SIZE)) {
14216
+ const params = [threadId];
14217
+ const tuples = batch.map((row) => {
14218
+ params.push(row.checkpoint_ns, row.checkpoint_id);
14219
+ return `($${params.length - 1}, $${params.length})`;
14220
+ });
14221
+ const deleted = await client.query(
14222
+ `DELETE FROM checkpoints
14223
+ WHERE thread_id=$1
14224
+ AND (checkpoint_ns, checkpoint_id) IN (${tuples.join(", ")})`,
14225
+ params
14226
+ );
14227
+ checkpoints += deleted.rowCount ?? 0;
14228
+ }
14229
+ const blobRows = await client.query(
14230
+ `SELECT checkpoint_ns, channel, version FROM checkpoint_blobs WHERE thread_id=$1`,
14231
+ [threadId]
14232
+ );
14233
+ const staleBlobs = blobRows.rows.filter(
14234
+ (row) => !fallbackNamespaces.has(row.checkpoint_ns) && !retainedBlobs.has(blobKey(row.checkpoint_ns, row.channel, row.version))
14235
+ );
14236
+ let blobs = 0;
14237
+ for (const batch of chunk(staleBlobs, DELETE_CHUNK_SIZE)) {
14238
+ const params = [threadId];
14239
+ const tuples = batch.map((row) => {
14240
+ params.push(row.checkpoint_ns, row.channel, row.version);
14241
+ return `($${params.length - 2}, $${params.length - 1}, $${params.length})`;
14242
+ });
14243
+ const deleted = await client.query(
14244
+ `DELETE FROM checkpoint_blobs
14245
+ WHERE thread_id=$1
14246
+ AND (checkpoint_ns, channel, version) IN (${tuples.join(", ")})`,
14247
+ params
14248
+ );
14249
+ blobs += deleted.rowCount ?? 0;
14250
+ }
14251
+ await client.query("COMMIT");
14252
+ return { checkpoints, blobs, writes };
14253
+ } catch (error) {
14254
+ await rollbackQuietly(client);
14255
+ throw error;
14256
+ } finally {
14257
+ client.release();
14258
+ }
14259
+ }
14260
+ /**
14261
+ * Delete the checkpoints that descend from a base checkpoint.
14262
+ *
14263
+ * Descendants are discovered by following both same-namespace
14264
+ * `parent_checkpoint_id` edges and cross-namespace `metadata.parents` edges
14265
+ * (LangGraph's `{ "<parent checkpoint_ns>": "<parent checkpoint id>" }` map on
14266
+ * subgraph checkpoints), recursively across every namespace. Out-of-order or
14267
+ * clock-skewed ids do not affect the result. Writes keyed to the base
14268
+ * checkpoint belong to the rolled-back run's first superstep and are deleted
14269
+ * too. Blobs referenced only by deleted checkpoints are removed.
14270
+ *
14271
+ * @param threadId - Thread whose later checkpoint history is deleted.
14272
+ * @param baseCheckpointId - Checkpoint to retain as the new tip, or `null` to
14273
+ * delete every checkpoint for the thread.
14274
+ * @returns Resolves when the deletion commits; no descendants is a no-op.
14275
+ * @throws {RangeError} When `baseCheckpointId` is an empty string.
14276
+ * @throws {CheckpointBaseNotFoundError} When the base checkpoint does not
14277
+ * exist on the thread.
14278
+ */
14279
+ async deleteAfter(threadId, baseCheckpointId) {
14280
+ if (baseCheckpointId !== null && baseCheckpointId.length === 0) {
14281
+ throw new RangeError("baseCheckpointId must be null or a non-empty checkpoint id");
14282
+ }
14283
+ const client = await this.pool.connect();
14284
+ try {
14285
+ await client.query("BEGIN");
14286
+ if (baseCheckpointId === null) {
14287
+ await client.query("DELETE FROM checkpoint_writes WHERE thread_id=$1", [threadId]);
14288
+ await client.query("DELETE FROM checkpoint_blobs WHERE thread_id=$1", [threadId]);
14289
+ await client.query("DELETE FROM checkpoints WHERE thread_id=$1", [threadId]);
14290
+ await client.query("COMMIT");
14291
+ return;
14292
+ }
14293
+ const base = await client.query(
14294
+ `SELECT checkpoint_ns
14295
+ FROM checkpoints
14296
+ WHERE thread_id=$1 AND checkpoint_id=$2`,
14297
+ [threadId, baseCheckpointId]
14298
+ );
14299
+ if (base.rows.length === 0) {
14300
+ throw new CheckpointBaseNotFoundError(threadId, baseCheckpointId);
14301
+ }
14302
+ const baseCheckpointNs = base.rows[0].checkpoint_ns;
14303
+ const descendantResult = await client.query(
14304
+ `WITH RECURSIVE lineage(checkpoint_ns, checkpoint_id) AS (
14305
+ SELECT $2::text, $3::text
14306
+ UNION
14307
+ SELECT c.checkpoint_ns, c.checkpoint_id
14308
+ FROM checkpoints c
14309
+ JOIN lineage l ON (
14310
+ (c.checkpoint_ns = l.checkpoint_ns
14311
+ AND c.parent_checkpoint_id = l.checkpoint_id)
14312
+ OR (c.metadata->'parents'->>l.checkpoint_ns = l.checkpoint_id)
14313
+ )
14314
+ WHERE c.thread_id=$1
14315
+ ),
14316
+ roots AS (
14317
+ SELECT $2::text AS checkpoint_ns, $3::text AS checkpoint_id
14318
+ )
14319
+ SELECT l.checkpoint_ns, l.checkpoint_id
14320
+ FROM lineage l
14321
+ WHERE NOT EXISTS (
14322
+ SELECT 1 FROM roots r
14323
+ WHERE r.checkpoint_ns = l.checkpoint_ns
14324
+ AND r.checkpoint_id = l.checkpoint_id
14325
+ )`,
14326
+ [threadId, baseCheckpointNs, baseCheckpointId]
14327
+ );
14328
+ const descendants = descendantResult.rows;
14329
+ await client.query(
14330
+ `DELETE FROM checkpoint_writes
14331
+ WHERE thread_id=$1 AND checkpoint_ns=$2 AND checkpoint_id=$3`,
14332
+ [threadId, baseCheckpointNs, baseCheckpointId]
14333
+ );
14334
+ for (const batch of chunk(descendants, DELETE_CHUNK_SIZE)) {
14335
+ const params = [threadId];
14336
+ const tuples = batch.map((row) => {
14337
+ params.push(row.checkpoint_ns, row.checkpoint_id);
14338
+ return `($${params.length - 1}, $${params.length})`;
14339
+ });
14340
+ await client.query(
14341
+ `DELETE FROM checkpoint_writes
14342
+ WHERE thread_id=$1
14343
+ AND (checkpoint_ns, checkpoint_id) IN (${tuples.join(", ")})`,
14344
+ params
14345
+ );
14346
+ await client.query(
14347
+ `DELETE FROM checkpoints
14348
+ WHERE thread_id=$1
14349
+ AND (checkpoint_ns, checkpoint_id) IN (${tuples.join(", ")})`,
14350
+ params
14351
+ );
14352
+ }
14353
+ const retainedResult = await client.query(
14354
+ `SELECT checkpoint_ns, checkpoint_id, checkpoint
14355
+ FROM checkpoints WHERE thread_id=$1`,
14356
+ [threadId]
14357
+ );
14358
+ const fallbackNamespaces = /* @__PURE__ */ new Set();
14359
+ const retainedBlobs = /* @__PURE__ */ new Set();
14360
+ for (const row of retainedResult.rows) {
14361
+ const versions = channelVersions(row.checkpoint);
14362
+ if (!versions) {
14363
+ fallbackNamespaces.add(row.checkpoint_ns);
14364
+ continue;
14365
+ }
14366
+ for (const [channel, version] of Object.entries(versions)) {
14367
+ retainedBlobs.add(blobKey(row.checkpoint_ns, channel, String(version)));
14368
+ }
14369
+ }
14370
+ const blobRows = await client.query(
14371
+ `SELECT checkpoint_ns, channel, version FROM checkpoint_blobs WHERE thread_id=$1`,
14372
+ [threadId]
14373
+ );
14374
+ const staleBlobs = blobRows.rows.filter(
14375
+ (row) => !fallbackNamespaces.has(row.checkpoint_ns) && !retainedBlobs.has(blobKey(row.checkpoint_ns, row.channel, row.version))
14376
+ );
14377
+ for (const batch of chunk(staleBlobs, DELETE_CHUNK_SIZE)) {
14378
+ const params = [threadId];
14379
+ const tuples = batch.map((row) => {
14380
+ params.push(row.checkpoint_ns, row.channel, row.version);
14381
+ return `($${params.length - 2}, $${params.length - 1}, $${params.length})`;
14382
+ });
14383
+ await client.query(
14384
+ `DELETE FROM checkpoint_blobs
14385
+ WHERE thread_id=$1
14386
+ AND (checkpoint_ns, channel, version) IN (${tuples.join(", ")})`,
14387
+ params
14388
+ );
14389
+ }
14390
+ await client.query("COMMIT");
14391
+ } catch (error) {
14392
+ await rollbackQuietly(client);
14393
+ throw error;
14394
+ } finally {
14395
+ client.release();
14396
+ }
14397
+ }
14398
+ /**
14399
+ * Delete every checkpoint row for a thread.
14400
+ *
14401
+ * @param threadId - Thread whose checkpoint rows are deleted.
14402
+ * @returns Resolves when the deletion commits.
14403
+ */
14404
+ async deleteThread(threadId) {
14405
+ const client = await this.pool.connect();
14406
+ try {
14407
+ await client.query("BEGIN");
14408
+ await client.query("DELETE FROM checkpoint_writes WHERE thread_id=$1", [threadId]);
14409
+ await client.query("DELETE FROM checkpoint_blobs WHERE thread_id=$1", [threadId]);
14410
+ await client.query("DELETE FROM checkpoints WHERE thread_id=$1", [threadId]);
14411
+ await client.query("COMMIT");
14412
+ } catch (error) {
14413
+ await rollbackQuietly(client);
14414
+ throw error;
14415
+ } finally {
14416
+ client.release();
14417
+ }
14418
+ }
14419
+ };
14420
+
14421
+ // src/stores/PostgreSQLStore.ts
14422
+ var import_protocols7 = require("@axiom-lattice/protocols");
14423
+ function mapRow10(row) {
14424
+ return {
14425
+ namespace: row.namespace,
14426
+ key: row.key,
14427
+ value: row.value ?? {},
14428
+ createdAt: row.created_at,
14429
+ updatedAt: row.updated_at,
14430
+ expiresAt: row.expires_at ?? void 0
14431
+ };
14432
+ }
14433
+ var PostgreSQLStore = class {
14434
+ /**
14435
+ * @param pool - Pool shared with the rest of the store config.
14436
+ */
14437
+ constructor(pool) {
14438
+ this.pool = pool;
14439
+ }
14440
+ async put(tenantId, input) {
14441
+ (0, import_protocols7.assertStoreIdentity)(input.namespace, input.key);
14442
+ const now = /* @__PURE__ */ new Date();
14443
+ const expiresAt = (0, import_protocols7.storeExpiresAt)(input.ttlMinutes, now) ?? null;
14444
+ const result = await this.pool.query(
14445
+ `INSERT INTO lattice_store_items
14446
+ (tenant_id, namespace, key, value, created_at, updated_at, expires_at)
14447
+ VALUES ($1, $2, $3, $4::jsonb, $5, $5, $6)
14448
+ ON CONFLICT (tenant_id, namespace, key) DO UPDATE SET
14449
+ value = EXCLUDED.value,
14450
+ updated_at = EXCLUDED.updated_at,
14451
+ expires_at = EXCLUDED.expires_at
14452
+ RETURNING tenant_id, namespace, key, value, created_at, updated_at, expires_at`,
14453
+ [tenantId, input.namespace, input.key, JSON.stringify(input.value), now, expiresAt]
14454
+ );
14455
+ return mapRow10(result.rows[0]);
14456
+ }
14457
+ async get(tenantId, namespace, key) {
14458
+ (0, import_protocols7.assertStoreIdentity)(namespace, key);
14459
+ const result = await this.pool.query(
14460
+ `SELECT tenant_id, namespace, key, value, created_at, updated_at, expires_at
14461
+ FROM lattice_store_items
14462
+ WHERE tenant_id = $1 AND namespace = $2 AND key = $3
14463
+ AND (expires_at IS NULL OR expires_at > $4)`,
14464
+ [tenantId, namespace, key, /* @__PURE__ */ new Date()]
14465
+ );
14466
+ return result.rows[0] ? mapRow10(result.rows[0]) : null;
14467
+ }
14468
+ async delete(tenantId, namespace, key) {
14469
+ (0, import_protocols7.assertStoreIdentity)(namespace, key);
14470
+ const result = await this.pool.query(
14471
+ `DELETE FROM lattice_store_items
14472
+ WHERE tenant_id = $1 AND namespace = $2 AND key = $3`,
14473
+ [tenantId, namespace, key]
14474
+ );
14475
+ return (result.rowCount ?? 0) > 0;
14476
+ }
14477
+ async search(tenantId, input) {
14478
+ const conditions = [
14479
+ "tenant_id = $1",
14480
+ "(expires_at IS NULL OR expires_at > $2)"
14481
+ ];
14482
+ const params = [tenantId, /* @__PURE__ */ new Date()];
14483
+ if (input.namespacePrefix) {
14484
+ (0, import_protocols7.assertStoreNamespacePrefix)(input.namespacePrefix);
14485
+ }
14486
+ if (input.namespacePrefix && input.namespacePrefix.length > 0) {
14487
+ params.push(input.namespacePrefix);
14488
+ conditions.push(`namespace[1:array_length($${params.length}::text[], 1)] = $${params.length}`);
14489
+ }
14490
+ if (input.filter !== void 0) {
14491
+ params.push(JSON.stringify(input.filter));
14492
+ conditions.push(`value @> $${params.length}::jsonb`);
14493
+ }
14494
+ if (input.query !== void 0 && input.query !== "") {
14495
+ params.push(input.query.replace(/[\\%_]/g, "\\$&"));
14496
+ conditions.push(`value::text ILIKE '%' || $${params.length} || '%' ESCAPE '\\'`);
14497
+ }
14498
+ params.push((0, import_protocols7.storePaginationValue)(input.limit, 10));
14499
+ const limitParam = params.length;
14500
+ params.push((0, import_protocols7.storePaginationValue)(input.offset, 0));
14501
+ const offsetParam = params.length;
14502
+ const result = await this.pool.query(
14503
+ `SELECT tenant_id, namespace, key, value, created_at, updated_at, expires_at
14504
+ FROM lattice_store_items
14505
+ WHERE ${conditions.join(" AND ")}
14506
+ ORDER BY updated_at DESC, namespace, key
14507
+ LIMIT $${limitParam} OFFSET $${offsetParam}`,
14508
+ params
14509
+ );
14510
+ return result.rows.map(mapRow10);
14511
+ }
14512
+ async listNamespaces(tenantId, prefix = [], limit = 100) {
14513
+ (0, import_protocols7.assertStoreNamespacePrefix)(prefix);
14514
+ const conditions = [
14515
+ "tenant_id = $1",
14516
+ "(expires_at IS NULL OR expires_at > $2)"
14517
+ ];
14518
+ const params = [tenantId, /* @__PURE__ */ new Date()];
14519
+ if (prefix.length > 0) {
14520
+ params.push(prefix);
14521
+ conditions.push(`namespace[1:array_length($${params.length}::text[], 1)] = $${params.length}`);
14522
+ }
14523
+ params.push((0, import_protocols7.storePaginationValue)(limit, 100));
14524
+ const result = await this.pool.query(
14525
+ `SELECT DISTINCT namespace
14526
+ FROM lattice_store_items
14527
+ WHERE ${conditions.join(" AND ")}
14528
+ ORDER BY namespace
14529
+ LIMIT $${params.length}`,
14530
+ params
14531
+ );
14532
+ return result.rows.map((row) => row.namespace);
14533
+ }
14534
+ async deleteExpired(now, limit) {
14535
+ const bounded = (0, import_protocols7.storePaginationValue)(limit, 0);
14536
+ const result = await this.pool.query(
14537
+ `WITH expired AS (
14538
+ SELECT ctid FROM lattice_store_items
14539
+ WHERE expires_at IS NOT NULL AND expires_at <= $1
14540
+ ORDER BY expires_at
14541
+ LIMIT $2
14542
+ )
14543
+ DELETE FROM lattice_store_items WHERE ctid IN (SELECT ctid FROM expired)`,
14544
+ [now, bounded]
14545
+ );
14546
+ return result.rowCount ?? 0;
14547
+ }
14548
+ };
14549
+
14550
+ // src/migrations/store_migrations.ts
14551
+ var createStoreItemsTable = {
14552
+ version: 195,
14553
+ name: "create_lattice_store_items_table",
14554
+ up: async (client) => {
14555
+ await client.query(`
14556
+ CREATE TABLE IF NOT EXISTS lattice_store_items (
14557
+ tenant_id TEXT NOT NULL,
14558
+ namespace TEXT[] NOT NULL,
14559
+ key TEXT NOT NULL,
14560
+ value JSONB NOT NULL,
14561
+ created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
14562
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
14563
+ expires_at TIMESTAMPTZ,
14564
+ PRIMARY KEY (tenant_id, namespace, key)
14565
+ )
14566
+ `);
14567
+ await client.query(`
14568
+ CREATE INDEX IF NOT EXISTS idx_lattice_store_items_namespace
14569
+ ON lattice_store_items USING GIN (namespace)
14570
+ `);
14571
+ await client.query(`
14572
+ CREATE INDEX IF NOT EXISTS idx_lattice_store_items_value
14573
+ ON lattice_store_items USING GIN (value)
14574
+ `);
14575
+ await client.query(`
14576
+ CREATE INDEX IF NOT EXISTS idx_lattice_store_items_expires_at
14577
+ ON lattice_store_items(expires_at)
14578
+ WHERE expires_at IS NOT NULL
14579
+ `);
14580
+ },
14581
+ down: async (client) => {
14582
+ await client.query("DROP INDEX IF EXISTS idx_lattice_store_items_expires_at");
14583
+ await client.query("DROP INDEX IF EXISTS idx_lattice_store_items_value");
14584
+ await client.query("DROP INDEX IF EXISTS idx_lattice_store_items_namespace");
14585
+ await client.query("DROP TABLE IF EXISTS lattice_store_items");
14586
+ }
14587
+ };
14588
+
14589
+ // src/createPgStoreConfig.ts
14590
+ async function createPgStoreConfig(connectionString) {
14591
+ const pool = new import_pg32.Pool({ connectionString });
14592
+ const mm = new MigrationManager(pool);
14593
+ mm.register(createThreadsTable);
14594
+ mm.register(createScheduledTasksTable);
14595
+ mm.register(createAssistantsTable);
14596
+ mm.register(createSkillsTable);
14597
+ mm.register(createWorkspacesTable);
14598
+ mm.register(createProjectsTable);
14599
+ mm.register(createUsersTable);
14600
+ mm.register(createTenantsTable);
14601
+ mm.register(addUserStatusColumn);
14602
+ mm.register(createUserTenantLinkTable);
14603
+ mm.register(migrateRemoveTenantIdFromUsers);
14604
+ mm.register(createMetricsConfigsTable);
14605
+ mm.register(createMcpServerConfigsTable);
14606
+ mm.register(addAssistantTenantId);
14607
+ mm.register(addSkillTenantId);
14608
+ mm.register(changeSkillPrimaryKey);
14609
+ mm.register(addScheduleTenantId);
14610
+ mm.register(changeThreadPrimaryKey);
14611
+ mm.register(createChannelIdentityMappingTables);
14612
+ mm.register(createChannelInstallationsTable);
14613
+ mm.register(addThreadTenantId);
14614
+ mm.register(addProjectConfigColumn);
14615
+ registerThreadMessageQueueMigrations(mm);
14616
+ mm.register(createWorkflowTrackingTables);
14617
+ for (const m of evalMigrations) {
14618
+ mm.register(m);
14619
+ }
14620
+ mm.register(alterChannelInstallationsTable);
14621
+ mm.register(createChannelBindingsTable);
14622
+ mm.register(createDatabaseConfigsTable);
14623
+ mm.register(changeAssistantPrimaryKey);
14624
+ mm.register(addStepThreadId);
14625
+ mm.register(createA2AApiKeysTable);
14626
+ mm.register(addAssistantOwnerUserId);
14627
+ for (const m of taskMigrations) {
14628
+ mm.register(m);
14629
+ }
14630
+ mm.register(createMenuItemsTable);
14631
+ mm.register(createSharedResourcesTable);
14632
+ mm.register(addFileContentType);
14633
+ mm.register(addMcpAppContentType);
14634
+ mm.register(createCollectionsTable);
12402
14635
  mm.register(createTaskWorkItemsMigration);
12403
14636
  mm.register(addWorkItemProjectFieldsMigration);
12404
14637
  mm.register(createConnectionConfigsTable);
@@ -12431,6 +14664,18 @@ async function createPgStoreConfig(connectionString) {
12431
14664
  mm.register(addModelProviderModelExtra);
12432
14665
  mm.register(addModelProviderModelDefault);
12433
14666
  mm.register(addModelProviderModelKind);
14667
+ mm.register(createAgentRunLedgerTable);
14668
+ mm.register(createAgentRunTaskQueueMigration);
14669
+ mm.register(createAgentRunCapabilityRevisionMigration);
14670
+ mm.register(createAgentRunBaseCheckpointCapturedMigration);
14671
+ mm.register(createAgentRunRollbackFailureMigration);
14672
+ mm.register(agentRunRunningIndexSourceMigration);
14673
+ mm.register(addThreadExpiresAt);
14674
+ mm.register(createStoreItemsTable);
14675
+ mm.register(createChannelReplyJobsTable);
14676
+ mm.register(createAgentRunRetentionMigration);
14677
+ mm.register(createAgentRunProjectTaskMigration);
14678
+ mm.register(agentRunProjectTaskRunningIndexMigration);
12434
14679
  await mm.migrate();
12435
14680
  const checkpoint = import_langgraph_checkpoint_postgres.PostgresSaver.fromConnString(connectionString);
12436
14681
  checkpoint.setup().catch((err) => {
@@ -12475,13 +14720,17 @@ async function createPgStoreConfig(connectionString) {
12475
14720
  lease: new PostgreSQLLeaseStore(opts),
12476
14721
  outbox: new PostgreSQLOutboxStore(opts),
12477
14722
  pluginDeployment: new PostgreSQLPluginDeploymentStore(opts),
14723
+ agentRun: new PostgreSQLAgentRunLedger(opts),
14724
+ channelReplyJob: new PostgreSQLChannelReplyJobStore(opts),
12478
14725
  vectorStoreProvider: new PGVectorStoreProvider(pool, connectionString),
12479
- checkpoint
14726
+ checkpoint,
14727
+ checkpointStorage: new PostgreSQLCheckpointStorage(pool),
14728
+ store: new PostgreSQLStore(pool)
12480
14729
  };
12481
14730
  }
12482
14731
 
12483
14732
  // src/stores/PostgreSQLSkillStore.ts
12484
- var import_pg31 = require("pg");
14733
+ var import_pg33 = require("pg");
12485
14734
  var PostgreSQLSkillStore = class {
12486
14735
  constructor(options) {
12487
14736
  this.initialized = false;
@@ -12494,9 +14743,9 @@ var PostgreSQLSkillStore = class {
12494
14743
  return;
12495
14744
  }
12496
14745
  if (typeof options.poolConfig === "string") {
12497
- this.pool = new import_pg31.Pool({ connectionString: options.poolConfig });
14746
+ this.pool = new import_pg33.Pool({ connectionString: options.poolConfig });
12498
14747
  } else if (options.poolConfig) {
12499
- this.pool = new import_pg31.Pool(options.poolConfig);
14748
+ this.pool = new import_pg33.Pool(options.poolConfig);
12500
14749
  } else {
12501
14750
  throw new Error("Either pool or poolConfig must be provided");
12502
14751
  }
@@ -12797,7 +15046,7 @@ var PostgreSQLSkillStore = class {
12797
15046
  };
12798
15047
 
12799
15048
  // src/stores/ChannelIdentityMappingStore.ts
12800
- var import_pg32 = require("pg");
15049
+ var import_pg34 = require("pg");
12801
15050
  var ChannelIdentityMappingStore = class {
12802
15051
  constructor(options) {
12803
15052
  this.initialized = false;
@@ -12809,7 +15058,7 @@ var ChannelIdentityMappingStore = class {
12809
15058
  this.initialized = true;
12810
15059
  return;
12811
15060
  }
12812
- this.pool = typeof options.poolConfig === "string" ? new import_pg32.Pool({ connectionString: options.poolConfig }) : options.poolConfig ? new import_pg32.Pool(options.poolConfig) : (() => {
15061
+ this.pool = typeof options.poolConfig === "string" ? new import_pg34.Pool({ connectionString: options.poolConfig }) : options.poolConfig ? new import_pg34.Pool(options.poolConfig) : (() => {
12813
15062
  throw new Error("Either pool or poolConfig must be provided");
12814
15063
  })();
12815
15064
  this.migrationManager = new MigrationManager(this.pool);
@@ -13031,6 +15280,8 @@ function mapRowToChannelIdentityMapping(row) {
13031
15280
  0 && (module.exports = {
13032
15281
  ChannelBindingStore,
13033
15282
  ChannelIdentityMappingStore,
15283
+ CheckpointBaseNotFoundError,
15284
+ CheckpointTargetNotEmptyError,
13034
15285
  DuplicateProjectMembershipError,
13035
15286
  DuplicateProjectRoomMessageIdempotencyKeyError,
13036
15287
  MenuStore,
@@ -13038,10 +15289,13 @@ function mapRowToChannelIdentityMapping(row) {
13038
15289
  PGVectorStoreProvider,
13039
15290
  Pool,
13040
15291
  PostgreSQLA2AApiKeyStore,
15292
+ PostgreSQLAgentRunLedger,
13041
15293
  PostgreSQLAgentWebAppStore,
13042
15294
  PostgreSQLAssistantStore,
13043
15295
  PostgreSQLCapabilityBundleStore,
13044
15296
  PostgreSQLChannelInstallationStore,
15297
+ PostgreSQLChannelReplyJobStore,
15298
+ PostgreSQLCheckpointStorage,
13045
15299
  PostgreSQLCollectionStore,
13046
15300
  PostgreSQLConnectionStore,
13047
15301
  PostgreSQLDatabaseConfigStore,
@@ -13059,6 +15313,7 @@ function mapRowToChannelIdentityMapping(row) {
13059
15313
  PostgreSQLProjectStore,
13060
15314
  PostgreSQLScheduleStorage,
13061
15315
  PostgreSQLSkillStore,
15316
+ PostgreSQLStore,
13062
15317
  PostgreSQLTaskWorkItemStore,
13063
15318
  PostgreSQLTenantStore,
13064
15319
  PostgreSQLThreadStore,
@@ -13072,6 +15327,7 @@ function mapRowToChannelIdentityMapping(row) {
13072
15327
  ProjectRoomMessageIdConflictError,
13073
15328
  ThreadMessageQueueStore,
13074
15329
  addAssistantTenantId,
15330
+ addCancelRequestedToEvalRuns,
13075
15331
  addEnvToEvalRuns,
13076
15332
  addFileContentType,
13077
15333
  addHoldoutToEvalRuns,
@@ -13083,6 +15339,7 @@ function mapRowToChannelIdentityMapping(row) {
13083
15339
  addSkillTenantId,
13084
15340
  addStepThreadId,
13085
15341
  addTaskIdToEvalRuns,
15342
+ addThreadExpiresAt,
13086
15343
  addThreadTenantId,
13087
15344
  addWorkflowRunsTenantStatusUpdatedIndex,
13088
15345
  alterChannelInstallationsTable,
@@ -13090,12 +15347,14 @@ function mapRowToChannelIdentityMapping(row) {
13090
15347
  changeSkillPrimaryKey,
13091
15348
  changeThreadPrimaryKey,
13092
15349
  createA2AApiKeysTable,
15350
+ createAgentRunLedgerTable,
13093
15351
  createAgentWebAppsTable,
13094
15352
  createAssistantsTable,
13095
15353
  createCapabilityBundlesTable,
13096
15354
  createChannelBindingsTable,
13097
15355
  createChannelIdentityMappingTables,
13098
15356
  createChannelInstallationsTable,
15357
+ createChannelReplyJobsTable,
13099
15358
  createCollectionsTable,
13100
15359
  createDatabaseConfigsTable,
13101
15360
  createEvalCasesTable,
@@ -13115,6 +15374,7 @@ function mapRowToChannelIdentityMapping(row) {
13115
15374
  createScheduledTasksTable,
13116
15375
  createSharedResourcesTable,
13117
15376
  createSkillsTable,
15377
+ createStoreItemsTable,
13118
15378
  createTenantsTable,
13119
15379
  createThreadMessageQueueTable,
13120
15380
  createThreadsTable,