fix: make artifact projection durable

This commit is contained in:
ZacharyZhang-NY committed 2026-04-23 21:50:49 -04:00
1 parent 84c00e0011
commit 88c905c381
15 files changed
+654 -76

No files matched your search

@@ -0,0 +1,177 @@
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { afterEach, describe, expect, it, vi } from "vitest";
import { RuntimeBridge } from "../../src/main/agent-runtime";
import { AppDatabase } from "../../src/main/database";
import type { CredentialResolver } from "../../src/main/credentials";
const tempDirs: string[] = [];
async function createTempDir(prefix: string): Promise<string> {
const dir = await mkdtemp(join(tmpdir(), prefix));
tempDirs.push(dir);
return dir;
}
afterEach(async () => {
await Promise.all(tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true })));
});
async function createHarness() {
const profileDir = await createTempDir("agentcompany-artifact-ack-");
const workspacePath = await createTempDir("agentcompany-artifact-workspace-");
const db = new AppDatabase(profileDir);
db.init();
const companyId = db.core.company.saveCompany({
name: "Artifact Ack Co",
description: "Durable artifact projection test",
status: "active",
});
const workspaceId = db.core.workspaces.saveWorkspace({
companyId,
name: "Primary",
localPath: workspacePath,
repoUrl: "",
repoRef: "",
isPrimary: true,
});
const agentId = db.core.agents.saveAgent({
companyId,
name: "Artifact Agent",
role: "Engineer",
title: "Engineer",
status: "active",
reportsTo: null,
connectorId: "openai-codex",
workspaceId,
model: null,
capabilities: "Writes files",
budgetMonthlyUsd: 100,
});
const taskId = db.core.tasks.saveTask({
companyId,
title: "Produce artifact",
description: "",
assigneeAgentId: agentId,
workspaceId,
priority: "medium",
status: "in_progress",
});
const run = db.core.runs.createRun({
companyId,
taskId,
agentId,
workspaceId: null,
connectorId: "openai-codex",
});
const bridge = new RuntimeBridge({
db,
emitEvent: vi.fn(),
logger: {
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
},
credentialResolver: {
resolve: vi.fn(async () => ({ apiKey: "test-api-key", extraEnv: {} })),
} as unknown as CredentialResolver,
});
const prepare = await bridge.handleRpc({
rpcId: "prepare",
method: "prepareRun",
runId: run.id,
payload: {
connectorId: "openai-codex",
taskSessionId: "task-session-artifact-ack",
agentId,
companyId,
taskId,
modelId: "gpt-5.3-codex",
workspaceId: null,
workspacePath,
workspaceMode: "none",
},
});
expect(prepare.ok).toBe(true);
return {
db,
bridge,
companyId,
workspacePath,
agentId,
taskId,
runId: run.id,
};
}
describe("artifact projection durable ack", () => {
it("persists afterToolCall projection outcomes to the run event tail", async () => {
const harness = await createHarness();
try {
await mkdir(join(harness.workspacePath, "reports"), { recursive: true });
await writeFile(join(harness.workspacePath, "reports", "hook.md"), "hook artifact", "utf8");
const response = await harness.bridge.handleRpc({
rpcId: "after-tool",
method: "afterToolCall",
runId: harness.runId,
payload: {
toolName: "write",
toolCallId: "tool-call-hook",
args: { path: "reports/hook.md" },
resultContent: [],
resultDetails: null,
isError: false,
},
});
expect(response.ok).toBe(true);
const event = harness.db.core.runs.listRunEvents(harness.runId, harness.companyId)[0];
expect(event).toMatchObject({
eventType: "artifact_projection",
level: "info",
});
expect(event?.payload).toMatchObject({
phase: "after_tool_call",
toolName: "write",
toolCallId: "tool-call-hook",
workspaceMode: "none",
counts: { inserted: 1, updated: 0, retired: 0, skipped: 0 },
});
} finally {
harness.db.close();
}
});
it("runs completion reconciliation before resource release for non-exclusive observation workspaces", async () => {
const harness = await createHarness();
try {
await mkdir(join(harness.workspacePath, "reports"), { recursive: true });
await writeFile(join(harness.workspacePath, "reports", "completion.md"), "completion artifact", "utf8");
harness.bridge.releaseRunResources(harness.runId);
expect(
harness.db.projection.snapshot.listSnapshot().workProducts.some(
(entry) => entry.externalId === "reports/completion.md",
),
).toBe(true);
const events = harness.db.core.runs.listRunEvents(harness.runId, harness.companyId);
expect(events).toHaveLength(1);
expect(events[0]).toMatchObject({
eventType: "artifact_projection",
level: "info",
});
expect(events[0]?.payload).toMatchObject({
phase: "completion_reconcile",
workspaceMode: "none",
counts: { inserted: 1, updated: 0, retired: 0, skipped: 0 },
});
} finally {
harness.db.close();
}
});
});
@@ -5,6 +5,7 @@ import { tmpdir } from "node:os";
import { afterEach, describe, expect, it } from "vitest";
import { AppDatabase } from "../../src/main/database";
import {
projectToolExecutionToWorkProduct,
reconcileWorkspaceArtifacts,
type ArtifactProjectorRunContext,
} from "../../src/main/agent-runtime/artifact-projector";
@@ -236,6 +237,85 @@ describe("work_products archived visibility", () => {
});
describe("work_products bulk retirement", () => {
it("registers bash artifacts from non-exclusive observation workspaces", async () => {
const harness = await createHarness();
try {
const ctx: ArtifactProjectorRunContext = {
runId: harness.runId,
agentId: harness.agentId,
companyId: harness.companyId,
taskId: harness.taskId,
workspaceId: null,
workspacePath: harness.workspacePath,
workspaceMode: "none",
workspaceBaseline: {
mtimeByRelPath: new Map<string, number>(),
capturedAt: Date.now(),
truncated: false,
},
};
await mkdir(join(harness.workspacePath, "reports"), { recursive: true });
await writeFile(join(harness.workspacePath, "reports", "non-exclusive.md"), "observed", "utf8");
const outcomes = projectToolExecutionToWorkProduct(harness.db, ctx, {
toolName: "bash",
args: { command: "printf observed > reports/non-exclusive.md" },
result: null,
isError: false,
});
expect(outcomes).toEqual([
expect.objectContaining({
kind: "inserted",
relativePath: "reports/non-exclusive.md",
}),
]);
expect(
harness.db.projection.snapshot.listSnapshot().workProducts.some(
(entry) => entry.externalId === "reports/non-exclusive.md",
),
).toBe(true);
} finally {
harness.db.close();
}
});
it("retires deleted artifacts from non-exclusive observation workspaces", async () => {
const harness = await createHarness();
try {
const relPath = "reports/non-exclusive-retired.md";
const workProductId = registerArtifact(harness, relPath);
await unlink(join(harness.workspacePath, relPath));
const outcomes = reconcileWorkspaceArtifacts(harness.db, {
runId: harness.runId,
agentId: harness.agentId,
companyId: harness.companyId,
taskId: harness.taskId,
workspaceId: null,
workspacePath: harness.workspacePath,
workspaceMode: "none",
workspaceBaseline: {
mtimeByRelPath: new Map<string, number>([[relPath, Date.now()]]),
capturedAt: Date.now(),
truncated: false,
},
});
expect(outcomes).toEqual([
expect.objectContaining({
kind: "retired",
relativePath: relPath,
}),
]);
expect(
harness.db.core.documents.resolveWorkProductTarget(workProductId, harness.companyId),
).toMatchObject({ kind: "error", reason: "archived" });
} finally {
harness.db.close();
}
});
it("archives every deletion past the previous 50-path budget", async () => {
const harness = await createHarness();
try {
+28
View File
@@ -0,0 +1,28 @@
import { describe, expect, it, vi } from "vitest";
import { buildAgentHooks } from "../../src/main/agent-runtime/agent-hooks";
import type { WorkerRpcClient } from "../../src/main/worker/entry/rpc";
function createRpc(response: { ok: true; result: unknown } | { ok: false; error: string }) {
return {
invoke: vi.fn(async () => response),
} as unknown as WorkerRpcClient & { invoke: ReturnType<typeof vi.fn> };
}
describe("agent hook RPC policy", () => {
it("fails afterToolCall when main cannot acknowledge artifact projection", async () => {
const rpc = createRpc({ ok: false, error: "RuntimeBridge unavailable" });
const hooks = buildAgentHooks({
runId: "11111111-1111-4111-8111-111111111111",
rpc,
});
await expect(
hooks.afterToolCall({
toolCall: { name: "write", id: "tool-call-1" },
args: { path: "report.md" },
result: { content: [], details: null },
isError: false,
} as unknown as Parameters<typeof hooks.afterToolCall>[0]),
).rejects.toThrow("afterToolCall RPC failed: RuntimeBridge unavailable");
});
});
+3 -3
View File
@@ -86,7 +86,7 @@ describe("artifact projector error taxonomy", () => {
expect(logger.error).not.toHaveBeenCalled();
});
it("skips broad bash projection when the run has no exclusive workspace lock", async () => {
it("skips broad bash projection when the run has no change source", async () => {
const workspacePath = await createTempDir("agentcompany-shared-artifacts-");
const ctx: ArtifactProjectorRunContext = {
runId: "run-shared",
@@ -105,9 +105,9 @@ describe("artifact projector error taxonomy", () => {
args: { command: "touch report.md" },
result: null,
isError: false,
})).toEqual([{ kind: "skipped", reason: "workspace_not_exclusive" }]);
})).toEqual([{ kind: "skipped", reason: "no_baseline" }]);
expect(reconcileWorkspaceArtifacts({} as AppDatabase, ctx)).toEqual([
{ kind: "skipped", reason: "workspace_not_exclusive" },
{ kind: "skipped", reason: "no_baseline" },
]);
});
});
+54 -2
View File
@@ -3,6 +3,7 @@ import { describe, it, expect, vi, afterEach } from "vitest";
import { handleWorkerMessage } from "@main/worker/entry/message-handler";
import { workerMessageSchema } from "@main/worker/entry/types";
import type { RunJob } from "@main/worker/runtime/helpers";
import type { RunResourceReleaseResult } from "@main/agent-runtime/runtime-artifact-reconciler";
const RUN_ID = "00000000-0000-4000-8000-000000000001";
const NEXT_RUN_ID = "00000000-0000-4000-8000-000000000002";
@@ -31,7 +32,11 @@ function createHandlerDeps(calls: string[]) {
});
const rotateSession = vi.fn(() => calls.push("rotateSession"));
const processRunCompletion = vi.fn(() => calls.push("processRunCompletion"));
const releaseRunResources = vi.fn(() => calls.push("releaseRunResources"));
const releaseRunResources = vi.fn((): RunResourceReleaseResult => {
calls.push("releaseRunResources");
return { ok: true as const };
});
const markRunCompletionFailure = vi.fn(() => calls.push("markRunCompletionFailure"));
const db = {
core: {
@@ -44,6 +49,7 @@ function createHandlerDeps(calls: string[]) {
workspaceId: "workspace-1",
})),
finishRun,
markRunCompletionFailure,
},
workspaces: { releaseWorkspaceLock },
activity: { addActivity: vi.fn(() => calls.push("addActivity")) },
@@ -104,6 +110,7 @@ function createHandlerDeps(calls: string[]) {
rotateSession,
processRunCompletion,
releaseRunResources,
markRunCompletionFailure,
};
}
@@ -137,7 +144,7 @@ describe("worker completion boundary", () => {
Reflect.deleteProperty(process, "parentPort");
});
it("releases the workspace lock after session, automation, and runtime resource release", async () => {
it("runs artifact reconciliation before automation and releases the workspace lock last", async () => {
const calls: string[] = [];
const {
deps,
@@ -167,6 +174,9 @@ describe("worker completion boundary", () => {
expect(releaseWorkspaceLock.mock.invocationCallOrder[0]).toBeGreaterThan(
rotateSession.mock.invocationCallOrder[0]!,
);
expect(processRunCompletion.mock.invocationCallOrder[0]).toBeGreaterThan(
releaseRunResources.mock.invocationCallOrder[0]!,
);
expect(releaseWorkspaceLock.mock.invocationCallOrder[0]).toBeGreaterThan(
processRunCompletion.mock.invocationCallOrder[0]!,
);
@@ -175,6 +185,48 @@ describe("worker completion boundary", () => {
);
});
it("marks run completion failed and blocks success automation when artifact reconciliation fails", async () => {
const calls: string[] = [];
const {
deps,
releaseWorkspaceLock,
processRunCompletion,
releaseRunResources,
markRunCompletionFailure,
} = createHandlerDeps(calls);
releaseRunResources.mockImplementationOnce(() => {
calls.push("releaseRunResources");
return { ok: false as const, errorMessage: "completion_reconcile failed" };
});
const worker = { postMessage: vi.fn(() => calls.push("completionAck")) };
await handleWorkerMessage(
runFinishedMessage(RUN_ID),
worker as never,
deps as never,
workerMessageSchema,
false,
2,
30_000,
new Map(),
3,
new Set(),
new Map(),
);
expect(markRunCompletionFailure).toHaveBeenCalledWith(RUN_ID, "completion_reconcile failed");
expect(processRunCompletion).toHaveBeenCalledWith(
expect.objectContaining({ id: RUN_ID }),
expect.objectContaining({
status: "failed",
errorMessage: "completion_reconcile failed",
usageStatus: "error_stop",
}),
);
expect(releaseWorkspaceLock).toHaveBeenCalledWith("workspace-1", RUN_ID);
expect(worker.postMessage).toHaveBeenCalledWith({ type: "run-completion-ack", runId: RUN_ID });
});
it("keeps same-workspace runs queued until parent acknowledges completion", async () => {
const parentPort = new EventEmitter() as EventEmitter & {
postMessage: ReturnType<typeof vi.fn>;