From 2d81789c141e8c3370b6d08b36ececf8f347c40f Mon Sep 17 00:00:00 2001 From: Dotta Date: Sun, 11 Oct 2026 14:43:27 -0500 Subject: [PATCH] fix(runner): bundle the Claude ACP runtime and explain missing packages Co-Authored-By: Paperclip --- doc/DEVELOPING.md | 8 +++++++ .../runner-core/src/acpx_sidecar_transport.rs | 2 ++ .../src/cli/acpx-sidecar-input.test.ts | 6 +++++ .../src/cli/acpx-sidecar-input.ts | 2 ++ .../acpx/installation-integrity.test.ts | 15 ++++++++++++ .../drivers/acpx/installation-integrity.ts | 17 +++++++++++++- scripts/acpx-patch-packaging.test.mjs | 13 +++++++++++ scripts/prepare-bundled-package.mjs | 11 ++++++--- server/package.json | 2 ++ .../verify-runner-vendor-dependencies.mjs | 18 ++++++++++++++- ...verify-runner-vendor-dependencies.test.mjs | 21 ++++++++++++++++- server/src/adapters/registry.test.ts | 7 ++++++ server/src/adapters/registry.ts | 7 +++++- .../native-session-executor.test.ts | 7 ++++++ .../native-runtime/native-session-executor.ts | 4 ++++ ui/src/components/RuntimeTestCard.test.tsx | 23 +++++++++++++++++++ ui/src/components/RuntimeTestCard.tsx | 7 +++++- ui/src/components/TaskChatThread.test.tsx | 13 +++++++++++ ui/src/components/TaskChatThread.tsx | 6 ++++- 19 files changed, 180 insertions(+), 9 deletions(-) create mode 100644 ui/src/components/RuntimeTestCard.test.tsx diff --git a/doc/DEVELOPING.md b/doc/DEVELOPING.md index 6fc8fa4f267..7593f838928 100644 --- a/doc/DEVELOPING.md +++ b/doc/DEVELOPING.md @@ -1457,6 +1457,14 @@ Claude, which can reject unavailable models. Package/version verification is independent of model selection. Environment tests verify runtime installation; a successful provider run additionally verifies credentials and model access. +The server must install and bundle `@agentclientprotocol/claude-agent-acp` +at the qualified version, including the patched bridge and pinned SDK. The +vendor dependency check covers this dynamic lookup as well as ordinary imports. +A missing provider package is reported as **Runtime missing**, with automatic +retries stopped. Reinstall or rebuild Paperclip with its provider dependencies, +restart the server, and run the agent's runtime test before retrying the task. +Changing the model or signing in again cannot repair a missing runtime package. + ACPX Claude supports Linux x64 and macOS ARM64/x64 with pinned SDK executables. On macOS the launcher uses private verified module/executable snapshots instead of Linux `/proc` descriptors. Dependency isolation, process ownership, and diff --git a/packages/paperclip-runner/runner/crates/runner-core/src/acpx_sidecar_transport.rs b/packages/paperclip-runner/runner/crates/runner-core/src/acpx_sidecar_transport.rs index 331f6ed17f3..ff99355038f 100644 --- a/packages/paperclip-runner/runner/crates/runner-core/src/acpx_sidecar_transport.rs +++ b/packages/paperclip-runner/runner/crates/runner-core/src/acpx_sidecar_transport.rs @@ -844,6 +844,7 @@ fn stderr_diagnostic_categories(value: &str) -> BTreeSet<&'static str> { fn response_error_classification(error: &ResponseError) -> &'static str { match error.code.as_str() { + "ACPX_RUNTIME_PACKAGE_MISSING" => return "runtime_package_missing", "ACP_MODEL_UNSUPPORTED" => return "requested_model_unsupported", "AGENT_STARTUP_FAILED" => return "agent_startup_failed", "AGENT_STARTUP_FAILED.UNVERIFIED_MODULE" => return "agent_startup_unverified_module", @@ -1038,6 +1039,7 @@ mod tests { #[test] fn candidate_auth_diagnostics_use_only_closed_codes_and_never_provider_text() { for (code, expected) in [ + ("ACPX_RUNTIME_PACKAGE_MISSING", "runtime_package_missing"), ("ACPX_TOOL_CALL_STALE", "provider_tool_call_retired"), ("ACPX_TOOL_CALL_STALE_EXTRA", "unclassified"), ("COPILOT_AUTH_REQUIRED", "authentication_required"), diff --git a/packages/paperclip-runner/src/cli/acpx-sidecar-input.test.ts b/packages/paperclip-runner/src/cli/acpx-sidecar-input.test.ts index 12c80c49221..0b6a280bdb3 100644 --- a/packages/paperclip-runner/src/cli/acpx-sidecar-input.test.ts +++ b/packages/paperclip-runner/src/cli/acpx-sidecar-input.test.ts @@ -186,3 +186,9 @@ describe("ACPX sidecar input sequencing", () => { ); }); }); + +it("preserves a missing runtime package category across wrapped bootstrap errors", () => { + const error = Object.assign(new Error("private resolver stack"), { code: "ACPX_RUNTIME_PACKAGE_MISSING" }); + expect(acpxSidecarErrorCode(new Error("bootstrap failed", { cause: error }))).toBe("ACPX_RUNTIME_PACKAGE_MISSING"); + expect(acpxSidecarErrorCode(acpxBootstrapBlockedError(error, "session.read")!)).toBe("ACPX_RUNTIME_PACKAGE_MISSING"); +}); diff --git a/packages/paperclip-runner/src/cli/acpx-sidecar-input.ts b/packages/paperclip-runner/src/cli/acpx-sidecar-input.ts index 6aeee58ca14..e9f4c645028 100644 --- a/packages/paperclip-runner/src/cli/acpx-sidecar-input.ts +++ b/packages/paperclip-runner/src/cli/acpx-sidecar-input.ts @@ -25,6 +25,7 @@ export function acpxBootstrapBlockedError( return failure ? new Error( `ACPX provider bootstrap failed before ${command}: ${failure.message}`, + { cause: failure }, ) : null; } @@ -130,6 +131,7 @@ const GENERIC_ACPX_OUTPUT_CODES = new Set([ ]); const STABLE_ACPX_SIDECAR_CODES = new Set([ + "ACPX_RUNTIME_PACKAGE_MISSING", "COPILOT_POLICY_VIOLATION", "COPILOT_DETACHED_WORK_UNSUPPORTED", "ACP_MODEL_UNSUPPORTED", diff --git a/packages/paperclip-runner/src/drivers/acpx/installation-integrity.test.ts b/packages/paperclip-runner/src/drivers/acpx/installation-integrity.test.ts index 089aeec06bc..7231142f0f8 100644 --- a/packages/paperclip-runner/src/drivers/acpx/installation-integrity.test.ts +++ b/packages/paperclip-runner/src/drivers/acpx/installation-integrity.test.ts @@ -54,6 +54,21 @@ afterEach(async () => { }); describe("ACPX installation integrity", () => { + it("explains a missing Claude ACP installation without leaking the resolver stack", async () => { + const missingPackage = Object.assign(new Error( + "Cannot find module '@agentclientprotocol/claude-agent-acp/package.json'\nRequire stack: /srv/private/server.js", + ), { code: "MODULE_NOT_FOUND" }); + const error = await verifyQualifiedAcpxInstallation( + resolveQualifiedAcpxProfile("claude", "claude-opus-5-5"), + () => { throw missingPackage; }, + ).catch(error => error); + expect(error).toMatchObject({ code: "ACPX_RUNTIME_PACKAGE_MISSING" }); + expect(error.message).toContain("@agentclientprotocol/claude-agent-acp"); + expect(error.message).toContain("Reinstall or rebuild"); + expect(error.message).not.toContain("/srv/private"); + expect(error.message).not.toContain("Require stack"); + }); + it.each([false, true])("admits only the declared qualified Codex platform in its npm-hoisted slot (published platform declaration: %s)", async (publishedDeclaration) => { const parent = await realpath(await mkdtemp(join(tmpdir(), "paperclip-codex-npm-hoist-"))); temporaryDirectories.push(parent); diff --git a/packages/paperclip-runner/src/drivers/acpx/installation-integrity.ts b/packages/paperclip-runner/src/drivers/acpx/installation-integrity.ts index 05fd89fdc47..119c91e8e24 100644 --- a/packages/paperclip-runner/src/drivers/acpx/installation-integrity.ts +++ b/packages/paperclip-runner/src/drivers/acpx/installation-integrity.ts @@ -645,7 +645,22 @@ export async function verifyQualifiedAcpxInstallation( if (builtin && (profile.agentServerPackage !== "builtin:grok-acp" || profile.agentServerVersion !== "1" || profile.agentRuntimePackage !== "native:grok" || profile.agentRuntimeVersion !== "1.0.13")) { throw new Error("Grok builtin profile identity mismatch"); } - const serverPackageJsonPath = builtin ? null : await realpath(resolvePackageJson(profile.agentServerPackage)); + let serverPackageJsonPath: string | null = null; + if (!builtin) { + try { + serverPackageJsonPath = await realpath(resolvePackageJson(profile.agentServerPackage)); + } catch (error) { + if ((profile.agent === "claude" || profile.agent === "codex") && + error instanceof Error && "code" in error && + (error.code === "MODULE_NOT_FOUND" || error.code === "ERR_MODULE_NOT_FOUND")) { + throw Object.assign(new Error( + `The ACPX ${profile.agent} runtime is incomplete: required package ${profile.agentServerPackage} is missing. ` + + "Reinstall or rebuild Paperclip with its bundled provider dependencies, restart the server, then test the agent's runtime before retrying.", + ), { code: "ACPX_RUNTIME_PACKAGE_MISSING" }); + } + throw error; + } + } const serverPackage: AcpxPackageMetadata = builtin ? { version: "1", bin: "launcher.cjs", type: "commonjs" } : await readPackageJson(serverPackageJsonPath!, profile.agentServerPackage); diff --git a/scripts/acpx-patch-packaging.test.mjs b/scripts/acpx-patch-packaging.test.mjs index fb7086946ef..9454ab009b0 100644 --- a/scripts/acpx-patch-packaging.test.mjs +++ b/scripts/acpx-patch-packaging.test.mjs @@ -176,6 +176,19 @@ test("published packages preserve the patched ACPX runtime", () => { assert.equal(cliEsbuildConfig.external.includes("acpx"), false); }); +test("published Claude bridge pins its qualified SDK and provider dependency closure", () => { + const published = materializePublishManifest(serverPackage); + const { installManifest } = configureBundledProviderOverrides( + createBundledInstallManifest(published, serverPackage.bundleDependencies), + serverPackage.bundleDependencies, rootPackage, profileData, + ); + assert.deepEqual(installManifest.overrides["@agentclientprotocol/claude-agent-acp@0.73.0"], { + "@anthropic-ai/claude-agent-sdk": "0.3.286", + "@agentclientprotocol/sdk": "1.4.0", + zod: "4.4.3", + }); +}); + test("published Codex bridge uses its qualified producer closure without consumer overrides", () => { const published = materializePublishManifest(serverPackage); const { installManifest, profiles } = configureBundledProviderOverrides( diff --git a/scripts/prepare-bundled-package.mjs b/scripts/prepare-bundled-package.mjs index cdc03387905..c6a4ad5f841 100644 --- a/scripts/prepare-bundled-package.mjs +++ b/scripts/prepare-bundled-package.mjs @@ -51,13 +51,14 @@ export function createBundledInstallManifest(publishManifest, bundledDependencie } // npm consumers cannot inherit the workspace's pnpm overrides or patches. -// Materialize the already-qualified Codex bridge closure in the tarball, +// Materialize the already-qualified provider bridge closures in the tarball, // with overrides confined to the temporary, scripts-disabled producer graph. export function configureBundledProviderOverrides(installManifest, bundledDependencies, rootPackage, profileData) { const result = structuredClone(installManifest); const selected = []; for (const [agent, serverPackage, runtimePackage] of [ ["codex", "@agentclientprotocol/codex-acp", "@openai/codex"], + ["claude", "@agentclientprotocol/claude-agent-acp", "@anthropic-ai/claude-agent-sdk"], ]) { if (!bundledDependencies.includes(serverPackage)) continue; const profile = profileData?.profiles?.[agent]; @@ -75,9 +76,13 @@ export function configureBundledProviderOverrides(installManifest, bundledDepend if (result.overrides?.[npmSelector] !== undefined) { throw new Error(`Bundled ${agent} runtime has a conflicting producer override`); } - const overrides = { [runtimePackage]: profile.agentRuntimeVersion }; + const overrides = { [runtimePackage]: profile.agentRuntimeVersion, + ...(agent === "claude" ? { "@agentclientprotocol/sdk": "1.4.0", zod: "4.4.3" } : {}), + }; result.overrides = { ...result.overrides, [npmSelector]: overrides }; - selected.push({ agent, ...profile }); + // Codex delegates its platform aliases below. Claude retains the SDK + // platform dependencies in its ordinary bundled provider closure. + if (agent === "codex") selected.push({ agent, ...profile }); } return { installManifest: result, profiles: selected }; } diff --git a/server/package.json b/server/package.json index ae6ce5ab0b9..657e961e729 100644 --- a/server/package.json +++ b/server/package.json @@ -44,6 +44,7 @@ "typecheck": "pnpm run prepare:runner-vendor && pnpm --filter @paperclipai/plugin-sdk ensure-build-deps && tsc --noEmit" }, "dependencies": { + "@agentclientprotocol/claude-agent-acp": "0.73.0", "@agentclientprotocol/codex-acp": "1.6.2", "@ai-sdk/amazon-bedrock": "5.0.113", "@ai-sdk/anthropic": "4.0.78", @@ -113,6 +114,7 @@ "zod": "^4.4.3" }, "bundleDependencies": [ + "@agentclientprotocol/claude-agent-acp", "@agentclientprotocol/codex-acp", "@chat-adapter/discord", "@chat-adapter/github", diff --git a/server/scripts/verify-runner-vendor-dependencies.mjs b/server/scripts/verify-runner-vendor-dependencies.mjs index 26bf6f3918d..78d855d021b 100644 --- a/server/scripts/verify-runner-vendor-dependencies.mjs +++ b/server/scripts/verify-runner-vendor-dependencies.mjs @@ -100,6 +100,21 @@ export async function findRunnerExternalPackages(entryPoints) { return externalPackageNames; } +// Provider entrypoints are resolved with createRequire from profile metadata, +// so esbuild cannot see them. These two bridges ship in the server package; +// other ACP agents use separately provisioned provider packs. +export async function findRunnerRuntimePackages(entryPoints, profileData) { + const packages = await findRunnerExternalPackages(entryPoints); + for (const agent of ["claude", "codex"]) { + const profile = profileData?.profiles?.[agent]; + if (profileData?.schema !== "paperclip.acpx-profiles.v1" || !profile?.agentServerPackage) { + throw new Error(`paperclip-runner vendor check: missing ${agent} provider profile.`); + } + packages.add(profile.agentServerPackage); + } + return packages; +} + function readDependencyNames(packageJsonPath) { const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8")); return new Map(Object.entries(packageJson.dependencies ?? {})); @@ -125,7 +140,8 @@ function explainMissingDependencies(missing, runnerDependencyNames) { async function main() { const entryPoints = ENTRY_POINT_NAMES.map((name) => resolve(runnerDist, name)); - const externalPackageNames = await findRunnerExternalPackages(entryPoints); + const profileData = JSON.parse(readFileSync(resolve(runnerRoot, "acpx-profiles.json"), "utf8")); + const externalPackageNames = await findRunnerRuntimePackages(entryPoints, profileData); const serverDependencyNames = readDependencyNames(resolve(serverRoot, "package.json")); const missing = findMissingVendorDependencies( diff --git a/server/scripts/verify-runner-vendor-dependencies.test.mjs b/server/scripts/verify-runner-vendor-dependencies.test.mjs index 07bbbcef3bf..3ffca3e2cf5 100644 --- a/server/scripts/verify-runner-vendor-dependencies.test.mjs +++ b/server/scripts/verify-runner-vendor-dependencies.test.mjs @@ -1,4 +1,4 @@ -import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; @@ -7,6 +7,7 @@ import { afterEach, describe, expect, it } from "vitest"; import { findMissingVendorDependencies, findRunnerExternalPackages, + findRunnerRuntimePackages, } from "./verify-runner-vendor-dependencies.mjs"; describe("findMissingVendorDependencies", () => { @@ -87,6 +88,15 @@ describe("findRunnerExternalPackages", () => { expect(externalPackageNames).toEqual(new Set(["smol-toml"])); }); + it("checks dynamically resolved bundled providers even without a JavaScript import", async () => { + const entryPoints = writeFixture(); + const profiles = JSON.parse(readFileSync(new URL("../../packages/paperclip-runner/acpx-profiles.json", import.meta.url), "utf8")); + const packages = await findRunnerRuntimePackages(entryPoints, profiles); + expect(findMissingVendorDependencies(packages, new Set(["smol-toml", "@agentclientprotocol/codex-acp"]))) + .toEqual(["@agentclientprotocol/claude-agent-acp"]); + expect(packages.has("pi-acp")).toBe(false); + }); + it("throws a clear, actionable error when an entry point is missing", async () => { fixtureDir = mkdtempSync(join(tmpdir(), "paperclip-runner-vendor-fixture-")); const missingEntryPoint = join(fixtureDir, "index.js"); @@ -96,3 +106,12 @@ describe("findRunnerExternalPackages", () => { ); }); }); + +// Claude is resolved through createRequire at runtime, outside esbuild's graph. +it("ships the qualified Claude ACP server with the vendored runner", () => { + const server = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")); + const profiles = JSON.parse(readFileSync(new URL("../../packages/paperclip-runner/acpx-profiles.json", import.meta.url), "utf8")); + const claude = profiles.profiles.claude; + expect(server.dependencies[claude.agentServerPackage]).toBe(claude.agentServerVersion); + expect(server.bundleDependencies).toContain(claude.agentServerPackage); +}); diff --git a/server/src/adapters/registry.test.ts b/server/src/adapters/registry.test.ts index d86b0144217..b59f7ffc878 100644 --- a/server/src/adapters/registry.test.ts +++ b/server/src/adapters/registry.test.ts @@ -118,6 +118,13 @@ describe("native ACPX environment checks", () => { })]); }); + it("identifies an incomplete installation and gives the operator a repair", async () => { + probeInstallation.mockRejectedValue(Object.assign(new Error("The Claude ACP runtime package is missing. Reinstall or rebuild Paperclip."), { code: "ACPX_RUNTIME_PACKAGE_MISSING" })); + const result = await requireServerAdapter("paperclip_runner").testEnvironment!(context); + expect(result.status).toBe("fail"); + expect(result.checks[0]).toMatchObject({ code: "acpx_runtime_package_missing", message: expect.stringContaining("Reinstall or rebuild") }); + }); + it("requires a successful installed runtime probe", async () => { const result = await requireServerAdapter("paperclip_runner").testEnvironment!(context); expect(result.status).toBe("pass"); diff --git a/server/src/adapters/registry.ts b/server/src/adapters/registry.ts index 54b72aa3bd5..1754f81e3df 100644 --- a/server/src/adapters/registry.ts +++ b/server/src/adapters/registry.ts @@ -447,7 +447,12 @@ const paperclipRunnerAdapter: ServerAdapterModule = { } catch (error) { return { adapterType: "paperclip_runner", status: "fail" as const, testedAt: new Date().toISOString(), - checks: [{ code: "acpx_runtime_unavailable", level: "error" as const, message: error instanceof Error ? error.message : "ACPX Claude runtime could not be verified." }], + checks: [{ + code: error instanceof Error && "code" in error && error.code === "ACPX_RUNTIME_PACKAGE_MISSING" + ? "acpx_runtime_package_missing" : "acpx_runtime_unavailable", + level: "error" as const, + message: error instanceof Error ? error.message : "The ACPX runtime could not be verified.", + }], }; } } diff --git a/server/src/services/native-runtime/native-session-executor.test.ts b/server/src/services/native-runtime/native-session-executor.test.ts index 6ae27897a98..7394ed04bee 100644 --- a/server/src/services/native-runtime/native-session-executor.test.ts +++ b/server/src/services/native-runtime/native-session-executor.test.ts @@ -13150,3 +13150,10 @@ it.each([ ), ).toBe("native_event_replay_conflict"); }); + +it("keeps an ACPX missing runtime package failure actionable and stops automatic retries", () => { + const failure = new Error('PRP command session.open failed: {"result":{"code":"command_execution_failed","message":"failed to start ACPX provider: ACPX sidecar command session.open was rejected (retryable=false, classification=runtime_package_missing)"},"status":"failed"}'); + const code = nativeSessionFailureSourceCode(failure); + expect(code).toBe("native_provider_runtime_missing"); + expect(nativeSessionFailureDisposition(1, new Date(), code)).toEqual({ phase: "terminal_failure", failureCode: code, nextAttemptAt: null }); +}); diff --git a/server/src/services/native-runtime/native-session-executor.ts b/server/src/services/native-runtime/native-session-executor.ts index 6b5275fc9d5..c18d53f3f76 100644 --- a/server/src/services/native-runtime/native-session-executor.ts +++ b/server/src/services/native-runtime/native-session-executor.ts @@ -6197,6 +6197,7 @@ export function nativeSessionFailureDisposition( sourceFailureCode?: ReturnType, ) { const permanentFailure = + sourceFailureCode === "native_provider_runtime_missing" || sourceFailureCode === "native_provider_model_rejected" || sourceFailureCode === "native_provider_approval_required" || sourceFailureCode === "native_event_replay_conflict" || @@ -6270,6 +6271,7 @@ export function nativeSessionFailureSourceCode( | "native_runner_process_exited" | "planning_mode_unsupported" | "native_event_replay_conflict" + | "native_provider_runtime_missing" | "native_provider_model_rejected" | "native_current_wake_comments_unread" | "native_current_wake_comments_changed_after_read" @@ -6292,6 +6294,8 @@ export function nativeSessionFailureSourceCode( if (error instanceof NativeSessionCleanupQuarantinedError) return "native_session_cleanup_quarantined"; const message = error instanceof Error ? error.message : String(error); + if (/classification=runtime_package_missing\)/.test(message)) + return "native_provider_runtime_missing"; if (/native_provider_model_rejected/i.test(message)) return "native_provider_model_rejected"; if (/native_adopted_runner_authentication_timeout/i.test(message)) { diff --git a/ui/src/components/RuntimeTestCard.test.tsx b/ui/src/components/RuntimeTestCard.test.tsx new file mode 100644 index 00000000000..b7ca0538ece --- /dev/null +++ b/ui/src/components/RuntimeTestCard.test.tsx @@ -0,0 +1,23 @@ +// @vitest-environment jsdom + +import { flushSync } from "react-dom"; +import { createRoot } from "react-dom/client"; +import { expect, it } from "vitest"; +import { RuntimeTestCard } from "./RuntimeTestCard"; + +it("shows the installation repair directly on a missing runtime test", () => { + const container = document.createElement("div"); + const root = createRoot(container); + const message = "The ACPX claude runtime is incomplete: required package @agentclientprotocol/claude-agent-acp is missing. Reinstall or rebuild Paperclip with its bundled provider dependencies, restart the server, then test the agent's runtime before retrying."; + try { + flushSync(() => root.render( {}} result={{ + adapterType: "paperclip_runner", status: "fail", testedAt: "2026-10-11T11:19:14Z", + checks: [{ code: "acpx_runtime_package_missing", level: "error", message }], + }} />)); + expect(container.querySelector('[role="alert"]')?.textContent).toContain("Runtime missing"); + expect(container.querySelector('[role="alert"]')?.textContent).toContain(message); + expect(container.textContent).not.toContain("Check your model and provider connection"); + } finally { + flushSync(() => root.unmount()); + } +}); diff --git a/ui/src/components/RuntimeTestCard.tsx b/ui/src/components/RuntimeTestCard.tsx index 24398642b14..66c4b033d9d 100644 --- a/ui/src/components/RuntimeTestCard.tsx +++ b/ui/src/components/RuntimeTestCard.tsx @@ -61,7 +61,12 @@ export function RuntimeTestCard({ disabled?: boolean; variant?: "connection" | "prerequisites"; }) { - const content = (variant === "prerequisites" ? prerequisiteCopy : copy)[state]; + const missingRuntime = state === "fail" + ? result?.checks.find(check => check.code === "acpx_runtime_package_missing") + : undefined; + const content = missingRuntime + ? { ...copy.fail, title: "Runtime missing", description: missingRuntime.message } + : (variant === "prerequisites" ? prerequisiteCopy : copy)[state]; const Icon = state === "running" ? Loader2 diff --git a/ui/src/components/TaskChatThread.test.tsx b/ui/src/components/TaskChatThread.test.tsx index b05b403b431..d7d255d6d82 100644 --- a/ui/src/components/TaskChatThread.test.tsx +++ b/ui/src/components/TaskChatThread.test.tsx @@ -4658,3 +4658,16 @@ describe("TaskChatThread composer execution controls", () => { expect(container.querySelector('[data-testid="task-chat-composer-stop"]')).toBeNull(); }); }); + +it("explains a missing provider runtime with an installation repair", () => { + render( {}} linkedRuns={[{ + runId: "missing-runtime", runtimeMode: "native", status: "failed", errorCode: "native_provider_runtime_missing", + agentId: "agent-1", agentName: "Content Strategist", adapterType: "paperclip_runner", + createdAt: "2026-10-11T11:19:08Z", startedAt: "2026-10-11T11:19:08Z", finishedAt: "2026-10-11T11:19:14Z", + }]} />); + expect(container.textContent).toContain("Runtime missing"); + const toggle = container.querySelector('[data-testid="task-chat-collapsible-marker"] button[aria-expanded]')!; + flushSync(() => toggle.click()); + expect(container.textContent).toContain("Reinstall or rebuild Paperclip"); + expect(container.textContent).toContain("test the agent's runtime"); +}); diff --git a/ui/src/components/TaskChatThread.tsx b/ui/src/components/TaskChatThread.tsx index bf497109f90..c1425f56e79 100644 --- a/ui/src/components/TaskChatThread.tsx +++ b/ui/src/components/TaskChatThread.tsx @@ -1646,7 +1646,9 @@ export function TaskChatThread(props: TaskChatThreadProps) { ? "native_runner_timed_out" : "native_runner_process_exited"); const label = - code === "native_provider_model_rejected" && source.status === "failed" + code === "native_provider_runtime_missing" && source.status === "failed" + ? "Runtime missing" + : code === "native_provider_model_rejected" && source.status === "failed" ? "Model unavailable" : code === "native_provider_approval_required" && source.status === "failed" ? "Approval required" @@ -1667,6 +1669,8 @@ export function TaskChatThread(props: TaskChatThreadProps) { ? `The run was cancelled${responseBoundary}.` : source.status === "interrupted" ? `The run was interrupted${responseBoundary}.` + : code === "native_provider_runtime_missing" + ? "The selected provider runtime is missing a required package. Reinstall or rebuild Paperclip with its bundled provider dependencies, restart the server, then test the agent's runtime before retrying." : code === "native_provider_approval_required" ? "This operation requires approval, but this runner has no interactive approval handler. Review the operation and update the agent's permission setting before retrying." : code === "native_provider_model_rejected"