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

+29 -32
View File
@@ -1,4 +1,3 @@
import { randomUUID } from "node:crypto";
import type { AppDatabase } from "../database";
import type { AgentRecord, ConnectorRecord } from "@shared/types";
import type { OrchestrationDependencies } from "./types";
@@ -165,22 +164,9 @@ export function createHeartbeat(
const candidateTasks = assignedTasks;
const candidateTask = candidateTasks.find((entry) => !entry.activeRunId) ?? null;
const runId = randomUUID();
let task: ReturnType<AppDatabase["entityCrud"]["getTask"]> | null = null;
if (candidateTask) {
const checkoutSuccess = db.tasks.checkoutTask(candidateTask.id, runId);
if (!checkoutSuccess) {
db.activityMod.addActivity({
companyId,
actor: "system",
action: "execution.checkout_rejected",
entityType: "task",
entityId: candidateTask.id,
detail: `Agent ${agent.name} lost checkout race on task "${candidateTask.title}"`,
});
return SKIPPED("checkout_race_lost");
}
task = db.entityCrud.getTask(candidateTask.id);
} else {
const hasActiveAssignedRun = candidateTasks.some(
@@ -209,16 +195,6 @@ export function createHeartbeat(
workspaceResolution = resolveWorkspace(task, agent, companyId);
} catch (error) {
if (error instanceof WorkspaceConfigurationError) {
// A coding-role task landed here with no bound workspace. We
// refuse to silently pick a shared workspace (the old
// company_fallback silent-corruption path) — the operator
// needs to bind a workspace explicitly. Release the lease
// the earlier checkoutTask just claimed so the task re-enters
// the pool at the next heartbeat instead of stalling as
// `in_progress` with no run.
if (task) {
db.tasks.releaseTaskLock(task.id, runId);
}
db.activityMod.addActivity({
companyId,
actor: "system",
@@ -237,14 +213,35 @@ export function createHeartbeat(
`[heartbeat] Workspace resolution for ${agent.name}: task.workspaceId=${task?.workspaceId ?? "null"}, agent.workspaceId=${agent.workspaceId ?? "null"}, projectId=${task?.projectId ?? "null"}, source=${workspaceResolution.source}, mode=${workspaceResolution.lockMode}, resolved=${workspace?.name ?? "NONE"}, localPath=${workspace?.localPath ?? "NONE"}`,
);
const run = db.runs.createRun({
id: runId,
companyId,
taskId: task?.id ?? null,
agentId: agent.id,
workspaceId: workspaceResolution.lockMode === "exclusive" ? (workspace?.id ?? null) : null,
connectorId: connector.id,
});
const run = task
? db.runs.createTaskRun({
companyId,
taskId: task.id,
agentId: agent.id,
workspaceId: workspaceResolution.lockMode === "exclusive" ? (workspace?.id ?? null) : null,
connectorId: connector.id,
})
: db.runs.createRun({
companyId,
taskId: null,
agentId: agent.id,
workspaceId: workspaceResolution.lockMode === "exclusive" ? (workspace?.id ?? null) : null,
connectorId: connector.id,
});
if (!run) {
if (!task) {
throw new Error("Taskless heartbeat run creation returned null.");
}
db.activityMod.addActivity({
companyId,
actor: "system",
action: "execution.checkout_rejected",
entityType: "task",
entityId: task.id,
detail: `Agent ${agent.name} lost checkout race on task "${task.title}"`,
});
return SKIPPED("checkout_race_lost");
}
const companyBasic = db.company.getCompanyBasic(companyId);
dispatchRunToWorker({
+4 -14
View File
@@ -1,4 +1,3 @@
import { randomUUID } from "node:crypto";
import type { AgentRecord, ConnectorRecord } from "@shared/types";
import type { DesktopResult } from "@shared/contracts";
import type { OrchestrationDependencies } from "./types";
@@ -87,22 +86,11 @@ export function createTaskRunner(
);
}
// CAS checkout — atomic claim (EXEC-01, EXEC-02)
const runId = randomUUID();
const checkoutSuccess = db.tasks.checkoutTask(task.id, runId);
if (!checkoutSuccess) {
return fail("CHECKOUT_RACE_LOST", "Another run claimed this task. Refresh and try again.");
}
let workspaceResolution: ReturnType<typeof resolveWorkspace>;
try {
workspaceResolution = resolveWorkspace(task, agent, task.companyId);
} catch (error) {
if (error instanceof WorkspaceConfigurationError) {
// Resolver refused to guess a workspace. Release the lease
// we just claimed so the task row does not stall at
// `in_progress`; surface the error to the caller.
db.tasks.releaseTaskLock(task.id, runId);
return fail("WORKSPACE_UNRESOLVED", error.message);
}
throw error;
@@ -112,14 +100,16 @@ export function createTaskRunner(
return fail("WORKSPACE_COMPANY_MISMATCH", "The selected workspace does not belong to the task company.");
}
const run = db.runs.createRun({
id: runId,
const run = db.runs.createTaskRun({
companyId: task.companyId,
taskId: task.id,
agentId: agent.id,
workspaceId: workspaceResolution.lockMode === "exclusive" ? (workspace?.id ?? null) : null,
connectorId: connector.id,
});
if (!run) {
return fail("CHECKOUT_RACE_LOST", "Another run claimed this task. Refresh and try again.");
}
const companyBasic = db.company.getCompanyBasic(task.companyId);
dispatchRunToWorker({