Merge master b189f586d into Codex runner default candidate

Preserve master transport, lifecycle, and release verification controls alongside macOS qualification.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-12 07:55:09 -05:00
commit f4abdc2cfc
749 files changed
+124181 -68418

No files matched your search

@@ -13,14 +13,58 @@ function fixture() {
const store = { get: vi.fn(), claim: vi.fn().mockResolvedValue(agent), renew: vi.fn().mockResolvedValue(true),
setRequiredPlugins: vi.fn().mockResolvedValue(true), completeHost: vi.fn().mockResolvedValue(true),
completePlugin: vi.fn().mockResolvedValue(true), defer: vi.fn().mockResolvedValue(true),
change: vi.fn().mockResolvedValue({ ...agent, lifecycleState: "ready" }), pending: vi.fn().mockResolvedValue([agent.id]) } satisfies LifecycleStore;
change: vi.fn().mockResolvedValue({ ...agent, lifecycleState: "ready", lifecycleVersion: 4, lifecycleOperation: null }), pending: vi.fn().mockResolvedValue([agent.id]) } satisfies LifecycleStore;
const driver = { requiredPluginIds: vi.fn().mockResolvedValue(["plugin"]), runHost: vi.fn().mockResolvedValue("complete"),
runPlugin: vi.fn().mockResolvedValue("complete") } satisfies LifecycleDriver;
runPlugin: vi.fn().mockResolvedValue("complete"), onReady: vi.fn().mockResolvedValue(undefined) } satisfies LifecycleDriver;
const report = vi.fn();
return { store, driver, report, worker: createLifecycleWorker(store, driver, report) };
}
afterEach(() => { vi.restoreAllMocks(); vi.useRealTimers(); });
describe("lifecycle readiness handoff", () => {
it("notifies the scheduler only after host, plugins, and readiness have committed", async () => {
const { store, driver, worker } = fixture();
let commit!: (ready: LifecycleAgent) => void;
store.change.mockReturnValue(new Promise(resolve => { commit = resolve; }));
driver.onReady.mockImplementation(async ready => {
expect(store.completeHost).toHaveBeenCalledOnce();
expect(store.completePlugin).toHaveBeenCalledOnce();
expect(store.change).toHaveBeenCalledOnce();
expect(ready).toMatchObject({ lifecycleState: "ready", lifecycleVersion: 4, lifecycleOperation: null });
});
const work = worker.process(agent.id);
await vi.waitFor(() => expect(store.change).toHaveBeenCalledOnce());
expect(driver.onReady).not.toHaveBeenCalled();
commit({ ...agent, lifecycleState: "ready", lifecycleVersion: 4, lifecycleOperation: null });
await work;
expect(driver.onReady).toHaveBeenCalledOnce();
await worker.stop();
});
it.each(["host", "plugin", "transition", "hold"])("does not dispatch when %s prevents readiness", async stage => {
const { store, driver, worker } = fixture();
if (stage === "host") driver.runHost.mockResolvedValue("pending");
if (stage === "plugin") driver.runPlugin.mockResolvedValue("pending");
if (stage === "transition") store.change.mockResolvedValue(null);
if (stage === "hold") store.change.mockResolvedValue({ ...agent, lifecycleState: "paused" });
await worker.process(agent.id);
expect(driver.onReady).not.toHaveBeenCalled();
await worker.stop();
});
it("preserves committed readiness after a dispatch failure for the queue sweep to recover", async () => {
const { store, driver, report, worker } = fixture();
const error = new Error("queue unavailable");
driver.onReady.mockRejectedValue(error);
await worker.process(agent.id);
expect(report).toHaveBeenCalledWith(expect.objectContaining({ stage: "ready", phase: "ready", version: 4 }), error);
expect(store.defer).not.toHaveBeenCalled();
expect(driver.runHost).toHaveBeenCalledOnce();
expect(store.change).toHaveBeenCalledOnce();
await worker.stop();
});
});
describe("lifecycle failure diagnostics", () => {
it.each(["host", "plugin", "claim", "renew", "scan", "defer"] as const)("reports %s failures with the available context", async stage => {
const { store, driver, report, worker } = fixture();
@@ -486,6 +486,51 @@ const support = await getEmbeddedPostgresTestSupport();
await expect(driver.runPlugin(snapshot, pluginId)).rejects.toThrow("Invalid lifecycle result");
});
it("drives required resource work through setup, pause, resume, and confirmed cleanup", async () => {
const pluginId = randomUUID();
await db.insert(plugins).values({ id: pluginId, pluginKey: pluginId, packageName: "resource-lifecycle-test", version: "1.0.0", status: "ready",
manifestJson: { agentLifecycle: true, capabilities: ["agents.lifecycle.manage"] } as never });
let resourcesReady = false;
let resourcesRemoved = false;
const requests: import("@paperclipai/shared").AgentLifecycleRequest[] = [];
const call = async (id: string, method: string, input: import("@paperclipai/shared").AgentLifecycleRequest) => {
expect(id).toBe(pluginId);
expect(method).toBe("agentLifecycle");
expect(input.companyId).toBe(companyId);
expect((await current(input.agentId)).lifecycleOperation?.hostComplete).toBe(true);
requests.push(input);
const pending = (input.phase === "preparing" && !resourcesReady) || (input.phase === "cleaning_up" && !resourcesRemoved);
return { operationId: input.operationId, version: input.version, status: pending ? "pending" : "complete" };
};
const driver = createLifecycleDriver(db, { call } as never);
// Provider/harness execution is stubbed; plugin dispatch and durable transitions are real.
const work = configureAgentLifecycle(db, { ...driver, requiredPluginIds: async () => [pluginId], runHost: async () => "complete" });
workers.push(work);
const lifecycle = createAgentLifecycle(db);
const agent = await hire();
await work.process(agent.id);
expect(await current(agent.id)).toMatchObject({ lifecycleState: "preparing", status: "paused", lifecycleError: null });
const preparation = requests[0];
resourcesReady = true;
await lifecycle.retry(agent.id); await work.process(agent.id);
expect(requests[1]).toEqual(preparation);
expect((await current(agent.id)).lifecycleState).toBe("ready");
await lifecycle.pauseAgent(agent.id); await work.process(agent.id);
expect((await current(agent.id)).lifecycleState).toBe("paused");
await lifecycle.resumeAgent(agent.id); await work.process(agent.id);
expect((await current(agent.id)).lifecycleState).toBe("ready");
await lifecycle.terminateAgent(agent.id); await work.process(agent.id);
expect((await current(agent.id)).lifecycleState).toBe("cleaning_up");
await expect(lifecycle.purgeAgent(agent.id)).rejects.toThrow("Complete termination");
resourcesRemoved = true;
await lifecycle.retry(agent.id); await work.process(agent.id);
expect((await current(agent.id)).lifecycleState).toBe("terminated");
expect(requests.map(request => request.phase)).toEqual([
"preparing", "preparing", "verifying", "pausing", "resuming", "terminating", "cleaning_up", "cleaning_up",
]);
expect(requests.every(request => request.agentId === agent.id)).toBe(true);
});
it("keeps host completion and required plugin results across a worker restart", async () => {
const pluginIds = [randomUUID(), randomUUID()];
const runHost = vi.fn(async () => "complete" as const);
@@ -27,11 +27,13 @@ export interface LifecycleDriver {
requiredPluginIds(agent: LifecycleAgent): Promise<string[]>;
runHost(agent: LifecycleAgent): Promise<"complete" | "pending">;
runPlugin(agent: LifecycleAgent, pluginId: string): Promise<"complete" | "pending">;
/** Notify the scheduler only after readiness has committed. */
onReady?(agent: LifecycleAgent): Promise<void>;
}
export interface LifecycleFailure {
stage: "claim" | "renew" | "select_plugins" | "save_plugins" | "host" | "plugin"
| "complete_host" | "complete_plugin" | "transition" | "defer" | "scan" | "policy_scan";
| "complete_host" | "complete_plugin" | "transition" | "defer" | "scan" | "policy_scan" | "ready";
agentId?: string;
companyId?: string;
phase?: AgentLifecycleState;
@@ -56,6 +56,14 @@ export function createLifecycleWorker(store: LifecycleStore, driver: LifecycleDr
if (!await runStep("complete_plugin", () => store.completePlugin(agent, owner, pluginId), pluginId)) continue phase;
}
const changed = await runStep("transition", () => store.change(id, "complete", { version: agent.lifecycleVersion, owner }));
if (changed?.lifecycleState === "ready" && !stopped && canRun()) {
// Accepted messages may already be waiting for verification. Do not
// leave them until the periodic queue sweep. Readiness is committed;
// a dispatch failure must not replay setup or defer the old phase.
await attempt({ ...context, stage: "ready", phase: changed.lifecycleState,
version: changed.lifecycleVersion, operationId: changed.lifecycleOperation?.id },
async () => { await driver.onReady?.(changed); }).catch(() => {});
}
if (changed && !isLifecycleWorkPending(changed.lifecycleState)) return;
} catch {
if (!await runStep("defer", () => store.defer(agent, owner, "The lifecycle step failed. Retry the operation.", new Date()))) continue;