diff --git a/src/main/orchestration/resolve-workspace.ts b/src/main/orchestration/resolve-workspace.ts new file mode 100644 index 00000000..72b2987d --- /dev/null +++ b/src/main/orchestration/resolve-workspace.ts @@ -0,0 +1,43 @@ +import type { AppDatabase } from "../database"; +import type { WorkspaceLockMode } from "./types"; + +/** + * Resolve the workspace and lock semantics for a given run context. + * + * Previously this logic lived both in run-dispatch.ts (normal dispatch path) + * and worker-crash-recovery.ts (re-enqueue after crash); the recovery + * copy silently dropped the exclusive/shared distinction. Sharing the + * helper guarantees both paths agree on which runs hold exclusive locks. + */ +export function resolveWorkspaceWithLockMode( + db: AppDatabase, + task: { workspaceId: string | null; projectId?: string | null }, + agent: { workspaceId: string | null }, + companyId: string, +): { + workspace: ReturnType; + lockMode: WorkspaceLockMode; + source: "task" | "agent" | "project" | "company_fallback" | "none"; +} { + const taskWorkspace = db.getWorkspace(task.workspaceId); + if (taskWorkspace) { + return { workspace: taskWorkspace, lockMode: "exclusive", source: "task" }; + } + + const agentWorkspace = db.getWorkspace(agent.workspaceId); + if (agentWorkspace) { + return { workspace: agentWorkspace, lockMode: "exclusive", source: "agent" }; + } + + const projectWorkspace = db.getProjectPrimaryWorkspace(task.projectId ?? null, companyId); + if (projectWorkspace) { + return { workspace: projectWorkspace, lockMode: "exclusive", source: "project" }; + } + + const fallbackWorkspace = db.getAnyCompanyWorkspace(companyId); + if (fallbackWorkspace) { + return { workspace: fallbackWorkspace, lockMode: "shared", source: "company_fallback" }; + } + + return { workspace: null, lockMode: "none", source: "none" }; +} diff --git a/src/main/orchestration/run-dispatch.ts b/src/main/orchestration/run-dispatch.ts index 9f015bd3..9a006094 100644 --- a/src/main/orchestration/run-dispatch.ts +++ b/src/main/orchestration/run-dispatch.ts @@ -1,8 +1,9 @@ import { getConnectorExecutionReadinessIssue } from "@shared/connector-policy"; import type { ConnectorRecord } from "@shared/types"; -import type { WorkspaceLockMode, OrchestrationDependencies } from "./types"; +import type { OrchestrationDependencies } from "./types"; import { buildRunPayload, type RunDispatchParams } from "./turn-context"; import { validateConnectorModelSelection } from "../connector-model-policy"; +import { resolveWorkspaceWithLockMode } from "./resolve-workspace"; export function createRunDispatch(deps: OrchestrationDependencies) { const { @@ -24,27 +25,7 @@ export function createRunDispatch(deps: OrchestrationDependencies) { agent: { workspaceId: string | null }, companyId: string, ) { - const taskWorkspace = db.getWorkspace(task.workspaceId); - if (taskWorkspace) { - return { workspace: taskWorkspace, lockMode: "exclusive" as WorkspaceLockMode, source: "task" as const }; - } - - const agentWorkspace = db.getWorkspace(agent.workspaceId); - if (agentWorkspace) { - return { workspace: agentWorkspace, lockMode: "exclusive" as WorkspaceLockMode, source: "agent" as const }; - } - - const projectWorkspace = db.getProjectPrimaryWorkspace(task.projectId ?? null, companyId); - if (projectWorkspace) { - return { workspace: projectWorkspace, lockMode: "exclusive" as WorkspaceLockMode, source: "project" as const }; - } - - const fallbackWorkspace = db.getAnyCompanyWorkspace(companyId); - if (fallbackWorkspace) { - return { workspace: fallbackWorkspace, lockMode: "shared" as WorkspaceLockMode, source: "company_fallback" as const }; - } - - return { workspace: null, lockMode: "none" as WorkspaceLockMode, source: "none" as const }; + return resolveWorkspaceWithLockMode(db, task, agent, companyId); } function getRunEligibilityFailure( diff --git a/src/main/worker/worker-crash-recovery.ts b/src/main/worker/worker-crash-recovery.ts index 40f0e0e2..5d1440b9 100644 --- a/src/main/worker/worker-crash-recovery.ts +++ b/src/main/worker/worker-crash-recovery.ts @@ -1,4 +1,5 @@ import type { WorkerServiceDependencies } from "./worker-types"; +import { resolveWorkspaceWithLockMode } from "../orchestration/resolve-workspace"; export function handleWorkerExit( code: number | null, @@ -94,20 +95,7 @@ export function handleWorkerExit( const task = db.getTask(candidate.taskId); const agent = db.getAgent(candidate.agentId); const connector = db.getConnector(agent.connectorId); - const workspaceResolution = { - taskWorkspace: db.getWorkspace(task.workspaceId), - agentWorkspace: db.getWorkspace(agent.workspaceId), - projectWorkspace: db.getProjectPrimaryWorkspace(task.projectId ?? null, candidate.companyId), - }; - const workspace = workspaceResolution.taskWorkspace - ?? workspaceResolution.agentWorkspace - ?? workspaceResolution.projectWorkspace - ?? db.getAnyCompanyWorkspace(candidate.companyId); - const workspaceMode = workspaceResolution.taskWorkspace || workspaceResolution.agentWorkspace || workspaceResolution.projectWorkspace - ? "exclusive" - : workspace - ? "shared" - : "none"; + const { workspace, lockMode } = resolveWorkspaceWithLockMode(db, task, agent, candidate.companyId); const recoveryCompany = recoverySnapshot.companies.find((company) => company.id === candidate.companyId); dispatchRunToWorker({ @@ -117,7 +105,7 @@ export function handleWorkerExit( task, connector, workspace, - workspaceMode, + workspaceMode: lockMode, companyName: recoveryCompany?.name ?? "Company", companyDescription: recoveryCompany?.description ?? "", snapshot: recoverySnapshot,