fix: handle same-version agent runtime updates (#2120)

This commit is contained in:
Carlos Florêncio
2026-08-01 11:18:37 +01:00
committed by GitHub
parent 03e179c4d3
commit 01beeb3753
11 changed files with 508 additions and 41 deletions
@@ -131,19 +131,25 @@ func (s *AgentUpdateJobStore) run(
defer cancel() defer cancel()
s.setStatus(job, dto.AgentUpdateJobStatusResolving) 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) target, err := s.updater.ResolveTarget(ctx, spec.Package)
if err != nil { if err != nil {
s.finishFailed(job, ctx, fmt.Errorf("resolve target version: %w", err), ref) s.finishFailed(job, ctx, fmt.Errorf("resolve target version: %w", err), ref)
return 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() s.mu.Lock()
job.TargetVersion = target job.TargetVersion = target
s.mu.Unlock() s.mu.Unlock()
if currentVersion != "" && currentVersion == target {
s.finishAlreadyUpToDate(job, ref)
return
}
s.setStatus(job, dto.AgentUpdateJobStatusUpdating) s.setStatus(job, dto.AgentUpdateJobStatusUpdating)
flusher := newUpdateOutputFlusher(s, job) flusher := newUpdateOutputFlusher(s, job)
@@ -201,6 +207,18 @@ func (s *AgentUpdateJobStore) finishFailed(
s.scheduleEviction(job.ID) 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( func (s *AgentUpdateJobStore) finishRefresh(
job *AgentUpdateJob, job *AgentUpdateJob,
ctx context.Context, ctx context.Context,
@@ -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) { func TestAgentUpdateAuthRequiredIsPackageSuccessWithRefreshError(t *testing.T) {
updater := &fakeRuntimeUpdater{ updater := &fakeRuntimeUpdater{
target: "1.1.0", target: "1.1.0",
@@ -20,6 +20,10 @@ import {
} from "@kandev/ui/drawer"; } from "@kandev/ui/drawer";
import { Tooltip, TooltipContent, TooltipTrigger } from "@kandev/ui/tooltip"; import { Tooltip, TooltipContent, TooltipTrigger } from "@kandev/ui/tooltip";
import { useResponsiveBreakpoint } from "@/hooks/use-responsive-breakpoint"; 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 { AgentUpdateJob, AgentUpdatePreview, InstallJob } from "@/lib/api";
import type { RuntimeUpdate } from "@/lib/types/http"; import type { RuntimeUpdate } from "@/lib/types/http";
import { useAgentUpdateDialogState } from "./use-agent-update-dialog-state"; 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 }) { function UpdateResult({ agentName, job }: { agentName: string; job?: AgentUpdateJob }) {
if (!job) return null; if (!job) return null;
const { versionsMatch } = resolveRuntimeVersionPair(null, job);
if (job.status === "succeeded" && job.refresh_error) { if (job.status === "succeeded" && job.refresh_error) {
return ( return (
<p <p
@@ -66,7 +71,7 @@ function UpdateResult({ agentName, job }: { agentName: string; job?: AgentUpdate
role="status" role="status"
data-testid={`agent-update-result-${agentName}`} data-testid={`agent-update-result-${agentName}`}
> >
Runtime updated successfully. {versionsMatch ? "Runtime already up to date." : "Runtime updated successfully."}
</p> </p>
); );
} }
@@ -94,6 +99,30 @@ type UpdateBodyProps = {
onRetryPreview: () => void; onRetryPreview: () => void;
}; };
function RuntimeVersionSummary({
preview,
job,
}: {
preview: AgentUpdatePreview;
job?: AgentUpdateJob;
}) {
const { currentVersion, targetVersion, versionsMatch } = resolveRuntimeVersionPair(preview, job);
return (
<div className="space-y-1">
<p className="font-medium">Runtime version</p>
<p className="break-words font-mono text-sm">
{versionsMatch ? currentVersion : `${currentVersion} → ${targetVersion}`}
</p>
{versionsMatch && (
<p className="text-sm text-muted-foreground" role="status">
Up to date
</p>
)}
</div>
);
}
function UpdateBody({ function UpdateBody({
agentName, agentName,
preview, preview,
@@ -143,14 +172,7 @@ function UpdateBody({
)} )}
{preview && ( {preview && (
<> <>
<div className="space-y-1"> <RuntimeVersionSummary preview={preview} job={job} />
<p className="font-medium">Runtime version</p>
<p className="break-words font-mono text-sm">
{(job?.current_version || preview.current_version || "Unknown") +
" → " +
(job?.target_version || preview.target_version)}
</p>
</div>
<div className="space-y-1 text-muted-foreground"> <div className="space-y-1 text-muted-foreground">
<p> <p>
This updates the managed runtime on this Kandev host, then refreshes its models and This updates the managed runtime on this Kandev host, then refreshes its models and
@@ -205,31 +227,6 @@ type UpdateFooterProps = {
mobile?: boolean; 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({ function UpdateFooter({
agentName, agentName,
preview, preview,
@@ -244,8 +241,9 @@ function UpdateFooter({
}: UpdateFooterProps) { }: UpdateFooterProps) {
const updateInFlight = Boolean(job && ACTIVE_UPDATE_STATUSES.has(job.status)); const updateInFlight = Boolean(job && ACTIVE_UPDATE_STATUSES.has(job.status));
const canRetry = job?.status === "failed"; const canRetry = job?.status === "failed";
const canApprove = canApproveUpdate({ const canApprove = canApproveAgentRuntimeUpdate({
preview, preview,
job,
previewError, previewError,
loading, loading,
updateInFlight, updateInFlight,
@@ -104,6 +104,59 @@ test.describe("managed agent runtime updates", () => {
expect(runtime.postCount()).toBe(0); 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 }) => { test("retries an update after its job fails", async ({ testPage }) => {
const runtime = await installRuntimeUpdateFixture(testPage); const runtime = await installRuntimeUpdateFixture(testPage);
@@ -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 }) => { test("keeps retry available after an update job fails", async ({ testPage }) => {
const runtime = await installRuntimeUpdateFixture(testPage); const runtime = await installRuntimeUpdateFixture(testPage);
+71
View File
@@ -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> = {}): 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> = {}): 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);
});
});
+49
View File
@@ -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
);
}
@@ -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.
@@ -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.
+6 -1
View File
@@ -36,7 +36,12 @@ release.
Select the update icon to deliberately check npm and refresh the managed Select the update icon to deliberately check npm and refresh the managed
runtime on the Kandev host. Before anything changes, the update dialog shows runtime on the Kandev host. Before anything changes, the update dialog shows
the current and upstream target versions, the exact command Kandev will run, 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 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 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 page. After the package update, Kandev automatically starts a fresh ACP
+9
View File
@@ -27,6 +27,10 @@ cache is already present.
resolves and shows the current runtime version, upstream target version, resolves and shows the current runtime version, upstream target version,
exact built-in command, host-only scope, capability refresh, and active exact built-in command, host-only scope, capability refresh, and active
session behavior before an update can start. 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 - 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 dialog then shows live stdout/stderr, progress, and the terminal success or
failure state. 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 **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 `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. 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 - **GIVEN** the update dialog has a resolved preview, **WHEN** the operator
approves the update, **THEN** Kandev starts exactly one update and the dialog approves the update, **THEN** Kandev starts exactly one update and the dialog
streams stdout/stderr until the job is terminal. streams stdout/stderr until the job is terminal.