diff --git a/src/main/orchestration/run-dispatch.ts b/src/main/orchestration/run-dispatch.ts index f7b46dc5..9f015bd3 100644 --- a/src/main/orchestration/run-dispatch.ts +++ b/src/main/orchestration/run-dispatch.ts @@ -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, diff --git a/src/main/worker/worker-message-handler.ts b/src/main/worker/worker-message-handler.ts index 8efe8c46..86eba496 100644 --- a/src/main/worker/worker-message-handler.ts +++ b/src/main/worker/worker-message-handler.ts @@ -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 {