@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.mjs CHANGED
@@ -1,8 +1,8 @@
1
1
  // src/index.ts
2
- import { Pool as Pool33 } from "pg";
2
+ import { Pool as Pool35 } from "pg";
3
3
 
4
4
  // src/createPgStoreConfig.ts
5
- import { Pool as Pool30 } from "pg";
5
+ import { Pool as Pool32 } from "pg";
6
6
 
7
7
  // src/migrations/migration.ts
8
8
  var MigrationManager = class {
@@ -215,6 +215,25 @@ var createThreadsTable = {
215
215
  await client.query("DROP TABLE IF EXISTS lattice_threads");
216
216
  }
217
217
  };
218
+ var addThreadExpiresAt = {
219
+ version: 194,
220
+ name: "add_thread_expires_at",
221
+ up: async (client) => {
222
+ await client.query(`
223
+ ALTER TABLE lattice_threads
224
+ ADD COLUMN IF NOT EXISTS expires_at TIMESTAMPTZ
225
+ `);
226
+ await client.query(`
227
+ CREATE INDEX IF NOT EXISTS idx_lattice_threads_expires_at
228
+ ON lattice_threads(expires_at)
229
+ WHERE expires_at IS NOT NULL
230
+ `);
231
+ },
232
+ down: async (client) => {
233
+ await client.query("DROP INDEX IF EXISTS idx_lattice_threads_expires_at");
234
+ await client.query("ALTER TABLE lattice_threads DROP COLUMN IF EXISTS expires_at");
235
+ }
236
+ };
218
237
 
219
238
  // src/migrations/thread_tenant_migration.ts
