Enable lock-free parallel agent runs
This commit is contained in:
1 parent
d13c9d83e8
commit
1a7eb8f1b3
18 files changed
+327
-194
No files matched your search
@@ -6,6 +6,7 @@ import type { AppDatabase } from "../../src/main/database";
|
||||
import {
|
||||
captureBaselineIfRequired,
|
||||
projectToolExecutionToWorkProduct,
|
||||
reconcileWorkspaceArtifacts,
|
||||
type ArtifactProjectorRunContext,
|
||||
} from "../../src/main/agent-runtime/artifact-projector";
|
||||
|
||||
@@ -56,6 +57,7 @@ describe("artifact projector error taxonomy", () => {
|
||||
taskId: "task-artifact-error",
|
||||
workspaceId: "workspace-artifact-error",
|
||||
workspacePath,
|
||||
workspaceMode: "exclusive",
|
||||
logger,
|
||||
workspaceBaseline: null,
|
||||
};
|
||||
@@ -83,4 +85,29 @@ describe("artifact projector error taxonomy", () => {
|
||||
);
|
||||
expect(logger.error).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("skips broad bash projection when the run has no exclusive workspace lock", async () => {
|
||||
const workspacePath = await createTempDir("agentcompany-shared-artifacts-");
|
||||
const ctx: ArtifactProjectorRunContext = {
|
||||
runId: "run-shared",
|
||||
agentId: "agent-shared",
|
||||
companyId: "company-shared",
|
||||
taskId: "task-shared",
|
||||
workspaceId: null,
|
||||
workspacePath,
|
||||
workspaceMode: "none",
|
||||
logger: createLogger(),
|
||||
workspaceBaseline: null,
|
||||
};
|
||||
|
||||
expect(projectToolExecutionToWorkProduct({} as AppDatabase, ctx, {
|
||||
toolName: "bash",
|
||||
args: { command: "touch report.md" },
|
||||
result: null,
|
||||
isError: false,
|
||||
})).toEqual([{ kind: "skipped", reason: "workspace_not_exclusive" }]);
|
||||
expect(reconcileWorkspaceArtifacts({} as AppDatabase, ctx)).toEqual([
|
||||
{ kind: "skipped", reason: "workspace_not_exclusive" },
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -64,6 +64,7 @@ function seedCofounderRun(db: AppDatabase) {
|
||||
taskSessionId: "session-1",
|
||||
workspaceId: null,
|
||||
workspacePath: null,
|
||||
workspaceMode: "none",
|
||||
workspaceBaseline: null,
|
||||
state: "agent_active",
|
||||
};
|
||||
|
||||
@@ -22,14 +22,13 @@ import {
|
||||
/**
|
||||
* Workspace lock policy (v52+).
|
||||
*
|
||||
* `lockMode='exclusive'` is opt-in: the task carries an explicit
|
||||
* workspaceId or the task's requiresFilesystem column is true.
|
||||
* Every other case returns `lockMode='none'` and `workspace=null`,
|
||||
* so four pure-cognitive agents run fully in parallel without
|
||||
* queueing on a single workspace lock. The previous
|
||||
* `company_fallback` step has been removed and now throws
|
||||
* `WorkspaceConfigurationError` for filesystem tasks with no
|
||||
* resolvable workspace.
|
||||
* `lockMode='exclusive'` is opt-in: the task's requiresFilesystem
|
||||
* column is true. A workspaceId can still provide the observation
|
||||
* root for artifact projection, but it does not imply a write lock.
|
||||
* Pure-cognitive agents run fully in parallel without queueing on a
|
||||
* single workspace lock. The previous `company_fallback` step has
|
||||
* been removed and now throws `WorkspaceConfigurationError` for
|
||||
* filesystem tasks with no resolvable workspace.
|
||||
*/
|
||||
|
||||
function seedCompany(db: AppDatabase) {
|
||||
@@ -70,11 +69,33 @@ describe("resolveWorkspaceWithLockMode", () => {
|
||||
await rm(tempDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("task.workspaceId set → exclusive on that workspace", () => {
|
||||
it("workspace-bound cognitive task stays lock-free and observes that workspace", () => {
|
||||
const { companyId } = seedCompany(db);
|
||||
const taskWorkspaceId = db.core.workspaces.saveWorkspace({
|
||||
companyId,
|
||||
name: "Task workspace",
|
||||
localPath: "/tmp/task-observation",
|
||||
repoUrl: "https://github.com/example/task-observation",
|
||||
repoRef: "main",
|
||||
isPrimary: false,
|
||||
});
|
||||
const result = resolveWorkspaceWithLockMode(
|
||||
db,
|
||||
{ workspaceId: taskWorkspaceId, projectId: null, requiresFilesystem: false },
|
||||
{ workspaceId: null, role: "CMO", department: "marketing" },
|
||||
companyId,
|
||||
);
|
||||
expect(result.source).toBe("none");
|
||||
expect(result.lockMode).toBe("none");
|
||||
expect(result.workspace).toBeNull();
|
||||
expect(result.observationPath).toBe("/tmp/task-observation");
|
||||
});
|
||||
|
||||
it("requiresFilesystem + task.workspaceId → exclusive on that workspace", () => {
|
||||
const { companyId, workspaceId } = seedCompany(db);
|
||||
const result = resolveWorkspaceWithLockMode(
|
||||
db,
|
||||
{ workspaceId, projectId: null },
|
||||
{ workspaceId, projectId: null, requiresFilesystem: true },
|
||||
{ workspaceId: null, role: "CMO", department: "marketing" },
|
||||
companyId,
|
||||
);
|
||||
@@ -194,7 +215,7 @@ describe("resolveWorkspaceWithLockMode", () => {
|
||||
expect(result.workspace).toBeNull();
|
||||
});
|
||||
|
||||
it("task.workspaceId wins over agent.workspaceId even when both are set", () => {
|
||||
it("filesystem task workspaceId wins over agent.workspaceId when both are set", () => {
|
||||
const { companyId, workspaceId: agentWsId } = seedCompany(db);
|
||||
const taskWsId = db.core.workspaces.saveWorkspace({
|
||||
companyId,
|
||||
@@ -206,7 +227,7 @@ describe("resolveWorkspaceWithLockMode", () => {
|
||||
});
|
||||
const result = resolveWorkspaceWithLockMode(
|
||||
db,
|
||||
{ workspaceId: taskWsId, projectId: null },
|
||||
{ workspaceId: taskWsId, projectId: null, requiresFilesystem: true },
|
||||
{ workspaceId: agentWsId, role: "CTO", department: "engineering" },
|
||||
companyId,
|
||||
);
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { createRunScopedBashOptions } from "../../src/main/worker/runtime/run-tools";
|
||||
|
||||
describe("run-scoped bash environment", () => {
|
||||
it("merges run env into the spawned command environment", () => {
|
||||
const options = createRunScopedBashOptions({
|
||||
AGENT_COMPANY_RUN_ID: "run-a",
|
||||
AGENT_COMPANY_API_KEY: "secret-a",
|
||||
});
|
||||
|
||||
const context = options.spawnHook?.({
|
||||
command: "env",
|
||||
cwd: "/tmp",
|
||||
env: {
|
||||
PATH: "/usr/bin",
|
||||
AGENT_COMPANY_RUN_ID: "base",
|
||||
},
|
||||
});
|
||||
|
||||
expect(context?.env.PATH).toBe("/usr/bin");
|
||||
expect(context?.env.AGENT_COMPANY_RUN_ID).toBe("run-a");
|
||||
expect(context?.env.AGENT_COMPANY_API_KEY).toBe("secret-a");
|
||||
});
|
||||
|
||||
it("keeps two run env hooks independent", () => {
|
||||
const runA = createRunScopedBashOptions({ AGENT_COMPANY_RUN_ID: "run-a" });
|
||||
const runB = createRunScopedBashOptions({ AGENT_COMPANY_RUN_ID: "run-b" });
|
||||
const base = { command: "env", cwd: "/tmp", env: { PATH: "/usr/bin" } };
|
||||
|
||||
expect(runA.spawnHook?.(base).env.AGENT_COMPANY_RUN_ID).toBe("run-a");
|
||||
expect(runB.spawnHook?.(base).env.AGENT_COMPANY_RUN_ID).toBe("run-b");
|
||||
});
|
||||
|
||||
it("freezes the env snapshot at factory creation", () => {
|
||||
const env = { AGENT_COMPANY_RUN_ID: "run-a" };
|
||||
const options = createRunScopedBashOptions(env);
|
||||
env.AGENT_COMPANY_RUN_ID = "mutated";
|
||||
|
||||
const context = options.spawnHook?.({
|
||||
command: "env",
|
||||
cwd: "/tmp",
|
||||
env: {},
|
||||
});
|
||||
|
||||
expect(context?.env.AGENT_COMPANY_RUN_ID).toBe("run-a");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user