@axiom-lattice/pg-stores 3.4.0 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axiom-lattice/pg-stores",
3
- "version": "3.4.0",
3
+ "version": "4.0.0",
4
4
  "description": "PG stores implementation for Axiom Lattice framework",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -25,8 +25,8 @@
25
25
  "@langchain/core": "1.1.30",
26
26
  "pg": "^8.16.3",
27
27
  "uuid": "^9.0.1",
28
- "@axiom-lattice/core": "9.0.0",
29
- "@axiom-lattice/protocols": "5.0.0"
28
+ "@axiom-lattice/core": "10.0.0",
29
+ "@axiom-lattice/protocols": "6.0.0"
30
30
  },
31
31
  "devDependencies": {
32
32
  "@types/jest": "^29.5.14",
@@ -19,7 +19,9 @@ function input(
19
19
  channelInstallationId: "install-1",
20
20
  replyTarget: { chatId: "chat-1" },
21
21
  inputMessageId: "input-1",
22
- availableAt: Date.now() - 1_000,
22
+ // Well in the past: the store compares against the database clock, which
23
+ // can trail the Node process clock by a few milliseconds.
24
+ availableAt: Date.now() - 60_000,
23
25
  ...overrides,
24
26
  };
25
27
  }
@@ -194,7 +196,7 @@ maybeDescribe("PostgreSQLChannelReplyJobStore (PostgreSQL)", () => {
194
196
  store.reschedule(created.tenantId, created.runId, "owner-b", "nope", Date.now(), 1),
195
197
  ).resolves.toBeNull();
196
198
 
197
- const availableAt = Date.now() - 1;
199
+ const availableAt = Date.now() - 60_000;
198
200
  const rescheduled = await store.reschedule(
199
201
  created.tenantId,
200
202
  created.runId,
@@ -6,16 +6,27 @@
6
6
  */
7
7
 
8
8
  import { describe, expect, it, jest } from "@jest/globals";
9
- import { addTaskIdToEvalRuns, evalMigrations } from "../migrations/eval_migrations";
9
+ import { addCancelRequestedToEvalRuns, addTaskIdToEvalRuns, evalMigrations } from "../migrations/eval_migrations";
10
10
 
11
11
  describe("add_task_id_to_eval_runs migration", () => {
12
12
  it("is registered in evalMigrations", () => {
13
13
  expect(evalMigrations.map((m) => m.name)).toContain("add_task_id_to_eval_runs");
14
14
  });
15
15
 
16
- it("has a version greater than all existing eval migrations", () => {
16
+ it("keeps task_id before the cancellation migration", () => {
17
17
  const versions = evalMigrations.map((m) => m.version);
18
- expect(addTaskIdToEvalRuns.version).toBe(Math.max(...versions));
18
+ expect(addCancelRequestedToEvalRuns.version).toBe(Math.max(...versions));
19
+ expect(addTaskIdToEvalRuns.version).toBeLessThan(addCancelRequestedToEvalRuns.version);
20
+ });
21
+
22
+ it("emits the cooperative cancel column in up() and down()", async () => {
23
+ const upQueries: string[] = [];
24
+ await addCancelRequestedToEvalRuns.up({ query: jest.fn(async (sql: string) => { upQueries.push(sql); }) } as never);
25
+ expect(upQueries.some((q) => q.includes("cancel_requested BOOLEAN NOT NULL DEFAULT FALSE"))).toBe(true);
26
+
27
+ const downQueries: string[] = [];
28
+ await addCancelRequestedToEvalRuns.down({ query: jest.fn(async (sql: string) => { downQueries.push(sql); }) } as never);
29
+ expect(downQueries.some((q) => q.includes("DROP COLUMN IF EXISTS cancel_requested"))).toBe(true);
19
30
  });
20
31
 
21
32
  it("emits ADD COLUMN task_id in up()", async () => {
@@ -381,6 +381,24 @@ export const addTaskIdToEvalRuns: Migration = {
381
381
  },
382
382
  };
383
383
 
384
+
385
+ /** Cooperative cancellation flag for running eval runs. */
386
+ export const addCancelRequestedToEvalRuns: Migration = {
387
+ version: 205,
388
+ name: "add_cancel_requested_to_eval_runs",
389
+ up: async (client: PoolClient) => {
390
+ await client.query(`
391
+ ALTER TABLE lattice_eval_runs
392
+ ADD COLUMN IF NOT EXISTS cancel_requested BOOLEAN NOT NULL DEFAULT FALSE
393
+ `);
394
+ },
395
+ down: async (client: PoolClient) => {
396
+ await client.query(`
397
+ ALTER TABLE lattice_eval_runs DROP COLUMN IF EXISTS cancel_requested
398
+ `);
399
+ },
400
+ };
401
+
384
402
  /** All eval migrations in version order */
385
403
  export const evalMigrations: Migration[] = [
386
404
  createEvalProjectsTable,
@@ -394,4 +412,5 @@ export const evalMigrations: Migration[] = [
394
412
  addInterruptPolicyToEvalCases,
395
413
  enforceUniqueEvalProjectName,
396
414
  addTaskIdToEvalRuns,
415
+ addCancelRequestedToEvalRuns,
397
416
  ];
@@ -112,6 +112,22 @@ export const addTaskDependenciesGinIndex: Migration = {
112
112
  },
113
113
  };
114
114
 
115
+ /** Index task execution threads for ownership checks on isolated worker threads. */
116
+ export const addTaskContextThreadIndex: Migration = {
117
+ version: 206,
118
+ name: "add_task_context_thread_index",
119
+ up: async (client) => {
120
+ await client.query(`
121
+ CREATE INDEX IF NOT EXISTS idx_lattice_tasks_context_thread
122
+ ON lattice_tasks (tenant_id, (context->>'thread_id'))
123
+ WHERE context->>'thread_id' IS NOT NULL
124
+ `);
125
+ },
126
+ down: async (client) => {
127
+ await client.query("DROP INDEX IF EXISTS idx_lattice_tasks_context_thread");
128
+ },
129
+ };
130
+
115
131
  /** All task migrations in version order */
116
132
  export const taskMigrations: Migration[] = [
117
133
  createTasksTable,
@@ -119,4 +135,5 @@ export const taskMigrations: Migration[] = [
119
135
  addTaskProjectFieldsMigration,
120
136
  addFilesToTasks,
121
137
  addTaskDependenciesGinIndex,
138
+ addTaskContextThreadIndex,
122
139
  ];
@@ -274,7 +274,10 @@ export class PostgreSQLAgentRunLedger implements IAgentRunLedger {
274
274
  const inputMessageIds = input.inputMessageIds
275
275
  ? normalizeAgentRunMessageIds(input.inputMessageIds)
276
276
  : extractAgentRunInputMessageIds(input.input);
277
- const availableAt = input.availableAt ?? Date.now();
277
+ // Default to the database clock. Mixing Node's `Date.now()` with the
278
+ // `available_at <= now()` claim predicate makes freshly created rows
279
+ // briefly unclaimable whenever the two clocks drift by a few milliseconds.
280
+ const availableAt = input.availableAt ?? null;
278
281
  const limit = input.maxActiveRunsForTenant;
279
282
  const client = await this.pool.connect();
280
283
  try {
@@ -333,7 +336,10 @@ export class PostgreSQLAgentRunLedger implements IAgentRunLedger {
333
336
  available_at, created_at, updated_at, task_id)
334
337
  VALUES ($1, $2, $3, $4, $5, $6, 'pending', $7, NULL, NULL, $8, 0,
335
338
  $9::jsonb, $10::jsonb, $11::jsonb, $12::jsonb, $13,
336
- to_timestamp($14::double precision / 1000.0),
339
+ COALESCE(
340
+ to_timestamp($14::double precision / 1000.0),
341
+ date_trunc('milliseconds', now())
342
+ ),
337
343
  date_trunc('milliseconds', now()),
338
344
  date_trunc('milliseconds', now()), $15)
339
345
  RETURNING *`,
@@ -971,14 +977,19 @@ export class PostgreSQLAgentRunLedger implements IAgentRunLedger {
971
977
  options?: SettleExpiredRunOptions,
972
978
  ): Promise<boolean> {
973
979
  if (outcome === "pending") {
974
- const availableAt = options?.availableAt ?? Date.now();
980
+ // Same clock rule as createPendingRun: only explicit backoff timestamps
981
+ // are caller-supplied; the default must come from the database.
982
+ const availableAt = options?.availableAt ?? null;
975
983
  const result = await this.pool.query(
976
984
  `UPDATE lattice_agent_runs
977
985
  SET status = 'pending',
978
986
  owner = NULL,
979
987
  lease_expires_at = NULL,
980
988
  started_at = NULL,
981
- available_at = to_timestamp($2::double precision / 1000.0),
989
+ available_at = COALESCE(
990
+ to_timestamp($2::double precision / 1000.0),
991
+ date_trunc('milliseconds', now())
992
+ ),
982
993
  finished_at = NULL,
983
994
  updated_at = now()
984
995
  WHERE run_id = $1
@@ -180,6 +180,7 @@ export class PostgreSQLEvalStore implements EvalStore {
180
180
  interruptedCases: (row.interrupted_cases as number) ?? 0,
181
181
  avgScore: (row.avg_score as number) ?? 0,
182
182
  error: row.error as string | undefined,
183
+ cancelRequested: (row.cancel_requested as boolean) ?? false,
183
184
  holdout: row.holdout as boolean | undefined,
184
185
  envProjectId: row.env_project_id as string | undefined,
185
186
  envWorkspaceId: row.env_workspace_id as string | undefined,
@@ -610,7 +611,7 @@ export class PostgreSQLEvalStore implements EvalStore {
610
611
  `SELECT id, project_id, tenant_id,
611
612
  status, concurrency, total_cases,
612
613
  passed_cases, failed_cases, interrupted_cases, avg_score,
613
- error, holdout, env_project_id, env_workspace_id, task_id, created_at, started_at, completed_at
614
+ error, holdout, env_project_id, env_workspace_id, task_id, cancel_requested, created_at, started_at, completed_at
614
615
  FROM lattice_eval_runs
615
616
  WHERE ${conditions.join(" AND ")}
616
617
  ORDER BY created_at DESC`,
@@ -632,7 +633,7 @@ export class PostgreSQLEvalStore implements EvalStore {
632
633
  `SELECT id, project_id, tenant_id,
633
634
  status, concurrency, total_cases,
634
635
  passed_cases, failed_cases, interrupted_cases, avg_score,
635
- error, holdout, env_project_id, env_workspace_id, task_id, created_at, started_at, completed_at
636
+ error, holdout, env_project_id, env_workspace_id, task_id, cancel_requested, created_at, started_at, completed_at
636
637
  FROM lattice_eval_runs
637
638
  WHERE id = $1 AND tenant_id = $2`,
638
639
  [id, tenantId]
@@ -662,7 +663,7 @@ export class PostgreSQLEvalStore implements EvalStore {
662
663
  RETURNING id, project_id, tenant_id,
663
664
  status, concurrency, total_cases,
664
665
  passed_cases, failed_cases, interrupted_cases, avg_score,
665
- error, holdout, env_project_id, env_workspace_id, task_id, created_at, started_at, completed_at`,
666
+ error, holdout, env_project_id, env_workspace_id, task_id, cancel_requested, created_at, started_at, completed_at`,
666
667
  [
667
668
  actualId,
668
669
  projectId,
@@ -722,12 +723,24 @@ export class PostgreSQLEvalStore implements EvalStore {
722
723
  RETURNING id, project_id, tenant_id,
723
724
  status, concurrency, total_cases,
724
725
  passed_cases, failed_cases, interrupted_cases, avg_score,
725
- error, holdout, env_project_id, env_workspace_id, task_id, created_at, started_at, completed_at`,
726
+ error, holdout, env_project_id, env_workspace_id, task_id, cancel_requested, created_at, started_at, completed_at`,
726
727
  vals
727
728
  );
728
729
  return rows.length ? this.mapRowToRun(rows[0] as unknown as Record<string, unknown>) : null;
729
730
  }
730
731
 
732
+ /** Request cooperative cancellation for a running evaluation. */
733
+ async requestCancel(tenantId: string, id: string): Promise<boolean> {
734
+ await this.ensureInitialized();
735
+ const { rowCount } = await this.pool.query(
736
+ `UPDATE lattice_eval_runs
737
+ SET cancel_requested = TRUE
738
+ WHERE tenant_id = $1 AND id = $2 AND status = 'running'`,
739
+ [tenantId, id],
740
+ );
741
+ return (rowCount ?? 0) > 0;
742
+ }
743
+
731
744
  /** Delete a run and its results */
732
745
  async deleteRun(tenantId: string, id: string): Promise<boolean> {
733
746
  await this.ensureInitialized();
@@ -283,6 +283,10 @@ export class PostgreSQLTaskStore implements TaskStore {
283
283
  conditions.push(`priority = $${paramIndex++}`);
284
284
  params.push(filter.priority);
285
285
  }
286
+ if (filter.threadId) {
287
+ conditions.push(`context->>'thread_id' = $${paramIndex++}`);
288
+ params.push(filter.threadId);
289
+ }
286
290
  if (filter.workspaceId) {
287
291
  conditions.push(`workspace_id = $${paramIndex++}`);
288
292
  params.push(filter.workspaceId);