From 019a5a4927badf367e73d19fdd78bd816f96537b Mon Sep 17 00:00:00 2001 From: Chris Sherwood Date: Mon, 11 May 2026 10:12:47 -0700 Subject: [PATCH] fix(AI): preserve semver tag in DB on AMD Ollama updates Closes #855. PR #804's AMD branch in `updateContainer()` overrode `newImage` to `ollama/ollama:rocm` and then persisted that literal string to `service.container_image` (line 1273). Two downstream consequences for every AMD user who clicked Update on AI Assistant: 1. Apps page (`apps.tsx`) extracts the displayed version from `container_image` and rendered the literal string "rocm". 2. `ContainerRegistryService.getAvailableUpdates()` parsed `currentTag = "rocm"`, which isn't semver, so `parseMajorVersion` returned NaN, the filter didn't reject newer tags by major-version, and `isNewerVersion` treated any future tag as newer. Result: the same update reappeared on every check, forever. Fix: separate "what we run" from "what we persist". `runtimeImage` holds the tag passed to `docker.pull()` and `createContainer()` (still `:rocm` for AMD), while `newImage` keeps the semver tag and is the value written to the DB. Surgical: 3 references renamed plus 1 declaration added. The install path (`_createContainer`) already had the right shape (runtime-only override, no DB write of the override), so this PR only touches `updateContainer`. Test plan: - `npm run typecheck` passes locally. - Manual repro on NOMAD2 (AMD HX 370 / 890M, rc.2): before fix, DB shows `container_image = ollama/ollama:rocm` after triggering an Ollama update via Settings > Apps; Apps page shows version "rocm"; `/api/system/services/check-updates` immediately re-reports the same update available. After fix, DB shows `container_image = ollama/ollama:`; Apps page shows the semver; check- updates does not re-report the same update. - nomad_ollama container itself still runs the `:rocm` image (verified via `docker inspect`). Co-Authored-By: Claude Opus 4.7 (1M context) --- admin/app/services/docker_service.ts | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/admin/app/services/docker_service.ts b/admin/app/services/docker_service.ts index 7e8fc4e..d054068 100644 --- a/admin/app/services/docker_service.ts +++ b/admin/app/services/docker_service.ts @@ -1103,13 +1103,17 @@ export class DockerService { this.activeInstallations.add(serviceName) - // Compute new image string. AMD-on-Ollama overrides this to the rolling :rocm tag - // (set during GPU detection below) since per-version ROCm tags aren't always published. + // newImage = the semver tag we record in the DB after the update (e.g. ollama/ollama:0.23.2). + // runtimeImage = the tag we actually pull and run. For AMD-on-Ollama these diverge: we run + // the rolling :rocm tag because per-version ROCm tags aren't always published, but the DB + // must keep the semver tag so the Apps page shows the actual version (not literally "rocm") + // and the registry update-check parses a valid tag (instead of looping on the same update). const currentImage = service.container_image const imageBase = currentImage.includes(':') ? currentImage.substring(0, currentImage.lastIndexOf(':')) : currentImage - let newImage = `${imageBase}:${targetVersion}` + const newImage = `${imageBase}:${targetVersion}` + let runtimeImage = newImage // GPU detection runs before the pull so AMD updates pull ollama/ollama:rocm rather // than the standard tag. Detection result is reused below when building the new @@ -1137,7 +1141,7 @@ export class DockerService { 'update-gpu-config', `AMD GPU detected. Using ROCm image with /dev/kfd and /dev/dri passthrough...` ) - newImage = 'ollama/ollama:rocm' + runtimeImage = 'ollama/ollama:rocm' updatedAmdDevices = await this._discoverAMDDevices() updatedAmdGpuConfigured = true } else { @@ -1158,9 +1162,9 @@ export class DockerService { } } - // Step 1: Pull new image - this._broadcast(serviceName, 'update-pulling', `Pulling image ${newImage}...`) - const pullStream = await this.docker.pull(newImage) + // Step 1: Pull new image (runtimeImage diverges from newImage for AMD, see above) + this._broadcast(serviceName, 'update-pulling', `Pulling image ${runtimeImage}...`) + const pullStream = await this.docker.pull(runtimeImage) await new Promise((res) => this.docker.modem.followProgress(pullStream, res)) // Step 2: Find and stop existing container @@ -1205,7 +1209,7 @@ export class DockerService { } const newContainerConfig: any = { - Image: newImage, + Image: runtimeImage, name: serviceName, Env: finalEnv.length > 0 ? finalEnv : undefined, Cmd: inspectData.Config?.Cmd || undefined,