fix(db): make task checkout atomic

This commit is contained in:
ZacharyZhang-NY committed 2026-04-22 13:38:54 -04:00
1 parent dffeccf3e2
commit 0785c9ba47
8 files changed
+329 -128

No files matched your search

+64 -8
View File
@@ -7,6 +7,13 @@ import { type ReadAll, type Transaction, nowIso, toBoolean } from "./helpers";
import { buildRunLogMethods } from "./run-logs";
import { buildRunEventMethods } from "./run-events";
import { buildCostEntryMethods } from "./cost-entries";
import { checkoutTaskInTransaction } from "./task-checkout";
class TaskCheckoutConflict extends Error {
constructor(readonly taskId: string) {
super(`Task checkout conflict: ${taskId}`);
}
}
export function buildRunMethods(
getDb: () => DatabaseSync,
@@ -16,6 +23,33 @@ export function buildRunMethods(
logsDir: string;
},
) {
function insertRunRow(input: {
id: string;
companyId: string;
taskId: string | null;
agentId: string;
workspaceId: string | null;
connectorId: ConnectorId;
logPath: string;
ts: string;
}): void {
getDb()
.prepare(
"insert into runs (id, company_id, task_id, agent_id, workspace_id, connector_id, status, summary, unattributed_cost, log_path, created_at, updated_at) values (?, ?, ?, ?, ?, ?, 'queued', '', 0, ?, ?, ?)",
)
.run(
input.id,
input.companyId,
input.taskId,
input.agentId,
input.workspaceId,
input.connectorId,
input.logPath,
input.ts,
input.ts,
);
}
return {
...buildRunLogMethods(getDb, readAll, transaction),
...buildRunEventMethods(getDb, readAll, transaction),
@@ -51,19 +85,41 @@ export function buildRunMethods(
const ts = nowIso();
const logPath = join(deps.logsDir, `${id}.log`);
transaction(() => {
getDb()
.prepare(
"insert into runs (id, company_id, task_id, agent_id, workspace_id, connector_id, status, summary, unattributed_cost, log_path, created_at, updated_at) values (?, ?, ?, ?, ?, ?, 'queued', '', 0, ?, ?, ?)",
)
.run(id, input.companyId, input.taskId, input.agentId, input.workspaceId, input.connectorId, logPath, ts, ts);
if (input.taskId !== null) {
getDb().prepare("update tasks set active_run_id = ?, updated_at = ? where id = ?").run(id, ts, input.taskId);
}
insertRunRow({ ...input, id, logPath, ts });
});
writeFileSync(logPath, "", "utf8");
return { id, logPath };
},
createTaskRun(input: {
id?: string;
companyId: string;
taskId: string;
agentId: string;
workspaceId: string | null;
connectorId: ConnectorId;
}): { id: string; logPath: string } | null {
const id = input.id ?? randomUUID();
const ts = nowIso();
const logPath = join(deps.logsDir, `${id}.log`);
try {
transaction(() => {
insertRunRow({ ...input, id, logPath, ts });
const checkoutOk = checkoutTaskInTransaction(getDb(), input.taskId, id);
if (!checkoutOk) {
throw new TaskCheckoutConflict(input.taskId);
}
});
} catch (error) {
if (error instanceof TaskCheckoutConflict) {
return null;
}
throw error;
}
writeFileSync(logPath, "", "utf8");
return { id, logPath };
},
markRunStatus(runId: string, status: RunRecord["status"], message: string | null) {
const ts = nowIso();
const startedAt = status === "running" ? ts : null;