From 01beeb3753d943d94a9e10a609db7fe316f4cdea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Flor=C3=AAncio?= Date: Sat, 1 Aug 2026 11:18:37 +0100 Subject: [PATCH] fix: handle same-version agent runtime updates (#2120) --- .../settings/controller/agent_update_job.go | 28 ++++- .../settings/controller/agent_update_test.go | 31 +++++ .../settings/agent-runtime-update-control.tsx | 68 +++++------ .../settings/agent-runtime-update.spec.ts | 53 ++++++++ .../mobile-agent-runtime-update.spec.ts | 29 +++++ apps/web/lib/agent-runtime-update.test.ts | 71 +++++++++++ apps/web/lib/agent-runtime-update.ts | 49 ++++++++ .../plan.md | 113 ++++++++++++++++++ .../task-01-disable-same-version-approval.md | 91 ++++++++++++++ docs/public/agents-and-profiles.md | 7 +- docs/specs/agents/runtime-updates.md | 9 ++ 11 files changed, 508 insertions(+), 41 deletions(-) create mode 100644 apps/web/lib/agent-runtime-update.test.ts create mode 100644 apps/web/lib/agent-runtime-update.ts create mode 100644 docs/plans/agent-runtime-same-version-approval/plan.md create mode 100644 docs/plans/agent-runtime-same-version-approval/task-01-disable-same-version-approval.md diff --git a/apps/backend/internal/agent/settings/controller/agent_update_job.go b/apps/backend/internal/agent/settings/controller/agent_update_job.go index e1d0c34a54..8c746bd603 100644 --- a/apps/backend/internal/agent/settings/controller/agent_update_job.go +++ b/apps/backend/internal/agent/settings/controller/agent_update_job.go @@ -131,19 +131,25 @@ func (s *AgentUpdateJobStore) run( defer cancel() s.setStatus(job, dto.AgentUpdateJobStatusResolving) - if caps, ok := s.updater.CurrentCapabilities(job.AgentName); ok { - s.mu.Lock() - job.CurrentVersion = caps.AgentVersion - s.mu.Unlock() - } target, err := s.updater.ResolveTarget(ctx, spec.Package) if err != nil { s.finishFailed(job, ctx, fmt.Errorf("resolve target version: %w", err), ref) return } + currentVersion := "" + if caps, ok := s.updater.CurrentCapabilities(job.AgentName); ok { + currentVersion = caps.AgentVersion + s.mu.Lock() + job.CurrentVersion = currentVersion + s.mu.Unlock() + } s.mu.Lock() job.TargetVersion = target s.mu.Unlock() + if currentVersion != "" && currentVersion == target { + s.finishAlreadyUpToDate(job, ref) + return + } s.setStatus(job, dto.AgentUpdateJobStatusUpdating) flusher := newUpdateOutputFlusher(s, job) @@ -201,6 +207,18 @@ func (s *AgentUpdateJobStore) finishFailed( s.scheduleEviction(job.ID) } +func (s *AgentUpdateJobStore) finishAlreadyUpToDate(job *AgentUpdateJob, ref MaintenanceJobRef) { + s.mu.Lock() + job.Status = dto.AgentUpdateJobStatusSucceeded + _, _ = job.Output.Write([]byte("Runtime already up to date.\n")) + s.maintenance.release(job.AgentName, ref) + s.finishLocked(job) + snapshot := job.snapshot() + s.mu.Unlock() + s.broadcast(ws.ActionAgentUpdateFinished, snapshot) + s.scheduleEviction(job.ID) +} + func (s *AgentUpdateJobStore) finishRefresh( job *AgentUpdateJob, ctx context.Context, diff --git a/apps/backend/internal/agent/settings/controller/agent_update_test.go b/apps/backend/internal/agent/settings/controller/agent_update_test.go index 73c1fd142b..e55f1b4380 100644 --- a/apps/backend/internal/agent/settings/controller/agent_update_test.go +++ b/apps/backend/internal/agent/settings/controller/agent_update_test.go @@ -364,6 +364,37 @@ func TestAgentUpdateJobResolvesUpdatesRefreshesAndStreams(t *testing.T) { } } +func TestAgentUpdateJobSkipsCommandWhenRuntimeIsAlreadyUpToDate(t *testing.T) { + updater := &fakeRuntimeUpdater{ + current: hostutility.AgentCapabilities{AgentVersion: "1.1.0"}, + currentFound: true, + target: "1.1.0", + refreshCaps: hostutility.AgentCapabilities{Status: hostutility.StatusOK, AgentVersion: "1.1.0"}, + } + store, completed := newUpdateTestStore(updater, newMaintenanceCoordinator(), nil) + + job, err := store.Enqueue("managed-acp", managedRuntimeSpec()) + if err != nil { + t.Fatalf("Enqueue: %v", err) + } + final := waitForUpdateStatus(t, completed, job.ID, dto.AgentUpdateJobStatusSucceeded) + if final.CurrentVersion != "1.1.0" || final.TargetVersion != "1.1.0" { + t.Fatalf("versions = %q -> %q, want 1.1.0 -> 1.1.0", final.CurrentVersion, final.TargetVersion) + } + if final.Output != "Runtime already up to date.\n" { + t.Fatalf("Output = %q, want no-op message", final.Output) + } + + updater.mu.Lock() + defer updater.mu.Unlock() + if updater.runCalls != 0 { + t.Fatalf("update calls = %d, want 0", updater.runCalls) + } + if updater.refreshCalls != 0 { + t.Fatalf("refresh calls = %d, want 0", updater.refreshCalls) + } +} + func TestAgentUpdateAuthRequiredIsPackageSuccessWithRefreshError(t *testing.T) { updater := &fakeRuntimeUpdater{ target: "1.1.0", diff --git a/apps/web/components/settings/agent-runtime-update-control.tsx b/apps/web/components/settings/agent-runtime-update-control.tsx index fdd2c472e9..beee0e5502 100644 --- a/apps/web/components/settings/agent-runtime-update-control.tsx +++ b/apps/web/components/settings/agent-runtime-update-control.tsx @@ -20,6 +20,10 @@ import { } from "@kandev/ui/drawer"; import { Tooltip, TooltipContent, TooltipTrigger } from "@kandev/ui/tooltip"; import { useResponsiveBreakpoint } from "@/hooks/use-responsive-breakpoint"; +import { + canApproveAgentRuntimeUpdate, + resolveRuntimeVersionPair, +} from "@/lib/agent-runtime-update"; import type { AgentUpdateJob, AgentUpdatePreview, InstallJob } from "@/lib/api"; import type { RuntimeUpdate } from "@/lib/types/http"; import { useAgentUpdateDialogState } from "./use-agent-update-dialog-state"; @@ -48,6 +52,7 @@ function updatePhase(status: AgentUpdateJob["status"] | undefined): string | nul function UpdateResult({ agentName, job }: { agentName: string; job?: AgentUpdateJob }) { if (!job) return null; + const { versionsMatch } = resolveRuntimeVersionPair(null, job); if (job.status === "succeeded" && job.refresh_error) { return (

- Runtime updated successfully. + {versionsMatch ? "Runtime already up to date." : "Runtime updated successfully."}

); } @@ -94,6 +99,30 @@ type UpdateBodyProps = { onRetryPreview: () => void; }; +function RuntimeVersionSummary({ + preview, + job, +}: { + preview: AgentUpdatePreview; + job?: AgentUpdateJob; +}) { + const { currentVersion, targetVersion, versionsMatch } = resolveRuntimeVersionPair(preview, job); + + return ( +
+

Runtime version

+

+ {versionsMatch ? currentVersion : `${currentVersion} → ${targetVersion}`} +

+ {versionsMatch && ( +

+ Up to date +

+ )} +
+ ); +} + function UpdateBody({ agentName, preview, @@ -143,14 +172,7 @@ function UpdateBody({ )} {preview && ( <> -
-

Runtime version

-

- {(job?.current_version || preview.current_version || "Unknown") + - " → " + - (job?.target_version || preview.target_version)} -

-
+

This updates the managed runtime on this Kandev host, then refreshes its models and @@ -205,31 +227,6 @@ type UpdateFooterProps = { mobile?: boolean; }; -function canApproveUpdate({ - preview, - previewError, - loading, - updateInFlight, - starting, - installInFlight, -}: { - preview: AgentUpdatePreview | null; - previewError: string | null; - loading: boolean; - updateInFlight: boolean; - starting: boolean; - installInFlight: boolean; -}) { - return ( - Boolean(preview?.current_version) && - !previewError && - !loading && - !updateInFlight && - !starting && - !installInFlight - ); -} - function UpdateFooter({ agentName, preview, @@ -244,8 +241,9 @@ function UpdateFooter({ }: UpdateFooterProps) { const updateInFlight = Boolean(job && ACTIVE_UPDATE_STATUSES.has(job.status)); const canRetry = job?.status === "failed"; - const canApprove = canApproveUpdate({ + const canApprove = canApproveAgentRuntimeUpdate({ preview, + job, previewError, loading, updateInFlight, diff --git a/apps/web/e2e/tests/settings/agent-runtime-update.spec.ts b/apps/web/e2e/tests/settings/agent-runtime-update.spec.ts index 02bb2bd1b4..25f8637b6e 100644 --- a/apps/web/e2e/tests/settings/agent-runtime-update.spec.ts +++ b/apps/web/e2e/tests/settings/agent-runtime-update.spec.ts @@ -104,6 +104,59 @@ test.describe("managed agent runtime updates", () => { expect(runtime.postCount()).toBe(0); }); + test("shows an up-to-date preview and disables approval when versions match", async ({ + testPage, + prCapture, + }) => { + const runtime = await installRuntimeUpdateFixture(testPage, { + previewResponse: { + agent_name: "claude-acp", + package: "@agentclientprotocol/claude-agent-acp", + current_version: "0.64.0", + target_version: "0.64.0", + command: ["npm", "exec"], + command_string: "npm exec", + }, + }); + + await testPage.goto("/settings/agents"); + await testPage.getByTestId(`agent-update-trigger-${runtime.agentName}`).click(); + + const dialog = testPage.getByTestId(`agent-update-dialog-${runtime.agentName}`); + await expect(dialog.getByText("0.64.0", { exact: true })).toBeVisible(); + await expect(dialog.getByText("Up to date", { exact: true })).toBeVisible(); + await expect(dialog).not.toContainText("0.64.0 → 0.64.0"); + await expect(testPage.getByTestId(`agent-update-confirm-${runtime.agentName}`)).toBeDisabled(); + expect(runtime.postCount()).toBe(0); + await prCapture.screenshot("desktop-update-up-to-date", { + caption: "Desktop update preview when the managed runtime is up to date", + }); + }); + + test("uses the job versions for a stale retry decision", async ({ testPage }) => { + const runtime = await installRuntimeUpdateFixture(testPage, { + postResponse: updateJob({ + status: "failed", + current_version: "0.63.0", + target_version: "0.63.0", + error: "The package registry is unavailable", + finished_at: "2026-07-26T12:01:00.000Z", + }), + }); + + await testPage.goto("/settings/agents"); + await testPage.getByTestId(`agent-update-trigger-${runtime.agentName}`).click(); + const dialog = testPage.getByTestId(`agent-update-dialog-${runtime.agentName}`); + const retryUpdate = testPage.getByTestId(`agent-update-confirm-${runtime.agentName}`); + await retryUpdate.click(); + + await expect(dialog.getByText("0.63.0", { exact: true })).toBeVisible(); + await expect(dialog.getByText("Up to date", { exact: true })).toBeVisible(); + await expect(retryUpdate).toHaveText("Retry update"); + await expect(retryUpdate).toBeDisabled(); + expect(runtime.postCount()).toBe(1); + }); + test("retries an update after its job fails", async ({ testPage }) => { const runtime = await installRuntimeUpdateFixture(testPage); diff --git a/apps/web/e2e/tests/settings/mobile-agent-runtime-update.spec.ts b/apps/web/e2e/tests/settings/mobile-agent-runtime-update.spec.ts index 0df32e5495..3005c2aa1f 100644 --- a/apps/web/e2e/tests/settings/mobile-agent-runtime-update.spec.ts +++ b/apps/web/e2e/tests/settings/mobile-agent-runtime-update.spec.ts @@ -50,6 +50,35 @@ test.describe("managed agent runtime updates on mobile", () => { ); }); + test("shows an up-to-date preview and disables approval when versions match", async ({ + testPage, + prCapture, + }) => { + const runtime = await installRuntimeUpdateFixture(testPage, { + previewResponse: { + agent_name: "claude-acp", + package: "@agentclientprotocol/claude-agent-acp", + current_version: "0.64.0", + target_version: "0.64.0", + command: ["npm", "exec"], + command_string: "npm exec", + }, + }); + + await testPage.goto("/settings/agents"); + await testPage.getByTestId(`agent-update-trigger-${runtime.agentName}`).tap(); + + const drawer = testPage.getByTestId(`agent-update-drawer-${runtime.agentName}`); + await expect(drawer.getByText("0.64.0", { exact: true })).toBeVisible(); + await expect(drawer.getByText("Up to date", { exact: true })).toBeVisible(); + await expect(drawer).not.toContainText("0.64.0 → 0.64.0"); + await expect(testPage.getByTestId(`agent-update-confirm-${runtime.agentName}`)).toBeDisabled(); + expect(runtime.postCount()).toBe(0); + await prCapture.screenshot("mobile-update-up-to-date", { + caption: "Mobile update preview when the managed runtime is up to date", + }); + }); + test("keeps retry available after an update job fails", async ({ testPage }) => { const runtime = await installRuntimeUpdateFixture(testPage); diff --git a/apps/web/lib/agent-runtime-update.test.ts b/apps/web/lib/agent-runtime-update.test.ts new file mode 100644 index 0000000000..7d97628c7d --- /dev/null +++ b/apps/web/lib/agent-runtime-update.test.ts @@ -0,0 +1,71 @@ +import { describe, expect, it } from "vitest"; +import type { AgentUpdateJob, AgentUpdatePreview } from "@/lib/api"; +import { canApproveAgentRuntimeUpdate, resolveRuntimeVersionPair } from "./agent-runtime-update"; + +const preview = (overrides: Partial = {}): AgentUpdatePreview => ({ + agent_name: "claude-acp", + package: "@agentclientprotocol/claude-agent-acp", + current_version: "0.62.0", + target_version: "0.63.0", + command: ["npm", "exec"], + command_string: "npm exec", + ...overrides, +}); + +const job = (overrides: Partial = {}): AgentUpdateJob => ({ + job_id: "runtime-update-job-1", + agent_name: "claude-acp", + status: "failed", + started_at: "2026-07-26T12:00:00.000Z", + ...overrides, +}); + +describe("resolveRuntimeVersionPair", () => { + it("uses job versions when a retry has newer resolved values", () => { + expect( + resolveRuntimeVersionPair( + preview(), + job({ current_version: "0.63.0", target_version: "0.63.0" }), + ), + ).toEqual({ currentVersion: "0.63.0", targetVersion: "0.63.0", versionsMatch: true }); + }); + + it("falls back to preview versions before a job exists", () => { + expect(resolveRuntimeVersionPair(preview())).toEqual({ + currentVersion: "0.62.0", + targetVersion: "0.63.0", + versionsMatch: false, + }); + }); +}); + +describe("canApproveAgentRuntimeUpdate", () => { + const ready = { + preview: preview(), + job: undefined, + previewError: null, + loading: false, + updateInFlight: false, + starting: false, + installInFlight: false, + }; + + it.each([ + ["differing versions", ready, true], + ["equal versions", { ...ready, preview: preview({ target_version: "0.62.0" }) }, false], + ["missing current version", { ...ready, preview: preview({ current_version: "" }) }, false], + ["missing target version", { ...ready, preview: preview({ target_version: "" }) }, false], + ["loading preview", { ...ready, loading: true }, false], + ["preview error", { ...ready, previewError: "preview failed" }, false], + ["update in flight", { ...ready, updateInFlight: true }, false], + ["starting update", { ...ready, starting: true }, false], + ["install in flight", { ...ready, installInFlight: true }, false], + [ + "stale retry with equal job versions", + { ...ready, job: job({ current_version: "0.63.0", target_version: "0.63.0" }) }, + false, + ], + ])("returns %s = %s", (_name, state, expected) => { + expect(canApproveAgentRuntimeUpdate(state)).toBe(expected); + }); +}); diff --git a/apps/web/lib/agent-runtime-update.ts b/apps/web/lib/agent-runtime-update.ts new file mode 100644 index 0000000000..210cd44fcc --- /dev/null +++ b/apps/web/lib/agent-runtime-update.ts @@ -0,0 +1,49 @@ +import type { AgentUpdateJob, AgentUpdatePreview } from "@/lib/api"; + +export type RuntimeVersionPair = { + currentVersion: string; + targetVersion?: string; + versionsMatch: boolean; +}; + +export function resolveRuntimeVersionPair( + preview: AgentUpdatePreview | null, + job?: AgentUpdateJob, +): RuntimeVersionPair { + const currentVersion = job?.current_version || preview?.current_version || "Unknown"; + const targetVersion = job?.target_version || preview?.target_version; + const versionsMatch = Boolean( + targetVersion && currentVersion !== "Unknown" && currentVersion === targetVersion, + ); + return { currentVersion, targetVersion, versionsMatch }; +} + +export function canApproveAgentRuntimeUpdate({ + preview, + job, + previewError, + loading, + updateInFlight, + starting, + installInFlight, +}: { + preview: AgentUpdatePreview | null; + job?: AgentUpdateJob; + previewError: string | null; + loading: boolean; + updateInFlight: boolean; + starting: boolean; + installInFlight: boolean; +}): boolean { + const { currentVersion, targetVersion } = resolveRuntimeVersionPair(preview, job); + return ( + Boolean( + preview && currentVersion !== "Unknown" && targetVersion && currentVersion !== targetVersion, + ) && + !previewError && + !loading && + !updateInFlight && + !starting && + !installInFlight + ); +} diff --git a/docs/plans/agent-runtime-same-version-approval/plan.md b/docs/plans/agent-runtime-same-version-approval/plan.md new file mode 100644 index 0000000000..6fa7ac6fc5 --- /dev/null +++ b/docs/plans/agent-runtime-same-version-approval/plan.md @@ -0,0 +1,113 @@ +--- +spec: docs/specs/agents/runtime-updates.md +created: 2026-07-31 +status: complete +--- + +# Fix Plan: Handle Same-Version Agent Runtime Updates + +## Overview + +The update preview already reports both the installed and upstream versions, +but the shared approval predicate checks only that the current version is +present. Render an identical target as one version with an **Up to date** +status, tighten the predicate so that preview is non-actionable, then cover the +shared desktop dialog and mobile drawer and document the no-op state. + +## Confirmed root cause + +`canApproveUpdate` in +`apps/web/components/settings/agent-runtime-update-control.tsx` returns true for +any successful preview with a non-empty `current_version`. It never requires a +non-empty `target_version` or compares the two values, so a preview such as +`0.64.0 → 0.64.0` both suggests a transition that does not exist and enables +**Approve update**, which can send the update POST. + +## Frontend + +### Shared approval predicate + +Update `canApproveUpdate` so approval requires non-empty current and target +versions whose reported values differ, in addition to the existing preview, +loading, maintenance, and request-state guards. Keep the normal upgrade and +failed-job retry paths unchanged when current and target versions differ. + +Update the version summary in `UpdateBody` so identical reported values render +the version once with a visible **Up to date** status and no arrow. Differing +versions continue to render the existing `current → target` transition. + +### Desktop and mobile behavior + +The existing desktop `Dialog` and phone `Drawer` both render `UpdateFooter`, so +the state rule remains shared rather than duplicated. This fix does not change +composition, navigation, scrolling, safe-area behavior, or touch targets. The +nearest mobile exemplar remains the shipped agent runtime update drawer in +`mobile-agent-runtime-update.spec.ts`; focused mobile Playwright coverage will +prove that the same-version preview is visible and its shared approval action +is disabled without sending a POST. + +### Public documentation + +Update `docs/public/agents-and-profiles.md` to say that a matching target is +shown once as **Up to date** and **Approve update** is available only when the +upstream target differs from the reported current version. + +## Tests + +- **What:** A successful preview with identical non-empty current and target + versions shows a single version with **Up to date** and cannot start an + update. + **File:** `apps/web/e2e/tests/settings/agent-runtime-update.spec.ts`. + **How:** Configure the existing route fixture with `0.64.0` for both values, + open the desktop dialog, assert the single-version summary, **Up to date** + status, absence of the arrow transition, disabled confirmation button, and a + zero POST count. +- **What:** Existing differing-version approval and failed-job retry behavior + stays enabled. + **File:** Existing scenarios in + `apps/web/e2e/tests/settings/agent-runtime-update.spec.ts` and + `apps/web/e2e/tests/settings/mobile-agent-runtime-update.spec.ts`. + **How:** Run both focused spec files after the predicate change. +- **What:** Public documentation remains structurally valid. + **File:** `docs/public/agents-and-profiles.md`. + **How:** Run the public-doc validators. + +No new public function or API contract is introduced; the private version +resolution helper and approval predicate are covered by focused unit tests and +the rendered shared action is exercised through the mocked update boundary. + +## E2E Tests + +- **Desktop scenario:** **GIVEN** current and target are both `0.64.0`, **WHEN** + the operator opens the update dialog, **THEN** it shows `0.64.0` once with + **Up to date**, omits the arrow transition, disables **Approve update**, and + sends no update request. + **File:** `apps/web/e2e/tests/settings/agent-runtime-update.spec.ts`. +- **Mobile scenario:** **GIVEN** the same preview on a phone viewport, **WHEN** + the operator opens the update drawer, **THEN** it uses the same single-version + **Up to date** presentation, the shared approval action is disabled, and no + update request is sent. + **File:** + `apps/web/e2e/tests/settings/mobile-agent-runtime-update.spec.ts`. + +## Implementation Wave + +- [x] [Task 01: Disable same-version approval](task-01-disable-same-version-approval.md) — done + +Execution is sequential in the primary conversation. No subagents are +authorized. + +## Risks and out of scope + +- Equality is based on the two reported preview strings. Normalizing alternate + SemVer spellings such as `v0.64.0` versus `0.64.0` is out of scope. +- The update icon remains available so operators can open a fresh read-only + preview; this change disables only the confirmation action after equality is + known. + +## Stale preview guard + +The backend resolves the target and then re-reads the current runtime version +before invoking the package command. If the versions now match, it completes a +successful no-op job with an explicit message and skips the update and refresh +commands. diff --git a/docs/plans/agent-runtime-same-version-approval/task-01-disable-same-version-approval.md b/docs/plans/agent-runtime-same-version-approval/task-01-disable-same-version-approval.md new file mode 100644 index 0000000000..36aa3219af --- /dev/null +++ b/docs/plans/agent-runtime-same-version-approval/task-01-disable-same-version-approval.md @@ -0,0 +1,91 @@ +--- +id: "01-disable-same-version-approval" +title: "Disable same-version approval" +status: done +wave: 1 +depends_on: [] +plan: "plan.md" +spec: "../../specs/agents/runtime-updates.md" +--- + +# Task 01: Disable same-version approval + +## Acceptance + +- A resolved preview with identical non-empty current and target versions shows + the version once with **Up to date**, omits the arrow transition, keeps + **Approve update** disabled, and sends no update POST. +- Differing-version approval and failed-job retry remain enabled. +- Desktop and mobile use the same predicate, and the public Agents guide + explains the unavailable approval state. +- A stale preview cannot invoke a package command after the backend resolves + that the runtime is already up to date. + +## TDD sequence + +1. Add the equal-version desktop and mobile Playwright scenarios and run each + focused test to confirm it fails because the UI shows an arrow transition + and the confirmation button is enabled. +2. Render one version plus **Up to date**, then tighten `canApproveUpdate` with + the minimal target-presence and inequality checks. +3. Run both runtime-update spec files to prove the new regression and existing + enabled paths. +4. Update and validate the public documentation. +5. Revalidate the current runtime version in the backend before executing a + package command and cover the no-op job path with a Go unit test. + +## Verification + +- From `apps/` in a fresh worktree, install dependencies: + `pnpm install --frozen-lockfile` +- Desktop E2E: + `pnpm --filter @kandev/web e2e:run --host tests/settings/agent-runtime-update.spec.ts` +- Mobile E2E: + `pnpm --filter @kandev/web e2e:run --host tests/settings/mobile-agent-runtime-update.spec.ts -- --project=mobile-chrome` +- Frontend typecheck: + `pnpm --filter @kandev/web run typecheck` +- Frontend unit tests: + `pnpm --filter @kandev/web test -- --run lib/agent-runtime-update.test.ts` +- Backend unit test: + `cd ../apps/backend && go test -run TestAgentUpdateJobSkipsCommandWhenRuntimeIsAlreadyUpToDate ./internal/agent/settings/controller` +- Public docs: + `node --test scripts/validate-public-docs.test.mjs && node scripts/validate-public-docs.mjs` + +## Files likely touched + +- `apps/web/components/settings/agent-runtime-update-control.tsx` +- `apps/web/e2e/tests/settings/agent-runtime-update.spec.ts` +- `apps/web/e2e/tests/settings/mobile-agent-runtime-update.spec.ts` +- `apps/web/lib/agent-runtime-update.ts` +- `apps/web/lib/agent-runtime-update.test.ts` +- `apps/backend/internal/agent/settings/controller/agent_update_job.go` +- `apps/backend/internal/agent/settings/controller/agent_update_test.go` +- `docs/public/agents-and-profiles.md` + +## Dependencies + +None. + +## Parallelism + +Sequential. The E2E scenarios depend on the same shared component change and +the task is one focused behavior slice. + +## Inputs + +- Equal-version behavior in `docs/specs/agents/runtime-updates.md` +- Shared body presentation and footer state in + `apps/web/components/settings/agent-runtime-update-control.tsx` +- Existing desktop and mobile runtime-update fixtures and scenarios +- Mobile parity contract in `plan.md` + +## Risks + +- Preserve failed-job retries when preview versions differ. +- Do not disable the card's update icon before a fresh preview has resolved. + +## Output contract + +Report RED/GREEN evidence, changed files, desktop/mobile results, docs +validation, any blockers or risks, and update this task plus `plan.md` status in +the same conversation. diff --git a/docs/public/agents-and-profiles.md b/docs/public/agents-and-profiles.md index 1663e90323..ac90942231 100644 --- a/docs/public/agents-and-profiles.md +++ b/docs/public/agents-and-profiles.md @@ -36,7 +36,12 @@ release. Select the update icon to deliberately check npm and refresh the managed runtime on the Kandev host. Before anything changes, the update dialog shows the current and upstream target versions, the exact command Kandev will run, -and how the update affects sessions. Select **Approve update** to start it. +and how the update affects sessions. When the reported current and target +versions match, the dialog shows the version once as **Up to date** and keeps +**Approve update** disabled. After a successful preview provides both current +and target versions and they differ, select **Approve update** to start it; +the action stays disabled when either version is missing, the preview is +loading, or the preview has an error. The dialog streams progress and stdout/stderr until the update finishes; those details do not appear on the agent card and are cleared when you restart the page. After the package update, Kandev automatically starts a fresh ACP diff --git a/docs/specs/agents/runtime-updates.md b/docs/specs/agents/runtime-updates.md index 5c70669e8e..fc5ddf1c5a 100644 --- a/docs/specs/agents/runtime-updates.md +++ b/docs/specs/agents/runtime-updates.md @@ -27,6 +27,10 @@ cache is already present. resolves and shows the current runtime version, upstream target version, exact built-in command, host-only scope, capability refresh, and active session behavior before an update can start. +- When the resolved current and upstream target versions are identical, the + update dialog shows the version once with an **Up to date** status and keeps + approval disabled because running the package update would not advance the + managed runtime. - The update starts only after the operator approves it in the dialog. The dialog then shows live stdout/stderr, progress, and the terminal success or failure state. @@ -199,6 +203,11 @@ Jobs are terminal after `succeeded` or `failed`. Retrying creates a new job. **WHEN** the operator opens its update action, **THEN** the dialog shows `0.62.0 → 0.63.0`, the exact built-in command, and how the host update affects capabilities and active sessions without starting the command. +- **GIVEN** the current runtime and upstream target both report version + `0.64.0`, **WHEN** the operator opens the update action, **THEN** the dialog + shows `0.64.0` once with **Up to date**, does not show a version transition, + keeps **Approve update** disabled on desktop and mobile, and starts no update + job. - **GIVEN** the update dialog has a resolved preview, **WHEN** the operator approves the update, **THEN** Kandev starts exactly one update and the dialog streams stdout/stderr until the job is terminal.