fix: compare installed Agent CLI versions as releases

The installer decided "up to date" and "already installed" with a
substring test, so 11.2.3 counted as 1.2.3 and a newer CLI was offered a
downgrade. It now extracts both versions and compares them as releases
(installed is current when it is the recipe's version or newer), falling
back to the old string match only when a version cannot be parsed.

Checks: agent-installer and the installer facade API test (37 pass,
including a new version comparison test). Full desktop suite: 1540 pass.
This commit is contained in:
Ryder Sun committed 2026-10-15 19:38:33 +08:00
1 parent 00ba2d3b38
commit 26d4be2404
4 files changed
+28 -2

No files matched your search

@@ -265,6 +265,7 @@ module.exports = {
AGENT_COMPATIBILITY,
assessAgentVersion,
capabilityProbes,
compareReleaseVersions,
extractAgentVersion,
identifyAgentVersion,
}
@@ -4,6 +4,17 @@ const fs = require('node:fs')
const path = require('node:path')
const { promisify } = require('node:util')
const { searchPath } = require('../cli/cli-adapters.cjs')
const { compareReleaseVersions, extractAgentVersion } = require('../agent-compatibility.cjs')
// True when the installed CLI is the recipe's version or newer. A substring
// test treated "11.2.3" as "1.2.3" and offered downgrades for newer CLIs.
function installedVersionCurrent(installedVersion, targetVersion) {
const installed = extractAgentVersion(installedVersion).split('-')[0]
const target = extractAgentVersion(targetVersion).split('-')[0]
const comparison = installed && target ? compareReleaseVersions(installed, target) : null
if (comparison != null) return comparison >= 0
return Boolean(targetVersion) && String(installedVersion || '').includes(targetVersion)
}
const execFileAsync = promisify(execFile)
const DETECTION_CACHE_TTL_MS = 30000
@@ -339,6 +350,7 @@ function verifiedRecipeAgent(agent, recipe) {
}
module.exports = {
installedVersionCurrent,
AGENT_CATALOG,
DETECTION_CACHE_TTL_MS,
abortError,
@@ -3,6 +3,7 @@ const fs = require('node:fs')
const { randomUUID } = require('node:crypto')
const { DISPLAY_LIMIT, listLocalAgentSkills } = require('../../skills/local-skill-catalog.cjs')
const {
installedVersionCurrent,
AGENT_CATALOG,
DETECTION_CACHE_TTL_MS,
abortError,
@@ -169,7 +170,8 @@ class AgentInstaller extends EventEmitter {
const recipe = installRecipe(profile.kind, this.platform)
const targetVersion = recipe?.detectedVersion || recipe?.version || ''
const currentVersion = String(agent?.version || '')
const upToDate = Boolean(agent && targetVersion && currentVersion.includes(targetVersion))
const upToDate = Boolean(agent && targetVersion
&& installedVersionCurrent(currentVersion, targetVersion))
let installSupported = Boolean(recipe)
let installErrorCode = ''
if (!recipe) {
@@ -241,7 +243,7 @@ class AgentInstaller extends EventEmitter {
const current = installed.find(agent => agent.kind === profile.kind)
const targetVersion = recipe.detectedVersion || recipe.version
if (current && (current.compatibilityState !== 'compatible'
|| String(current.version || '').includes(targetVersion))) {
|| installedVersionCurrent(current.version, targetVersion))) {
throw installerError('INSTALL_AGENT_ALREADY_INSTALLED')
}
let command = recipe.interpreter || ''
@@ -1105,3 +1105,14 @@ setInterval(() => {}, 1000)
errorCode: '',
})
})
test('installed versions compare as releases instead of substrings', () => {
const { installedVersionCurrent } = require('../../../src/agents/installer/agent-installer-contract.cjs')
assert.equal(installedVersionCurrent('codex-cli 0.146.0', '0.146.0'), true)
assert.equal(installedVersionCurrent('0.150.1', '0.146.0'), true)
assert.equal(installedVersionCurrent('0.84.2', '0.84.20'), false)
assert.equal(installedVersionCurrent('11.2.3', '1.2.3'), true)
assert.equal(installedVersionCurrent('1.2.3', '11.2.3'), false)
assert.equal(installedVersionCurrent('openclaw 2026.7.1-2', '2026.7.1-2'), true)
assert.equal(installedVersionCurrent('', '0.20.0'), false)
})