diff --git a/apps/web/components/task/simple/task-chat.test.tsx b/apps/web/components/task/simple/task-chat.test.tsx
index d8d25944db..6a67195e80 100644
--- a/apps/web/components/task/simple/task-chat.test.tsx
+++ b/apps/web/components/task/simple/task-chat.test.tsx
@@ -592,6 +592,29 @@ describe("TaskChat run error entries", () => {
expect(screen.getByTestId("run-error-raw-payload").textContent).toContain("Internal error");
});
+ it("renders the managed npm recovery entry with one touch-safe action", () => {
+ const sessions: TaskSession[] = [
+ failedSession("s-npm", "agent-npm", "2026-05-01T11:30:00Z", "runtime failed"),
+ ];
+ sessions[0].metadata = {
+ last_agent_error: {
+ message: "managed npm runtime failed to prepare",
+ code: "managed_runtime_npm_resolution",
+ details: "npm error code ETARGET",
+ },
+ };
+ render(wrap(
));
+
+ expect(screen.getByTestId("run-error-managed-runtime-npm-recovery")).toBeTruthy();
+ expect(screen.getByText(/npm could not prepare the runtime/i)).toBeTruthy();
+ expect(screen.getByTestId("run-error-managed-runtime-retry-button")).toBeTruthy();
+ expect(screen.getByTestId("run-error-managed-runtime-retry-button").className).toContain(
+ "min-h-11",
+ );
+ expect(screen.queryByTestId("run-error-resume-button")).toBeNull();
+ expect(screen.queryByTestId("run-error-fresh-button")).toBeNull();
+ });
+
it("does not render an error entry for non-FAILED sessions", () => {
const sessions: TaskSession[] = [
{
diff --git a/apps/web/e2e/tests/office/managed-runtime-npm-recovery.spec.ts b/apps/web/e2e/tests/office/managed-runtime-npm-recovery.spec.ts
new file mode 100644
index 0000000000..a245da65bb
--- /dev/null
+++ b/apps/web/e2e/tests/office/managed-runtime-npm-recovery.spec.ts
@@ -0,0 +1,55 @@
+import { test, expect } from "../../fixtures/office-fixture";
+
+test("keeps managed npm recovery visible in Office chat", async ({
+ testPage,
+ apiClient,
+ officeSeed,
+}) => {
+ const sentFrames: string[] = [];
+ testPage.on("websocket", (ws) => {
+ if (!ws.url().endsWith("/ws")) return;
+ ws.on("framesent", (event) => {
+ if (typeof event.payload === "string") sentFrames.push(event.payload);
+ });
+ });
+
+ const task = await apiClient.createTask(officeSeed.workspaceId, "Office managed npm recovery", {
+ workflow_id: officeSeed.workflowId,
+ });
+ await apiClient.rawRequest("PATCH", `/api/v1/office/tasks/${task.id}`, {
+ assignee_agent_profile_id: officeSeed.agentId,
+ });
+ await apiClient.seedTaskSession(task.id, {
+ state: "FAILED",
+ agentProfileId: officeSeed.agentId,
+ completedAt: "2026-08-16T09:15:44Z",
+ metadata: {
+ last_agent_error: {
+ message: "managed npm runtime failed to prepare",
+ code: "managed_runtime_npm_resolution",
+ details: "npm error code ETARGET\nnpm error notarget No matching version found",
+ },
+ },
+ });
+
+ await testPage.goto(`/office/tasks/${task.id}`);
+ await expect(
+ testPage.getByRole("heading", { name: "Office managed npm recovery" }),
+ ).toBeVisible();
+
+ const recovery = testPage.getByTestId("run-error-managed-runtime-npm-recovery");
+ await expect(recovery).toBeVisible();
+ await expect(recovery.getByText("npm could not prepare the runtime")).toBeVisible();
+ await expect(recovery.getByTestId("run-error-managed-runtime-retry-button")).toHaveCount(1);
+ await expect(recovery.getByTestId("run-error-resume-button")).toHaveCount(0);
+ await expect(recovery.getByTestId("run-error-fresh-button")).toHaveCount(0);
+ await expect(recovery.getByRole("button", { name: "Technical details" })).toHaveAttribute(
+ "aria-expanded",
+ "false",
+ );
+
+ await recovery.getByTestId("run-error-managed-runtime-retry-button").click();
+ await expect
+ .poll(() => sentFrames.find((frame) => frame.includes('"action":"session.recover"')) ?? "")
+ .toContain('"action":"runtime_retry"');
+});
diff --git a/apps/web/e2e/tests/session/managed-runtime-npm-recovery.spec.ts b/apps/web/e2e/tests/session/managed-runtime-npm-recovery.spec.ts
new file mode 100644
index 0000000000..0b9c7884d6
--- /dev/null
+++ b/apps/web/e2e/tests/session/managed-runtime-npm-recovery.spec.ts
@@ -0,0 +1,89 @@
+import { test, expect } from "../../fixtures/test-base";
+import type { ApiClient } from "../../helpers/api-client";
+import type { SeedData } from "../../fixtures/test-base";
+import { assertNoDocumentHorizontalOverflow } from "../../helpers/layout-assertions";
+import { SessionPage } from "../../pages/session-page";
+
+async function seedManagedRuntimeFailure(apiClient: ApiClient, seedData: SeedData) {
+ const task = await apiClient.createTask(seedData.workspaceId, "Managed npm runtime recovery", {
+ workflow_id: seedData.workflowId,
+ workflow_step_id: seedData.startStepId,
+ });
+ const { session_id: sessionId } = await apiClient.seedTaskSession(task.id, {
+ state: "FAILED",
+ completedAt: "2026-08-16T09:15:44Z",
+ });
+ await apiClient.seedSessionMessage(sessionId, {
+ type: "status",
+ content: "managed npm runtime failed to prepare",
+ metadata: {
+ recovery_actions: true,
+ failure_kind: "managed_runtime_npm_resolution",
+ error_output: "npm error code ETARGET\nnpm error notarget No matching version found",
+ actions: [
+ {
+ type: "ws_request",
+ label: "Retry runtime",
+ test_id: "managed-runtime-npm-retry-button",
+ params: {
+ method: "session.recover",
+ payload: { task_id: task.id, session_id: sessionId, action: "runtime_retry" },
+ },
+ },
+ ],
+ },
+ });
+ return task;
+}
+
+test("renders managed npm recovery with one retry action", async ({
+ testPage,
+ apiClient,
+ seedData,
+ prCapture,
+}, testInfo) => {
+ const sentFrames: string[] = [];
+ testPage.on("websocket", (ws) => {
+ if (!ws.url().endsWith("/ws")) return;
+ ws.on("framesent", (event) => {
+ if (typeof event.payload === "string") sentFrames.push(event.payload);
+ });
+ });
+
+ const task = await seedManagedRuntimeFailure(apiClient, seedData);
+ await testPage.goto(`/t/${task.id}`);
+ const session = new SessionPage(testPage);
+ await session.waitForLoad();
+
+ const recovery = session.activeChat().getByTestId("managed-runtime-npm-recovery");
+ await expect(recovery).toBeVisible();
+ await expect(
+ recovery.getByRole("heading", { name: "npm could not prepare the runtime" }),
+ ).toBeVisible();
+ await expect(recovery).toContainText(
+ "Kandev refreshed package data and retried the same runtime once.",
+ );
+ await expect(recovery).not.toContainText("ACP");
+ await expect(recovery.locator("details")).not.toHaveAttribute("open");
+ await expect(recovery.getByTestId("managed-runtime-npm-retry-button")).toHaveCount(1);
+ await expect(recovery.getByRole("button")).toHaveCount(1);
+
+ await assertNoDocumentHorizontalOverflow(testPage, "managed npm recovery");
+ await testPage.screenshot({
+ path: testInfo.outputPath("managed-runtime-npm-recovery-desktop.png"),
+ fullPage: true,
+ });
+ await prCapture.screenshot("managed-runtime-npm-recovery-desktop", {
+ caption: "Managed npm runtime recovery card",
+ fullPage: true,
+ });
+
+ await recovery.getByText("Technical details", { exact: true }).click();
+ await expect(recovery.locator("pre")).toBeVisible();
+ await expect(recovery.locator("pre")).toContainText("ETARGET");
+
+ await recovery.getByTestId("managed-runtime-npm-retry-button").click();
+ await expect
+ .poll(() => sentFrames.find((frame) => frame.includes('"action":"session.recover"')) ?? "")
+ .toContain('"action":"runtime_retry"');
+});
diff --git a/apps/web/e2e/tests/session/mobile-managed-runtime-npm-recovery.spec.ts b/apps/web/e2e/tests/session/mobile-managed-runtime-npm-recovery.spec.ts
new file mode 100644
index 0000000000..ad1b18c8b6
--- /dev/null
+++ b/apps/web/e2e/tests/session/mobile-managed-runtime-npm-recovery.spec.ts
@@ -0,0 +1,85 @@
+// Filename starts with "mobile-" so this runs on the mobile-chrome project.
+import { test, expect } from "../../fixtures/test-base";
+import type { ApiClient } from "../../helpers/api-client";
+import type { SeedData } from "../../fixtures/test-base";
+import { assertNoDocumentHorizontalOverflow } from "../../helpers/layout-assertions";
+import { SessionPage } from "../../pages/session-page";
+
+async function seedManagedRuntimeFailure(apiClient: ApiClient, seedData: SeedData) {
+ const task = await apiClient.createTask(seedData.workspaceId, "Mobile managed npm recovery", {
+ workflow_id: seedData.workflowId,
+ workflow_step_id: seedData.startStepId,
+ });
+ const { session_id: sessionId } = await apiClient.seedTaskSession(task.id, {
+ state: "FAILED",
+ completedAt: "2026-08-16T09:15:44Z",
+ });
+ await apiClient.seedSessionMessage(sessionId, {
+ type: "status",
+ content: "managed npm runtime failed to prepare",
+ metadata: {
+ recovery_actions: true,
+ failure_kind: "managed_runtime_npm_resolution",
+ error_output: "npm error code ETARGET\nnpm error notarget No matching version found",
+ actions: [
+ {
+ type: "ws_request",
+ label: "Retry runtime",
+ test_id: "managed-runtime-npm-retry-button",
+ params: {
+ method: "session.recover",
+ payload: { task_id: task.id, session_id: sessionId, action: "runtime_retry" },
+ },
+ },
+ ],
+ },
+ });
+ return task;
+}
+
+test("keeps managed npm recovery touch-safe on mobile", async ({
+ testPage,
+ apiClient,
+ seedData,
+ prCapture,
+}, testInfo) => {
+ const sentFrames: string[] = [];
+ testPage.on("websocket", (ws) => {
+ if (!ws.url().endsWith("/ws")) return;
+ ws.on("framesent", (event) => {
+ if (typeof event.payload === "string") sentFrames.push(event.payload);
+ });
+ });
+
+ const task = await seedManagedRuntimeFailure(apiClient, seedData);
+ await testPage.goto(`/t/${task.id}`);
+ const session = new SessionPage(testPage);
+ await session.waitForLoad();
+
+ const recovery = session.activeChat().getByTestId("managed-runtime-npm-recovery");
+ await expect(recovery).toBeVisible();
+ await expect(recovery.getByRole("button")).toHaveCount(1);
+ const retry = recovery.getByTestId("managed-runtime-npm-retry-button");
+ await expect(retry).toBeInViewport();
+ const box = await retry.boundingBox();
+ expect(box).not.toBeNull();
+ expect(box!.height).toBeGreaterThanOrEqual(44);
+ await expect(recovery.locator("details")).not.toHaveAttribute("open");
+ await expect(testPage.getByRole("dialog")).toHaveCount(0);
+
+ await retry.tap();
+ await expect
+ .poll(() => sentFrames.find((frame) => frame.includes('"action":"session.recover"')) ?? "")
+ .toContain('"action":"runtime_retry"');
+ await expect(testPage.getByRole("dialog")).toHaveCount(0);
+ await assertNoDocumentHorizontalOverflow(testPage, "mobile managed npm recovery");
+
+ await testPage.screenshot({
+ path: testInfo.outputPath("managed-runtime-npm-recovery-mobile.png"),
+ fullPage: true,
+ });
+ await prCapture.screenshot("managed-runtime-npm-recovery-mobile", {
+ caption: "Managed npm runtime recovery remains touch-safe on mobile",
+ fullPage: true,
+ });
+});
diff --git a/apps/web/lib/session-last-agent-error.test.ts b/apps/web/lib/session-last-agent-error.test.ts
index 8c5776c844..2b3f216a16 100644
--- a/apps/web/lib/session-last-agent-error.test.ts
+++ b/apps/web/lib/session-last-agent-error.test.ts
@@ -16,8 +16,42 @@ const OTHER_TAB_SESSION_ID = "session-other-tab";
const THIS_TAB_SESSION_ID = "session-this-tab";
const OTHER_TAB_STAMP = "stamp-other";
const THIS_TAB_STAMP = "stamp-this";
+const MANAGED_RUNTIME_NPM_FAILURE = "managed_runtime_npm_resolution";
+const NPM_ERROR_CODE_ETARGET = "npm error code ETARGET";
describe("readLastAgentError", () => {
+ it("reads structured failure fields in snake_case", () => {
+ expect(
+ readLastAgentError({
+ last_agent_error: {
+ message: AGENT_ERROR_MESSAGE,
+ failure_code: MANAGED_RUNTIME_NPM_FAILURE,
+ failure_details: NPM_ERROR_CODE_ETARGET,
+ },
+ }),
+ ).toMatchObject({
+ code: MANAGED_RUNTIME_NPM_FAILURE,
+ details: NPM_ERROR_CODE_ETARGET,
+ });
+ });
+
+ it("reads structured failure fields in camelCase", () => {
+ expect(
+ readLastAgentError({
+ last_agent_error: {
+ message: AGENT_ERROR_MESSAGE,
+ code: MANAGED_RUNTIME_NPM_FAILURE,
+ details: NPM_ERROR_CODE_ETARGET,
+ },
+ }),
+ ).toMatchObject({
+ code: MANAGED_RUNTIME_NPM_FAILURE,
+ details: NPM_ERROR_CODE_ETARGET,
+ });
+ });
+});
+
+describe("readLastAgentError optional metadata", () => {
it("reads snake_case metadata and keeps occurredAt optional", () => {
expect(
readLastAgentError({
diff --git a/apps/web/lib/session-last-agent-error.ts b/apps/web/lib/session-last-agent-error.ts
index f0637eb71d..5f4fad8fd6 100644
--- a/apps/web/lib/session-last-agent-error.ts
+++ b/apps/web/lib/session-last-agent-error.ts
@@ -6,6 +6,8 @@ export type LastAgentError = {
agentExecutionId?: string;
/** Adapter-validated provider remediation URL; never derived from prose. */
remediationUrl?: string;
+ code?: string;
+ details?: string;
dismissedAt?: string;
};
@@ -49,19 +51,37 @@ export function readLastAgentError(metadata: Record
| null | un
const record = raw as Record;
const message = typeof record.message === "string" ? record.message : "";
if (!message) return null;
- const dismissedAt =
- readOptionalString(record.dismissed_at) ?? readOptionalString(record.dismissedAt);
+ const dismissedAt = readFirstOptionalString(record, ["dismissed_at", "dismissedAt"]);
if (dismissedAt) return null;
return {
message,
- occurredAt: readOptionalString(record.occurred_at) ?? readOptionalString(record.occurredAt),
- agentExecutionId:
- readOptionalString(record.agent_execution_id) ?? readOptionalString(record.agentExecutionId),
- remediationUrl:
- readOptionalString(record.remediation_url) ?? readOptionalString(record.remediationUrl),
+ occurredAt: readFirstOptionalString(record, ["occurred_at", "occurredAt"]),
+ agentExecutionId: readFirstOptionalString(record, ["agent_execution_id", "agentExecutionId"]),
+ remediationUrl: readFirstOptionalString(record, ["remediation_url", "remediationUrl"]),
+ ...readStructuredFailureMetadata(record),
} satisfies LastAgentError;
}
+function readStructuredFailureMetadata(record: Record) {
+ return {
+ code: readFirstOptionalString(record, ["code", "failure_code", "failureCode"]),
+ details: readFirstOptionalString(record, [
+ "details",
+ "failure_details",
+ "failureDetails",
+ "error_output",
+ ]),
+ };
+}
+
+function readFirstOptionalString(record: Record, keys: string[]) {
+ for (const key of keys) {
+ const value = readOptionalString(record[key]);
+ if (value) return value;
+ }
+ return undefined;
+}
+
/**
* Stable identifier for a specific error event. Two errors share a stamp iff
* they have the same occurredAt timestamp and message. Used to decide whether
diff --git a/apps/web/src/locales/en/chat.json b/apps/web/src/locales/en/chat.json
index c5380898b1..05149ddfd8 100644
--- a/apps/web/src/locales/en/chat.json
+++ b/apps/web/src/locales/en/chat.json
@@ -130,6 +130,9 @@
"sendNowReferenceOverflow": "Combined entity references exceed the message limit.",
"sortable": "sortable",
"technicalDetails": "Technical details",
+ "managedRuntimeNpmTitle": "npm could not prepare the runtime",
+ "managedRuntimeNpmBody": "Kandev refreshed package data and retried the same runtime once.",
+ "managedRuntimeRetry": "Retry runtime",
"theAssociatedWorktree": "The associated worktree",
"theTaskAndItsData": "The task and its data",
"thisActionCannotBeUndone": "This action cannot be undone.",
diff --git a/apps/web/src/locales/pseudo/chat.json b/apps/web/src/locales/pseudo/chat.json
index 9a7fa85981..993b661708 100644
--- a/apps/web/src/locales/pseudo/chat.json
+++ b/apps/web/src/locales/pseudo/chat.json
@@ -130,6 +130,9 @@
"sendNowReferenceOverflow": "Ćōḿƀĩńēď ēńţĩţŷ ŕēƒēŕēńćēś ēxćēēď ţĥē ḿēśśàĝē ĺĩḿĩţ.",
"sortable": "śōŕţàƀĺē",
"technicalDetails": "Ţēćĥńĩćàĺ ďēţàĩĺś",
+ "managedRuntimeNpmTitle": "ńƥḿ ćōũĺď ńōţ ƥŕēƥàŕē ţĥē ŕũńţĩḿē",
+ "managedRuntimeNpmBody": "Ķàńďēvē ƒŕēśĥēď ƥàćķàĝē ďàţà àńď ŕēţŕĩēď ţĥē śàḿē ŕũńţĩḿē ōńćē.",
+ "managedRuntimeRetry": "Ŕēţŕŷ ŕũńţĩḿē",
"theAssociatedWorktree": "Ţĥē àśśōćĩàţēď ŵōŕķţŕēē",
"theTaskAndItsData": "Ţĥē ţàśķ àńď ĩţś ďàţà",
"thisActionCannotBeUndone": "Ţĥĩś àćţĩōń ćàńńōţ ƀē ũńďōńē.",
diff --git a/apps/web/src/locales/pt-pt/chat.json b/apps/web/src/locales/pt-pt/chat.json
index ac713a1319..16c202bab0 100644
--- a/apps/web/src/locales/pt-pt/chat.json
+++ b/apps/web/src/locales/pt-pt/chat.json
@@ -114,6 +114,9 @@
"sendNowAttachmentOverflow": "Os anexos combinados excedem os limites da mensagem.",
"sendNowReferenceOverflow": "As referências a entidades combinadas excedem o limite da mensagem.",
"technicalDetails": "Detalhes técnicos",
+ "managedRuntimeNpmTitle": "O npm não conseguiu preparar o runtime",
+ "managedRuntimeNpmBody": "O Kandev atualizou os dados do pacote e repetiu o mesmo runtime uma vez.",
+ "managedRuntimeRetry": "Repetir runtime",
"theAssociatedWorktree": "O worktree associado",
"theTaskAndItsData": "A tarefa e os respetivos dados",
"thisActionCannotBeUndone": "Esta ação não pode ser anulada.",
diff --git a/apps/web/src/locales/zh-cn/chat.json b/apps/web/src/locales/zh-cn/chat.json
index 702a636ec4..7a113eb9b2 100644
--- a/apps/web/src/locales/zh-cn/chat.json
+++ b/apps/web/src/locales/zh-cn/chat.json
@@ -56,6 +56,9 @@
"reorderQueuedMessage": "重新排序队列消息",
"sortable": "可排序",
"technicalDetails": "技术详情",
+ "managedRuntimeNpmTitle": "npm 无法准备运行时",
+ "managedRuntimeNpmBody": "Kandev 已刷新软件包数据,并重试了同一运行时一次。",
+ "managedRuntimeRetry": "重试运行时",
"providerQuotaTitle": "{{provider}} 使用额度已达上限",
"newChatTab": "新建聊天",
"newQuickChat": "新建快速聊天",
diff --git a/apps/web/src/locales/zh-hk/chat.json b/apps/web/src/locales/zh-hk/chat.json
index db08706c5a..4383d3a40b 100644
--- a/apps/web/src/locales/zh-hk/chat.json
+++ b/apps/web/src/locales/zh-hk/chat.json
@@ -56,6 +56,9 @@
"reorderQueuedMessage": "重新排序佇列訊息",
"sortable": "可排序",
"technicalDetails": "技術詳情",
+ "managedRuntimeNpmTitle": "npm 無法準備執行環境",
+ "managedRuntimeNpmBody": "Kandev 已重新整理套件資料,並重試相同的執行環境一次。",
+ "managedRuntimeRetry": "重試執行環境",
"providerQuotaTitle": "{{provider}} 使用額度已達上限",
"newChatTab": "新建聊天",
"newQuickChat": "新建快速聊天",
diff --git a/apps/web/src/locales/zh-tw/chat.json b/apps/web/src/locales/zh-tw/chat.json
index 5537fc4eae..2cc80ae15c 100644
--- a/apps/web/src/locales/zh-tw/chat.json
+++ b/apps/web/src/locales/zh-tw/chat.json
@@ -56,6 +56,9 @@
"reorderQueuedMessage": "重新排序佇列訊息",
"sortable": "可排序",
"technicalDetails": "技術詳情",
+ "managedRuntimeNpmTitle": "npm 無法準備執行環境",
+ "managedRuntimeNpmBody": "Kandev 已重新整理套件資料,並重試相同的執行環境一次。",
+ "managedRuntimeRetry": "重試執行環境",
"providerQuotaTitle": "{{provider}} 使用額度已達上限",
"newChatTab": "新建聊天",
"newQuickChat": "新建快速聊天",
diff --git a/docs/plans/managed-runtime-stale-npm-recovery/plan.md b/docs/plans/managed-runtime-stale-npm-recovery/plan.md
new file mode 100644
index 0000000000..097a4d5f71
--- /dev/null
+++ b/docs/plans/managed-runtime-stale-npm-recovery/plan.md
@@ -0,0 +1,207 @@
+---
+spec: docs/specs/agents/runtime-updates.md
+created: 2026-08-16
+status: implemented
+---
+
+# Implementation Plan: Recover Stale npm Runtime Metadata
+
+## Overview
+
+Managed npm runtimes normally start with `npx --prefer-offline`. A stale npm
+packument can then hide a dependency version that the configured registry
+contains. The managed process exits, and ACP reports only that its peer
+disconnected. This plan detects the strict npm `ETARGET` signature in captured
+stderr, invalidates only the trusted runtime's deterministic `_npx` tree, and
+retries the same package and version once with `--prefer-online`. If recovery
+fails, Kanban and Office show an npm-specific inline card with one runtime retry
+action and collapsed technical details.
+
+The implementation does not run `npm cache clean --force`. It does not change
+the registry, package, active version, model, permissions, or session identity.
+
+## Backend
+
+### Classification and trusted command construction
+
+- `apps/backend/internal/agent/runtime/routingerr/runtime_rules.go`: add a
+ stable code for a managed npm version-resolution failure. Require npm
+ `ETARGET` and `No matching version found for package@version` in the same
+ bounded stderr sample. Match the supported old and new npm error prefixes.
+ Do not classify a generic ACP disconnect or unrelated npm failure.
+- `apps/backend/internal/agent/agents/managed_npm_runtime.go`: add a trusted
+ command option for online metadata refresh. The option changes only
+ `--prefer-offline` to `--prefer-online`. The managed runtime spec still owns
+ the package and ACP arguments.
+- Move deterministic `_npx` cache-key discovery and safe deletion behind one
+ helper in `apps/backend/internal/agent/managedruntime`. Reuse it from runtime
+ update jobs and launch recovery. Reject the npm cache root, the `_npx` root,
+ symlink escapes, and every path outside the exact derived key.
+
+### Bounded launch recovery
+
+- `apps/backend/internal/agent/runtime/lifecycle/manager_startup.go`: after a
+ host-local managed runtime fails ACP initialization, fetch its bounded stderr
+ and classify the combined failure evidence. Retry only the new exact
+ classifier.
+- Stop the first child, invalidate the exact execution tree, rebuild the same
+ command in online mode, reconnect streams, and initialize once more. Preserve
+ the active exact version, command prefix, model, permissions, and session ID.
+- Track a startup attempt generation on `AgentExecution`. Completion and stream
+ events from the first child must not publish a terminal state for the retry.
+ Cancellation and shutdown must win over recovery.
+- Emit no user-facing failure when the online retry succeeds. If it fails,
+ publish one final event with a stable failure code and bounded sanitized
+ details. Never include raw host paths or use stderr values as command input.
+
+### Failure persistence and recovery actions
+
+- Extend the lifecycle and watcher event data with optional failure code and
+ details fields. Keep existing generic error fields compatible.
+- Extend `models.LastAgentError` with optional code and details fields. Store
+ the fields in existing session JSON metadata, so no schema migration is
+ required.
+- `apps/backend/internal/orchestrator/event_handlers_agent.go`: recognize the
+ structured npm failure. Persist one recovery message with
+ `failure_kind = managed_runtime_npm_resolution`, sanitized `error_output`,
+ and one **Retry runtime** action. Use resume mode when a resume token exists.
+ Otherwise, use the existing fresh-run recovery mode behind the same label.
+
+## Frontend
+
+### Shared recovery presentation
+
+- Extend `RecoveryMetadata` and `LastAgentError` parsers with the optional
+ structured failure fields. Accept snake case from the API and camel case
+ after store serialization.
+- `apps/web/components/task/chat/messages/action-message.tsx`: render the
+ managed npm failure through the existing inline special-recovery path. State
+ that npm could not prepare the runtime and that Kandev refreshed package data
+ and retried once. Keep technical details collapsed and show only
+ **Retry runtime**.
+- `apps/web/components/task/simple/chat-entries.ts` and
+ `apps/web/components/task/simple/components/run-error-entry.tsx`: carry the
+ same failure code and details to Office chat and render the same meaning and
+ action set.
+- Add all copy to the existing i18n catalog. Do not mention ACP in user-facing
+ text.
+
+### Mobile design contract
+
+Kanban and Office keep their current single chat scroll owner. The recovery
+card remains inline on desktop and phone. It does not open a dialog, sheet, or
+drawer. On phone widths, actions stack when needed and each control is at least
+44 px high. The nearest shipped mobile exemplar is the provider recovery card
+in `action-message.tsx`. Business state, recovery requests, error parsing, and
+copy keys remain shared across viewports.
+
+## Documentation
+
+- Update the troubleshooting and managed runtime sections in
+ `docs/public/agents-and-profiles.md`. Explain the automatic same-version
+ retry, the configured-registry check, and why global npm cache cleanup is not
+ the normal recovery path.
+- Treat the page as a combined how-to and reference page. Keep commands and UI
+ names synchronized with the shipped behavior.
+
+## Tests
+
+- Classifier tests cover the reported npm error, old and new npm prefixes,
+ bounded sanitization, and false positives.
+- Managed command tests prove that normal launches stay offline-preferred and
+ recovery changes only the npm freshness flag.
+- Cache helper tests prove exact-key deletion, absent-key idempotency, and path
+ and symlink refusal.
+- Lifecycle tests prove one successful retry, one terminal repeated failure,
+ preserved command identity, no intermediate failed event, stale event
+ rejection, and cancel or shutdown precedence. They also prove no retry for
+ remote, native, passthrough, and unrelated failures.
+- Orchestrator tests prove structured error persistence and the single runtime
+ retry action for sessions with and without resume tokens.
+- Frontend unit tests prove structured metadata parsing and the specialized
+ Kanban and Office presentations.
+
+## E2E Tests
+
+- **Scenario:** GIVEN a terminal failure during managed npm resolution in Kanban chat,
+ WHEN the recovery card renders on desktop, THEN it names npm, keeps details
+ collapsed, offers only **Retry runtime**, and sends the expected recovery
+ request.
+ **File:** `apps/web/e2e/tests/session/managed-runtime-npm-recovery.spec.ts`.
+- **Scenario:** GIVEN the same failure on a phone viewport, WHEN the user opens
+ the task and taps **Retry runtime**, THEN the action remains touch-safe, the
+ chat has no horizontal overflow, and no nested overlay appears.
+ **File:**
+ `apps/web/e2e/tests/session/mobile-managed-runtime-npm-recovery.spec.ts`.
+- **Scenario:** GIVEN the same persisted `last_agent_error` in Office chat,
+ WHEN the task reloads, THEN the npm-specific copy and one retry action remain
+ visible.
+ **File:** `apps/web/e2e/tests/office/managed-runtime-npm-recovery.spec.ts`.
+
+## Verification Commands
+
+Bootstrap a fresh worktree before frontend checks:
+
+```bash
+cd apps
+pnpm install --frozen-lockfile
+```
+
+Run focused backend tests:
+
+```bash
+cd apps/backend
+go test ./internal/agent/runtime/routingerr ./internal/agent/agents ./internal/agent/managedruntime
+go test ./internal/agent/runtime/lifecycle -run 'Test.*ManagedRuntime.*Npm|Test.*StartupRecovery'
+go test ./internal/orchestrator -run 'Test.*ManagedRuntime.*Recovery|Test.*RecoveryActions|Test.*LastAgentError'
+```
+
+Run focused frontend and contract checks:
+
+```bash
+cd apps/web
+pnpm exec vitest run components/task/chat/messages/action-message.test.tsx components/task/simple/chat-entries.test.ts components/task/simple/components/run-error-entry.test.tsx lib/session-last-agent-error.test.ts
+pnpm run typecheck
+pnpm run i18n:check
+pnpm run i18n:ratchet
+```
+
+Build once and run the desktop and mobile browser scenarios:
+
+```bash
+make build-web
+make build-backend
+cd apps/web
+pnpm e2e:run --no-build tests/session/managed-runtime-npm-recovery.spec.ts tests/office/managed-runtime-npm-recovery.spec.ts
+pnpm e2e:run --no-build --project mobile-chrome tests/session/mobile-managed-runtime-npm-recovery.spec.ts
+```
+
+Validate public documentation:
+
+```bash
+node --test scripts/validate-public-docs.test.mjs
+node scripts/validate-public-docs.mjs
+```
+
+## Implementation Waves And Parallel Candidates
+
+Wave 1:
+
+- [x] [task-01-classify-and-build-recovery-command](task-01-classify-and-build-recovery-command.md)
+
+Wave 2:
+
+- [x] [task-02-retry-managed-runtime-launch](task-02-retry-managed-runtime-launch.md)
+
+Wave 3:
+
+- [x] [task-03-present-npm-recovery](task-03-present-npm-recovery.md)
+
+Wave 4:
+
+- [x] [task-04-prove-responsive-recovery](task-04-prove-responsive-recovery.md)
+- [x] [task-05-document-runtime-recovery](task-05-document-runtime-recovery.md)
+
+Tasks 04 and 05 can run in parallel after the backend and frontend contracts
+are stable. The default execution order remains sequential. No subagent
+delegation is authorized by this plan.
diff --git a/docs/plans/managed-runtime-stale-npm-recovery/task-01-classify-and-build-recovery-command.md b/docs/plans/managed-runtime-stale-npm-recovery/task-01-classify-and-build-recovery-command.md
new file mode 100644
index 0000000000..1ffeb4da43
--- /dev/null
+++ b/docs/plans/managed-runtime-stale-npm-recovery/task-01-classify-and-build-recovery-command.md
@@ -0,0 +1,49 @@
+---
+id: "01-classify-and-build-recovery-command"
+title: "Classify stale npm metadata"
+status: completed
+wave: 1
+depends_on: []
+plan: "plan.md"
+spec: "../../specs/agents/runtime-updates.md"
+---
+
+# Task 01: Classify stale npm metadata
+
+Create the strict error signal and trusted recovery primitives before changing
+the launch lifecycle.
+
+- **Acceptance:** Classification requires npm `ETARGET` and the matching
+ missing `package@version` message in bounded stderr. Generic ACP disconnects,
+ network errors, and unrelated npm failures do not match.
+- **Acceptance:** Normal managed runtime commands keep `--prefer-offline`. A
+ trusted recovery option changes only that flag to `--prefer-online` and keeps
+ the same package, version, and ACP arguments.
+- **Acceptance:** One shared cache helper derives and removes only the exact
+ `_npx` execution key for the trusted package spec. It rejects broad, escaped,
+ and symlinked targets and is idempotent when the key is absent.
+- **Acceptance:** Runtime update jobs use the shared helper without changing
+ their current one-retry behavior.
+- **Verification:** Add failing tests first, then run:
+
+ ```bash
+ cd apps/backend
+ go test ./internal/agent/runtime/routingerr ./internal/agent/agents ./internal/agent/managedruntime
+ go test ./internal/agent/settings/controller -run 'Test.*ExecutionCache|Test.*AgentUpdate'
+ ```
+
+- **Files likely touched:**
+ `apps/backend/internal/agent/runtime/routingerr/runtime_rules.go`,
+ `apps/backend/internal/agent/runtime/routingerr/runtime_rules_test.go`,
+ `apps/backend/internal/agent/agents/managed_npm_runtime.go`,
+ `apps/backend/internal/agent/agents/managed_npm_runtime_test.go`,
+ `apps/backend/internal/agent/managedruntime/cache.go`,
+ `apps/backend/internal/agent/managedruntime/cache_test.go`, and
+ `apps/backend/internal/agent/settings/controller/agent_update.go`.
+- **Dependencies:** None.
+- **Parallelism:** sequential foundation.
+- **Inputs:** Launch-time stale metadata recovery and failure behavior in the
+ managed runtime spec.
+- **Output contract:** Report files changed, RED and GREEN commands and results,
+ classifier false-positive evidence, cache deletion safety evidence, and
+ synchronized task and plan status.
diff --git a/docs/plans/managed-runtime-stale-npm-recovery/task-02-retry-managed-runtime-launch.md b/docs/plans/managed-runtime-stale-npm-recovery/task-02-retry-managed-runtime-launch.md
new file mode 100644
index 0000000000..2439554ac1
--- /dev/null
+++ b/docs/plans/managed-runtime-stale-npm-recovery/task-02-retry-managed-runtime-launch.md
@@ -0,0 +1,50 @@
+---
+id: "02-retry-managed-runtime-launch"
+title: "Retry managed runtime launch"
+status: done
+wave: 2
+depends_on: ["01-classify-and-build-recovery-command"]
+plan: "plan.md"
+spec: "../../specs/agents/runtime-updates.md"
+---
+
+# Task 02: Retry managed runtime launch
+
+Add one race-safe online retry to host-local managed runtime startup.
+
+- **Acceptance:** A strict npm version-resolution failure during ACP
+ initialization stops the first child, invalidates the exact execution key,
+ and retries the same trusted runtime once with current npm metadata.
+- **Acceptance:** The retry preserves the active exact version, command prefix,
+ model, permissions, session identity, and ACP arguments.
+- **Acceptance:** A startup attempt generation prevents delayed completion and
+ stream events from the first child from publishing a terminal state for the
+ retry.
+- **Acceptance:** Cancellation and shutdown win. Native, remote, passthrough,
+ unrelated, and repeated online failures do not start another recovery.
+- **Acceptance:** Success continues the original launch without a failure
+ event. Repeated failure publishes one stable failure code and bounded
+ sanitized details.
+- **Verification:** Add failing lifecycle tests first, then run:
+
+ ```bash
+ cd apps/backend
+ go test ./internal/agent/runtime/lifecycle -run 'Test.*ManagedRuntime.*Npm|Test.*StartupRecovery'
+ go test ./internal/agent/runtime/lifecycle
+ ```
+
+- **Files likely touched:**
+ `apps/backend/internal/agent/runtime/lifecycle/types.go`,
+ `apps/backend/internal/agent/runtime/lifecycle/manager_startup.go`,
+ `apps/backend/internal/agent/runtime/lifecycle/manager_events.go`,
+ `apps/backend/internal/agent/runtime/lifecycle/event_types.go`,
+ `apps/backend/internal/agent/runtime/lifecycle/events.go`, and focused new or
+ existing lifecycle test files.
+- **Dependencies:** Task 01.
+- **Parallelism:** sequential because this consumes the classifier, command,
+ and cache contracts.
+- **Inputs:** The Task 01 classifier and trusted helpers, plus lifecycle process
+ identity and shutdown rules.
+- **Output contract:** Report files changed, RED and GREEN commands and results,
+ attempt-generation race evidence, exact retry-count evidence, and
+ synchronized task and plan status.
diff --git a/docs/plans/managed-runtime-stale-npm-recovery/task-03-present-npm-recovery.md b/docs/plans/managed-runtime-stale-npm-recovery/task-03-present-npm-recovery.md
new file mode 100644
index 0000000000..6f15804334
--- /dev/null
+++ b/docs/plans/managed-runtime-stale-npm-recovery/task-03-present-npm-recovery.md
@@ -0,0 +1,59 @@
+---
+id: "03-present-npm-recovery"
+title: "Present npm runtime recovery"
+status: done
+wave: 3
+depends_on: ["02-retry-managed-runtime-launch"]
+plan: "plan.md"
+spec: "../../specs/agents/runtime-updates.md"
+---
+
+# Task 03: Present npm runtime recovery
+
+Carry the structured terminal failure to Kanban and Office, then render one
+clear runtime retry path.
+
+- **Acceptance:** Lifecycle and watcher events carry optional failure code and
+ sanitized details without changing generic error compatibility.
+- **Acceptance:** `last_agent_error` stores those optional fields in existing
+ JSON metadata and reads both API snake case and store camel case.
+- **Acceptance:** The orchestrator creates a specialized recovery message with
+ `failure_kind = managed_runtime_npm_resolution`, collapsed technical details,
+ and only **Retry runtime**. It uses resume mode when possible and the existing
+ fresh-run mode otherwise.
+- **Acceptance:** Kanban and Office explain that npm could not prepare the
+ runtime and that Kandev refreshed package data and retried once. User copy
+ does not mention ACP.
+- **Acceptance:** All new copy uses i18n. Desktop and mobile use the same
+ parsing, state, actions, and translation keys.
+- **Verification:** Add failing backend and frontend tests first, then run:
+
+ ```bash
+ cd apps/backend
+ go test ./internal/orchestrator -run 'Test.*ManagedRuntime.*Recovery|Test.*RecoveryActions|Test.*LastAgentError'
+
+ cd ../../apps/web
+ pnpm exec vitest run components/task/chat/messages/action-message.test.tsx components/task/simple/chat-entries.test.ts components/task/simple/components/run-error-entry.test.tsx lib/session-last-agent-error.test.ts
+ pnpm run typecheck
+ pnpm run i18n:check
+ pnpm run i18n:ratchet
+ ```
+
+- **Files likely touched:**
+ `apps/backend/internal/orchestrator/event_handlers_agent.go`,
+ `apps/backend/internal/orchestrator/watcher/watcher.go`,
+ `apps/backend/internal/task/models/models.go`,
+ `apps/web/lib/session-last-agent-error.ts`,
+ `apps/web/components/task/chat/types.ts`,
+ `apps/web/components/task/chat/messages/action-message.tsx`,
+ `apps/web/components/task/simple/chat-entries.ts`,
+ `apps/web/components/task/simple/components/run-error-entry.tsx`, relevant
+ tests, and the matching locale catalogs.
+- **Dependencies:** Task 02.
+- **Parallelism:** sequential because UI metadata depends on the final backend
+ event contract.
+- **Inputs:** The terminal failure payload from Task 02 and the existing
+ provider recovery card pattern.
+- **Output contract:** Report files changed, RED and GREEN commands and results,
+ persisted metadata shape, action payloads for resumable and non-resumable
+ sessions, i18n results, and synchronized task and plan status.
diff --git a/docs/plans/managed-runtime-stale-npm-recovery/task-04-prove-responsive-recovery.md b/docs/plans/managed-runtime-stale-npm-recovery/task-04-prove-responsive-recovery.md
new file mode 100644
index 0000000000..e2e6b0812e
--- /dev/null
+++ b/docs/plans/managed-runtime-stale-npm-recovery/task-04-prove-responsive-recovery.md
@@ -0,0 +1,42 @@
+---
+id: "04-prove-responsive-recovery"
+title: "Prove responsive npm recovery"
+status: done
+wave: 4
+depends_on: ["03-present-npm-recovery"]
+plan: "plan.md"
+spec: "../../specs/agents/runtime-updates.md"
+---
+
+# Task 04: Prove responsive npm recovery
+
+Exercise the shipped Kanban and Office recovery surfaces in real browser
+layouts.
+
+- **Acceptance:** Desktop Kanban shows npm-specific copy, collapsed details,
+ one **Retry runtime** action, and the expected recovery request.
+- **Acceptance:** A phone viewport keeps the inline card within the chat, has no
+ horizontal overflow, uses a touch target of at least 44 px, and opens no
+ nested overlay.
+- **Acceptance:** Office restores the specialized card from persisted
+ `last_agent_error` metadata after reload and offers the same single action.
+- **Verification:** Build once, add the browser scenarios, and run:
+
+ ```bash
+ make build-web
+ make build-backend
+ cd apps/web
+ pnpm e2e:run --no-build tests/session/managed-runtime-npm-recovery.spec.ts tests/office/managed-runtime-npm-recovery.spec.ts
+ pnpm e2e:run --no-build --project mobile-chrome tests/session/mobile-managed-runtime-npm-recovery.spec.ts
+ ```
+
+- **Files likely touched:**
+ `apps/web/e2e/tests/session/managed-runtime-npm-recovery.spec.ts`,
+ `apps/web/e2e/tests/session/mobile-managed-runtime-npm-recovery.spec.ts`, and
+ `apps/web/e2e/tests/office/managed-runtime-npm-recovery.spec.ts`.
+- **Dependencies:** Task 03.
+- **Parallelism:** Can run in parallel with Task 05 after Task 03.
+- **Inputs:** The shipped structured failure metadata and shared recovery UI.
+- **Output contract:** Report files changed, desktop and mobile command results,
+ viewport and touch-target evidence, overflow evidence, Office reload evidence,
+ and synchronized task and plan status.
diff --git a/docs/plans/managed-runtime-stale-npm-recovery/task-05-document-runtime-recovery.md b/docs/plans/managed-runtime-stale-npm-recovery/task-05-document-runtime-recovery.md
new file mode 100644
index 0000000000..67efbe5f02
--- /dev/null
+++ b/docs/plans/managed-runtime-stale-npm-recovery/task-05-document-runtime-recovery.md
@@ -0,0 +1,35 @@
+---
+id: "05-document-runtime-recovery"
+title: "Document npm runtime recovery"
+status: done
+wave: 4
+depends_on: ["03-present-npm-recovery"]
+plan: "plan.md"
+spec: "../../specs/agents/runtime-updates.md"
+---
+
+# Task 05: Document npm runtime recovery
+
+Update public troubleshooting guidance after the behavior and labels are
+stable.
+
+- **Acceptance:** Public docs explain the automatic same-version online retry
+ and its one-attempt limit.
+- **Acceptance:** The troubleshooting path explains how to inspect the
+ configured npm registry when the focused error remains.
+- **Acceptance:** The docs state that global npm cache cleanup is not the normal
+ recovery path and do not instruct users to run `npm cache clean --force`.
+- **Acceptance:** Commands, labels, and behavior match the implementation.
+- **Verification:** Run:
+
+ ```bash
+ node --test scripts/validate-public-docs.test.mjs
+ node scripts/validate-public-docs.mjs
+ ```
+
+- **Files likely touched:** `docs/public/agents-and-profiles.md`.
+- **Dependencies:** Task 03.
+- **Parallelism:** Can run in parallel with Task 04 after Task 03.
+- **Inputs:** Final retry behavior and UI labels from Tasks 01 through 03.
+- **Output contract:** Report files changed, validation commands and results,
+ verified links and commands, and synchronized task and plan status.
diff --git a/docs/public/agents-and-profiles.md b/docs/public/agents-and-profiles.md
index 2a622fe67c..69c77269e5 100644
--- a/docs/public/agents-and-profiles.md
+++ b/docs/public/agents-and-profiles.md
@@ -69,6 +69,27 @@ configuration options, commands, and runtime version without a page reload.
- If preparation, ACP validation, authentication, or persistence fails, Kandev keeps the previous active version and capability catalogue. Select another stable version or retry the same target.
- Kandev may prepare the exact version again if npm removes its cache entry. Kandev does not own an offline package inventory, and global npm cache cleanup is not required.
+#### Recover a stale npm runtime lookup
+
+Host-local managed runtimes normally start with npm's offline-preferred lookup.
+If npm has stale package metadata and cannot resolve the selected
+`package@version`, the first ACP startup can fail even though the configured
+registry contains that exact version. Kandev recognizes this specific npm
+resolution error, removes only the deterministic `_npx` execution tree for the
+selected package and version, then retries the same command once with an
+online-preferred metadata lookup.
+
+The retry keeps the selected package, exact version, command prefix, model,
+permissions, and session identity. It does not change the npm registry or
+silently select another version. When the retry succeeds, no recovery card is
+shown. When it fails again, Kanban and Office show one **Retry runtime** action
+with collapsed technical details.
+
+Do not use `npm cache clean --force` as the normal recovery step. It removes
+unrelated npm data and does not target the stale execution tree. If the
+specialized retry cannot resolve the runtime, check that the Kandev service
+uses the expected npm installation and configured registry. Run `npm config get registry` as the Kandev service user to inspect the registry used by that process. Then use the runtime update controls to select and prepare another trusted stable version.
+
Add a custom terminal agent
@@ -250,6 +271,7 @@ Only custom TUI agents can be deleted from the agent list. Built-in definitions
- **Login required:** use the agent card's login terminal or sign in under Kandev's operating-system user; signing in as another user does not help the service.
- **Model, mode, or command probe fails:** authenticate first, refresh discovery, and choose a value advertised by the installed version.
- **Launch fails after editing flags:** inspect the command preview, remove stale arguments, and correct unmatched quotes or trailing escapes.
+- **Managed npm runtime cannot resolve its selected version:** Kandev checks the configured registry and retries the same version once after refreshing its exact `_npx` execution tree. If the retry fails, verify the service user's npm configuration and registry, then use **Settings > Agents** to prepare another trusted stable version. Do not start with `npm cache clean --force`.
- **Environment value is absent:** confirm the secret still exists, the key is not reserved, and an executor/runtime variable is not already taking precedence.
- **MCP server is absent:** confirm agent MCP support, valid JSON, transport mode, executor policy, and the session warning logs.
- **MCP tools are missing from one agent session:** open the neutral plug button in that session's chat toolbar. **Delivered** means Kandev included the server in the agent launch but cannot yet observe a connection; **Connected** means the built-in server saw MCP initialize; **Active** means it served `tools/list`. A gray filtered/unavailable row explains an intentional omission, while red is reserved for an explicit sanitized error. Third-party profile servers normally remain Delivered because they connect directly to the agent and Kandev cannot inspect them. The report is scoped to that session and its current execution, so compare the affected agent's own toolbar rather than another agent on the task.
diff --git a/docs/specs/agents/runtime-updates.md b/docs/specs/agents/runtime-updates.md
index bb757cdeaf..a69a697713 100644
--- a/docs/specs/agents/runtime-updates.md
+++ b/docs/specs/agents/runtime-updates.md
@@ -15,6 +15,12 @@ partly published, incompatible with ACP, or otherwise cannot start. Rebuilding
an npm cache is not sufficient when an unversioned command selects the same
broken release again.
+Managed runtimes can also fail when npm has stale package metadata. In that
+case, npm can report `ETARGET` and `No matching version found` for a dependency
+that the configured registry now contains. Kandev currently reports the later
+ACP disconnect instead of the npm cause. The user then has no useful recovery
+action unless they inspect backend logs.
+
## What
- Settings exposes version management for the built-in managed npm runtimes
@@ -46,6 +52,11 @@ broken release again.
recipe.
- Jobs for one agent are idempotent while queued or running. Installation and
version management for the same agent cannot run concurrently.
+- A host-local managed runtime launch detects a strict npm `ETARGET` version
+ resolution failure before ACP initialization. Kandev refreshes npm metadata
+ and retries the same trusted package and version once.
+- If the retry also fails, Kandev reports an npm runtime preparation error and
+ offers one runtime retry action. The user does not need to understand ACP.
The managed package set is:
@@ -209,6 +220,33 @@ version while its current probe is unknown produces a repair job.
- Candidate validation bypasses the active selection only for the trusted exact
candidate command created by the version job.
+## Launch-time stale metadata recovery
+
+- Normal host-local managed runtime commands continue to use
+ `--prefer-offline`.
+- Kandev inspects bounded process stderr when ACP initialization ends before a
+ response. Automatic recovery requires both npm `ETARGET` and a matching
+ `No matching version found for package@version` message.
+- Kandev classifies the npm error from stderr. It does not build a command from
+ package names, versions, paths, or registry values found in stderr.
+- Recovery removes only the deterministic `_npx` execution tree for the
+ trusted built-in package specification. It never clears npm's full cache.
+- Kandev starts the same managed runtime once with `--prefer-online`. The
+ command keeps the same trusted package, exact active version when one exists,
+ ACP arguments, configured npm registry, command prefix, permissions, model,
+ and session identity.
+- Recovery is limited to one retry for each launch attempt. A delayed event
+ from the first child process cannot fail or complete the replacement process.
+- User cancellation and backend shutdown stop recovery. Kandev does not retry
+ a remote executor, a native runtime, a passthrough command, an unrelated npm
+ error, or a second failed online attempt.
+- A successful retry continues the original session without a failure card.
+ Kandev records structured recovery telemetry without exposing host paths or
+ raw process logs.
+- A failed retry emits a stable npm runtime failure code and bounded sanitized
+ details. This evidence is stored with the last agent error so the focused UI
+ survives a page reload.
+
## Failure and recovery behavior
- Registry failure during preview keeps approval disabled and runs no command.
@@ -224,6 +262,8 @@ version while its current probe is unknown produces a repair job.
- Browser disconnect does not cancel a running job. The jobs endpoint can
recover process-local progress while the backend remains running.
- Active sessions are never restarted, replaced, or hot-swapped.
+- A launch-time stale metadata retry is not a version rollback. It prepares the
+ same package selection again and does not change the active version.
## Persistence guarantees
@@ -241,6 +281,9 @@ version while its current probe is unknown produces a repair job.
version again; it must not advance to another version.
- Dialog selection, output, and result remain page-local after a browser page
restart.
+- A terminal launch-time npm resolution error stores its stable failure code
+ and bounded sanitized details in `last_agent_error`. No database migration is
+ required because the record is JSON metadata with optional fields.
## Desktop and mobile behavior
@@ -256,6 +299,16 @@ version while its current probe is unknown produces a repair job.
output remain viewport-contained.
- Selection state, preview loading, operation labels, request payloads, and
terminal results are shared across desktop and mobile presentations.
+- A failed automatic launch retry uses the existing inline recovery card in
+ Kanban chat and Office chat. It does not open a dialog or drawer.
+- The card states that npm could not prepare the agent runtime. It states that
+ Kandev refreshed package data and retried once. Technical details are
+ collapsed initially.
+- The card offers one **Retry runtime** action. When a resume token exists, the
+ action resumes the session. Otherwise, it starts a replacement run. The card
+ does not present session history loss as a fix for an npm problem.
+- Phone actions stack when needed, remain at least 44 px high, and do not add a
+ second scroll container.
## Scenarios
@@ -284,10 +337,27 @@ version while its current probe is unknown produces a repair job.
- **GIVEN** a phone viewport and a long version catalogue or process log,
**WHEN** the operator selects and activates a version, **THEN** the drawer
remains contained and the primary action remains touch-reachable.
+- **GIVEN** a host-local managed runtime exits before ACP initialization with
+ npm `ETARGET` and a matching missing dependency version, **WHEN** Kandev
+ reads the captured stderr, **THEN** it removes only the trusted package's
+ deterministic `_npx` tree and retries the same runtime once with current npm
+ metadata.
+- **GIVEN** that online retry starts successfully, **WHEN** ACP initialization
+ completes, **THEN** the original session continues and no recovery card is
+ shown.
+- **GIVEN** that online retry fails with the same npm resolution error,
+ **WHEN** the failure reaches Kanban or Office chat, **THEN** the UI explains
+ the npm cause, keeps sanitized technical details collapsed, and offers only
+ **Retry runtime**.
+- **GIVEN** a native, SSH, container, passthrough, or unrelated failed launch,
+ **WHEN** Kandev classifies the error, **THEN** this automatic npm recovery
+ does not run.
## Out of scope
- Scheduled or automatic updates and automatic rollback after launch failure.
+- Global npm cache cleanup, registry replacement, dependency substitution, or
+ automatic selection of another package version.
- Prerelease, tag, arbitrary package-spec, registry, or shell-command input.
- Kandev-owned npm artifact retention or a package lockfile.
- Applying host selections to SSH, container, or other remote runtimes.