Merge current master lifecycle and CI fixes into Claude behavior

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* codex/claude-runner-protocol:
  fix(grok): avoid routine wakes for proactive admission (#16422)
  feat(ci): profile full CODEOWNER PR suites in Honeycomb (#16378)
  test: bound command snapshot forwarding checks (#16421)
  fix(agents): report actionable lifecycle failure steps (#16418)
This commit is contained in:
DottaandPaperclip committed 2026-10-15 07:23:22 -05:00
commit bd3ceab2b2
50 files changed
+1630 -60

No files matched your search

@@ -5,6 +5,7 @@ import type { LifecycleAgent, LifecycleStore, LifecycleDriver } from "../applica
import { startAgentLifecycle } from "../index.js";
import { createAgentLifecycleEffects } from "../../../services/agent-lifecycle.js";
import { logger } from "../../../middleware/logger.js";
import { lifecycleFailureMessage } from "../application/failure.js";
const agent: LifecycleAgent = { id: "agent", companyId: "company", lifecycleState: "verifying", lifecycleVersion: 3,
lifecycleRequiredPluginIds: null, lifecycleError: null,
@@ -66,6 +67,30 @@ describe("lifecycle readiness handoff", () => {
});
describe("lifecycle failure diagnostics", () => {
it.each([
["host", "harness_model_unavailable", "Execution configuration check failed. The selected model is unavailable."],
["plugin", "required_plugin_unavailable", "Execution configuration check: required plugin step failed. A required plugin is unavailable."],
["select_plugins", "unknown private code", "Execution configuration check: finding required plugins failed. Paperclip could not complete this step."],
["transition", "40P01", "Execution configuration check: saving progress failed. Paperclip could not complete this step."],
] as const)("persists safe actionable errors for %s", async (stage, code, expected) => {
const { store, driver, worker } = fixture();
const error = Object.assign(new Error("secret provider output"), { code });
if (stage === "host") driver.runHost.mockRejectedValue(error);
if (stage === "plugin") driver.runPlugin.mockRejectedValue(error);
if (stage === "select_plugins") driver.requiredPluginIds.mockRejectedValue(error);
if (stage === "transition") store.change.mockRejectedValue(error);
await worker.process(agent.id);
expect(store.defer).toHaveBeenCalledWith(agent, expect.any(String), expect.stringContaining(expected), expect.any(Date));
expect(JSON.stringify(store.defer.mock.calls)).not.toContain("secret provider output");
expect(JSON.stringify(store.defer.mock.calls)).not.toContain("unknown private code");
await worker.stop();
});
it("does not treat prototype properties as allowed failure codes", () => {
expect(lifecycleFailureMessage({ stage: "host", phase: "verifying" }, { code: "toString" }))
.toContain("Paperclip could not complete this step");
});
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();
const error = new Error("private provider detail");
@@ -100,6 +125,7 @@ describe("lifecycle failure diagnostics", () => {
it.each([
[{ code: "required_plugin_unavailable" }, "required_plugin_unavailable"],
[{ code: "harness_test_failed" }, "harness_test_failed"],
[{ code: "harness_auth_failed" }, "harness_auth_failed"],
[{ cause: { code: "40P01", message: "private query" } }, "40P01"],
[{ code: "private credential value" }, "unclassified"],
])("logs known codes without provider error contents", (details, failureCode) => {
@@ -140,7 +140,7 @@ const support = await getEmbeddedPostgresTestSupport();
const work = worker(createLifecycleDriver(db, {} as never).runHost);
await work.process(agent.id);
expect(probe).toHaveBeenCalled();
expect(await current(agent.id)).toMatchObject({ lifecycleState: "verifying", lifecycleError: "The lifecycle step failed. Retry the operation." });
expect(await current(agent.id)).toMatchObject({ lifecycleState: "verifying", lifecycleError: expect.stringContaining("Execution configuration check failed. Authentication failed.") });
} finally { probe.mockRestore(); }
});
@@ -0,0 +1,40 @@
import type { LifecycleFailure } from "./ports.js";
// Only fixed text may leave this boundary. Provider errors can contain secrets.
const reasons = {
harness_test_failed: "The execution test did not pass. Open Configuration and run the agent test to inspect the failed checks.",
harness_auth_failed: "Authentication failed. Check the agent's AI connection or sign in to its execution tool, then retry.",
harness_model_unavailable: "The selected model is unavailable. Choose a model supported by the agent's AI connection in Configuration, then retry.",
harness_command_unavailable: "The execution command was not found. Check the command and its installation in the agent's environment, then retry.",
harness_cwd_invalid: "The working directory is unavailable. Check the working directory in Configuration, then retry.",
harness_probe_timed_out: "The execution test timed out. Paperclip will retry automatically. If this continues, run the agent test in Configuration.",
harness_probe_unexpected_output: "The execution test did not return the expected response. Run the agent test in Configuration to inspect the failed checks.",
test_environment_unavailable: "The test environment is unavailable. Check the agent's environment in Configuration. Paperclip will retry automatically.",
required_plugin_unavailable: "A required plugin is unavailable. Check the plugin's status in Settings. Paperclip will retry automatically.",
invalid_lifecycle_result: "A required plugin returned an invalid result. Check the plugin's status in Settings. Paperclip will retry automatically.",
} as const;
const transientCodes = new Set(["ECONNREFUSED", "ECONNRESET", "ETIMEDOUT", "40001", "40P01", "53300", "57P01"]);
export function lifecycleFailureCode(error: unknown): string | undefined {
const candidate = error as { code?: unknown; cause?: { code?: unknown } } | null;
return [candidate?.code, candidate?.cause?.code].find((code): code is string =>
typeof code === "string" && (Object.hasOwn(reasons, code) || transientCodes.has(code)));
}
export function lifecycleFailureMessage(context: LifecycleFailure, error: unknown): string {
const phases = {
preparing: "Agent preparation", verifying: "Execution configuration check", pausing: "Stopping agent execution",
resuming: "Starting agent resources", terminating: "Stopping agent execution", cleaning_up: "Agent resource cleanup",
};
const phase = context.phase && Object.hasOwn(phases, context.phase)
? phases[context.phase as keyof typeof phases] : "Agent setup";
const step = context.stage === "plugin" ? `${phase}: required plugin step`
: context.stage === "select_plugins" ? `${phase}: finding required plugins`
: ["save_plugins", "complete_host", "complete_plugin", "transition"].includes(context.stage)
? `${phase}: saving progress` : phase;
const code = lifecycleFailureCode(error);
const reason = code && Object.hasOwn(reasons, code) ? reasons[code as keyof typeof reasons]
: "Paperclip could not complete this step. It will retry automatically. If this continues, contact your instance administrator.";
return `${step} failed. ${reason}`;
}
@@ -1,6 +1,7 @@
import { randomUUID } from "node:crypto";
import type { LifecycleDriver, LifecycleStore, LifecycleFailure } from "./ports.js";
import { isLifecycleWorkPending } from "../domain/policy.js";
import { lifecycleFailureMessage } from "./failure.js";
/** Database leases protect each attempt across server processes. */
export function createLifecycleWorker(store: LifecycleStore, driver: LifecycleDriver, reportFailure: (context: LifecycleFailure, error: unknown) => void, canRun = () => true) {
@@ -20,10 +21,13 @@ export function createLifecycleWorker(store: LifecycleStore, driver: LifecycleDr
if (!agent || !isLifecycleWorkPending(agent.lifecycleState)) return;
const context = { agentId: agent.id, companyId: agent.companyId, phase: agent.lifecycleState,
operationId: agent.lifecycleOperation?.id, version: agent.lifecycleVersion };
const runStep = <T>(stage: LifecycleFailure["stage"], work: () => Promise<T>, pluginId?: string) =>
attempt({ ...context, stage, ...(pluginId ? { pluginId } : {}) }, work);
let failureContext: LifecycleFailure = { ...context, stage: "host" };
const runStep = <T>(stage: LifecycleFailure["stage"], work: () => Promise<T>, pluginId?: string) => {
const stepContext = { ...context, stage, ...(pluginId ? { pluginId } : {}) };
return attempt(stepContext, work).catch(error => { failureContext = stepContext; throw error; });
};
const renewal = setInterval(() => {
void runStep("renew", () => store.renew(agent, owner, new Date())).catch(() => {});
void attempt({ ...context, stage: "renew" }, () => store.renew(agent, owner, new Date())).catch(() => {});
}, 30_000);
renewal.unref?.();
try {
@@ -65,8 +69,9 @@ export function createLifecycleWorker(store: LifecycleStore, driver: LifecycleDr
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;
} catch (error) {
const message = lifecycleFailureMessage(failureContext, error);
if (!await runStep("defer", () => store.defer(agent, owner, message, new Date()))) continue;
return;
} finally {
clearInterval(renewal);
@@ -142,3 +142,5 @@ export function startAgentLifecycle(db: Db, effects: LifecycleEffects, driver: L
},
};
}
export { lifecycleFailureCode } from "./application/failure.js";