220
239
  var addThreadTenantId = {
@@ -342,6 +361,7 @@ var PostgreSQLThreadStore = class {
342
361
  this.migrationManager.register(createThreadsTable);
343
362
  this.migrationManager.register(addThreadTenantId);
344
363
  this.migrationManager.register(changeThreadPrimaryKey);
364
+ this.migrationManager.register(addThreadExpiresAt);
345
365
  if (options.autoMigrate !== false) {
346
366
  this.initialize().catch((error) => {
347
367
  console.error("Failed to initialize PostgreSQLThreadStore:", error);
@@ -386,7 +406,7 @@ var PostgreSQLThreadStore = class {
386
406
  await this.ensureInitialized();
387
407
  const result = await this.pool.query(
388
408
  `
389
- SELECT id, tenant_id, assistant_id, metadata, created_at, updated_at
409
+ SELECT id, tenant_id, assistant_id, metadata, created_at, updated_at, expires_at
390
410
  FROM lattice_threads
391
411
  WHERE tenant_id = $1 AND assistant_id = $2
392
412
  ORDER BY created_at DESC
@@ -410,7 +430,7 @@ var PostgreSQLThreadStore = class {
410
430
  await this.ensureInitialized();
411
431
  const result = await this.pool.query(
412
432
  `
413
- SELECT id, tenant_id, assistant_id, metadata, created_at, updated_at
433
+ SELECT id, tenant_id, assistant_id, metadata, created_at, updated_at, expires_at
414
434
  FROM lattice_threads
415
435
  WHERE tenant_id = $1 AND id = $2
416
436
  `,
@@ -430,14 +450,15 @@ var PostgreSQLThreadStore = class {
430
450
  const metadata = data.metadata || {};
431
451
  await this.pool.query(
432
452
  `
433
- INSERT INTO lattice_threads (id, tenant_id, assistant_id, metadata, created_at, updated_at)
434
- VALUES ($1, $2, $3, $4, $5, $6)
453
+ INSERT INTO lattice_threads (id, tenant_id, assistant_id, metadata, created_at, updated_at, expires_at)
454
+ VALUES ($1, $2, $3, $4, $5, $6, $7)
435
455
  ON CONFLICT (id, tenant_id) DO UPDATE SET
436
456
  assistant_id = EXCLUDED.assistant_id,
437
457
  metadata = EXCLUDED.metadata,
438
- updated_at = EXCLUDED.updated_at
458
+ updated_at = EXCLUDED.updated_at,
459
+ expires_at = EXCLUDED.expires_at
439
460
  `,
440
- [threadId, tenantId, assistantId, JSON.stringify(metadata), now, now]
461
+ [threadId, tenantId, assistantId, JSON.stringify(metadata), now, now, data.expiresAt ?? null]
441
462
  );
442
463
  return {
443
464
  id: threadId,
@@ -445,7 +466,8 @@ var PostgreSQLThreadStore = class {
445
466
  assistantId,
446
467
  metadata,
447
468
  createdAt: now,
448
- updatedAt: now
469
+ updatedAt: now,
470
+ expiresAt: data.expiresAt
449
471
  };
450
472
  }
451
473
  /**
@@ -462,18 +484,20 @@ var PostgreSQLThreadStore = class {
462
484
  ...updates.metadata || {}
463
485
  };
464
486
  const now = /* @__PURE__ */ new Date();
487
+ const expiresAt = updates.expiresAt !== void 0 ? updates.expiresAt : existing.expiresAt ?? null;
465
488
  await this.pool.query(
466
489
  `
467
490
  UPDATE lattice_threads
468
- SET metadata = $1, updated_at = $2
469
- WHERE tenant_id = $3 AND id = $4
491
+ SET metadata = $1, updated_at = $2, expires_at = $3
492
+ WHERE tenant_id = $4 AND id = $5
470
493
  `,
471
- [JSON.stringify(updatedMetadata), now, tenantId, threadId]
494
+ [JSON.stringify(updatedMetadata), now, expiresAt, tenantId, threadId]
472
495
  );
473
496
  return {
474
497
  ...existing,
475
498
  metadata: updatedMetadata,
476
- updatedAt: now
499
+ updatedAt: now,
500
+ expiresAt: expiresAt ?? void 0
477
501
  };
478
502
  }
479
503
  /**
@@ -505,6 +529,23 @@ var PostgreSQLThreadStore = class {
505
529
  );
506
530
  return result.rows.length > 0;
507
531
  }
532
+ /**
533
+ * List expired threads across all tenants, oldest expiry first.
534
+ */
535
+ async listExpiredThreads(now, limit) {
536
+ await this.ensureInitialized();
537
+ const result = await this.pool.query(
538
+ `
539
+ SELECT id, tenant_id, assistant_id, metadata, created_at, updated_at, expires_at
540
+ FROM lattice_threads
541
+ WHERE expires_at IS NOT NULL AND expires_at <= $1
542
+ ORDER BY expires_at ASC
543
+ LIMIT $2
544
+ `,
545
+ [now, Math.max(0, limit)]
546
+ );
547
+ return result.rows.map(this.mapRowToThread);
548
+ }
508
549
  /**
509
550
  * Ensure store is initialized
510
551
  */
@@ -523,7 +564,8 @@ var PostgreSQLThreadStore = class {
523
564
  assistantId: row.assistant_id,
524
565
  metadata: typeof row.metadata === "string" ? JSON.parse(row.metadata) : row.metadata || {},
525
566
  createdAt: row.created_at,
526
- updatedAt: row.updated_at
567
+ updatedAt: row.updated_at,
568
+ expiresAt: row.expires_at ?? void 0
527
569
  };
528
570
  }
529
571
  };
@@ -4249,6 +4291,21 @@ var addTaskIdToEvalRuns = {
4249
4291
  `);
4250
4292
  }
4251
4293
  };
4294
+ var addCancelRequestedToEvalRuns = {
4295
+ version: 205,
4296
+ name: "add_cancel_requested_to_eval_runs",
4297
+ up: async (client) => {
4298
+ await client.query(`
4299
+ ALTER TABLE lattice_eval_runs
4300
+ ADD COLUMN IF NOT EXISTS cancel_requested BOOLEAN NOT NULL DEFAULT FALSE
4301
+ `);
4302
+ },
4303
+ down: async (client) => {
4304
+ await client.query(`
4305
+ ALTER TABLE lattice_eval_runs DROP COLUMN IF EXISTS cancel_requested
4306
+ `);
4307
+ }
4308
+ };
4252
4309
  var evalMigrations = [
4253
4310
  createEvalProjectsTable,
4254
4311
  createEvalSuitesTable,
@@ -4260,7 +4317,8 @@ var evalMigrations = [
4260
4317
  addInterruptColumnsToEval,
4261
4318
  addInterruptPolicyToEvalCases,
4262
4319
  enforceUniqueEvalProjectName,
4263
- addTaskIdToEvalRuns
4320
+ addTaskIdToEvalRuns,
4321
+ addCancelRequestedToEvalRuns
4264
4322
  ];
4265
4323
 
4266
4324
  // src/stores/PostgreSQLEvalStore.ts
@@ -4385,6 +4443,7 @@ var PostgreSQLEvalStore = class {
4385
4443
  interruptedCases: row.interrupted_cases ?? 0,
4386
4444
  avgScore: row.avg_score ?? 0,
4387
4445
  error: row.error,
4446
+ cancelRequested: row.cancel_requested ?? false,
4388
4447
  holdout: row.holdout,
4389
4448
  envProjectId: row.env_project_id,
4390
4449
  envWorkspaceId: row.env_workspace_id,
@@ -4756,7 +4815,7 @@ var PostgreSQLEvalStore = class {
4756
4815
  `SELECT id, project_id, tenant_id,
4757
4816
  status, concurrency, total_cases,
4758
4817
  passed_cases, failed_cases, interrupted_cases, avg_score,
4759
- error, holdout, env_project_id, env_workspace_id, task_id, created_at, started_at, completed_at
4818
+ error, holdout, env_project_id, env_workspace_id, task_id, cancel_requested, created_at, started_at, completed_at
4760
4819
  FROM lattice_eval_runs
4761
4820
  WHERE ${conditions.join(" AND ")}
4762
4821
  ORDER BY created_at DESC`,
@@ -4771,7 +4830,7 @@ var PostgreSQLEvalStore = class {
4771
4830
  `SELECT id, project_id, tenant_id,
4772
4831
  status, concurrency, total_cases,
4773
4832
  passed_cases, failed_cases, interrupted_cases, avg_score,
4774
- error, holdout, env_project_id, env_workspace_id, task_id, created_at, started_at, completed_at
4833
+ error, holdout, env_project_id, env_workspace_id, task_id, cancel_requested, created_at, started_at, completed_at
4775
4834
  FROM lattice_eval_runs
4776
4835
  WHERE id = $1 AND tenant_id = $2`,
4777
4836
  [id, tenantId]
@@ -4789,7 +4848,7 @@ var PostgreSQLEvalStore = class {
4789
4848
  RETURNING id, project_id, tenant_id,
4790
4849
  status, concurrency, total_cases,
4791
4850
  passed_cases, failed_cases, interrupted_cases, avg_score,
4792
- error, holdout, env_project_id, env_workspace_id, task_id, created_at, started_at, completed_at`,
4851
+ error, holdout, env_project_id, env_workspace_id, task_id, cancel_requested, created_at, started_at, completed_at`,
4793
4852
  [
4794
4853
  actualId,
4795
4854
  projectId,
@@ -4851,11 +4910,22 @@ var PostgreSQLEvalStore = class {
4851
4910
  RETURNING id, project_id, tenant_id,
4852
4911
  status, concurrency, total_cases,
4853
4912
  passed_cases, failed_cases, interrupted_cases, avg_score,
4854
- error, holdout, env_project_id, env_workspace_id, task_id, created_at, started_at, completed_at`,
4913
+ error, holdout, env_project_id, env_workspace_id, task_id, cancel_requested, created_at, started_at, completed_at`,
4855
4914
  vals
4856
4915
  );
4857
4916
  return rows.length ? this.mapRowToRun(rows[0]) : null;
4858
4917
  }
4918
+ /** Request cooperative cancellation for a running evaluation. */
4919
+ async requestCancel(tenantId, id) {
4920
+ await this.ensureInitialized();
4921
+ const { rowCount } = await this.pool.query(
4922
+ `UPDATE lattice_eval_runs
4923
+ SET cancel_requested = TRUE
4924
+ WHERE tenant_id = $1 AND id = $2 AND status = 'running'`,
4925
+ [tenantId, id]
4926
+ );
4927
+ return (rowCount ?? 0) > 0;
4928
+ }
4859
4929
  /** Delete a run and its results */
4860
4930
  async deleteRun(tenantId, id) {
4861
4931
  await this.ensureInitialized();
@@ -5443,20 +5513,25 @@ var ThreadMessageQueueStore = class {
5443
5513
  }
5444
5514
  /**
5445
5515
  * Get all threads with pending or processing messages
5516
+ *
5517
+ * @returns One entry per full scope, with `hasPending` distinguishing
5518
+ * processing-only threads (no pending row) from threads with work to drain.
5446
5519
  */
5447
5520
  async getThreadsWithPendingMessages() {
5448
5521
  const result = await this.pool.query(
5449
- `SELECT DISTINCT ON (tenant_id, assistant_id, workspace_id, project_id, thread_id) tenant_id, assistant_id, thread_id, workspace_id, project_id
5450
- FROM lattice_thread_message_queue
5451
- WHERE status IN ('pending', 'processing')
5452
- ORDER BY tenant_id, assistant_id, workspace_id, project_id, thread_id`
5522
+ `SELECT tenant_id, assistant_id, thread_id, workspace_id, project_id,
5523
+ bool_or(status = 'pending') AS has_pending
5524
+ FROM lattice_thread_message_queue
5525
+ WHERE status IN ('pending', 'processing')
5526
+ GROUP BY tenant_id, assistant_id, thread_id, workspace_id, project_id`
5453
5527
  );
5454
5528
  return result.rows.map((row) => ({
5455
5529
  tenantId: row.tenant_id,
5456
5530
  assistantId: row.assistant_id,
5457
5531
  threadId: row.thread_id,
5458
5532
  workspaceId: row.workspace_id,
5459
- projectId: row.project_id
5533
+ projectId: row.project_id,
5534
+ hasPending: row.has_pending
5460
5535
  }));
5461
5536
  }
5462
5537
  /**
@@ -7603,12 +7678,27 @@ var addTaskDependenciesGinIndex = {
7603
7678
  await client.query("DROP INDEX IF EXISTS idx_lattice_tasks_dependencies_gin");
7604
7679
  }
7605
7680
  };
7681
+ var addTaskContextThreadIndex = {
7682
+ version: 206,
7683
+ name: "add_task_context_thread_index",
7684
+ up: async (client) => {
7685
+ await client.query(`
7686
+ CREATE INDEX IF NOT EXISTS idx_lattice_tasks_context_thread
7687
+ ON lattice_tasks (tenant_id, (context->>'thread_id'))
7688
+ WHERE context->>'thread_id' IS NOT NULL
7689
+ `);
7690
+ },
7691
+ down: async (client) => {
7692
+ await client.query("DROP INDEX IF EXISTS idx_lattice_tasks_context_thread");
7693
+ }
7694
+ };
7606
7695
  var taskMigrations = [
7607
7696
  createTasksTable,
7608
7697
  addTaskFieldsMigration,
7609
7698
  addTaskProjectFieldsMigration,
7610
7699
  addFilesToTasks,
7611
- addTaskDependenciesGinIndex
7700
+ addTaskDependenciesGinIndex,
7701
+ addTaskContextThreadIndex
7612
7702
  ];
7613
7703
 
7614
7704
  // src/stores/PostgreSQLTaskStore.ts
@@ -7819,6 +7909,10 @@ var PostgreSQLTaskStore = class {
7819
7909
  conditions.push(`priority = $${paramIndex++}`);
7820
7910
  params.push(filter.priority);
7821
7911
  }
7912
+ if (filter.threadId) {
7913
+ conditions.push(`context->>'thread_id' = $${paramIndex++}`);
7914
+ params.push(filter.threadId);
7915
+ }
7822
7916
  if (filter.workspaceId) {
7823
7917
  conditions.push(`workspace_id = $${paramIndex++}`);
7824
7918
  params.push(filter.workspaceId);
@@ -9890,6 +9984,299 @@ var createCollectionsTable = {
9890
9984
  }
9891
9985
  };
9892
9986
 
9987
+ // src/migrations/agent_run_ledger_migration.ts
9988
+ var createAgentRunLedgerTable = {
9989
+ version: 193,
9990
+ name: "create_lattice_agent_runs_table",
9991
+ up: async (client) => {
9992
+ await client.query(`
9993
+ CREATE TABLE IF NOT EXISTS lattice_agent_runs (
9994
+ run_id UUID PRIMARY KEY,
9995
+ tenant_id VARCHAR(255) NOT NULL,
9996
+ workspace_id VARCHAR(255),
9997
+ project_id VARCHAR(255),
9998
+ assistant_id VARCHAR(255) NOT NULL,
9999
+ thread_id VARCHAR(255) NOT NULL,
10000
+ status VARCHAR(32) NOT NULL,
10001
+ owner VARCHAR(255) NOT NULL,
10002
+ lease_expires_at TIMESTAMPTZ NOT NULL,
10003
+ input_message_ids TEXT[] NOT NULL DEFAULT '{}',
10004
+ attempts INTEGER NOT NULL DEFAULT 1,
10005
+ error TEXT,
10006
+ cancel_requested BOOLEAN NOT NULL DEFAULT FALSE,
10007
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
10008
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
10009
+ started_at TIMESTAMPTZ NOT NULL DEFAULT now(),
10010
+ finished_at TIMESTAMPTZ
10011
+ )
10012
+ `);
10013
+ await client.query(`
10014
+ CREATE UNIQUE INDEX IF NOT EXISTS uq_lattice_agent_runs_one_running
10015
+ ON lattice_agent_runs (tenant_id, thread_id) WHERE status = 'running'
10016
+ `);
10017
+ await client.query(`
10018
+ CREATE INDEX IF NOT EXISTS idx_lattice_agent_runs_history
10019
+ ON lattice_agent_runs (tenant_id, thread_id, created_at DESC)
10020
+ `);
10021
+ await client.query(`
10022
+ CREATE INDEX IF NOT EXISTS idx_lattice_agent_runs_reap
10023
+ ON lattice_agent_runs (status, lease_expires_at)
10024
+ `);
10025
+ },
10026
+ down: async (client) => {
10027
+ await client.query("DROP TABLE IF EXISTS lattice_agent_runs");
10028
+ }
10029
+ };
10030
+
10031
+ // src/migrations/channel_reply_job_migration.ts
10032
+ var DB_CLOCK_MS = "FLOOR(EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint";
10033
+ var createChannelReplyJobsTable = {
10034
+ version: 200,
10035
+ name: "create_lattice_channel_reply_jobs",
10036
+ up: async (client) => {
10037
+ await client.query(`
10038
+ CREATE TABLE IF NOT EXISTS lattice_channel_reply_jobs (
10039
+ run_id TEXT PRIMARY KEY,
10040
+ tenant_id VARCHAR(255) NOT NULL,
10041
+ assistant_id TEXT NOT NULL,
10042
+ thread_id TEXT NOT NULL,
10043
+ adapter_channel TEXT NOT NULL,
10044
+ channel_installation_id TEXT NOT NULL,
10045
+ reply_target JSONB NOT NULL,
10046
+ input_message_id TEXT NOT NULL,
10047
+ stream_cursor TEXT NULL,
10048
+ status TEXT NOT NULL,
10049
+ attempts INT NOT NULL DEFAULT 0,
10050
+ available_at BIGINT NOT NULL,
10051
+ lease_owner TEXT NULL,
10052
+ lease_expires_at BIGINT NULL,
10053
+ last_error TEXT NULL,
10054
+ output_message_id TEXT NULL,
10055
+ created_at BIGINT NOT NULL DEFAULT (${DB_CLOCK_MS}),
10056
+ updated_at BIGINT NOT NULL DEFAULT (${DB_CLOCK_MS}),
10057
+ delivered_at BIGINT NULL
10058
+ )
10059
+ `);
10060
+ await client.query(`
10061
+ CREATE INDEX IF NOT EXISTS idx_lattice_channel_reply_jobs_due
10062
+ ON lattice_channel_reply_jobs (status, available_at)
10063
+ `);
10064
+ await client.query(`
10065
+ CREATE INDEX IF NOT EXISTS idx_lattice_channel_reply_jobs_tenant_thread
10066
+ ON lattice_channel_reply_jobs (tenant_id, thread_id)
10067
+ `);
10068
+ },
10069
+ down: async (client) => {
10070
+ await client.query("DROP INDEX IF EXISTS idx_lattice_channel_reply_jobs_tenant_thread");
10071
+ await client.query("DROP INDEX IF EXISTS idx_lattice_channel_reply_jobs_due");
10072
+ await client.query("DROP TABLE IF EXISTS lattice_channel_reply_jobs");
10073
+ }
10074
+ };
10075
+
10076
+ // src/migrations/agent_run_task_queue_migration.ts
10077
+ var createAgentRunTaskQueueMigration = {
10078
+ version: 196,
10079
+ name: "expand_lattice_agent_runs_task_queue",
10080
+ up: async (client) => {
10081
+ await client.query(`
10082
+ ALTER TABLE lattice_agent_runs
10083
+ ALTER COLUMN owner DROP NOT NULL,
10084
+ ALTER COLUMN lease_expires_at DROP NOT NULL,
10085
+ ALTER COLUMN started_at DROP NOT NULL,
10086
+ ALTER COLUMN started_at DROP DEFAULT
10087
+ `);
10088
+ await client.query(`
10089
+ ALTER TABLE lattice_agent_runs
10090
+ ADD COLUMN IF NOT EXISTS source VARCHAR(32) NOT NULL DEFAULT 'legacy',
10091
+ ADD COLUMN IF NOT EXISTS input JSONB,
10092
+ ADD COLUMN IF NOT EXISTS command JSONB,
10093
+ ADD COLUMN IF NOT EXISTS config JSONB,
10094
+ ADD COLUMN IF NOT EXISTS metadata JSONB,
10095
+ ADD COLUMN IF NOT EXISTS base_checkpoint_id TEXT,
10096
+ ADD COLUMN IF NOT EXISTS available_at TIMESTAMPTZ NOT NULL DEFAULT now(),
10097
+ ADD COLUMN IF NOT EXISTS rollback_requested BOOLEAN NOT NULL DEFAULT FALSE
10098
+ `);
10099
+ await client.query(`
10100
+ ALTER TABLE lattice_agent_runs
10101
+ ALTER COLUMN source SET DEFAULT 'legacy'
10102
+ `);
10103
+ await client.query(`
10104
+ CREATE INDEX IF NOT EXISTS idx_lattice_agent_runs_run_queue
10105
+ ON lattice_agent_runs (source, status, available_at, created_at)
10106
+ `);
10107
+ },
10108
+ down: async (client) => {
10109
+ await client.query(`
10110
+ DROP INDEX IF EXISTS idx_lattice_agent_runs_run_queue
10111
+ `);
10112
+ await client.query(`
10113
+ ALTER TABLE lattice_agent_runs
10114
+ DROP COLUMN IF EXISTS rollback_requested,
10115
+ DROP COLUMN IF EXISTS available_at,
10116
+ DROP COLUMN IF EXISTS base_checkpoint_id,
10117
+ DROP COLUMN IF EXISTS metadata,
10118
+ DROP COLUMN IF EXISTS config,
10119
+ DROP COLUMN IF EXISTS command,
10120
+ DROP COLUMN IF EXISTS input,
10121
+ DROP COLUMN IF EXISTS source
10122
+ `);
10123
+ await client.query(`
10124
+ ALTER TABLE lattice_agent_runs
10125
+ ALTER COLUMN started_at SET DEFAULT now()
10126
+ `);
10127
+ }
10128
+ };
10129
+
10130
+ // src/migrations/agent_run_capability_revision_migration.ts
10131
+ var createAgentRunCapabilityRevisionMigration = {
10132
+ version: 197,
10133
+ name: "add_agent_run_capability_revision",
10134
+ up: async (client) => {
10135
+ await client.query(`
10136
+ ALTER TABLE lattice_agent_runs
10137
+ ADD COLUMN IF NOT EXISTS capability_runtime_revision TEXT
10138
+ `);
10139
+ },
10140
+ down: async (client) => {
10141
+ await client.query(`
10142
+ ALTER TABLE lattice_agent_runs
10143
+ DROP COLUMN IF EXISTS capability_runtime_revision
10144
+ `);
10145
+ }
10146
+ };
10147
+
10148
+ // src/migrations/agent_run_base_checkpoint_captured_migration.ts
10149
+ var createAgentRunBaseCheckpointCapturedMigration = {
10150
+ version: 198,
10151
+ name: "add_agent_run_base_checkpoint_captured",
10152
+ up: async (client) => {
10153
+ await client.query(`
10154
+ ALTER TABLE lattice_agent_runs
10155
+ ADD COLUMN IF NOT EXISTS base_checkpoint_captured BOOLEAN NOT NULL DEFAULT FALSE
10156
+ `);
10157
+ await client.query(`
10158
+ UPDATE lattice_agent_runs
10159
+ SET base_checkpoint_captured = TRUE
10160
+ WHERE base_checkpoint_id IS NOT NULL
10161
+ `);
10162
+ },
10163
+ down: async (client) => {
10164
+ await client.query(`
10165
+ ALTER TABLE lattice_agent_runs
10166
+ DROP COLUMN IF EXISTS base_checkpoint_captured
10167
+ `);
10168
+ }
10169
+ };
10170
+
10171
+ // src/migrations/agent_run_rollback_failure_migration.ts
10172
+ var createAgentRunRollbackFailureMigration = {
10173
+ version: 199,
10174
+ name: "add_agent_run_rollback_failure",
10175
+ up: async (client) => {
10176
+ await client.query(`
10177
+ ALTER TABLE lattice_agent_runs
10178
+ ADD COLUMN IF NOT EXISTS rollback_attempts INTEGER NOT NULL DEFAULT 0,
10179
+ ADD COLUMN IF NOT EXISTS rollback_error TEXT
10180
+ `);
10181
+ },
10182
+ down: async (client) => {
10183
+ await client.query(`
10184
+ ALTER TABLE lattice_agent_runs
10185
+ DROP COLUMN IF EXISTS rollback_error,
10186
+ DROP COLUMN IF EXISTS rollback_attempts
10187
+ `);
10188
+ }
10189
+ };
10190
+
10191
+ // src/migrations/agent_run_running_index_source_migration.ts
10192
+ var agentRunRunningIndexSourceMigration = {
10193
+ version: 202,
10194
+ name: "scope_agent_run_running_index_to_run_api",
10195
+ up: async (client) => {
10196
+ await client.query("DROP INDEX IF EXISTS uq_lattice_agent_runs_one_running");
10197
+ await client.query(`
10198
+ CREATE UNIQUE INDEX IF NOT EXISTS uq_lattice_agent_runs_one_running
10199
+ ON lattice_agent_runs (tenant_id, thread_id)
10200
+ WHERE status = 'running' AND source = 'run_api'
10201
+ `);
10202
+ },
10203
+ down: async (client) => {
10204
+ await client.query("DROP INDEX IF EXISTS uq_lattice_agent_runs_one_running");
10205
+ await client.query(`
10206
+ CREATE UNIQUE INDEX IF NOT EXISTS uq_lattice_agent_runs_one_running
10207
+ ON lattice_agent_runs (tenant_id, thread_id)
10208
+ WHERE status = 'running'
10209
+ `);
10210
+ }
10211
+ };
10212
+
10213
+ // src/migrations/agent_run_retention_migration.ts
10214
+ var createAgentRunRetentionMigration = {
10215
+ version: 201,
10216
+ name: "add_agent_run_retention_index",
10217
+ up: async (client) => {
10218
+ await client.query(`
10219
+ CREATE INDEX IF NOT EXISTS idx_lattice_agent_runs_terminal_finished_at
10220
+ ON lattice_agent_runs (finished_at)
10221
+ WHERE status IN ('success', 'error', 'interrupted', 'cancelled', 'timeout')
10222
+ `);
10223
+ },
10224
+ down: async (client) => {
10225
+ await client.query(`
10226
+ DROP INDEX IF EXISTS idx_lattice_agent_runs_terminal_finished_at
10227
+ `);
10228
+ }
10229
+ };
10230
+
10231
+ // src/migrations/agent_run_project_task_migration.ts
10232
+ var createAgentRunProjectTaskMigration = {
10233
+ version: 203,
10234
+ name: "add_agent_run_project_task",
10235
+ up: async (client) => {
10236
+ await client.query(`
10237
+ ALTER TABLE lattice_agent_runs
10238
+ ADD COLUMN IF NOT EXISTS task_id TEXT NULL
10239
+ `);
10240
+ await client.query(`
10241
+ CREATE INDEX IF NOT EXISTS idx_lattice_agent_runs_project_task
10242
+ ON lattice_agent_runs (tenant_id, task_id, status, created_at DESC)
10243
+ WHERE task_id IS NOT NULL
10244
+ `);
10245
+ },
10246
+ down: async (client) => {
10247
+ await client.query(`
10248
+ DROP INDEX IF EXISTS idx_lattice_agent_runs_project_task
10249
+ `);
10250
+ await client.query(`
10251
+ ALTER TABLE lattice_agent_runs
10252
+ DROP COLUMN IF EXISTS task_id
10253
+ `);
10254
+ }
10255
+ };
10256
+
10257
+ // src/migrations/agent_run_project_task_running_index_migration.ts
10258
+ var agentRunProjectTaskRunningIndexMigration = {
10259
+ version: 204,
10260
+ name: "scope_agent_run_running_index_to_queue_sources",
10261
+ up: async (client) => {
10262
+ await client.query("DROP INDEX IF EXISTS uq_lattice_agent_runs_one_running");
10263
+ await client.query(`
10264
+ CREATE UNIQUE INDEX IF NOT EXISTS uq_lattice_agent_runs_one_running
10265
+ ON lattice_agent_runs (tenant_id, thread_id)
10266
+ WHERE status = 'running'
10267
+ AND source IN ('run_api', 'project_task')
10268
+ `);
10269
+ },
10270
+ down: async (client) => {
10271
+ await client.query("DROP INDEX IF EXISTS uq_lattice_agent_runs_one_running");
10272
+ await client.query(`
10273
+ CREATE UNIQUE INDEX IF NOT EXISTS uq_lattice_agent_runs_one_running
10274
+ ON lattice_agent_runs (tenant_id, thread_id)
10275
+ WHERE status = 'running' AND source = 'run_api'
10276
+ `);
10277
+ }
10278
+ };
10279
+
9893
10280
  // src/migrations/capability_bundle_migration.ts
9894
10281
  var createCapabilityBundlesTable = {
9895
10282
  version: 170,
@@ -12231,51 +12618,1903 @@ var PostgreSQLPluginDeploymentStore = class {
12231
12618
  }
12232
12619
  };
12233
12620
 
12234
- // src/createPgStoreConfig.ts
12235
- async function createPgStoreConfig(connectionString) {
12236
- const pool = new Pool30({ connectionString });
12237
- const mm = new MigrationManager(pool);
12238
- mm.register(createThreadsTable);
12239
- mm.register(createScheduledTasksTable);
12240
- mm.register(createAssistantsTable);
12241
- mm.register(createSkillsTable);
12242
- mm.register(createWorkspacesTable);
12243
- mm.register(createProjectsTable);
12244
- mm.register(createUsersTable);
12245
- mm.register(createTenantsTable);
12246
- mm.register(addUserStatusColumn);
12247
- mm.register(createUserTenantLinkTable);
12248
- mm.register(migrateRemoveTenantIdFromUsers);
12249
- mm.register(createMetricsConfigsTable);
12250
- mm.register(createMcpServerConfigsTable);
12251
- mm.register(addAssistantTenantId);
12252
- mm.register(addSkillTenantId);
12253
- mm.register(changeSkillPrimaryKey);
12254
- mm.register(addScheduleTenantId);
12255
- mm.register(changeThreadPrimaryKey);
12256
- mm.register(createChannelIdentityMappingTables);
12257
- mm.register(createChannelInstallationsTable);
12258
- mm.register(addThreadTenantId);
12259
- mm.register(addProjectConfigColumn);
12260
- registerThreadMessageQueueMigrations(mm);
12261
- mm.register(createWorkflowTrackingTables);
12262
- for (const m of evalMigrations) {
12263
- mm.register(m);
12264
- }
12265
- mm.register(alterChannelInstallationsTable);
12266
- mm.register(createChannelBindingsTable);
12267
- mm.register(createDatabaseConfigsTable);
12268
- mm.register(changeAssistantPrimaryKey);
12269
- mm.register(addStepThreadId);
12270
- mm.register(createA2AApiKeysTable);
12271
- mm.register(addAssistantOwnerUserId);
12272
- for (const m of taskMigrations) {
12273
- mm.register(m);
12621
+ // src/stores/PostgreSQLAgentRunLedger.ts
12622
+ import { Pool as Pool30 } from "pg";
12623
+ import {
12624
+ AgentRunAlreadyActiveError,
12625
+ AgentRunDuplicateMessageIdError,
12626
+ AgentRunInputCommandConflictError,
12627
+ AgentRunInvalidTaskIdError,
12628
+ AgentRunTenantQuotaExceededError,
12629
+ assertQueueSources,
12630
+ extractAgentRunInputMessageIds,
12631
+ normalizeAgentRunMessageIds
12632
+ } from "@axiom-lattice/core";
12633
+ var DEFAULT_LIST_LIMIT = 50;
12634
+ var MAX_LIST_LIMIT = 200;
12635
+ var CLAIM_RUN_SQL = `UPDATE lattice_agent_runs AS r
12636
+ SET status = 'running',
12637
+ owner = $2,
12638
+ lease_expires_at = now() + ($3::bigint || ' milliseconds')::interval,
12639
+ started_at = now(),
12640
+ attempts = r.attempts + 1,
12641
+ input_message_ids = COALESCE($4::text[], r.input_message_ids),
12642
+ updated_at = now()
12643
+ WHERE r.run_id = $1
12644
+ AND r.source = ANY($5::text[])
12645
+ AND r.status = 'pending'
12646
+ AND r.available_at <= now()
12647
+ AND NOT EXISTS (
12648
+ SELECT 1 FROM lattice_agent_runs AS o
12649
+ WHERE o.tenant_id = r.tenant_id
12650
+ AND o.thread_id = r.thread_id
12651
+ AND o.source IN ('run_api', 'project_task')
12652
+ AND o.status = 'running'
12653
+ )
12654
+ AND NOT EXISTS (
12655
+ SELECT 1 FROM lattice_agent_runs AS b
12656
+ WHERE b.tenant_id = r.tenant_id
12657
+ AND b.thread_id = r.thread_id
12658
+ AND b.source = 'run_api'
12659
+ AND b.rollback_requested = TRUE
12660
+ )
12661
+ RETURNING r.*`;
12662
+ var PostgreSQLAgentRunLedger = class {
12663
+ /**
12664
+ * @param options An externally managed pool, or a pool config/connection URL.
12665
+ */
12666
+ constructor(options) {
12667
+ this.ownsPool = true;
12668
+ this.initialized = false;
12669
+ this.initPromise = null;
12670
+ if (options.pool) {
12671
+ this.pool = options.pool;
12672
+ this.ownsPool = false;
12673
+ this.initialized = true;
12674
+ return;
12675
+ }
12676
+ if (typeof options.poolConfig === "string") {
12677
+ this.pool = new Pool30({ connectionString: options.poolConfig });
12678
+ } else if (options.poolConfig) {
12679
+ this.pool = new Pool30(options.poolConfig);
12680
+ } else {
12681
+ throw new Error("Either pool or poolConfig must be provided");
12682
+ }
12683
+ this.migrationManager = new MigrationManager(this.pool);
12684
+ this.migrationManager.register(createAgentRunLedgerTable);
12685
+ this.migrationManager.register(createAgentRunTaskQueueMigration);
12686
+ this.migrationManager.register(createAgentRunCapabilityRevisionMigration);
12687
+ this.migrationManager.register(createAgentRunBaseCheckpointCapturedMigration);
12688
+ this.migrationManager.register(createAgentRunRollbackFailureMigration);
12689
+ this.migrationManager.register(createAgentRunRetentionMigration);
12690
+ this.migrationManager.register(agentRunRunningIndexSourceMigration);
12691
+ this.migrationManager.register(createAgentRunProjectTaskMigration);
12692
+ this.migrationManager.register(agentRunProjectTaskRunningIndexMigration);
12693
+ if (options.autoMigrate !== false) {
12694
+ this.initialize().catch((error) => {
12695
+ console.error("Failed to initialize PostgreSQLAgentRunLedger:", error);
12696
+ throw error;
12697
+ });
12698
+ }
12274
12699
  }
12275
- mm.register(createMenuItemsTable);
12276
- mm.register(createSharedResourcesTable);
12277
- mm.register(addFileContentType);
12278
- mm.register(addMcpAppContentType);
12700
+ /** Runs pending migrations once. */
12701
+ async initialize() {
12702
+ if (this.initialized) return;
12703
+ if (this.initPromise) return this.initPromise;
12704
+ this.initPromise = (async () => {
12705
+ try {
12706
+ await this.migrationManager.migrate();
12707
+ this.initialized = true;
12708
+ } finally {
12709
+ this.initPromise = null;
12710
+ }
12711
+ })();
12712
+ return this.initPromise;
12713
+ }
12714
+ /** Ends the pool when this store owns it. */
12715
+ async dispose() {
12716
+ if (this.ownsPool && this.pool) {
12717
+ await this.pool.end();
12718
+ }
12719
+ }
12720
+ /**
12721
+ * Creates a running legacy run, reconciling any existing running row first.
12722
+ *
12723
+ * @remarks The caller holds the `thread-run` lease, so a surviving `running`
12724
+ * row is stale (its owner can no longer hold the lease) even when its ledger
12725
+ * lease has not expired yet.
12726
+ */
12727
+ async beginRun(input) {
12728
+ const client = await this.pool.connect();
12729
+ try {
12730
+ await client.query("BEGIN");
12731
+ await client.query(
12732
+ `UPDATE lattice_agent_runs
12733
+ SET status = 'interrupted',
12734
+ error = COALESCE(error, 'superseded by a new run on the same thread'),
12735
+ finished_at = now(),
12736
+ updated_at = now()
12737
+ WHERE tenant_id = $1 AND thread_id = $2
12738
+ AND status = 'running'`,
12739
+ [input.tenantId, input.threadId]
12740
+ );
12741
+ const result = await client.query(
12742
+ `INSERT INTO lattice_agent_runs
12743
+ (run_id, tenant_id, workspace_id, project_id, assistant_id, thread_id,
12744
+ status, source, owner, lease_expires_at, input_message_ids, attempts,
12745
+ started_at)
12746
+ VALUES ($1, $2, $3, $4, $5, $6, 'running', 'legacy', $7,
12747
+ now() + ($8::bigint || ' milliseconds')::interval, $9, 1, now())
12748
+ RETURNING *`,
12749
+ [
12750
+ input.runId,
12751
+ input.tenantId,
12752
+ input.workspaceId ?? null,
12753
+ input.projectId ?? null,
12754
+ input.assistantId,
12755
+ input.threadId,
12756
+ input.owner,
12757
+ input.leaseMs,
12758
+ input.inputMessageIds
12759
+ ]
12760
+ );
12761
+ await client.query("COMMIT");
12762
+ return rowToRecord(result.rows[0]);
12763
+ } catch (error) {
12764
+ await client.query("ROLLBACK");
12765
+ if (isActiveRunViolation(error)) {
12766
+ throw new AgentRunAlreadyActiveError();
12767
+ }
12768
+ throw error;
12769
+ } finally {
12770
+ client.release();
12771
+ }
12772
+ }
12773
+ /** Creates an unowned pending run-queue row. */
12774
+ async createPendingRun(input) {
12775
+ const source = input.source ?? "run_api";
12776
+ if (source === "project_task") {
12777
+ if (input.taskId === void 0 || input.taskId.trim().length === 0) {
12778
+ throw new AgentRunInvalidTaskIdError(
12779
+ "A project_task run requires a non-empty taskId"
12780
+ );
12781
+ }
12782
+ } else if (input.taskId !== void 0) {
12783
+ throw new AgentRunInvalidTaskIdError(
12784
+ `A ${source} run cannot carry a taskId`
12785
+ );
12786
+ }
12787
+ if (input.input != null && input.command != null) {
12788
+ throw new AgentRunInputCommandConflictError();
12789
+ }
12790
+ const inputMessageIds = input.inputMessageIds ? normalizeAgentRunMessageIds(input.inputMessageIds) : extractAgentRunInputMessageIds(input.input);
12791
+ const availableAt = input.availableAt ?? null;
12792
+ const limit = input.maxActiveRunsForTenant;
12793
+ const client = await this.pool.connect();
12794
+ try {
12795
+ await client.query("BEGIN");
12796
+ if (limit !== void 0 && limit > 0) {
12797
+ await client.query("SELECT pg_advisory_xact_lock(hashtext($1))", [
12798
+ input.tenantId
12799
+ ]);
12800
+ }
12801
+ await client.query("SELECT pg_advisory_xact_lock(hashtext($1))", [
12802
+ `agent-run:${input.tenantId}:${input.threadId}`
12803
+ ]);
12804
+ if (inputMessageIds.length > 0) {
12805
+ const duplicate = await client.query(
12806
+ `SELECT input_message_ids
12807
+ FROM lattice_agent_runs
12808
+ WHERE tenant_id = $1 AND thread_id = $2
12809
+ AND status IN ('pending', 'running')
12810
+ AND input_message_ids && $3::text[]
12811
+ LIMIT 1`,
12812
+ [input.tenantId, input.threadId, inputMessageIds]
12813
+ );
12814
+ if ((duplicate.rowCount ?? 0) > 0) {
12815
+ const existing = duplicate.rows[0].input_message_ids;
12816
+ const messageId = existing.find((id) => inputMessageIds.includes(id)) ?? inputMessageIds[0];
12817
+ throw new AgentRunDuplicateMessageIdError(messageId);
12818
+ }
12819
+ }
12820
+ if (limit !== void 0 && limit > 0) {
12821
+ const active = await client.query(
12822
+ `SELECT count(*)::text AS count
12823
+ FROM lattice_agent_runs
12824
+ WHERE tenant_id = $1
12825
+ AND source = 'run_api'
12826
+ AND status IN ('pending', 'running')`,
12827
+ [input.tenantId]
12828
+ );
12829
+ if (Number(active.rows[0].count) >= limit) {
12830
+ throw new AgentRunTenantQuotaExceededError(input.tenantId, limit);
12831
+ }
12832
+ }
12833
+ const result = await client.query(
12834
+ `INSERT INTO lattice_agent_runs
12835
+ (run_id, tenant_id, workspace_id, project_id, assistant_id, thread_id,
12836
+ status, source, owner, lease_expires_at, input_message_ids, attempts,
12837
+ input, command, config, metadata, capability_runtime_revision,
12838
+ available_at, created_at, updated_at, task_id)
12839
+ VALUES ($1, $2, $3, $4, $5, $6, 'pending', $7, NULL, NULL, $8, 0,
12840
+ $9::jsonb, $10::jsonb, $11::jsonb, $12::jsonb, $13,
12841
+ COALESCE(
12842
+ to_timestamp($14::double precision / 1000.0),
12843
+ date_trunc('milliseconds', now())
12844
+ ),
12845
+ date_trunc('milliseconds', now()),
12846
+ date_trunc('milliseconds', now()), $15)
12847
+ RETURNING *`,
12848
+ [
12849
+ input.runId,
12850
+ input.tenantId,
12851
+ input.workspaceId ?? null,
12852
+ input.projectId ?? null,
12853
+ input.assistantId,
12854
+ input.threadId,
12855
+ source,
12856
+ inputMessageIds,
12857
+ jsonParam(input.input),
12858
+ jsonParam(input.command),
12859
+ jsonParam(input.config),
12860
+ jsonParam(input.metadata),
12861
+ input.capabilityRuntimeRevision ?? null,
12862
+ availableAt,
12863
+ input.taskId ?? null
12864
+ ]
12865
+ );
12866
+ await client.query("COMMIT");
12867
+ return rowToRecord(result.rows[0]);
12868
+ } catch (error) {
12869
+ await client.query("ROLLBACK");
12870
+ throw error;
12871
+ } finally {
12872
+ client.release();
12873
+ }
12874
+ }
12875
+ /** Lists claimable run-queue rows, oldest available first. */
12876
+ async listPendingRuns(options) {
12877
+ const limit = clampLimit(options?.limit);
12878
+ const after = options?.after;
12879
+ const fairTenants = options?.fairTenants === true;
12880
+ if (fairTenants && after !== void 0) {
12881
+ throw new RangeError(
12882
+ "listPendingRuns fairTenants does not support the after cursor"
12883
+ );
12884
+ }
12885
+ const maxRunning = positiveLimit(options?.maxRunningPerTenant);
12886
+ const sources = options?.sources === void 0 ? ["run_api"] : [...options.sources];
12887
+ assertQueueSources(sources);
12888
+ const params = [limit];
12889
+ let cursorClause = "";
12890
+ if (after) {
12891
+ params.push(
12892
+ new Date(after.availableAt),
12893
+ new Date(after.createdAt),
12894
+ after.runId
12895
+ );
12896
+ cursorClause = `
12897
+ AND (r.available_at, r.created_at, r.run_id)
12898
+ > ($2::timestamptz, $3::timestamptz, $4::uuid)`;
12899
+ }
12900
+ params.push(maxRunning ?? null);
12901
+ const capIndex = params.length;
12902
+ params.push(sources);
12903
+ const sourcesIndex = params.length;
12904
+ const capClause = `
12905
+ AND ($${capIndex}::bigint IS NULL OR (
12906
+ SELECT count(*) FROM lattice_agent_runs AS c
12907
+ WHERE c.tenant_id = r.tenant_id
12908
+ AND c.source = ANY($${sourcesIndex}::text[])
12909
+ AND c.status = 'running'
12910
+ ) < $${capIndex}::bigint)`;
12911
+ const filterClause = `WHERE r.source = ANY($${sourcesIndex}::text[]) AND r.status = 'pending'
12912
+ AND r.available_at <= now()
12913
+ AND NOT EXISTS (
12914
+ SELECT 1 FROM lattice_agent_runs AS o
12915
+ WHERE o.tenant_id = r.tenant_id
12916
+ AND o.thread_id = r.thread_id
12917
+ AND o.source IN ('run_api', 'project_task')
12918
+ AND o.status = 'running'
12919
+ )
12920
+ AND NOT EXISTS (
12921
+ SELECT 1 FROM lattice_agent_runs AS b
12922
+ WHERE b.tenant_id = r.tenant_id
12923
+ AND b.thread_id = r.thread_id
12924
+ AND b.source = 'run_api'
12925
+ AND b.rollback_requested = TRUE
12926
+ )${capClause}${cursorClause}`;
12927
+ if (fairTenants) {
12928
+ const ranked = await this.pool.query(
12929
+ `WITH ranked AS (
12930
+ SELECT r.*,
12931
+ row_number() OVER (
12932
+ PARTITION BY r.tenant_id
12933
+ ORDER BY r.available_at ASC, r.created_at ASC, r.run_id ASC
12934
+ ) AS tenant_rank
12935
+ FROM lattice_agent_runs AS r
12936
+ ${filterClause}
12937
+ )
12938
+ SELECT * FROM ranked
12939
+ ORDER BY tenant_rank ASC, available_at ASC, created_at ASC,
12940
+ run_id ASC, tenant_id ASC
12941
+ LIMIT $1`,
12942
+ params
12943
+ );
12944
+ return ranked.rows.map(rowToRecord);
12945
+ }
12946
+ const result = await this.pool.query(
12947
+ `SELECT r.* FROM lattice_agent_runs AS r
12948
+ ${filterClause}
12949
+ ORDER BY r.available_at ASC, r.created_at ASC, r.run_id ASC
12950
+ LIMIT $1`,
12951
+ params
12952
+ );
12953
+ return result.rows.map(rowToRecord);
12954
+ }
12955
+ /** Atomically claims a pending run-queue row. */
12956
+ async claimRun(input) {
12957
+ const maxRunning = positiveLimit(input.maxRunningPerTenant);
12958
+ const sources = input.sources === void 0 ? ["run_api"] : [...input.sources];
12959
+ assertQueueSources(sources);
12960
+ if (maxRunning !== void 0) {
12961
+ return this.claimRunWithTenantCap(input, maxRunning, sources);
12962
+ }
12963
+ try {
12964
+ const result = await this.pool.query(
12965
+ CLAIM_RUN_SQL,
12966
+ [
12967
+ input.runId,
12968
+ input.owner,
12969
+ input.leaseMs,
12970
+ input.inputMessageIds ?? null,
12971
+ sources
12972
+ ]
12973
+ );
12974
+ return result.rowCount === 0 ? void 0 : rowToRecord(result.rows[0]);
12975
+ } catch (error) {
12976
+ if (isActiveRunViolation(error)) return void 0;
12977
+ throw error;
12978
+ }
12979
+ }
12980
+ /**
12981
+ * Claims under a per-tenant running cap.
12982
+ *
12983
+ * @remarks Serializes claims for one tenant with a transaction-scoped
12984
+ * advisory lock, counts running rows, then runs the same claim statement.
12985
+ * The lock is shared with admission creates so cap checks cannot race.
12986
+ */
12987
+ async claimRunWithTenantCap(input, maxRunning, sources) {
12988
+ const client = await this.pool.connect();
12989
+ try {
12990
+ await client.query("BEGIN");
12991
+ const candidate = await client.query(
12992
+ `SELECT tenant_id FROM lattice_agent_runs
12993
+ WHERE run_id = $1 AND source = ANY($2::text[]) AND status = 'pending'`,
12994
+ [input.runId, sources]
12995
+ );
12996
+ if (candidate.rowCount === 0) {
12997
+ await client.query("COMMIT");
12998
+ return void 0;
12999
+ }
13000
+ const tenantId = candidate.rows[0].tenant_id;
13001
+ await client.query("SELECT pg_advisory_xact_lock(hashtext($1))", [
13002
+ tenantId
13003
+ ]);
13004
+ const running = await client.query(
13005
+ `SELECT count(*)::text AS count
13006
+ FROM lattice_agent_runs
13007
+ WHERE tenant_id = $1
13008
+ AND source = ANY($2::text[])
13009
+ AND status = 'running'`,
13010
+ [tenantId, sources]
13011
+ );
13012
+ if (Number(running.rows[0].count) >= maxRunning) {
13013
+ await client.query("COMMIT");
13014
+ return void 0;
13015
+ }
13016
+ const result = await client.query(
13017
+ CLAIM_RUN_SQL,
13018
+ [
13019
+ input.runId,
13020
+ input.owner,
13021
+ input.leaseMs,
13022
+ input.inputMessageIds ?? null,
13023
+ sources
13024
+ ]
13025
+ );
13026
+ await client.query("COMMIT");
13027
+ return result.rowCount === 0 ? void 0 : rowToRecord(result.rows[0]);
13028
+ } catch (error) {
13029
+ await client.query("ROLLBACK");
13030
+ if (isActiveRunViolation(error)) return void 0;
13031
+ throw error;
13032
+ } finally {
13033
+ client.release();
13034
+ }
13035
+ }
13036
+ /** Captures the checkpoint a running run-queue row started from. */
13037
+ async setBaseCheckpoint(tenantId, runId, owner, baseCheckpointId) {
13038
+ const result = await this.pool.query(
13039
+ `UPDATE lattice_agent_runs
13040
+ SET base_checkpoint_id = $4,
13041
+ base_checkpoint_captured = TRUE,
13042
+ updated_at = now()
13043
+ WHERE tenant_id = $1 AND run_id = $2 AND owner = $3
13044
+ AND status = 'running'
13045
+ AND source IN ('run_api', 'project_task')
13046
+ RETURNING run_id`,
13047
+ [tenantId, runId, owner, baseCheckpointId]
13048
+ );
13049
+ return (result.rowCount ?? 0) > 0;
13050
+ }
13051
+ /** Records the resolved capability runtime revision for a running run. */
13052
+ async setCapabilityRevision(tenantId, runId, owner, revision) {
13053
+ const result = await this.pool.query(
13054
+ `UPDATE lattice_agent_runs
13055
+ SET capability_runtime_revision = $4, updated_at = now()
13056
+ WHERE tenant_id = $1 AND run_id = $2 AND owner = $3
13057
+ AND status = 'running'
13058
+ AND source IN ('run_api', 'project_task')
13059
+ RETURNING run_id`,
13060
+ [tenantId, runId, owner, revision]
13061
+ );
13062
+ return (result.rowCount ?? 0) > 0;
13063
+ }
13064
+ /** Deletes a run row immediately, optionally guarded by status/owner. */
13065
+ async deleteRun(tenantId, runId, expected) {
13066
+ const params = [tenantId, runId];
13067
+ let predicate = "tenant_id = $1 AND run_id = $2";
13068
+ if (expected?.status !== void 0) {
13069
+ params.push(expected.status);
13070
+ predicate += ` AND status = $${params.length}`;
13071
+ }
13072
+ if (expected?.owner !== void 0) {
13073
+ params.push(expected.owner);
13074
+ predicate += ` AND owner = $${params.length}`;
13075
+ }
13076
+ const result = await this.pool.query(
13077
+ `DELETE FROM lattice_agent_runs WHERE ${predicate}`,
13078
+ params
13079
+ );
13080
+ return (result.rowCount ?? 0) > 0;
13081
+ }
13082
+ /** Deletes terminal rows finished before `cutoffMs`, oldest first. */
13083
+ async deleteTerminalBefore(cutoffMs, limit) {
13084
+ if (!Number.isFinite(cutoffMs)) return 0;
13085
+ const max = Number.isFinite(limit) ? Math.floor(limit) : 0;
13086
+ if (max <= 0) return 0;
13087
+ const result = await this.pool.query(
13088
+ `DELETE FROM lattice_agent_runs
13089
+ WHERE run_id IN (
13090
+ SELECT run_id FROM lattice_agent_runs
13091
+ WHERE status IN ('success', 'error', 'interrupted', 'cancelled', 'timeout')
13092
+ AND finished_at < to_timestamp($1::double precision / 1000.0)
13093
+ ORDER BY finished_at ASC
13094
+ LIMIT $2
13095
+ )`,
13096
+ [cutoffMs, max]
13097
+ );
13098
+ return result.rowCount ?? 0;
13099
+ }
13100
+ /** Extends a running run's lease when `owner` still holds it. */
13101
+ async heartbeatRun(runId, owner, leaseMs) {
13102
+ const result = await this.pool.query(
13103
+ `UPDATE lattice_agent_runs
13104
+ SET lease_expires_at = now() + ($3::bigint || ' milliseconds')::interval,
13105
+ updated_at = now()
13106
+ WHERE run_id = $1 AND owner = $2 AND status = 'running'
13107
+ RETURNING run_id`,
13108
+ [runId, owner, leaseMs]
13109
+ );
13110
+ return (result.rowCount ?? 0) > 0;
13111
+ }
13112
+ /** Settles a running run when `owner` still holds it. */
13113
+ async settleRun(runId, owner, status, error) {
13114
+ const result = await this.pool.query(
13115
+ `UPDATE lattice_agent_runs
13116
+ SET status = $3, error = $4, finished_at = now(), updated_at = now()
13117
+ WHERE run_id = $1 AND owner = $2 AND status = 'running'
13118
+ RETURNING run_id`,
13119
+ [runId, owner, status, error ?? null]
13120
+ );
13121
+ return (result.rowCount ?? 0) > 0;
13122
+ }
13123
+ /** Settles a running run only when no rollback is pending. */
13124
+ async settleRunUnlessRollbackRequested(runId, owner, status, error) {
13125
+ const result = await this.pool.query(
13126
+ `UPDATE lattice_agent_runs
13127
+ SET status = $3, error = $4, finished_at = now(), updated_at = now()
13128
+ WHERE run_id = $1 AND owner = $2 AND status = 'running'
13129
+ AND rollback_requested = FALSE
13130
+ RETURNING run_id`,
13131
+ [runId, owner, status, error ?? null]
13132
+ );
13133
+ return (result.rowCount ?? 0) > 0;
13134
+ }
13135
+ /** Terminates an uncaptured rollback run with a repair signal. */
13136
+ async settleRollbackFailure(tenantId, runId, error) {
13137
+ const result = await this.pool.query(
13138
+ `UPDATE lattice_agent_runs
13139
+ SET status = 'error',
13140
+ error = $3,
13141
+ rollback_requested = FALSE,
13142
+ rollback_error = $3,
13143
+ finished_at = now(),
13144
+ updated_at = now()
13145
+ WHERE tenant_id = $1 AND run_id = $2
13146
+ AND source = 'run_api'
13147
+ AND rollback_requested = TRUE
13148
+ AND base_checkpoint_captured = FALSE
13149
+ AND status IN ('running', 'interrupted')
13150
+ RETURNING run_id`,
13151
+ [tenantId, runId, error]
13152
+ );
13153
+ return (result.rowCount ?? 0) > 0;
13154
+ }
13155
+ /** Records one bounded rollback failure and optionally converges. */
13156
+ async recordRollbackFailure(tenantId, runId, error, maxAttempts) {
13157
+ const result = await this.pool.query(
13158
+ `UPDATE lattice_agent_runs
13159
+ SET rollback_attempts = rollback_attempts + 1,
13160
+ rollback_error = $3,
13161
+ status = CASE
13162
+ WHEN rollback_attempts + 1 >= $4 THEN 'error'
13163
+ ELSE status
13164
+ END,
13165
+ error = CASE
13166
+ WHEN rollback_attempts + 1 >= $4 THEN $3
13167
+ ELSE error
13168
+ END,
13169
+ rollback_requested = CASE
13170
+ WHEN rollback_attempts + 1 >= $4 THEN FALSE
13171
+ ELSE rollback_requested
13172
+ END,
13173
+ finished_at = CASE
13174
+ WHEN rollback_attempts + 1 >= $4 THEN now()
13175
+ ELSE finished_at
13176
+ END,
13177
+ updated_at = now()
13178
+ WHERE tenant_id = $1
13179
+ AND run_id = $2
13180
+ AND source = 'run_api'
13181
+ AND rollback_requested = TRUE
13182
+ AND status IN ('running', 'interrupted')
13183
+ RETURNING rollback_attempts, status`,
13184
+ [tenantId, runId, error, maxAttempts]
13185
+ );
13186
+ if (result.rowCount === 0) return void 0;
13187
+ const row = result.rows[0];
13188
+ return {
13189
+ attempts: row.rollback_attempts,
13190
+ terminal: row.status === "error"
13191
+ };
13192
+ }
13193
+ /** Reads a run within a tenant. */
13194
+ async getRun(tenantId, runId) {
13195
+ const result = await this.pool.query(
13196
+ `SELECT * FROM lattice_agent_runs WHERE tenant_id = $1 AND run_id = $2`,
13197
+ [tenantId, runId]
13198
+ );
13199
+ return result.rowCount === 0 ? void 0 : rowToRecord(result.rows[0]);
13200
+ }
13201
+ /** Finds the active (pending/running) run for a ProjectTask. */
13202
+ async getActiveRunForTask(tenantId, taskId) {
13203
+ const result = await this.pool.query(
13204
+ `SELECT * FROM lattice_agent_runs
13205
+ WHERE tenant_id = $1 AND task_id = $2
13206
+ AND status IN ('pending', 'running')
13207
+ ORDER BY created_at DESC, run_id DESC
13208
+ LIMIT 1`,
13209
+ [tenantId, taskId]
13210
+ );
13211
+ return result.rowCount === 0 ? void 0 : rowToRecord(result.rows[0]);
13212
+ }
13213
+ /** Lists a thread's runs, newest first (including pending rows). */
13214
+ async listRuns(tenantId, threadId, options) {
13215
+ const limit = clampLimit(options?.limit);
13216
+ const offset = Math.max(0, Math.floor(options?.offset ?? 0));
13217
+ const result = await this.pool.query(
13218
+ `SELECT * FROM lattice_agent_runs
13219
+ WHERE tenant_id = $1 AND thread_id = $2
13220
+ ORDER BY created_at DESC
13221
+ LIMIT $3 OFFSET $4`,
13222
+ [tenantId, threadId, limit, offset]
13223
+ );
13224
+ return result.rows.map(rowToRecord);
13225
+ }
13226
+ /** Finds the thread's running run, if any. */
13227
+ async findRunning(tenantId, threadId) {
13228
+ const result = await this.pool.query(
13229
+ `SELECT * FROM lattice_agent_runs
13230
+ WHERE tenant_id = $1 AND thread_id = $2 AND status = 'running'
13231
+ LIMIT 1`,
13232
+ [tenantId, threadId]
13233
+ );
13234
+ return result.rowCount === 0 ? void 0 : rowToRecord(result.rows[0]);
13235
+ }
13236
+ /** Requests cancellation, settling pending/expired rows interrupted. */
13237
+ async requestCancel(tenantId, runId) {
13238
+ const client = await this.pool.connect();
13239
+ try {
13240
+ await client.query("BEGIN");
13241
+ const current = await client.query(
13242
+ `SELECT status,
13243
+ COALESCE(lease_expires_at < now(), TRUE) AS expired
13244
+ FROM lattice_agent_runs
13245
+ WHERE tenant_id = $1 AND run_id = $2
13246
+ FOR UPDATE`,
13247
+ [tenantId, runId]
13248
+ );
13249
+ if (current.rowCount === 0) {
13250
+ await client.query("COMMIT");
13251
+ return "not_found";
13252
+ }
13253
+ const row = current.rows[0];
13254
+ if (row.status === "pending") {
13255
+ await client.query(
13256
+ `UPDATE lattice_agent_runs
13257
+ SET status = 'interrupted', cancel_requested = TRUE,
13258
+ finished_at = now(), updated_at = now()
13259
+ WHERE tenant_id = $1 AND run_id = $2`,
13260
+ [tenantId, runId]
13261
+ );
13262
+ await client.query("COMMIT");
13263
+ return "cancelled";
13264
+ }
13265
+ if (row.status !== "running") {
13266
+ await client.query("COMMIT");
13267
+ return "terminal";
13268
+ }
13269
+ if (row.expired) {
13270
+ await client.query(
13271
+ `UPDATE lattice_agent_runs
13272
+ SET status = 'interrupted', cancel_requested = TRUE,
13273
+ finished_at = now(), updated_at = now()
13274
+ WHERE tenant_id = $1 AND run_id = $2`,
13275
+ [tenantId, runId]
13276
+ );
13277
+ await client.query("COMMIT");
13278
+ return "cancelled";
13279
+ }
13280
+ await client.query(
13281
+ `UPDATE lattice_agent_runs
13282
+ SET cancel_requested = TRUE, updated_at = now()
13283
+ WHERE tenant_id = $1 AND run_id = $2`,
13284
+ [tenantId, runId]
13285
+ );
13286
+ await client.query("COMMIT");
13287
+ return "requested";
13288
+ } catch (error) {
13289
+ await client.query("ROLLBACK");
13290
+ throw error;
13291
+ } finally {
13292
+ client.release();
13293
+ }
13294
+ }
13295
+ /** Deletes a pending run or flags a running run for rollback. */
13296
+ async requestRollback(tenantId, runId) {
13297
+ const client = await this.pool.connect();
13298
+ try {
13299
+ await client.query("BEGIN");
13300
+ const current = await client.query(
13301
+ `SELECT status, source FROM lattice_agent_runs
13302
+ WHERE tenant_id = $1 AND run_id = $2
13303
+ FOR UPDATE`,
13304
+ [tenantId, runId]
13305
+ );
13306
+ if (current.rowCount === 0) {
13307
+ await client.query("COMMIT");
13308
+ return "not_found";
13309
+ }
13310
+ const { status, source } = current.rows[0];
13311
+ if (source !== "run_api") {
13312
+ await client.query("COMMIT");
13313
+ return "terminal";
13314
+ }
13315
+ if (status === "pending") {
13316
+ await client.query(
13317
+ `DELETE FROM lattice_agent_runs WHERE tenant_id = $1 AND run_id = $2`,
13318
+ [tenantId, runId]
13319
+ );
13320
+ await client.query("COMMIT");
13321
+ return "deleted";
13322
+ }
13323
+ if (status !== "running") {
13324
+ await client.query("COMMIT");
13325
+ return "terminal";
13326
+ }
13327
+ await client.query(
13328
+ `UPDATE lattice_agent_runs
13329
+ SET rollback_requested = TRUE, cancel_requested = TRUE,
13330
+ updated_at = now()
13331
+ WHERE tenant_id = $1 AND run_id = $2`,
13332
+ [tenantId, runId]
13333
+ );
13334
+ await client.query("COMMIT");
13335
+ return "requested";
13336
+ } catch (error) {
13337
+ await client.query("ROLLBACK");
13338
+ throw error;
13339
+ } finally {
13340
+ client.release();
13341
+ }
13342
+ }
13343
+ /** Atomically reclaims an expired running row without requeueing it. */
13344
+ async recoverExpiredRun(runId, owner, leaseMs) {
13345
+ const result = await this.pool.query(
13346
+ `UPDATE lattice_agent_runs
13347
+ SET owner = $2,
13348
+ lease_expires_at = now() + ($3::bigint || ' milliseconds')::interval,
13349
+ attempts = attempts + 1,
13350
+ updated_at = now()
13351
+ WHERE run_id = $1
13352
+ AND source IN ('run_api', 'project_task')
13353
+ AND status = 'running'
13354
+ AND lease_expires_at < now()
13355
+ RETURNING *`,
13356
+ [runId, owner, leaseMs]
13357
+ );
13358
+ return result.rowCount === 0 ? void 0 : rowToRecord(result.rows[0]);
13359
+ }
13360
+ /** Atomically requeues or interrupts an expired run-queue row. */
13361
+ async settleExpiredRun(runId, outcome, options) {
13362
+ if (outcome === "pending") {
13363
+ const availableAt = options?.availableAt ?? null;
13364
+ const result2 = await this.pool.query(
13365
+ `UPDATE lattice_agent_runs
13366
+ SET status = 'pending',
13367
+ owner = NULL,
13368
+ lease_expires_at = NULL,
13369
+ started_at = NULL,
13370
+ available_at = COALESCE(
13371
+ to_timestamp($2::double precision / 1000.0),
13372
+ date_trunc('milliseconds', now())
13373
+ ),
13374
+ finished_at = NULL,
13375
+ updated_at = now()
13376
+ WHERE run_id = $1
13377
+ AND source IN ('run_api', 'project_task')
13378
+ AND status = 'running'
13379
+ AND lease_expires_at < now()
13380
+ RETURNING run_id`,
13381
+ [runId, availableAt]
13382
+ );
13383
+ return (result2.rowCount ?? 0) > 0;
13384
+ }
13385
+ const result = await this.pool.query(
13386
+ `UPDATE lattice_agent_runs
13387
+ SET status = $3,
13388
+ error = COALESCE($2, error),
13389
+ finished_at = now(),
13390
+ updated_at = now()
13391
+ WHERE run_id = $1
13392
+ AND source IN ('run_api', 'project_task')
13393
+ AND status = 'running'
13394
+ AND lease_expires_at < now()
13395
+ RETURNING run_id`,
13396
+ [
13397
+ runId,
13398
+ options?.error ?? null,
13399
+ outcome === "timeout" ? "timeout" : "interrupted"
13400
+ ]
13401
+ );
13402
+ return (result.rowCount ?? 0) > 0;
13403
+ }
13404
+ /** Counts runs grouped by lifecycle status. */
13405
+ async counts() {
13406
+ const counts = {
13407
+ pending: 0,
13408
+ running: 0,
13409
+ success: 0,
13410
+ error: 0,
13411
+ interrupted: 0,
13412
+ cancelled: 0,
13413
+ timeout: 0
13414
+ };
13415
+ const result = await this.pool.query(
13416
+ `SELECT status, count(*)::int AS count
13417
+ FROM lattice_agent_runs
13418
+ GROUP BY status`
13419
+ );
13420
+ for (const row of result.rows) {
13421
+ const status = parseStatus(row.status);
13422
+ if (status) counts[status] = row.count;
13423
+ }
13424
+ return counts;
13425
+ }
13426
+ /** Reports expired run-queue rows for the requested sources. */
13427
+ async reapExpired(sources = ["run_api"]) {
13428
+ assertQueueSources(sources);
13429
+ const result = await this.pool.query(
13430
+ `SELECT * FROM lattice_agent_runs
13431
+ WHERE source = ANY($1::text[]) AND status = 'running'
13432
+ AND lease_expires_at < now()
13433
+ ORDER BY lease_expires_at ASC`,
13434
+ [[...sources]]
13435
+ );
13436
+ return result.rows.map(reapedFromRow);
13437
+ }
13438
+ /** Lists rollback work that still needs a checkpoint rewind. */
13439
+ async listRollbackRuns() {
13440
+ const result = await this.pool.query(
13441
+ `SELECT * FROM lattice_agent_runs
13442
+ WHERE source = 'run_api'
13443
+ AND rollback_requested = TRUE
13444
+ AND (
13445
+ status = 'interrupted'
13446
+ OR (status = 'running' AND lease_expires_at < now())
13447
+ )
13448
+ ORDER BY updated_at ASC`
13449
+ );
13450
+ return result.rows.map(reapedFromRow);
13451
+ }
13452
+ /** Settles expired legacy rows, preserving the historical supervisor behavior. */
13453
+ async reapLegacyExpired() {
13454
+ const result = await this.pool.query(
13455
+ `UPDATE lattice_agent_runs
13456
+ SET status = 'interrupted',
13457
+ error = COALESCE(error, 'run owner lost'),
13458
+ finished_at = now(),
13459
+ updated_at = now()
13460
+ WHERE source = 'legacy' AND status = 'running'
13461
+ AND lease_expires_at < now()
13462
+ RETURNING run_id, tenant_id, assistant_id, thread_id, workspace_id, project_id`
13463
+ );
13464
+ return result.rows.map((row) => ({
13465
+ runId: row.run_id,
13466
+ tenantId: row.tenant_id,
13467
+ assistantId: row.assistant_id,
13468
+ threadId: row.thread_id,
13469
+ workspaceId: row.workspace_id ?? void 0,
13470
+ projectId: row.project_id ?? void 0
13471
+ }));
13472
+ }
13473
+ };
13474
+ function positiveLimit(limit) {
13475
+ return limit !== void 0 && Number.isSafeInteger(limit) && limit > 0 ? limit : void 0;
13476
+ }
13477
+ function clampLimit(limit) {
13478
+ if (limit === void 0) return DEFAULT_LIST_LIMIT;
13479
+ const floored = Math.floor(limit);
13480
+ if (!Number.isFinite(floored)) return DEFAULT_LIST_LIMIT;
13481
+ return Math.min(MAX_LIST_LIMIT, Math.max(1, floored));
13482
+ }
13483
+ function jsonParam(value) {
13484
+ return value === void 0 || value === null ? null : JSON.stringify(value);
13485
+ }
13486
+ function parseStatus(value) {
13487
+ switch (value) {
13488
+ case "pending":
13489
+ case "running":
13490
+ case "success":
13491
+ case "error":
13492
+ case "interrupted":
13493
+ case "cancelled":
13494
+ case "timeout":
13495
+ return value;
13496
+ default:
13497
+ return void 0;
13498
+ }
13499
+ }
13500
+ function parseSource(value) {
13501
+ switch (value) {
13502
+ case "run_api":
13503
+ return "run_api";
13504
+ case "project_task":
13505
+ return "project_task";
13506
+ default:
13507
+ return "legacy";
13508
+ }
13509
+ }
13510
+ function toJsonObject(value) {
13511
+ if (value === null || value === void 0) return null;
13512
+ if (typeof value !== "object" || Array.isArray(value)) return null;
13513
+ return value;
13514
+ }
13515
+ function rowToRecord(row) {
13516
+ return {
13517
+ runId: row.run_id,
13518
+ tenantId: row.tenant_id,
13519
+ workspaceId: row.workspace_id ?? void 0,
13520
+ projectId: row.project_id ?? void 0,
13521
+ assistantId: row.assistant_id,
13522
+ threadId: row.thread_id,
13523
+ status: parseStatus(row.status) ?? "error",
13524
+ source: parseSource(row.source),
13525
+ taskId: row.task_id ?? void 0,
13526
+ owner: row.owner,
13527
+ leaseExpiresAt: row.lease_expires_at === null ? null : row.lease_expires_at.getTime(),
13528
+ inputMessageIds: row.input_message_ids,
13529
+ attempts: row.attempts,
13530
+ error: row.error,
13531
+ cancelRequested: row.cancel_requested,
13532
+ rollbackRequested: row.rollback_requested,
13533
+ rollbackAttempts: row.rollback_attempts,
13534
+ rollbackError: row.rollback_error,
13535
+ input: toJsonObject(row.input),
13536
+ command: toJsonObject(row.command),
13537
+ config: toJsonObject(row.config),
13538
+ metadata: toJsonObject(row.metadata),
13539
+ baseCheckpointId: row.base_checkpoint_id,
13540
+ baseCheckpointCaptured: row.base_checkpoint_captured,
13541
+ capabilityRuntimeRevision: row.capability_runtime_revision,
13542
+ availableAt: row.available_at.getTime(),
13543
+ createdAt: row.created_at.getTime(),
13544
+ updatedAt: row.updated_at.getTime(),
13545
+ startedAt: row.started_at === null ? null : row.started_at.getTime(),
13546
+ finishedAt: row.finished_at === null ? null : row.finished_at.getTime()
13547
+ };
13548
+ }
13549
+ function reapedFromRow(row) {
13550
+ return {
13551
+ runId: row.run_id,
13552
+ tenantId: row.tenant_id,
13553
+ assistantId: row.assistant_id,
13554
+ threadId: row.thread_id,
13555
+ workspaceId: row.workspace_id ?? void 0,
13556
+ projectId: row.project_id ?? void 0,
13557
+ source: parseSource(row.source),
13558
+ taskId: row.task_id ?? void 0,
13559
+ attempts: row.attempts,
13560
+ cancelRequested: row.cancel_requested,
13561
+ rollbackRequested: row.rollback_requested,
13562
+ baseCheckpointId: row.base_checkpoint_id,
13563
+ createdAt: row.created_at.getTime(),
13564
+ startedAt: row.started_at === null ? null : row.started_at.getTime(),
13565
+ leaseExpiresAt: row.lease_expires_at === null ? null : row.lease_expires_at.getTime()
13566
+ };
13567
+ }
13568
+ function isActiveRunViolation(error) {
13569
+ if (typeof error !== "object" || error === null) return false;
13570
+ const { code, constraint } = error;
13571
+ return code === "23505" && constraint === "uq_lattice_agent_runs_one_running";
13572
+ }
13573
+
13574
+ // src/stores/PostgreSQLChannelReplyJobStore.ts
13575
+ import { Pool as Pool31 } from "pg";
13576
+ import {
13577
+ assertValidCreateChannelReplyJobInput,
13578
+ normalizeChannelReplyJobListLimit
13579
+ } from "@axiom-lattice/protocols";
13580
+ var DB_CLOCK_MS2 = "FLOOR(EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint";
13581
+ var COLUMNS = `run_id, tenant_id, assistant_id, thread_id, adapter_channel,
13582
+ channel_installation_id, reply_target, input_message_id, stream_cursor, status,
13583
+ attempts, available_at, lease_owner, lease_expires_at, last_error,
13584
+ output_message_id, created_at, updated_at, delivered_at`;
13585
+ function toNumber(value) {
13586
+ if (typeof value === "number") return value;
13587
+ if (typeof value === "string") return Number(value);
13588
+ throw new Error("Invalid channel reply job numeric column");
13589
+ }
13590
+ function toNullableNumber(value) {
13591
+ return value === null ? null : toNumber(value);
13592
+ }
13593
+ function toStatus(value) {
13594
+ if (value === "pending" || value === "delivered" || value === "failed") {
13595
+ return value;
13596
+ }
13597
+ throw new Error(`Invalid channel reply job status: ${value}`);
13598
+ }
13599
+ function mapRow9(row) {
13600
+ return {
13601
+ runId: row.run_id,
13602
+ tenantId: row.tenant_id,
13603
+ assistantId: row.assistant_id,
13604
+ threadId: row.thread_id,
13605
+ adapterChannel: row.adapter_channel,
13606
+ channelInstallationId: row.channel_installation_id,
13607
+ replyTarget: row.reply_target,
13608
+ inputMessageId: row.input_message_id,
13609
+ streamCursor: row.stream_cursor,
13610
+ status: toStatus(row.status),
13611
+ attempts: toNumber(row.attempts),
13612
+ availableAt: toNumber(row.available_at),
13613
+ leaseOwner: row.lease_owner,
13614
+ leaseExpiresAt: toNullableNumber(row.lease_expires_at),
13615
+ lastError: row.last_error,
13616
+ outputMessageId: row.output_message_id,
13617
+ createdAt: toNumber(row.created_at),
13618
+ updatedAt: toNumber(row.updated_at),
13619
+ deliveredAt: toNullableNumber(row.delivered_at)
13620
+ };
13621
+ }
13622
+ var PostgreSQLChannelReplyJobStore = class {
13623
+ /** @param options An externally managed pool, or a pool config/connection URL. */
13624
+ constructor(options) {
13625
+ this.ownsPool = true;
13626
+ this.initialized = false;
13627
+ this.initPromise = null;
13628
+ if (options.pool) {
13629
+ this.pool = options.pool;
13630
+ this.ownsPool = false;
13631
+ this.initialized = true;
13632
+ return;
13633
+ }
13634
+ if (typeof options.poolConfig === "string") {
13635
+ this.pool = new Pool31({ connectionString: options.poolConfig });
13636
+ } else if (options.poolConfig) {
13637
+ this.pool = new Pool31(options.poolConfig);
13638
+ } else {
13639
+ throw new Error("Either pool or poolConfig must be provided");
13640
+ }
13641
+ this.migrationManager = new MigrationManager(this.pool);
13642
+ this.migrationManager.register(createChannelReplyJobsTable);
13643
+ if (options.autoMigrate !== false) {
13644
+ this.initialize().catch((error) => {
13645
+ console.error("Failed to initialize PostgreSQLChannelReplyJobStore:", error);
13646
+ throw error;
13647
+ });
13648
+ }
13649
+ }
13650
+ /** Runs pending migrations once. */
13651
+ async initialize() {
13652
+ if (this.initialized) return;
13653
+ if (this.initPromise) return this.initPromise;
13654
+ this.initPromise = (async () => {
13655
+ try {
13656
+ await this.migrationManager.migrate();
13657
+ this.initialized = true;
13658
+ } finally {
13659
+ this.initPromise = null;
13660
+ }
13661
+ })();
13662
+ return this.initPromise;
13663
+ }
13664
+ /** Ends the pool when this store owns it. */
13665
+ async dispose() {
13666
+ if (this.ownsPool) await this.pool.end();
13667
+ }
13668
+ /** Creates a pending job with database-clock created/updated timestamps. */
13669
+ async create(input) {
13670
+ assertValidCreateChannelReplyJobInput(input);
13671
+ const result = await this.pool.query(
13672
+ `WITH clock AS (SELECT ${DB_CLOCK_MS2} AS now_ms)
13673
+ INSERT INTO lattice_channel_reply_jobs (
13674
+ run_id, tenant_id, assistant_id, thread_id, adapter_channel,
13675
+ channel_installation_id, reply_target, input_message_id, stream_cursor,
13676
+ status, attempts, available_at, lease_owner, lease_expires_at,
13677
+ last_error, output_message_id, created_at, updated_at, delivered_at
13678
+ )
13679
+ SELECT
13680
+ $1, $2, $3, $4, $5,
13681
+ $6, $7::jsonb, $8, $9,
13682
+ 'pending', 0, $10, NULL, NULL,
13683
+ NULL, NULL, clock.now_ms, clock.now_ms, NULL
13684
+ FROM clock
13685
+ RETURNING ${COLUMNS}`,
13686
+ [
13687
+ input.runId,
13688
+ input.tenantId,
13689
+ input.assistantId,
13690
+ input.threadId,
13691
+ input.adapterChannel,
13692
+ input.channelInstallationId,
13693
+ JSON.stringify(input.replyTarget),
13694
+ input.inputMessageId,
13695
+ input.streamCursor ?? null,
13696
+ input.availableAt
13697
+ ]
13698
+ );
13699
+ return mapRow9(result.rows[0]);
13700
+ }
13701
+ /** Reads one job within a tenant scope. */
13702
+ async get(tenantId, runId) {
13703
+ const result = await this.pool.query(
13704
+ `SELECT ${COLUMNS}
13705
+ FROM lattice_channel_reply_jobs
13706
+ WHERE tenant_id = $1 AND run_id = $2`,
13707
+ [tenantId, runId]
13708
+ );
13709
+ return result.rowCount === 0 ? null : mapRow9(result.rows[0]);
13710
+ }
13711
+ /** Lists claimable pending jobs that are due at `now`, oldest first. */
13712
+ async listPending(now, limit) {
13713
+ const result = await this.pool.query(
13714
+ `SELECT ${COLUMNS}
13715
+ FROM lattice_channel_reply_jobs
13716
+ WHERE status = 'pending'
13717
+ AND available_at <= $1
13718
+ AND (lease_owner IS NULL OR lease_expires_at IS NULL OR lease_expires_at <= $1)
13719
+ ORDER BY available_at ASC, created_at ASC, run_id ASC
13720
+ LIMIT $2`,
13721
+ [now, normalizeChannelReplyJobListLimit(limit)]
13722
+ );
13723
+ return result.rows.map(mapRow9);
13724
+ }
13725
+ /**
13726
+ * Atomically leases a claimable job.
13727
+ *
13728
+ * @param now Accepted for interface symmetry; PostgreSQL uses the database clock.
13729
+ */
13730
+ async claim(tenantId, runId, owner, leaseMs, now) {
13731
+ void now;
13732
+ const result = await this.pool.query(
13733
+ `WITH clock AS (SELECT ${DB_CLOCK_MS2} AS now_ms)
13734
+ UPDATE lattice_channel_reply_jobs AS j
13735
+ SET lease_owner = $3,
13736
+ lease_expires_at = clock.now_ms + $4,
13737
+ updated_at = clock.now_ms
13738
+ FROM clock
13739
+ WHERE j.tenant_id = $1
13740
+ AND j.run_id = $2
13741
+ AND j.status = 'pending'
13742
+ AND j.available_at <= clock.now_ms
13743
+ AND (j.lease_owner IS NULL OR j.lease_expires_at IS NULL OR j.lease_expires_at <= clock.now_ms)
13744
+ RETURNING ${COLUMNS}`,
13745
+ [tenantId, runId, owner, leaseMs]
13746
+ );
13747
+ return result.rowCount === 0 ? null : mapRow9(result.rows[0]);
13748
+ }
13749
+ /**
13750
+ * Marks a job delivered when `owner` still holds the lease.
13751
+ *
13752
+ * @param now Accepted for interface symmetry; PostgreSQL uses the database clock.
13753
+ */
13754
+ async markDelivered(tenantId, runId, owner, outputMessageId = null, now) {
13755
+ void now;
13756
+ const result = await this.pool.query(
13757
+ `WITH clock AS (SELECT ${DB_CLOCK_MS2} AS now_ms)
13758
+ UPDATE lattice_channel_reply_jobs AS j
13759
+ SET status = 'delivered',
13760
+ output_message_id = $4,
13761
+ lease_owner = NULL,
13762
+ lease_expires_at = NULL,
13763
+ delivered_at = clock.now_ms,
13764
+ updated_at = clock.now_ms
13765
+ FROM clock
13766
+ WHERE j.tenant_id = $1
13767
+ AND j.run_id = $2
13768
+ AND j.lease_owner = $3
13769
+ AND j.status = 'pending'
13770
+ RETURNING ${COLUMNS}`,
13771
+ [tenantId, runId, owner, outputMessageId]
13772
+ );
13773
+ return result.rowCount === 0 ? null : mapRow9(result.rows[0]);
13774
+ }
13775
+ /**
13776
+ * Returns a job to pending with an incremented attempt count.
13777
+ *
13778
+ * @param now Accepted for interface symmetry; PostgreSQL uses the database clock.
13779
+ */
13780
+ async reschedule(tenantId, runId, owner, error, availableAt, now) {
13781
+ void now;
13782
+ const result = await this.pool.query(
13783
+ `WITH clock AS (SELECT ${DB_CLOCK_MS2} AS now_ms)
13784
+ UPDATE lattice_channel_reply_jobs AS j
13785
+ SET status = 'pending',
13786
+ attempts = attempts + 1,
13787
+ lease_owner = NULL,
13788
+ lease_expires_at = NULL,
13789
+ last_error = $4,
13790
+ available_at = $5,
13791
+ updated_at = clock.now_ms
13792
+ FROM clock
13793
+ WHERE j.tenant_id = $1
13794
+ AND j.run_id = $2
13795
+ AND j.lease_owner = $3
13796
+ AND j.status = 'pending'
13797
+ RETURNING ${COLUMNS}`,
13798
+ [tenantId, runId, owner, error, availableAt]
13799
+ );
13800
+ return result.rowCount === 0 ? null : mapRow9(result.rows[0]);
13801
+ }
13802
+ /**
13803
+ * Marks a job terminally failed when `owner` still holds the lease.
13804
+ *
13805
+ * @param now Accepted for interface symmetry; PostgreSQL uses the database clock.
13806
+ */
13807
+ async markFailed(tenantId, runId, owner, error, now) {
13808
+ void now;
13809
+ const result = await this.pool.query(
13810
+ `WITH clock AS (SELECT ${DB_CLOCK_MS2} AS now_ms)
13811
+ UPDATE lattice_channel_reply_jobs AS j
13812
+ SET status = 'failed',
13813
+ last_error = $4,
13814
+ lease_owner = NULL,
13815
+ lease_expires_at = NULL,
13816
+ updated_at = clock.now_ms
13817
+ FROM clock
13818
+ WHERE j.tenant_id = $1
13819
+ AND j.run_id = $2
13820
+ AND j.lease_owner = $3
13821
+ AND j.status = 'pending'
13822
+ RETURNING ${COLUMNS}`,
13823
+ [tenantId, runId, owner, error]
13824
+ );
13825
+ return result.rowCount === 0 ? null : mapRow9(result.rows[0]);
13826
+ }
13827
+ /** Deletes one job. Returns whether a row was removed. */
13828
+ async delete(tenantId, runId) {
13829
+ const result = await this.pool.query(
13830
+ `DELETE FROM lattice_channel_reply_jobs WHERE tenant_id = $1 AND run_id = $2`,
13831
+ [tenantId, runId]
13832
+ );
13833
+ return (result.rowCount ?? 0) > 0;
13834
+ }
13835
+ /** Deletes terminal jobs whose last update is older than `before`. */
13836
+ async deleteTerminalBefore(before, limit) {
13837
+ const result = await this.pool.query(
13838
+ `DELETE FROM lattice_channel_reply_jobs
13839
+ WHERE run_id IN (
13840
+ SELECT run_id
13841
+ FROM lattice_channel_reply_jobs
13842
+ WHERE status IN ('delivered', 'failed')
13843
+ AND updated_at < $1
13844
+ ORDER BY updated_at ASC, run_id ASC
13845
+ LIMIT $2
13846
+ )`,
13847
+ [before, normalizeChannelReplyJobListLimit(limit)]
13848
+ );
13849
+ return result.rowCount ?? 0;
13850
+ }
13851
+ };
13852
+
13853
+ // src/stores/PostgreSQLCheckpointStorage.ts
13854
+ var CheckpointTargetNotEmptyError = class extends Error {
13855
+ /**
13856
+ * @param threadId - Non-empty target thread id.
13857
+ */
13858
+ constructor(threadId) {
13859
+ super(`Checkpoint target thread ${threadId} is not empty`);
13860
+ /** Machine-readable conflict code. */
13861
+ this.code = "CHECKPOINT_TARGET_NOT_EMPTY";
13862
+ this.name = "CheckpointTargetNotEmptyError";
13863
+ }
13864
+ };
13865
+ var CheckpointBaseNotFoundError = class extends Error {
13866
+ /**
13867
+ * @param threadId - Thread owning the missing base checkpoint.
13868
+ * @param baseCheckpointId - Base checkpoint id that was not found.
13869
+ */
13870
+ constructor(threadId, baseCheckpointId) {
13871
+ super(`Checkpoint ${baseCheckpointId} does not exist on thread ${threadId}`);
13872
+ /** Machine-readable failure code. */
13873
+ this.code = "CHECKPOINT_BASE_NOT_FOUND";
13874
+ this.name = "CheckpointBaseNotFoundError";
13875
+ this.threadId = threadId;
13876
+ this.baseCheckpointId = baseCheckpointId;
13877
+ }
13878
+ };
13879
+ async function rollbackQuietly(client) {
13880
+ try {
13881
+ await client.query("ROLLBACK");
13882
+ } catch {
13883
+ }
13884
+ }
13885
+ function isUniqueViolation3(error) {
13886
+ return typeof error === "object" && error !== null && "code" in error && error.code === "23505";
13887
+ }
13888
+ function isRecord4(value) {
13889
+ return typeof value === "object" && value !== null && !Array.isArray(value);
13890
+ }
13891
+ function checkpointKey(checkpointNs, checkpointId) {
13892
+ return `${checkpointNs}\0${checkpointId}`;
13893
+ }
13894
+ function blobKey(checkpointNs, channel, version) {
13895
+ return `${checkpointNs}\0${channel}\0${version}`;
13896
+ }
13897
+ function checkpointRecord(value) {
13898
+ if (typeof value === "string") {
13899
+ try {
13900
+ const parsed = JSON.parse(value);
13901
+ return isRecord4(parsed) ? parsed : void 0;
13902
+ } catch {
13903
+ return void 0;
13904
+ }
13905
+ }
13906
+ return isRecord4(value) ? value : void 0;
13907
+ }
13908
+ function channelVersions(value) {
13909
+ const checkpoint = checkpointRecord(value);
13910
+ const versions = checkpoint?.channel_versions;
13911
+ return isRecord4(versions) ? versions : void 0;
13912
+ }
13913
+ var DELETE_CHUNK_SIZE = 100;
13914
+ function chunk(items, size) {
13915
+ const chunks = [];
13916
+ for (let index = 0; index < items.length; index += size) {
13917
+ chunks.push(items.slice(index, index + size));
13918
+ }
13919
+ return chunks;
13920
+ }
13921
+ function toCount(value) {
13922
+ const parsed = Number(value ?? 0);
13923
+ return Number.isFinite(parsed) ? parsed : 0;
13924
+ }
13925
+ var PostgreSQLCheckpointStorage = class {
13926
+ /**
13927
+ * @param pool - PostgreSQL pool shared with the rest of the store config.
13928
+ */
13929
+ constructor(pool) {
13930
+ this.pool = pool;
13931
+ }
13932
+ /**
13933
+ * Measure the checkpoint storage used by one thread.
13934
+ *
13935
+ * @param threadId - Thread id whose checkpoint rows should be measured.
13936
+ * @returns Counts and byte sizes grouped by checkpoint storage category.
13937
+ */
13938
+ async get(threadId) {
13939
+ const result = await this.pool.query(
13940
+ `SELECT
13941
+ (SELECT count(*)::bigint FROM checkpoints WHERE thread_id=$1) AS checkpoint_count,
13942
+ (SELECT coalesce(sum(pg_column_size(checkpoint) + pg_column_size(metadata)),0)::bigint FROM checkpoints WHERE thread_id=$1) AS checkpoint_bytes,
13943
+ (SELECT count(*)::bigint FROM checkpoint_blobs WHERE thread_id=$1) AS blob_count,
13944
+ (SELECT coalesce(sum(coalesce(octet_length(blob),0)),0)::bigint FROM checkpoint_blobs WHERE thread_id=$1) AS blob_bytes,
13945
+ (SELECT count(*)::bigint FROM checkpoint_writes WHERE thread_id=$1) AS write_count,
13946
+ (SELECT coalesce(sum(octet_length(blob)),0)::bigint FROM checkpoint_writes WHERE thread_id=$1) AS write_bytes`,
13947
+ [threadId]
13948
+ );
13949
+ const row = result.rows[0];
13950
+ const checkpointCount = toCount(row?.checkpoint_count);
13951
+ const checkpointBytes = toCount(row?.checkpoint_bytes);
13952
+ const blobCount = toCount(row?.blob_count);
13953
+ const blobBytes = toCount(row?.blob_bytes);
13954
+ const writeCount = toCount(row?.write_count);
13955
+ const writeBytes = toCount(row?.write_bytes);
13956
+ return {
13957
+ threadId,
13958
+ totalBytes: checkpointBytes + blobBytes + writeBytes,
13959
+ checkpoints: { count: checkpointCount, bytes: checkpointBytes },
13960
+ channelValues: { count: blobCount, bytes: blobBytes },
13961
+ pendingWrites: { count: writeCount, bytes: writeBytes }
13962
+ };
13963
+ }
13964
+ /**
13965
+ * Copy every checkpoint row from a source thread to an empty target thread.
13966
+ *
13967
+ * @param sourceThreadId - Thread whose rows are copied.
13968
+ * @param targetThreadId - Empty thread that receives the rows.
13969
+ * @returns Resolves when the copy commits; an empty source is a no-op.
13970
+ * @throws {CheckpointTargetNotEmptyError} When the target already owns rows.
13971
+ */
13972
+ async copyThread(sourceThreadId, targetThreadId) {
13973
+ const client = await this.pool.connect();
13974
+ try {
13975
+ await client.query("BEGIN");
13976
+ const target = await client.query(
13977
+ `SELECT (
13978
+ EXISTS (SELECT 1 FROM checkpoints WHERE thread_id=$1)
13979
+ OR EXISTS (SELECT 1 FROM checkpoint_blobs WHERE thread_id=$1)
13980
+ OR EXISTS (SELECT 1 FROM checkpoint_writes WHERE thread_id=$1)
13981
+ ) AS not_empty`,
13982
+ [targetThreadId]
13983
+ );
13984
+ if (target.rows[0]?.not_empty) {
13985
+ throw new CheckpointTargetNotEmptyError(targetThreadId);
13986
+ }
13987
+ await client.query(
13988
+ `INSERT INTO checkpoints (
13989
+ thread_id, checkpoint_ns, checkpoint_id, parent_checkpoint_id, type, checkpoint, metadata
13990
+ )
13991
+ SELECT $2, checkpoint_ns, checkpoint_id, parent_checkpoint_id, type, checkpoint,
13992
+ jsonb_set(coalesce(metadata, '{}'::jsonb), '{thread_id}', to_jsonb($2::text), true)
13993
+ FROM checkpoints WHERE thread_id=$1`,
13994
+ [sourceThreadId, targetThreadId]
13995
+ );
13996
+ await client.query(
13997
+ `INSERT INTO checkpoint_blobs (thread_id, checkpoint_ns, channel, version, type, blob)
13998
+ SELECT $2, checkpoint_ns, channel, version, type, blob
13999
+ FROM checkpoint_blobs WHERE thread_id=$1`,
14000
+ [sourceThreadId, targetThreadId]
14001
+ );
14002
+ await client.query(
14003
+ `INSERT INTO checkpoint_writes (
14004
+ thread_id, checkpoint_ns, checkpoint_id, task_id, idx, channel, type, blob
14005
+ )
14006
+ SELECT $2, checkpoint_ns, checkpoint_id, task_id, idx, channel, type, blob
14007
+ FROM checkpoint_writes WHERE thread_id=$1`,
14008
+ [sourceThreadId, targetThreadId]
14009
+ );
14010
+ await client.query("COMMIT");
14011
+ } catch (error) {
14012
+ await rollbackQuietly(client);
14013
+ if (isUniqueViolation3(error)) {
14014
+ throw new CheckpointTargetNotEmptyError(targetThreadId);
14015
+ }
14016
+ throw error;
14017
+ } finally {
14018
+ client.release();
14019
+ }
14020
+ }
14021
+ /**
14022
+ * Retain only the latest N checkpoints per checkpoint namespace.
14023
+ *
14024
+ * @param threadId - Thread whose checkpoint history is pruned.
14025
+ * @param options - Retention options.
14026
+ * @param options.keepLatest - Number of latest checkpoints to retain per namespace.
14027
+ * @returns Counts of deleted checkpoint, blob, and write rows.
14028
+ */
14029
+ async pruneThread(threadId, options) {
14030
+ if (!Number.isSafeInteger(options.keepLatest) || options.keepLatest < 1) {
14031
+ throw new RangeError("keepLatest must be an integer >= 1");
14032
+ }
14033
+ const keepLatest = options.keepLatest;
14034
+ const client = await this.pool.connect();
14035
+ try {
14036
+ await client.query("BEGIN");
14037
+ const retainedResult = await client.query(
14038
+ `SELECT checkpoint_ns, checkpoint_id, checkpoint FROM (
14039
+ SELECT checkpoint_ns, checkpoint_id, checkpoint,
14040
+ row_number() OVER (
14041
+ PARTITION BY checkpoint_ns ORDER BY checkpoint_id DESC
14042
+ ) AS rn
14043
+ FROM checkpoints WHERE thread_id=$1
14044
+ ) t WHERE rn <= $2`,
14045
+ [threadId, keepLatest]
14046
+ );
14047
+ const retained = retainedResult.rows;
14048
+ const retainedCheckpoints = new Set(
14049
+ retained.map((row) => checkpointKey(row.checkpoint_ns, row.checkpoint_id))
14050
+ );
14051
+ const fallbackNamespaces = /* @__PURE__ */ new Set();
14052
+ const retainedBlobs = /* @__PURE__ */ new Set();
14053
+ for (const row of retained) {
14054
+ const versions = channelVersions(row.checkpoint);
14055
+ if (!versions) {
14056
+ fallbackNamespaces.add(row.checkpoint_ns);
14057
+ continue;
14058
+ }
14059
+ for (const [channel, version] of Object.entries(versions)) {
14060
+ retainedBlobs.add(blobKey(row.checkpoint_ns, channel, String(version)));
14061
+ }
14062
+ }
14063
+ const writeRows = await client.query(
14064
+ `SELECT checkpoint_ns, checkpoint_id, task_id, idx
14065
+ FROM checkpoint_writes WHERE thread_id=$1`,
14066
+ [threadId]
14067
+ );
14068
+ const staleWrites = writeRows.rows.filter(
14069
+ (row) => !retainedCheckpoints.has(checkpointKey(row.checkpoint_ns, row.checkpoint_id))
14070
+ );
14071
+ let writes = 0;
14072
+ for (const batch of chunk(staleWrites, DELETE_CHUNK_SIZE)) {
14073
+ const params = [threadId];
14074
+ const tuples = batch.map((row) => {
14075
+ params.push(row.checkpoint_ns, row.checkpoint_id, row.task_id, row.idx);
14076
+ return `($${params.length - 3}, $${params.length - 2}, $${params.length - 1}, $${params.length})`;
14077
+ });
14078
+ const deleted = await client.query(
14079
+ `DELETE FROM checkpoint_writes
14080
+ WHERE thread_id=$1
14081
+ AND (checkpoint_ns, checkpoint_id, task_id, idx) IN (${tuples.join(", ")})`,
14082
+ params
14083
+ );
14084
+ writes += deleted.rowCount ?? 0;
14085
+ }
14086
+ const checkpointRows = await client.query(
14087
+ `SELECT checkpoint_ns, checkpoint_id FROM checkpoints WHERE thread_id=$1`,
14088
+ [threadId]
14089
+ );
14090
+ const staleCheckpoints = checkpointRows.rows.filter(
14091
+ (row) => !retainedCheckpoints.has(checkpointKey(row.checkpoint_ns, row.checkpoint_id))
14092
+ );
14093
+ let checkpoints = 0;
14094
+ for (const batch of chunk(staleCheckpoints, DELETE_CHUNK_SIZE)) {
14095
+ const params = [threadId];
14096
+ const tuples = batch.map((row) => {
14097
+ params.push(row.checkpoint_ns, row.checkpoint_id);
14098
+ return `($${params.length - 1}, $${params.length})`;
14099
+ });
14100
+ const deleted = await client.query(
14101
+ `DELETE FROM checkpoints
14102
+ WHERE thread_id=$1
14103
+ AND (checkpoint_ns, checkpoint_id) IN (${tuples.join(", ")})`,
14104
+ params
14105
+ );
14106
+ checkpoints += deleted.rowCount ?? 0;
14107
+ }
14108
+ const blobRows = await client.query(
14109
+ `SELECT checkpoint_ns, channel, version FROM checkpoint_blobs WHERE thread_id=$1`,
14110
+ [threadId]
14111
+ );
14112
+ const staleBlobs = blobRows.rows.filter(
14113
+ (row) => !fallbackNamespaces.has(row.checkpoint_ns) && !retainedBlobs.has(blobKey(row.checkpoint_ns, row.channel, row.version))
14114
+ );
14115
+ let blobs = 0;
14116
+ for (const batch of chunk(staleBlobs, DELETE_CHUNK_SIZE)) {
14117
+ const params = [threadId];
14118
+ const tuples = batch.map((row) => {
14119
+ params.push(row.checkpoint_ns, row.channel, row.version);
14120
+ return `($${params.length - 2}, $${params.length - 1}, $${params.length})`;
14121
+ });
14122
+ const deleted = await client.query(
14123
+ `DELETE FROM checkpoint_blobs
14124
+ WHERE thread_id=$1
14125
+ AND (checkpoint_ns, channel, version) IN (${tuples.join(", ")})`,
14126
+ params
14127
+ );
14128
+ blobs += deleted.rowCount ?? 0;
14129
+ }
14130
+ await client.query("COMMIT");
14131
+ return { checkpoints, blobs, writes };
14132
+ } catch (error) {
14133
+ await rollbackQuietly(client);
14134
+ throw error;
14135
+ } finally {
14136
+ client.release();
14137
+ }
14138
+ }
14139
+ /**
14140
+ * Delete the checkpoints that descend from a base checkpoint.
14141
+ *
14142
+ * Descendants are discovered by following both same-namespace
14143
+ * `parent_checkpoint_id` edges and cross-namespace `metadata.parents` edges
14144
+ * (LangGraph's `{ "<parent checkpoint_ns>": "<parent checkpoint id>" }` map on
14145
+ * subgraph checkpoints), recursively across every namespace. Out-of-order or
14146
+ * clock-skewed ids do not affect the result. Writes keyed to the base
14147
+ * checkpoint belong to the rolled-back run's first superstep and are deleted
14148
+ * too. Blobs referenced only by deleted checkpoints are removed.
14149
+ *
14150
+ * @param threadId - Thread whose later checkpoint history is deleted.
14151
+ * @param baseCheckpointId - Checkpoint to retain as the new tip, or `null` to
14152
+ * delete every checkpoint for the thread.
14153
+ * @returns Resolves when the deletion commits; no descendants is a no-op.
14154
+ * @throws {RangeError} When `baseCheckpointId` is an empty string.
14155
+ * @throws {CheckpointBaseNotFoundError} When the base checkpoint does not
14156
+ * exist on the thread.
14157
+ */
14158
+ async deleteAfter(threadId, baseCheckpointId) {
14159
+ if (baseCheckpointId !== null && baseCheckpointId.length === 0) {
14160
+ throw new RangeError("baseCheckpointId must be null or a non-empty checkpoint id");
14161
+ }
14162
+ const client = await this.pool.connect();
14163
+ try {
14164
+ await client.query("BEGIN");
14165
+ if (baseCheckpointId === null) {
14166
+ await client.query("DELETE FROM checkpoint_writes WHERE thread_id=$1", [threadId]);
14167
+ await client.query("DELETE FROM checkpoint_blobs WHERE thread_id=$1", [threadId]);
14168
+ await client.query("DELETE FROM checkpoints WHERE thread_id=$1", [threadId]);
14169
+ await client.query("COMMIT");
14170
+ return;
14171
+ }
14172
+ const base = await client.query(
14173
+ `SELECT checkpoint_ns
14174
+ FROM checkpoints
14175
+ WHERE thread_id=$1 AND checkpoint_id=$2`,
14176
+ [threadId, baseCheckpointId]
14177
+ );
14178
+ if (base.rows.length === 0) {
14179
+ throw new CheckpointBaseNotFoundError(threadId, baseCheckpointId);
14180
+ }
14181
+ const baseCheckpointNs = base.rows[0].checkpoint_ns;
14182
+ const descendantResult = await client.query(
14183
+ `WITH RECURSIVE lineage(checkpoint_ns, checkpoint_id) AS (
14184
+ SELECT $2::text, $3::text
14185
+ UNION
14186
+ SELECT c.checkpoint_ns, c.checkpoint_id
14187
+ FROM checkpoints c
14188
+ JOIN lineage l ON (
14189
+ (c.checkpoint_ns = l.checkpoint_ns
14190
+ AND c.parent_checkpoint_id = l.checkpoint_id)
14191
+ OR (c.metadata->'parents'->>l.checkpoint_ns = l.checkpoint_id)
14192
+ )
14193
+ WHERE c.thread_id=$1
14194
+ ),
14195
+ roots AS (
14196
+ SELECT $2::text AS checkpoint_ns, $3::text AS checkpoint_id
14197
+ )
14198
+ SELECT l.checkpoint_ns, l.checkpoint_id
14199
+ FROM lineage l
14200
+ WHERE NOT EXISTS (
14201
+ SELECT 1 FROM roots r
14202
+ WHERE r.checkpoint_ns = l.checkpoint_ns
14203
+ AND r.checkpoint_id = l.checkpoint_id
14204
+ )`,
14205
+ [threadId, baseCheckpointNs, baseCheckpointId]
14206
+ );
14207
+ const descendants = descendantResult.rows;
14208
+ await client.query(
14209
+ `DELETE FROM checkpoint_writes
14210
+ WHERE thread_id=$1 AND checkpoint_ns=$2 AND checkpoint_id=$3`,
14211
+ [threadId, baseCheckpointNs, baseCheckpointId]
14212
+ );
14213
+ for (const batch of chunk(descendants, DELETE_CHUNK_SIZE)) {
14214
+ const params = [threadId];
14215
+ const tuples = batch.map((row) => {
14216
+ params.push(row.checkpoint_ns, row.checkpoint_id);
14217
+ return `($${params.length - 1}, $${params.length})`;
14218
+ });
14219
+ await client.query(
14220
+ `DELETE FROM checkpoint_writes
14221
+ WHERE thread_id=$1
14222
+ AND (checkpoint_ns, checkpoint_id) IN (${tuples.join(", ")})`,
14223
+ params
14224
+ );
14225
+ await client.query(
14226
+ `DELETE FROM checkpoints
14227
+ WHERE thread_id=$1
14228
+ AND (checkpoint_ns, checkpoint_id) IN (${tuples.join(", ")})`,
14229
+ params
14230
+ );
14231
+ }
14232
+ const retainedResult = await client.query(
14233
+ `SELECT checkpoint_ns, checkpoint_id, checkpoint
14234
+ FROM checkpoints WHERE thread_id=$1`,
14235
+ [threadId]
14236
+ );
14237
+ const fallbackNamespaces = /* @__PURE__ */ new Set();
14238
+ const retainedBlobs = /* @__PURE__ */ new Set();
14239
+ for (const row of retainedResult.rows) {
14240
+ const versions = channelVersions(row.checkpoint);
14241
+ if (!versions) {
14242
+ fallbackNamespaces.add(row.checkpoint_ns);
14243
+ continue;
14244
+ }
14245
+ for (const [channel, version] of Object.entries(versions)) {
14246
+ retainedBlobs.add(blobKey(row.checkpoint_ns, channel, String(version)));
14247
+ }
14248
+ }
14249
+ const blobRows = await client.query(
14250
+ `SELECT checkpoint_ns, channel, version FROM checkpoint_blobs WHERE thread_id=$1`,
14251
+ [threadId]
14252
+ );
14253
+ const staleBlobs = blobRows.rows.filter(
14254
+ (row) => !fallbackNamespaces.has(row.checkpoint_ns) && !retainedBlobs.has(blobKey(row.checkpoint_ns, row.channel, row.version))
14255
+ );
14256
+ for (const batch of chunk(staleBlobs, DELETE_CHUNK_SIZE)) {
14257
+ const params = [threadId];
14258
+ const tuples = batch.map((row) => {
14259
+ params.push(row.checkpoint_ns, row.channel, row.version);
14260
+ return `($${params.length - 2}, $${params.length - 1}, $${params.length})`;
14261
+ });
14262
+ await client.query(
14263
+ `DELETE FROM checkpoint_blobs
14264
+ WHERE thread_id=$1
14265
+ AND (checkpoint_ns, channel, version) IN (${tuples.join(", ")})`,
14266
+ params
14267
+ );
14268
+ }
14269
+ await client.query("COMMIT");
14270
+ } catch (error) {
14271
+ await rollbackQuietly(client);
14272
+ throw error;
14273
+ } finally {
14274
+ client.release();
14275
+ }
14276
+ }
14277
+ /**
14278
+ * Delete every checkpoint row for a thread.
14279
+ *
14280
+ * @param threadId - Thread whose checkpoint rows are deleted.
14281
+ * @returns Resolves when the deletion commits.
14282
+ */
14283
+ async deleteThread(threadId) {
14284
+ const client = await this.pool.connect();
14285
+ try {
14286
+ await client.query("BEGIN");
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
+ } catch (error) {
14292
+ await rollbackQuietly(client);
14293
+ throw error;
14294
+ } finally {
14295
+ client.release();
14296
+ }
14297
+ }
14298
+ };
14299
+
14300
+ // src/stores/PostgreSQLStore.ts
14301
+ import {
14302
+ assertStoreIdentity,
14303
+ assertStoreNamespacePrefix,
14304
+ storeExpiresAt,
14305
+ storePaginationValue
14306
+ } from "@axiom-lattice/protocols";
14307
+ function mapRow10(row) {
14308
+ return {
14309
+ namespace: row.namespace,
14310
+ key: row.key,
14311
+ value: row.value ?? {},
14312
+ createdAt: row.created_at,
14313
+ updatedAt: row.updated_at,
14314
+ expiresAt: row.expires_at ?? void 0
14315
+ };
14316
+ }
14317
+ var PostgreSQLStore = class {
14318
+ /**
14319
+ * @param pool - Pool shared with the rest of the store config.
14320
+ */
14321
+ constructor(pool) {
14322
+ this.pool = pool;
14323
+ }
14324
+ async put(tenantId, input) {
14325
+ assertStoreIdentity(input.namespace, input.key);
14326
+ const now = /* @__PURE__ */ new Date();
14327
+ const expiresAt = storeExpiresAt(input.ttlMinutes, now) ?? null;
14328
+ const result = await this.pool.query(
14329
+ `INSERT INTO lattice_store_items
14330
+ (tenant_id, namespace, key, value, created_at, updated_at, expires_at)
14331
+ VALUES ($1, $2, $3, $4::jsonb, $5, $5, $6)
14332
+ ON CONFLICT (tenant_id, namespace, key) DO UPDATE SET
14333
+ value = EXCLUDED.value,
14334
+ updated_at = EXCLUDED.updated_at,
14335
+ expires_at = EXCLUDED.expires_at
14336
+ RETURNING tenant_id, namespace, key, value, created_at, updated_at, expires_at`,
14337
+ [tenantId, input.namespace, input.key, JSON.stringify(input.value), now, expiresAt]
14338
+ );
14339
+ return mapRow10(result.rows[0]);
14340
+ }
14341
+ async get(tenantId, namespace, key) {
14342
+ assertStoreIdentity(namespace, key);
14343
+ const result = await this.pool.query(
14344
+ `SELECT tenant_id, namespace, key, value, created_at, updated_at, expires_at
14345
+ FROM lattice_store_items
14346
+ WHERE tenant_id = $1 AND namespace = $2 AND key = $3
14347
+ AND (expires_at IS NULL OR expires_at > $4)`,
14348
+ [tenantId, namespace, key, /* @__PURE__ */ new Date()]
14349
+ );
14350
+ return result.rows[0] ? mapRow10(result.rows[0]) : null;
14351
+ }
14352
+ async delete(tenantId, namespace, key) {
14353
+ assertStoreIdentity(namespace, key);
14354
+ const result = await this.pool.query(
14355
+ `DELETE FROM lattice_store_items
14356
+ WHERE tenant_id = $1 AND namespace = $2 AND key = $3`,
14357
+ [tenantId, namespace, key]
14358
+ );
14359
+ return (result.rowCount ?? 0) > 0;
14360
+ }
14361
+ async search(tenantId, input) {
14362
+ const conditions = [
14363
+ "tenant_id = $1",
14364
+ "(expires_at IS NULL OR expires_at > $2)"
14365
+ ];
14366
+ const params = [tenantId, /* @__PURE__ */ new Date()];
14367
+ if (input.namespacePrefix) {
14368
+ assertStoreNamespacePrefix(input.namespacePrefix);
14369
+ }
14370
+ if (input.namespacePrefix && input.namespacePrefix.length > 0) {
14371
+ params.push(input.namespacePrefix);
14372
+ conditions.push(`namespace[1:array_length($${params.length}::text[], 1)] = $${params.length}`);
14373
+ }
14374
+ if (input.filter !== void 0) {
14375
+ params.push(JSON.stringify(input.filter));
14376
+ conditions.push(`value @> $${params.length}::jsonb`);
14377
+ }
14378
+ if (input.query !== void 0 && input.query !== "") {
14379
+ params.push(input.query.replace(/[\\%_]/g, "\\$&"));
14380
+ conditions.push(`value::text ILIKE '%' || $${params.length} || '%' ESCAPE '\\'`);
14381
+ }
14382
+ params.push(storePaginationValue(input.limit, 10));
14383
+ const limitParam = params.length;
14384
+ params.push(storePaginationValue(input.offset, 0));
14385
+ const offsetParam = params.length;
14386
+ const result = await this.pool.query(
14387
+ `SELECT tenant_id, namespace, key, value, created_at, updated_at, expires_at
14388
+ FROM lattice_store_items
14389
+ WHERE ${conditions.join(" AND ")}
14390
+ ORDER BY updated_at DESC, namespace, key
14391
+ LIMIT $${limitParam} OFFSET $${offsetParam}`,
14392
+ params
14393
+ );
14394
+ return result.rows.map(mapRow10);
14395
+ }
14396
+ async listNamespaces(tenantId, prefix = [], limit = 100) {
14397
+ assertStoreNamespacePrefix(prefix);
14398
+ const conditions = [
14399
+ "tenant_id = $1",
14400
+ "(expires_at IS NULL OR expires_at > $2)"
14401
+ ];
14402
+ const params = [tenantId, /* @__PURE__ */ new Date()];
14403
+ if (prefix.length > 0) {
14404
+ params.push(prefix);
14405
+ conditions.push(`namespace[1:array_length($${params.length}::text[], 1)] = $${params.length}`);
14406
+ }
14407
+ params.push(storePaginationValue(limit, 100));
14408
+ const result = await this.pool.query(
14409
+ `SELECT DISTINCT namespace
14410
+ FROM lattice_store_items
14411
+ WHERE ${conditions.join(" AND ")}
14412
+ ORDER BY namespace
14413
+ LIMIT $${params.length}`,
14414
+ params
14415
+ );
14416
+ return result.rows.map((row) => row.namespace);
14417
+ }
14418
+ async deleteExpired(now, limit) {
14419
+ const bounded = storePaginationValue(limit, 0);
14420
+ const result = await this.pool.query(
14421
+ `WITH expired AS (
14422
+ SELECT ctid FROM lattice_store_items
14423
+ WHERE expires_at IS NOT NULL AND expires_at <= $1
14424
+ ORDER BY expires_at
14425
+ LIMIT $2
14426
+ )
14427
+ DELETE FROM lattice_store_items WHERE ctid IN (SELECT ctid FROM expired)`,
14428
+ [now, bounded]
14429
+ );
14430
+ return result.rowCount ?? 0;
14431
+ }
14432
+ };
14433
+
14434
+ // src/migrations/store_migrations.ts
14435
+ var createStoreItemsTable = {
14436
+ version: 195,
14437
+ name: "create_lattice_store_items_table",
14438
+ up: async (client) => {
14439
+ await client.query(`
14440
+ CREATE TABLE IF NOT EXISTS lattice_store_items (
14441
+ tenant_id TEXT NOT NULL,
14442
+ namespace TEXT[] NOT NULL,
14443
+ key TEXT NOT NULL,
14444
+ value JSONB NOT NULL,
14445
+ created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
14446
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
14447
+ expires_at TIMESTAMPTZ,
14448
+ PRIMARY KEY (tenant_id, namespace, key)
14449
+ )
14450
+ `);
14451
+ await client.query(`
14452
+ CREATE INDEX IF NOT EXISTS idx_lattice_store_items_namespace
14453
+ ON lattice_store_items USING GIN (namespace)
14454
+ `);
14455
+ await client.query(`
14456
+ CREATE INDEX IF NOT EXISTS idx_lattice_store_items_value
14457
+ ON lattice_store_items USING GIN (value)
14458
+ `);
14459
+ await client.query(`
14460
+ CREATE INDEX IF NOT EXISTS idx_lattice_store_items_expires_at
14461
+ ON lattice_store_items(expires_at)
14462
+ WHERE expires_at IS NOT NULL
14463
+ `);
14464
+ },
14465
+ down: async (client) => {
14466
+ await client.query("DROP INDEX IF EXISTS idx_lattice_store_items_expires_at");
14467
+ await client.query("DROP INDEX IF EXISTS idx_lattice_store_items_value");
14468
+ await client.query("DROP INDEX IF EXISTS idx_lattice_store_items_namespace");
14469
+ await client.query("DROP TABLE IF EXISTS lattice_store_items");
14470
+ }
14471
+ };
14472
+
14473
+ // src/createPgStoreConfig.ts
14474
+ async function createPgStoreConfig(connectionString) {
14475
+ const pool = new Pool32({ connectionString });
14476
+ const mm = new MigrationManager(pool);
14477
+ mm.register(createThreadsTable);
14478
+ mm.register(createScheduledTasksTable);
14479
+ mm.register(createAssistantsTable);
14480
+ mm.register(createSkillsTable);
14481
+ mm.register(createWorkspacesTable);
14482
+ mm.register(createProjectsTable);
14483
+ mm.register(createUsersTable);
14484
+ mm.register(createTenantsTable);
14485
+ mm.register(addUserStatusColumn);
14486
+ mm.register(createUserTenantLinkTable);
14487
+ mm.register(migrateRemoveTenantIdFromUsers);
14488
+ mm.register(createMetricsConfigsTable);
14489
+ mm.register(createMcpServerConfigsTable);
14490
+ mm.register(addAssistantTenantId);
14491
+ mm.register(addSkillTenantId);
14492
+ mm.register(changeSkillPrimaryKey);
14493
+ mm.register(addScheduleTenantId);
14494
+ mm.register(changeThreadPrimaryKey);
14495
+ mm.register(createChannelIdentityMappingTables);
14496
+ mm.register(createChannelInstallationsTable);
14497
+ mm.register(addThreadTenantId);
14498
+ mm.register(addProjectConfigColumn);
14499
+ registerThreadMessageQueueMigrations(mm);
14500
+ mm.register(createWorkflowTrackingTables);
14501
+ for (const m of evalMigrations) {
14502
+ mm.register(m);
14503
+ }
14504
+ mm.register(alterChannelInstallationsTable);
14505
+ mm.register(createChannelBindingsTable);
14506
+ mm.register(createDatabaseConfigsTable);
14507
+ mm.register(changeAssistantPrimaryKey);
14508
+ mm.register(addStepThreadId);
14509
+ mm.register(createA2AApiKeysTable);
14510
+ mm.register(addAssistantOwnerUserId);
14511
+ for (const m of taskMigrations) {
14512
+ mm.register(m);
14513
+ }
14514
+ mm.register(createMenuItemsTable);
14515
+ mm.register(createSharedResourcesTable);
14516
+ mm.register(addFileContentType);
14517
+ mm.register(addMcpAppContentType);
12279
14518
  mm.register(createCollectionsTable);
12280
14519
  mm.register(createTaskWorkItemsMigration);
12281
14520
  mm.register(addWorkItemProjectFieldsMigration);
@@ -12309,6 +14548,18 @@ async function createPgStoreConfig(connectionString) {
12309
14548
  mm.register(addModelProviderModelExtra);
12310
14549
  mm.register(addModelProviderModelDefault);
12311
14550
  mm.register(addModelProviderModelKind);
14551
+ mm.register(createAgentRunLedgerTable);
14552
+ mm.register(createAgentRunTaskQueueMigration);
14553
+ mm.register(createAgentRunCapabilityRevisionMigration);
14554
+ mm.register(createAgentRunBaseCheckpointCapturedMigration);
14555
+ mm.register(createAgentRunRollbackFailureMigration);
14556
+ mm.register(agentRunRunningIndexSourceMigration);
14557
+ mm.register(addThreadExpiresAt);
14558
+ mm.register(createStoreItemsTable);
14559
+ mm.register(createChannelReplyJobsTable);
14560
+ mm.register(createAgentRunRetentionMigration);
14561
+ mm.register(createAgentRunProjectTaskMigration);
14562
+ mm.register(agentRunProjectTaskRunningIndexMigration);
12312
14563
  await mm.migrate();
12313
14564
  const checkpoint = PostgresSaver.fromConnString(connectionString);
12314
14565
  checkpoint.setup().catch((err) => {
@@ -12353,13 +14604,17 @@ async function createPgStoreConfig(connectionString) {
12353
14604
  lease: new PostgreSQLLeaseStore(opts),
12354
14605
  outbox: new PostgreSQLOutboxStore(opts),
12355
14606
  pluginDeployment: new PostgreSQLPluginDeploymentStore(opts),
14607
+ agentRun: new PostgreSQLAgentRunLedger(opts),
14608
+ channelReplyJob: new PostgreSQLChannelReplyJobStore(opts),
12356
14609
  vectorStoreProvider: new PGVectorStoreProvider(pool, connectionString),
12357
- checkpoint
14610
+ checkpoint,
14611
+ checkpointStorage: new PostgreSQLCheckpointStorage(pool),
14612
+ store: new PostgreSQLStore(pool)
12358
14613
  };
12359
14614
  }
12360
14615
 
12361
14616
  // src/stores/PostgreSQLSkillStore.ts
12362
- import { Pool as Pool31 } from "pg";
14617
+ import { Pool as Pool33 } from "pg";
12363
14618
  var PostgreSQLSkillStore = class {
12364
14619
  constructor(options) {
12365
14620
  this.initialized = false;
@@ -12372,9 +14627,9 @@ var PostgreSQLSkillStore = class {
12372
14627
  return;
12373
14628
  }
12374
14629
  if (typeof options.poolConfig === "string") {
12375
- this.pool = new Pool31({ connectionString: options.poolConfig });
14630
+ this.pool = new Pool33({ connectionString: options.poolConfig });
12376
14631
  } else if (options.poolConfig) {
12377
- this.pool = new Pool31(options.poolConfig);
14632
+ this.pool = new Pool33(options.poolConfig);
12378
14633
  } else {
12379
14634
  throw new Error("Either pool or poolConfig must be provided");
12380
14635
  }
@@ -12675,7 +14930,7 @@ var PostgreSQLSkillStore = class {
12675
14930
  };
12676
14931
 
12677
14932
  // src/stores/ChannelIdentityMappingStore.ts
12678
- import { Pool as Pool32 } from "pg";
14933
+ import { Pool as Pool34 } from "pg";
12679
14934
  var ChannelIdentityMappingStore = class {
12680
14935
  constructor(options) {
12681
14936
  this.initialized = false;
@@ -12687,7 +14942,7 @@ var ChannelIdentityMappingStore = class {
12687
14942
  this.initialized = true;
12688
14943
  return;
12689
14944
  }
12690
- this.pool = typeof options.poolConfig === "string" ? new Pool32({ connectionString: options.poolConfig }) : options.poolConfig ? new Pool32(options.poolConfig) : (() => {
14945
+ this.pool = typeof options.poolConfig === "string" ? new Pool34({ connectionString: options.poolConfig }) : options.poolConfig ? new Pool34(options.poolConfig) : (() => {
12691
14946
  throw new Error("Either pool or poolConfig must be provided");
12692
14947
  })();
12693
14948
  this.migrationManager = new MigrationManager(this.pool);
@@ -12908,17 +15163,22 @@ function mapRowToChannelIdentityMapping(row) {
12908
15163
  export {
12909
15164
  ChannelBindingStore,
12910
15165
  ChannelIdentityMappingStore,
15166
+ CheckpointBaseNotFoundError,
15167
+ CheckpointTargetNotEmptyError,
12911
15168
  DuplicateProjectMembershipError,
12912
15169
  DuplicateProjectRoomMessageIdempotencyKeyError,
12913
15170
  MenuStore,
12914
15171
  MigrationManager,
12915
15172
  PGVectorStoreProvider,
12916
- Pool33 as Pool,
15173
+ Pool35 as Pool,
12917
15174
  PostgreSQLA2AApiKeyStore,
15175
+ PostgreSQLAgentRunLedger,
12918
15176
  PostgreSQLAgentWebAppStore,
12919
15177
  PostgreSQLAssistantStore,
12920
15178
  PostgreSQLCapabilityBundleStore,
12921
15179
  PostgreSQLChannelInstallationStore,
15180
+ PostgreSQLChannelReplyJobStore,
15181
+ PostgreSQLCheckpointStorage,
12922
15182
  PostgreSQLCollectionStore,
12923
15183
  PostgreSQLConnectionStore,
12924
15184
  PostgreSQLDatabaseConfigStore,
@@ -12936,6 +15196,7 @@ export {
12936
15196
  PostgreSQLProjectStore,
12937
15197
  PostgreSQLScheduleStorage,
12938
15198
  PostgreSQLSkillStore,
15199
+ PostgreSQLStore,
12939
15200
  PostgreSQLTaskWorkItemStore,
12940
15201
  PostgreSQLTenantStore,
12941
15202
  PostgreSQLThreadStore,
@@ -12949,6 +15210,7 @@ export {
12949
15210
  ProjectRoomMessageIdConflictError,
12950
15211
  ThreadMessageQueueStore,
12951
15212
  addAssistantTenantId,
15213
+ addCancelRequestedToEvalRuns,
12952
15214
  addEnvToEvalRuns,
12953
15215
  addFileContentType,
12954
15216
  addHoldoutToEvalRuns,
@@ -12960,6 +15222,7 @@ export {
12960
15222
  addSkillTenantId,
12961
15223
  addStepThreadId,
12962
15224
  addTaskIdToEvalRuns,
15225
+ addThreadExpiresAt,
12963
15226
  addThreadTenantId,
12964
15227
  addWorkflowRunsTenantStatusUpdatedIndex,
12965
15228
  alterChannelInstallationsTable,
@@ -12967,12 +15230,14 @@ export {
12967
15230
  changeSkillPrimaryKey,
12968
15231
  changeThreadPrimaryKey,
12969
15232
  createA2AApiKeysTable,
15233
+ createAgentRunLedgerTable,
12970
15234
  createAgentWebAppsTable,
12971
15235
  createAssistantsTable,
12972
15236
  createCapabilityBundlesTable,
12973
15237
  createChannelBindingsTable,
12974
15238
  createChannelIdentityMappingTables,
12975
15239
  createChannelInstallationsTable,
15240
+ createChannelReplyJobsTable,
12976
15241
  createCollectionsTable,
12977
15242
  createDatabaseConfigsTable,
12978
15243
  createEvalCasesTable,
@@ -12992,6 +15257,7 @@ export {
12992
15257
  createScheduledTasksTable,
12993
15258
  createSharedResourcesTable,
12994
15259
  createSkillsTable,
15260
+ createStoreItemsTable,
12995
15261
  createTenantsTable,
12996
15262
  createThreadMessageQueueTable,
12997
15263
  createThreadsTable,