@axiom-lattice/pg-stores 3.3.13 → 3.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/.turbo/turbo-build.log +10 -10
  2. package/CHANGELOG.md +170 -0
  3. package/dist/index.d.mts +353 -3
  4. package/dist/index.d.ts +353 -3
  5. package/dist/index.js +2288 -83
  6. package/dist/index.js.map +1 -1
  7. package/dist/index.mjs +2294 -82
  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 +333 -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/createPgStoreConfig.ts +35 -2
  23. package/src/index.ts +7 -0
  24. package/src/migrations/agent_run_base_checkpoint_captured_migration.ts +34 -0
  25. package/src/migrations/agent_run_capability_revision_migration.ts +24 -0
  26. package/src/migrations/agent_run_ledger_migration.ts +51 -0
  27. package/src/migrations/agent_run_project_task_migration.ts +34 -0
  28. package/src/migrations/agent_run_project_task_running_index_migration.ts +33 -0
  29. package/src/migrations/agent_run_retention_migration.ts +25 -0
  30. package/src/migrations/agent_run_rollback_failure_migration.ts +28 -0
  31. package/src/migrations/agent_run_running_index_source_migration.ts +31 -0
  32. package/src/migrations/agent_run_task_queue_migration.ts +65 -0
  33. package/src/migrations/channel_reply_job_migration.ts +55 -0
  34. package/src/migrations/store_migrations.ts +50 -0
  35. package/src/migrations/thread_migrations.ts +25 -0
  36. package/src/stores/PostgreSQLAgentRunLedger.ts +1210 -0
  37. package/src/stores/PostgreSQLChannelReplyJobStore.ts +373 -0
  38. package/src/stores/PostgreSQLCheckpointStorage.ts +548 -0
  39. package/src/stores/PostgreSQLStore.ts +167 -0
  40. package/src/stores/PostgreSQLThreadStore.ts +49 -10
  41. package/src/stores/ThreadMessageQueueStore.ts +10 -5
