@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/.turbo/turbo-build.log +10 -10
- package/CHANGELOG.md +38 -0
- package/dist/index.d.mts +5 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.js +65 -10
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +64 -10
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/PostgreSQLChannelReplyJobStore.test.ts +4 -2
- package/src/__tests__/eval-task-id.test.ts +14 -3
- package/src/migrations/eval_migrations.ts +19 -0
- package/src/migrations/task_migration.ts +17 -0
- package/src/stores/PostgreSQLAgentRunLedger.ts +15 -4
- package/src/stores/PostgreSQLEvalStore.ts +17 -4
- package/src/stores/PostgreSQLTaskStore.ts +4 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@axiom-lattice/pg-stores",
|
|
3
|
-
"version": "
|
|
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": "
|
|
29
|
-
"@axiom-lattice/protocols": "
|
|
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
|
-
|
|
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() -
|
|
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("
|
|
16
|
+
it("keeps task_id before the cancellation migration", () => {
|
|
17
17
|
const versions = evalMigrations.map((m) => m.version);
|
|
18
|
-
expect(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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);
|