fix(worker): claim workspace lock in main before enqueue (D-02)

The lock was previously claimed when the worker reported `running`, which
is after `agent.prompt` had already started streaming. Two concurrent
runs targeting the same workspace could both pass the main-side checks,
both get enqueued, and both read half-written state before one of them
lost the lock race and was cancelled mid-flight.

Move the `claimWorkspaceLock` call into `dispatchRunToWorker` so main
coordinates exclusivity authoritatively. If the lock is unavailable,
the run is finished as failed and never enqueued. The worker-side
lease-renewal path is preserved since it is a lease, not a lock.
This commit is contained in:
ZacharyZhang-NY committed 2026-04-16 21:49:30 -04:00
1 parent f86b3ddbac
commit ff0c01fe87
2 files changed
+27 -4

No files matched your search

+24
View File
@@ -86,6 +86,30 @@ export function createRunDispatch(deps: OrchestrationDependencies) {
throw new Error(connectorIssue);
}
// D-02: Claim the workspace lock synchronously before enqueuing the run.
// Previously the worker claimed the lock only after it emitted `running`,
// so two runs could race between enqueue and startup and both read the
// same workspace mid-write. Main is the authoritative coordinator.
if (
params.workspaceMode === "exclusive" &&
params.workspace &&
!db.claimWorkspaceLock(params.workspace.id, params.runId)
) {
db.finishRun({
runId: params.runId,
status: "failed",
summary: null,
errorMessage: `Workspace ${params.workspace.id} is locked by another run.`,
exitCode: null,
signal: null,
model: null,
sessionDisplayId: null,
costUsd: null,
unattributedCost: true,
});
return;
}
const payload = buildRunPayload(params, {
db,
logger,
+3 -4
View File
@@ -110,12 +110,11 @@ export async function handleWorkerMessage(
});
}
if (event.status === "running") {
// D-02: Workspace lock is claimed authoritatively in main's
// dispatchRunToWorker before enqueue. Only lease renewal and
// run-started housekeeping run here.
try {
const run = db.getRun(event.runId);
if (run.workspaceId && !db.claimWorkspaceLock(run.workspaceId, run.id)) {
logger.error(`[workspace] Failed to claim lock for workspace ${run.workspaceId} when run ${run.id} entered running state. Cancelling run.`);
worker?.postMessage({ type: "cancel-run", runId: run.id });
}
db.renewTaskLease(run.taskId, run.id);
onRunStarted?.(run.id, run.taskId, run.agentId, run.companyId);
} catch {