@@ -0,0 +1,28 @@
1
+ import type { Migration } from "./migration";
2
+
3
+ /**
4
+ * Adds bounded rollback failure tracking to run records.
5
+ *
6
+ * @remarks Recovery increments `rollback_attempts` and stores
7
+ * `rollback_error`. When the attempt cap is reached the row settles `error`
8
+ * with `rollback_requested=false`, so join/list converge instead of retrying a
9
+ * permanently failing checkpoint rewind forever.
10
+ */
11
+ export const createAgentRunRollbackFailureMigration: Migration = {
12
+ version: 199,
13
+ name: "add_agent_run_rollback_failure",
14
+ up: async (client) => {
15
+ await client.query(`
16
+ ALTER TABLE lattice_agent_runs
17
+ ADD COLUMN IF NOT EXISTS rollback_attempts INTEGER NOT NULL DEFAULT 0,
18
+ ADD COLUMN IF NOT EXISTS rollback_error TEXT
19
+ `);
20
+ },
21
+ down: async (client) => {
22
+ await client.query(`
23
+ ALTER TABLE lattice_agent_runs
24
+ DROP COLUMN IF EXISTS rollback_error,
25
+ DROP COLUMN IF EXISTS rollback_attempts
26
+ `);
27
+ },
28
+ };
@@ -0,0 +1,31 @@
1
+ import { PoolClient } from "pg";
2
+ import { Migration } from "./migration";
3
+
4
+ /**
5
+ * Scope the one-running-row-per-thread index to the durable run queue.
6
+ *
7
+ * @remarks Legacy message-queue ledger rows also use `status='running'`. A
8
+ * crashed legacy owner can leave such a row behind; it must not make a
9
+ * `run_api` pending row unclaimable after the `thread-run` lease expires.
10
+ * Real execution exclusion remains the shared `thread-run` lease.
11
+ */
12
+ export const agentRunRunningIndexSourceMigration: Migration = {
13
+ version: 202,
14
+ name: "scope_agent_run_running_index_to_run_api",
15
+ up: async (client: PoolClient) => {
16
+ await client.query("DROP INDEX IF EXISTS uq_lattice_agent_runs_one_running");
17
+ await client.query(`
18
+ CREATE UNIQUE INDEX IF NOT EXISTS uq_lattice_agent_runs_one_running
19
+ ON lattice_agent_runs (tenant_id, thread_id)
20
+ WHERE status = 'running' AND source = 'run_api'
21
+ `);
22
+ },
23
+ down: async (client: PoolClient) => {
24
+ await client.query("DROP INDEX IF EXISTS uq_lattice_agent_runs_one_running");
25
+ await client.query(`
26
+ CREATE UNIQUE INDEX IF NOT EXISTS uq_lattice_agent_runs_one_running
27
+ ON lattice_agent_runs (tenant_id, thread_id)
28
+ WHERE status = 'running'
29
+ `);
30
+ },
31
+ };
@@ -0,0 +1,65 @@
1
+ import type { Migration } from "./migration";
2
+
3
+ /**
4
+ * Expands `lattice_agent_runs` into a durable run task queue.
5
+ *
6
+ * @remarks
7
+ * - `source` separates new run-queue work (`run_api`) from the existing
8
+ * observational ledger path (`legacy`, the default for old/inserted rows).
9
+ * - Pending rows have no owner, lease or start time, so those columns become
10
+ * nullable.
11
+ * - Payload columns are JSONB; `available_at` is the earliest claim time.
12
+ * - The partial unique running index is intentionally left untouched.
13
+ */
14
+ export const createAgentRunTaskQueueMigration: Migration = {
15
+ version: 196,
16
+ name: "expand_lattice_agent_runs_task_queue",
17
+ up: async (client) => {
18
+ await client.query(`
19
+ ALTER TABLE lattice_agent_runs
20
+ ALTER COLUMN owner DROP NOT NULL,
21
+ ALTER COLUMN lease_expires_at DROP NOT NULL,
22
+ ALTER COLUMN started_at DROP NOT NULL,
23
+ ALTER COLUMN started_at DROP DEFAULT
24
+ `);
25
+ await client.query(`
26
+ ALTER TABLE lattice_agent_runs
27
+ ADD COLUMN IF NOT EXISTS source VARCHAR(32) NOT NULL DEFAULT 'legacy',
28
+ ADD COLUMN IF NOT EXISTS input JSONB,
29
+ ADD COLUMN IF NOT EXISTS command JSONB,
30
+ ADD COLUMN IF NOT EXISTS config JSONB,
31
+ ADD COLUMN IF NOT EXISTS metadata JSONB,
32
+ ADD COLUMN IF NOT EXISTS base_checkpoint_id TEXT,
33
+ ADD COLUMN IF NOT EXISTS available_at TIMESTAMPTZ NOT NULL DEFAULT now(),
34
+ ADD COLUMN IF NOT EXISTS rollback_requested BOOLEAN NOT NULL DEFAULT FALSE
35
+ `);
36
+ await client.query(`
37
+ ALTER TABLE lattice_agent_runs
38
+ ALTER COLUMN source SET DEFAULT 'legacy'
39
+ `);
40
+ await client.query(`
41
+ CREATE INDEX IF NOT EXISTS idx_lattice_agent_runs_run_queue
42
+ ON lattice_agent_runs (source, status, available_at, created_at)
43
+ `);
44
+ },
45
+ down: async (client) => {
46
+ await client.query(`
47
+ DROP INDEX IF EXISTS idx_lattice_agent_runs_run_queue
48
+ `);
49
+ await client.query(`
50
+ ALTER TABLE lattice_agent_runs
51
+ DROP COLUMN IF EXISTS rollback_requested,
52
+ DROP COLUMN IF EXISTS available_at,
53
+ DROP COLUMN IF EXISTS base_checkpoint_id,
54
+ DROP COLUMN IF EXISTS metadata,
55
+ DROP COLUMN IF EXISTS config,
56
+ DROP COLUMN IF EXISTS command,
57
+ DROP COLUMN IF EXISTS input,
58
+ DROP COLUMN IF EXISTS source
59
+ `);
60
+ await client.query(`
61
+ ALTER TABLE lattice_agent_runs
62
+ ALTER COLUMN started_at SET DEFAULT now()
63
+ `);
64
+ },
65
+ };
@@ -0,0 +1,55 @@
1
+ import type { PoolClient } from "pg";
2
+ import type { Migration } from "./migration";
3
+
4
+ const DB_CLOCK_MS =
5
+ "FLOOR(EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint";
6
+
7
+ /**
8
+ * Durable channel reply jobs used by the cross-replica reply dispatcher.
9
+ *
10
+ * @remarks Lease-based claims make the row the single source of truth for
11
+ * "which replica is delivering this run's channel reply". Timestamps are epoch
12
+ * milliseconds from the database clock.
13
+ */
14
+ export const createChannelReplyJobsTable: Migration = {
15
+ version: 200,
16
+ name: "create_lattice_channel_reply_jobs",
17
+ up: async (client: PoolClient) => {
18
+ await client.query(`
19
+ CREATE TABLE IF NOT EXISTS lattice_channel_reply_jobs (
20
+ run_id TEXT PRIMARY KEY,
21
+ tenant_id VARCHAR(255) NOT NULL,
22
+ assistant_id TEXT NOT NULL,
23
+ thread_id TEXT NOT NULL,
24
+ adapter_channel TEXT NOT NULL,
25
+ channel_installation_id TEXT NOT NULL,
26
+ reply_target JSONB NOT NULL,
27
+ input_message_id TEXT NOT NULL,
28
+ stream_cursor TEXT NULL,
29
+ status TEXT NOT NULL,
30
+ attempts INT NOT NULL DEFAULT 0,
31
+ available_at BIGINT NOT NULL,
32
+ lease_owner TEXT NULL,
33
+ lease_expires_at BIGINT NULL,
34
+ last_error TEXT NULL,
35
+ output_message_id TEXT NULL,
36
+ created_at BIGINT NOT NULL DEFAULT (${DB_CLOCK_MS}),
37
+ updated_at BIGINT NOT NULL DEFAULT (${DB_CLOCK_MS}),
38
+ delivered_at BIGINT NULL
39
+ )
40
+ `);
41
+ await client.query(`
42
+ CREATE INDEX IF NOT EXISTS idx_lattice_channel_reply_jobs_due
43
+ ON lattice_channel_reply_jobs (status, available_at)
44
+ `);
45
+ await client.query(`
46
+ CREATE INDEX IF NOT EXISTS idx_lattice_channel_reply_jobs_tenant_thread
47
+ ON lattice_channel_reply_jobs (tenant_id, thread_id)
48
+ `);
49
+ },
50
+ down: async (client: PoolClient) => {
51
+ await client.query("DROP INDEX IF EXISTS idx_lattice_channel_reply_jobs_tenant_thread");
52
+ await client.query("DROP INDEX IF EXISTS idx_lattice_channel_reply_jobs_due");
53
+ await client.query("DROP TABLE IF EXISTS lattice_channel_reply_jobs");
54
+ },
55
+ };
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Namespaced KV store migrations.
3
+ */
4
+
5
+ import { PoolClient } from "pg";
6
+ import { Migration } from "./migration";
7
+
8
+ /**
9
+ * Migration: create the tenant-scoped namespaced KV store table.
10
+ *
11
+ * GIN indexes support namespace prefix and JSONB containment filters; the
12
+ * partial expiry index supports the TTL sweeper.
13
+ */
14
+ export const createStoreItemsTable: Migration = {
15
+ version: 195,
16
+ name: "create_lattice_store_items_table",
17
+ up: async (client: PoolClient) => {
18
+ await client.query(`
19
+ CREATE TABLE IF NOT EXISTS lattice_store_items (
20
+ tenant_id TEXT NOT NULL,
21
+ namespace TEXT[] NOT NULL,
22
+ key TEXT NOT NULL,
23
+ value JSONB NOT NULL,
24
+ created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
25
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
26
+ expires_at TIMESTAMPTZ,
27
+ PRIMARY KEY (tenant_id, namespace, key)
28
+ )
29
+ `);
30
+ await client.query(`
31
+ CREATE INDEX IF NOT EXISTS idx_lattice_store_items_namespace
32
+ ON lattice_store_items USING GIN (namespace)
33
+ `);
34
+ await client.query(`
35
+ CREATE INDEX IF NOT EXISTS idx_lattice_store_items_value
36
+ ON lattice_store_items USING GIN (value)
37
+ `);
38
+ await client.query(`
39
+ CREATE INDEX IF NOT EXISTS idx_lattice_store_items_expires_at
40
+ ON lattice_store_items(expires_at)
41
+ WHERE expires_at IS NOT NULL
42
+ `);
43
+ },
44
+ down: async (client: PoolClient) => {
45
+ await client.query("DROP INDEX IF EXISTS idx_lattice_store_items_expires_at");
46
+ await client.query("DROP INDEX IF EXISTS idx_lattice_store_items_value");
47
+ await client.query("DROP INDEX IF EXISTS idx_lattice_store_items_namespace");
48
+ await client.query("DROP TABLE IF EXISTS lattice_store_items");
49
+ },
50
+ };
@@ -40,3 +40,28 @@ export const createThreadsTable: Migration = {
40
40
  await client.query("DROP TABLE IF EXISTS lattice_threads");
41
41
  },
42
42
  };
43
+
44
+ /**
45
+ * Migration: Add per-thread expiry (`expires_at`) for the TTL sweeper.
46
+ *
47
+ * A partial index keeps expiry scans cheap while leaving non-TTL threads out.
48
+ */
49
+ export const addThreadExpiresAt: Migration = {
50
+ version: 194,
51
+ name: "add_thread_expires_at",
52
+ up: async (client: PoolClient) => {
53
+ await client.query(`
54
+ ALTER TABLE lattice_threads
55
+ ADD COLUMN IF NOT EXISTS expires_at TIMESTAMPTZ
56
+ `);
57
+ await client.query(`
58
+ CREATE INDEX IF NOT EXISTS idx_lattice_threads_expires_at
59
+ ON lattice_threads(expires_at)
60
+ WHERE expires_at IS NOT NULL
61
+ `);
62
+ },
63
+ down: async (client: PoolClient) => {
64
+ await client.query("DROP INDEX IF EXISTS idx_lattice_threads_expires_at");
65
+ await client.query("ALTER TABLE lattice_threads DROP COLUMN IF EXISTS expires_at");
66
+ },
67
+ };