fix(desktop): skip the installed runtime when ignore-existing is set

Main stopped reading HERMES_DESKTOP_IGNORE_EXISTING when the PATH rung was
removed, so the ACTIVE_HERMES_ROOT rung always started a local backend.
Gate that rung in resolveHermesBackend so the flag falls through to the
first-run connect-or-install choice. The post-install re-resolve marks the
runtime as installed by this launch, so it and later resolves use it
instead of starting the installer again. Replace the unused rung model
(its PATH and system-Python rungs no longer exist) with the gate main.ts
calls.
This commit is contained in:
Hermes Agent
2026-09-24 23:44:26 -05:00
committed by brooklyn!
parent 1753fe1612
commit ed9a63bda4
3 changed files with 86 additions and 266 deletions

View File

@@ -1,141 +1,62 @@
import assert from 'node:assert/strict'
import { describe, expect, it, vi } from 'vitest'
import { test } from 'vitest'
import { createInstalledRuntimeGate } from './backend-resolution'
import {
type BackendResolutionInput,
isIgnoreExisting,
postBootstrapResolveOptions,
selectBackendRung
} from './backend-resolution'
const ROOT = '/home/u/.hermes/hermes-agent'
const active = { label: 'active runtime' }
function discovered(overrides: Partial<BackendResolutionInput> = {}): BackendResolutionInput {
return {
ignoreExisting: false,
justInstalled: false,
bootstrapRepairRequested: false,
hasExplicitRoot: false,
hasUnpackagedSource: false,
activeRuntimeUsable: false,
hasExplicitCommand: false,
hasPathHermes: false,
hasSystemPython: false,
...overrides
}
function gate(env: NodeJS.ProcessEnv) {
const log = vi.fn()
return { log, runtime: createInstalledRuntimeGate(env, log) }
}
test('ignore-existing is exactly the string 1', () => {
assert.equal(isIgnoreExisting('1'), true)
assert.equal(isIgnoreExisting('true'), false)
assert.equal(isIgnoreExisting('0'), false)
assert.equal(isIgnoreExisting(''), false)
assert.equal(isIgnoreExisting(undefined), false)
})
describe('installed runtime gate', () => {
it('uses the installed runtime without the flag', async () => {
const { runtime } = gate({})
const probe = vi.fn(async () => active)
test('ignore-existing skips the active runtime and system python, not only hermes on PATH', () => {
// The flag used to wrap only the PATH probe. A usable managed install at
// ACTIVE_HERMES_ROOT, or a system Python that can import hermes_cli, still
// started a local serve.
assert.equal(
selectBackendRung(
discovered({
ignoreExisting: true,
activeRuntimeUsable: true,
hasPathHermes: true,
hasSystemPython: true
})
),
'onboarding'
)
})
test('explicit root and an unpackaged source checkout stay above the ignore-existing gate', () => {
const ignored = {
ignoreExisting: true,
activeRuntimeUsable: true,
hasPathHermes: true,
hasSystemPython: true
}
assert.equal(selectBackendRung(discovered({ ...ignored, hasExplicitRoot: true })), 'explicit-root')
assert.equal(selectBackendRung(discovered({ ...ignored, hasUnpackagedSource: true })), 'unpackaged-source')
})
test('an explicit hermes command is not a discovered runtime', () => {
assert.equal(
selectBackendRung(
discovered({
ignoreExisting: true,
activeRuntimeUsable: true,
hasExplicitCommand: true,
hasPathHermes: true,
hasSystemPython: true
})
),
'explicit-command'
)
})
test('the post-bootstrap re-resolve uses the runtime just installed instead of repeating onboarding', () => {
const ignored = discovered({
ignoreExisting: true,
activeRuntimeUsable: true,
hasPathHermes: true,
hasSystemPython: true
await expect(runtime.resolve(ROOT, probe)).resolves.toBe(active)
expect(probe).toHaveBeenCalledOnce()
})
assert.equal(selectBackendRung(ignored), 'onboarding')
assert.equal(
selectBackendRung({
...ignored,
justInstalled: postBootstrapResolveOptions().justInstalled
}),
'active'
)
})
it('skips the installed runtime without probing it when HERMES_DESKTOP_IGNORE_EXISTING=1', async () => {
const { log, runtime } = gate({ HERMES_DESKTOP_IGNORE_EXISTING: '1' })
const probe = vi.fn(async () => active)
test('a just-installed runtime that is not usable does not fall back to a discovered runtime or repeat onboarding', () => {
assert.equal(
selectBackendRung(
discovered({
ignoreExisting: true,
justInstalled: true,
activeRuntimeUsable: false,
hasPathHermes: true,
hasSystemPython: true
})
),
'installed-unusable'
)
})
await expect(runtime.resolve(ROOT, probe)).resolves.toBeNull()
expect(probe).not.toHaveBeenCalled()
expect(log).toHaveBeenCalledWith(expect.stringContaining(ROOT))
})
test('without the flag a usable active runtime still wins, and repair still bypasses it', () => {
assert.equal(
selectBackendRung(
discovered({
activeRuntimeUsable: true,
hasPathHermes: true,
hasSystemPython: true
})
),
'active'
)
assert.equal(
selectBackendRung(
discovered({
bootstrapRepairRequested: true,
activeRuntimeUsable: true,
hasPathHermes: true
})
),
'path'
)
assert.equal(
selectBackendRung(
discovered({
hasSystemPython: true
})
),
'system-python'
)
it('honours only the value the CLI exports', async () => {
for (const value of ['0', 'true', '']) {
const { runtime } = gate({ HERMES_DESKTOP_IGNORE_EXISTING: value })
await expect(runtime.resolve(ROOT, async () => active)).resolves.toBe(active)
}
})
it('uses the runtime this launch installed so the post-install re-resolve does not reinstall', async () => {
const { runtime } = gate({ HERMES_DESKTOP_IGNORE_EXISTING: '1' })
const probe = vi.fn(async () => active)
// main.ts: resolveHermesBackend's ACTIVE_HERMES_ROOT rung, else bootstrap-needed.
const resolveHermesBackend = async () => (await runtime.resolve(ROOT, probe)) ?? 'bootstrap-needed'
await expect(resolveHermesBackend()).resolves.toBe('bootstrap-needed')
expect(probe).not.toHaveBeenCalled()
// main.ts ensureRuntime: the re-resolve after a successful bootstrap.
await expect(runtime.afterInstall(resolveHermesBackend)).resolves.toBe(active)
// Later resolves (pool backends, TUI resume) keep the new runtime too.
await expect(resolveHermesBackend()).resolves.toBe(active)
expect(probe).toHaveBeenCalledTimes(2)
})
it('reads the flag at resolve time', async () => {
const env: NodeJS.ProcessEnv = {}
const { runtime } = gate(env)
env.HERMES_DESKTOP_IGNORE_EXISTING = '1'
await expect(runtime.resolve(ROOT, async () => active)).resolves.toBeNull()
})
})

View File

@@ -1,143 +1,35 @@
/**
* Precedence for Desktop backend resolution below the two explicit source
* rungs (HERMES_DESKTOP_HERMES_ROOT and an unpackaged checkout).
* HERMES_DESKTOP_IGNORE_EXISTING=1 (`hermes desktop --ignore-existing`) keeps
* Desktop off the installed runtime at ACTIVE_HERMES_ROOT. Backend resolution
* then falls through to bootstrap-needed, which shows the first-run choice
* (connect a remote, or install) instead of starting a local serve.
*
* HERMES_DESKTOP_IGNORE_EXISTING=1 skips discovered rungs — the managed
* install, `hermes` on PATH, and system Python — so no local serve starts
* and the caller falls through to connect/onboarding. It does not skip an
* explicit HERMES_DESKTOP_HERMES command.
* The bundled payload, HERMES_DESKTOP_HERMES_ROOT, an unpackaged checkout and
* HERMES_DESKTOP_HERMES resolve before this rung and are not affected.
*
* The post-bootstrap re-resolve must pass `justInstalled`. Otherwise the
* flag would skip the runtime this process just installed and the installer
* would see onboarding again and repeat.
* A runtime this launch installed is always used. Skipping it would send the
* post-install re-resolve (and every later resolve) back to the installer.
*/
export type BackendRung =
| 'explicit-root'
| 'unpackaged-source'
| 'active'
| 'explicit-command'
| 'path'
| 'system-python'
| 'onboarding'
| 'installed-unusable'
export function createInstalledRuntimeGate(env: NodeJS.ProcessEnv, log: (message: string) => void) {
let installedThisLaunch = false
export interface BackendResolutionInput {
ignoreExisting: boolean
justInstalled: boolean
bootstrapRepairRequested: boolean
hasExplicitRoot: boolean
hasUnpackagedSource: boolean
activeRuntimeUsable: boolean
hasExplicitCommand: boolean
hasPathHermes: boolean
hasSystemPython: boolean
}
return {
/** The ACTIVE_HERMES_ROOT rung: probe the installed runtime unless the flag skips it. */
async resolve<T>(root: string, probe: () => Promise<T | null>): Promise<T | null> {
if (env.HERMES_DESKTOP_IGNORE_EXISTING === '1' && !installedThisLaunch) {
log(`[bootstrap] HERMES_DESKTOP_IGNORE_EXISTING=1; skipping the installed runtime at ${root}`)
const NO_CANDIDATES: BackendResolutionInput = {
ignoreExisting: false,
justInstalled: false,
bootstrapRepairRequested: false,
hasExplicitRoot: false,
hasUnpackagedSource: false,
activeRuntimeUsable: false,
hasExplicitCommand: false,
hasPathHermes: false,
hasSystemPython: false
}
return null
}
export const INSTALLED_RUNTIME_UNUSABLE =
'Hermes install finished, but the new runtime could not be resolved. Desktop will not start the installer again.'
return probe()
},
export function isIgnoreExisting(value: string | undefined): boolean {
return value === '1'
}
/** Re-resolve after a successful bootstrap; the new runtime is exempt from the flag. */
afterInstall<T>(resolveBackend: () => Promise<T>): Promise<T> {
installedThisLaunch = true
/** Options the re-resolve after a successful bootstrap must pass. */
export function postBootstrapResolveOptions(): { justInstalled: true } {
return { justInstalled: true }
}
export function selectBackendRung(input: BackendResolutionInput): BackendRung {
if (input.hasExplicitRoot) {
return 'explicit-root'
return resolveBackend()
}
}
if (input.hasUnpackagedSource) {
return 'unpackaged-source'
}
const skipDiscovered = input.ignoreExisting && !input.justInstalled
if (input.activeRuntimeUsable && !input.bootstrapRepairRequested && !skipDiscovered) {
return 'active'
}
if (input.hasExplicitCommand) {
return 'explicit-command'
}
if (!input.ignoreExisting && input.hasPathHermes) {
return 'path'
}
if (!input.ignoreExisting && input.hasSystemPython) {
return 'system-python'
}
// The installer already ran. Onboarding here would start it again; PATH and
// system Python are the discovered runtimes the flag promised to skip.
if (input.ignoreExisting && input.justInstalled) {
return 'installed-unusable'
}
return 'onboarding'
}
export function shouldProbeActiveRuntime(input: {
ignoreExisting: boolean
justInstalled: boolean
bootstrapRepairRequested: boolean
}): boolean {
return (
selectBackendRung({
...NO_CANDIDATES,
...input,
activeRuntimeUsable: true
}) === 'active'
)
}
export function shouldProbeDiscoveredPath(input: { ignoreExisting: boolean }): boolean {
return (
selectBackendRung({
...NO_CANDIDATES,
ignoreExisting: input.ignoreExisting,
hasPathHermes: true
}) === 'path'
)
}
export function shouldProbeSystemPython(input: { ignoreExisting: boolean }): boolean {
return (
selectBackendRung({
...NO_CANDIDATES,
ignoreExisting: input.ignoreExisting,
hasSystemPython: true
}) === 'system-python'
)
}
export function unresolvedDiscoveredRuntime(input: {
ignoreExisting: boolean
justInstalled: boolean
}): 'onboarding' | 'installed-unusable' {
const rung = selectBackendRung({
...NO_CANDIDATES,
ignoreExisting: input.ignoreExisting,
justInstalled: input.justInstalled,
hasPathHermes: true,
hasSystemPython: true
})
return rung === 'installed-unusable' ? 'installed-unusable' : 'onboarding'
}

View File

@@ -79,6 +79,7 @@ import { canImportHermesCli, PROBE_TIMEOUT_MS, shouldTrustHermesOverride, verify
import { waitForDashboardPortAnnouncement } from './backend-ready'
import { recycleOwnedBackend } from './backend-recycle'
import { isPidAliveWindows, waitForBackendRelease } from './backend-release-gate'
import { createInstalledRuntimeGate } from './backend-resolution'
import { createBackendServeSupportResolver } from './backend-serve-support'
import {
isHostKeyChangedBootFailure,
@@ -4752,6 +4753,8 @@ function writeDefaultProjectDir(dir) {
}
}
const installedRuntimeGate = createInstalledRuntimeGate(process.env, rememberLog)
async function resolveHermesBackend(backendArgs: string[]): Promise<ResolvedHermesBackend> {
const payload = bundledPayload(process.resourcesPath)
@@ -4869,9 +4872,10 @@ async function resolveHermesBackend(backendArgs: string[]): Promise<ResolvedHerm
// builds could leave a healthy install behind without the marker. If the
// active runtime is usable, launch it directly; only fall through to
// bootstrap when the runtime itself is unusable.
const activeBackend: SourceBackend | null = await resolveSourceInstallationBackend(ACTIVE_HERMES_ROOT, backendArgs, {
hermesHome: HERMES_HOME
})
// HERMES_DESKTOP_IGNORE_EXISTING=1 skips this rung (see backend-resolution).
const activeBackend: SourceBackend | null = await installedRuntimeGate.resolve(ACTIVE_HERMES_ROOT, () =>
resolveSourceInstallationBackend(ACTIVE_HERMES_ROOT, backendArgs, { hermesHome: HERMES_HOME })
)
const activeRuntime: ActiveRuntimeState = activeRuntimeState(activeBackend)
@@ -5064,7 +5068,10 @@ async function ensureRuntime(
rememberLog('[bootstrap] bootstrap complete; marker written. Re-resolving backend.')
// Resolve the newly published launcher after the installer completes.
return ensureRuntime(await resolveHermesBackend(backend.args), assertStillOwned)
return ensureRuntime(
await installedRuntimeGate.afterInstall(() => resolveHermesBackend(backend.args)),
assertStillOwned
)
}
throw new Error(`Unexpected bootstrap backend: ${backend.kind}`)