mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-27 17:58:07 +00:00
fix(desktop): launch a usable active runtime without a bootstrap marker
Marker presence was the launch gate, but the marker is provenance about who ran the install -- not proof the runtime works. A CLI-installed repo+venv, or a healthy install whose marker a repair deleted, both read as "never installed" and dropped the user into first-run bootstrap on every launch. Split the two questions: classifyActiveRuntime() reports marker validity and runtime usability separately, and the resolver launches whenever the runtime is usable, logging when it proceeds without a marker. An unusable runtime still falls through to bootstrap even with a valid marker, so an interrupted install can't spawn a dead backend. Drops isBootstrapComplete(), which had no callers left once the gate moved. Co-authored-by: iveywest <iveywest@users.noreply.github.com> Co-authored-by: lihengming <lihengming@users.noreply.github.com>
This commit is contained in:
parent
2b0b5e4c53
commit
048e9b2150
3 changed files with 150 additions and 37 deletions
60
apps/desktop/electron/active-runtime-state.test.ts
Normal file
60
apps/desktop/electron/active-runtime-state.test.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import assert from 'node:assert/strict'
|
||||
|
||||
import { test } from 'vitest'
|
||||
|
||||
import { classifyActiveRuntime, hasValidBootstrapMarker } from './active-runtime-state'
|
||||
|
||||
const VALID_MARKER = {
|
||||
pinnedCommit: '1234567890abcdef1234567890abcdef12345678',
|
||||
schemaVersion: 1
|
||||
}
|
||||
|
||||
test('hasValidBootstrapMarker accepts the current schema with a real-looking commit', () => {
|
||||
assert.equal(hasValidBootstrapMarker(VALID_MARKER, 1), true)
|
||||
})
|
||||
|
||||
test('hasValidBootstrapMarker rejects missing, wrong-schema, and too-short markers', () => {
|
||||
assert.equal(hasValidBootstrapMarker(null, 1), false)
|
||||
assert.equal(hasValidBootstrapMarker({ schemaVersion: 2, pinnedCommit: VALID_MARKER.pinnedCommit }, 1), false)
|
||||
assert.equal(hasValidBootstrapMarker({ schemaVersion: 1, pinnedCommit: 'abc123' }, 1), false)
|
||||
})
|
||||
|
||||
test('classifyActiveRuntime uses a healthy active runtime even when the bootstrap marker is missing', () => {
|
||||
assert.deepEqual(classifyActiveRuntime(null, 1, true), {
|
||||
hasValidMarker: false,
|
||||
shouldUseActiveRuntime: true,
|
||||
usabilityReason: 'usable'
|
||||
})
|
||||
})
|
||||
|
||||
test('classifyActiveRuntime uses a healthy active runtime even when the marker is stale or malformed', () => {
|
||||
assert.deepEqual(classifyActiveRuntime({ schemaVersion: 999, pinnedCommit: 'abc1234' }, 1, true), {
|
||||
hasValidMarker: false,
|
||||
shouldUseActiveRuntime: true,
|
||||
usabilityReason: 'usable'
|
||||
})
|
||||
})
|
||||
|
||||
test('classifyActiveRuntime refuses an unusable runtime even if a valid marker exists', () => {
|
||||
assert.deepEqual(classifyActiveRuntime(VALID_MARKER, 1, false), {
|
||||
hasValidMarker: true,
|
||||
shouldUseActiveRuntime: false,
|
||||
usabilityReason: 'unusable'
|
||||
})
|
||||
})
|
||||
|
||||
test('a CLI-installed runtime with no marker launches instead of re-running bootstrap', () => {
|
||||
// The reported symptom (#60721): install.sh / install.ps1 produced a healthy
|
||||
// repo+venv, no desktop-managed marker was ever written, and every launch
|
||||
// dropped the user back into the first-run installer.
|
||||
const state = classifyActiveRuntime(null, 1, true)
|
||||
|
||||
assert.equal(state.shouldUseActiveRuntime, true, 'a usable runtime must launch')
|
||||
assert.equal(state.hasValidMarker, false, 'marker provenance stays honest')
|
||||
})
|
||||
|
||||
test('a repair that deleted the marker does not strand a healthy install', () => {
|
||||
// #72166: the repair handler clears the marker unconditionally. Runtime
|
||||
// usability, not marker presence, must decide the next boot.
|
||||
assert.equal(classifyActiveRuntime(null, 1, true).shouldUseActiveRuntime, true)
|
||||
})
|
||||
58
apps/desktop/electron/active-runtime-state.ts
Normal file
58
apps/desktop/electron/active-runtime-state.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
export interface BootstrapMarkerLike {
|
||||
pinnedCommit?: unknown
|
||||
schemaVersion?: unknown
|
||||
}
|
||||
|
||||
export interface ActiveRuntimeState {
|
||||
hasValidMarker: boolean
|
||||
shouldUseActiveRuntime: boolean
|
||||
usabilityReason: 'usable' | 'unusable'
|
||||
}
|
||||
|
||||
export function hasValidBootstrapMarker(
|
||||
marker: BootstrapMarkerLike | null | undefined,
|
||||
schemaVersion: number
|
||||
): boolean {
|
||||
if (!marker || typeof marker !== 'object') {
|
||||
return false
|
||||
}
|
||||
|
||||
if (marker.schemaVersion !== schemaVersion) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (typeof marker.pinnedCommit !== 'string' || marker.pinnedCommit.length < 7) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// The active install at ~/.hermes/hermes-agent can be real and runnable even if
|
||||
// Desktop never wrote its first-run bootstrap marker (for example when Hermes
|
||||
// was installed by the CLI first, or when a past desktop build forgot the
|
||||
// marker). Runtime usability is authoritative for "can we launch local Hermes
|
||||
// right now?"; the marker is only provenance about how that install was
|
||||
// created. A missing/stale marker must never force a healthy local install into
|
||||
// the first-run bootstrap UI.
|
||||
export function classifyActiveRuntime(
|
||||
marker: BootstrapMarkerLike | null | undefined,
|
||||
schemaVersion: number,
|
||||
runtimeUsable: boolean
|
||||
): ActiveRuntimeState {
|
||||
const hasValidMarker = hasValidBootstrapMarker(marker, schemaVersion)
|
||||
|
||||
if (!runtimeUsable) {
|
||||
return {
|
||||
hasValidMarker,
|
||||
shouldUseActiveRuntime: false,
|
||||
usabilityReason: 'unusable'
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
hasValidMarker,
|
||||
shouldUseActiveRuntime: true,
|
||||
usabilityReason: 'usable'
|
||||
}
|
||||
}
|
||||
|
|
@ -30,6 +30,7 @@ import {
|
|||
} from 'electron'
|
||||
import nodePty from 'node-pty'
|
||||
|
||||
import { classifyActiveRuntime } from './active-runtime-state'
|
||||
import { stopBackendChild as stopBackendChildImpl } from './backend-child'
|
||||
import { dashboardFallbackArgs, sourceDeclaresServe } from './backend-command'
|
||||
import { createBackendConnectionState } from './backend-connection-state'
|
||||
|
|
@ -3383,11 +3384,11 @@ function readJson(filePath) {
|
|||
}
|
||||
}
|
||||
|
||||
// Bootstrap-complete marker helpers. The marker is written ONCE by the
|
||||
// first-launch bootstrap runner (Phase 1D) after install.ps1 stages succeed
|
||||
// AND the user has finished initial configuration. On every subsequent boot
|
||||
// we check `isBootstrapComplete()` and skip the bootstrap flow entirely if
|
||||
// the marker is present and current-schema.
|
||||
// Bootstrap-complete marker helpers. The marker is written by whichever
|
||||
// installer ran: install.ps1, install.sh, the Rust bootstrap installer, or the
|
||||
// first-launch bootstrap runner. It is provenance ("a bootstrap finished
|
||||
// here"), NOT the launch gate -- activeRuntimeState() decides that, because a
|
||||
// healthy runtime can predate the marker or outlive a repair that cleared it.
|
||||
//
|
||||
// Marker schema (version 1):
|
||||
// {
|
||||
|
|
@ -3420,29 +3421,13 @@ function isActiveRuntimeUsable() {
|
|||
)
|
||||
}
|
||||
|
||||
function isBootstrapComplete() {
|
||||
const marker = readBootstrapMarker()
|
||||
|
||||
if (!marker || typeof marker !== 'object') {
|
||||
return false
|
||||
}
|
||||
|
||||
if (marker.schemaVersion !== BOOTSTRAP_MARKER_SCHEMA_VERSION) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (typeof marker.pinnedCommit !== 'string' || marker.pinnedCommit.length < 7) {
|
||||
return false
|
||||
}
|
||||
|
||||
function activeRuntimeState() {
|
||||
// We DELIBERATELY do NOT verify that the checkout is currently at the
|
||||
// pinned commit -- users update via the in-app update path or `hermes
|
||||
// update`, which moves HEAD legitimately. The marker just attests "we
|
||||
// ran the bootstrap successfully at least once." We DO additionally require
|
||||
// a runnable venv: an interrupted or split-home install can leave the marker
|
||||
// + checkout without a venv, and trusting that spawns a dead backend
|
||||
// ("gateway offline") instead of re-running bootstrap to repair it.
|
||||
return isActiveRuntimeUsable()
|
||||
// update`, which moves HEAD legitimately. The marker only attests "a
|
||||
// desktop-managed bootstrap ran here at least once"; runtime usability is
|
||||
// what decides whether we can actually launch.
|
||||
return classifyActiveRuntime(readBootstrapMarker(), BOOTSTRAP_MARKER_SCHEMA_VERSION, isActiveRuntimeUsable())
|
||||
}
|
||||
|
||||
function writeBootstrapMarker(payload) {
|
||||
|
|
@ -3702,13 +3687,23 @@ function resolveHermesBackend(backendArgs) {
|
|||
}
|
||||
}
|
||||
|
||||
// 3. Bootstrap-complete ACTIVE_HERMES_ROOT -- the canonical install at
|
||||
// %LOCALAPPDATA%\hermes\hermes-agent (Windows) or ~/.hermes/hermes-agent.
|
||||
// The bootstrap marker means install.ps1 stages finished and the user
|
||||
// completed initial configuration; we trust the install and go straight
|
||||
// to spawning hermes. Updates flow through the in-app update path
|
||||
// (applyUpdates -> git pull) or `hermes update` from the CLI.
|
||||
if (isBootstrapComplete()) {
|
||||
// 3. ACTIVE_HERMES_ROOT — the canonical install at
|
||||
// %LOCALAPPDATA%\\hermes\\hermes-agent (Windows) or ~/.hermes/hermes-agent.
|
||||
// A valid bootstrap marker proves Desktop finished the first-run install
|
||||
// flow, but marker provenance is NOT the same thing as runtime usability:
|
||||
// the CLI can create the exact same repo+venv layout, and older desktop
|
||||
// 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 activeRuntime = activeRuntimeState()
|
||||
|
||||
if (activeRuntime.shouldUseActiveRuntime) {
|
||||
if (!activeRuntime.hasValidMarker) {
|
||||
rememberLog(
|
||||
`[bootstrap] Active Hermes runtime at ${ACTIVE_HERMES_ROOT} is usable but the bootstrap marker is missing or stale; skipping first-run bootstrap.`
|
||||
)
|
||||
}
|
||||
|
||||
return createActiveBackend(backendArgs)
|
||||
}
|
||||
|
||||
|
|
@ -3979,10 +3974,10 @@ async function ensureRuntime(backend) {
|
|||
// No venv at the expected location AND no bootstrap-needed sentinel
|
||||
// means we have a half-installed checkout: .git exists, source files
|
||||
// exist, but venv is missing or broken. This shouldn't happen in
|
||||
// normal flow because isBootstrapComplete() requires
|
||||
// isHermesSourceRoot() and the bootstrap writes the marker only after
|
||||
// install.ps1 succeeds. If we hit this, the user (or a deleted venv)
|
||||
// broke the invariant; tell them to re-run the install.
|
||||
// normal flow because activeRuntimeState() requires isHermesSourceRoot()
|
||||
// plus an importable hermes_cli before it hands back the active runtime.
|
||||
// If we hit this, the user (or a deleted venv) broke the invariant; tell
|
||||
// them to re-run the install.
|
||||
throw new Error(
|
||||
`Hermes venv missing at ${VENV_ROOT}. Re-run the desktop installer or ` + '`scripts/install.ps1` to rebuild it.'
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue