Fix desktop runtime cold start handling (#1233)

* fix desktop runtime cold start handling

* fix windows desktop python startup env

* Revert "fix windows desktop python startup env"

This reverts commit 3718ba7586ab1a672c7e599ff1e315dfa76d7cda.

* bump desktop release version to 0.6.8
This commit is contained in:
ekko
2026-06-02 10:58:15 +08:00
committed by GitHub
parent 1acfb6486b
commit 7aa483f003
6 changed files with 48 additions and 26 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "hermes-web-ui", "name": "hermes-web-ui",
"version": "0.6.7", "version": "0.6.8",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "hermes-web-ui", "name": "hermes-web-ui",
"version": "0.6.7", "version": "0.6.8",
"description": "Self-hosted AI chat dashboard for Hermes Agent — multi-model web UI with multi-platform integration", "description": "Self-hosted AI chat dashboard for Hermes Agent — multi-model web UI with multi-platform integration",
"repository": { "repository": {
"type": "git", "type": "git",
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "hermes-studio", "name": "hermes-studio",
"version": "0.6.7", "version": "0.6.8",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "hermes-studio", "name": "hermes-studio",
"version": "0.6.7", "version": "0.6.8",
"license": "BSL-1.1", "license": "BSL-1.1",
"dependencies": { "dependencies": {
"electron-updater": "^6.3.9" "electron-updater": "^6.3.9"
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "hermes-studio", "name": "hermes-studio",
"version": "0.6.7", "version": "0.6.8",
"description": "Hermes Studio desktop distribution with bundled Python runtime and hermes-agent", "description": "Hermes Studio desktop distribution with bundled Python runtime and hermes-agent",
"homepage": "https://ekkolearnai.com", "homepage": "https://ekkolearnai.com",
"author": { "author": {
+38 -16
View File
@@ -13,7 +13,7 @@ import {
} from 'node:fs' } from 'node:fs'
import { get as httpGet } from 'node:http' import { get as httpGet } from 'node:http'
import { get as httpsGet } from 'node:https' import { get as httpsGet } from 'node:https'
import { basename, dirname, join } from 'node:path' import { basename, dirname, join, relative } from 'node:path'
import { promisify } from 'node:util' import { promisify } from 'node:util'
import { app } from 'electron' import { app } from 'electron'
import { import {
@@ -58,6 +58,25 @@ export type RuntimeProgress = {
type RuntimeProgressHandler = (progress: RuntimeProgress) => void type RuntimeProgressHandler = (progress: RuntimeProgress) => void
function requiredRuntimeFiles(root: string): string[] {
const pythonBin = process.platform === 'win32'
? join(root, 'python', 'python.exe')
: join(root, 'python', 'bin', 'python3')
const hermesBin = process.platform === 'win32'
? join(root, 'python', 'Scripts', 'hermes.exe')
: join(root, 'python', 'bin', 'hermes')
const nodeBin = process.platform === 'win32'
? join(root, 'node', 'node.exe')
: join(root, 'node', 'bin', 'node')
const files = [pythonBin, hermesBin, nodeBin, join(root, RUNTIME_MANIFEST_NAME)]
if (process.platform === 'win32') files.push(join(root, 'git', 'cmd', 'git.exe'))
return files
}
function missingRuntimeFiles(root: string): string[] {
return requiredRuntimeFiles(root).filter(file => !existsSync(file))
}
function runtimeReady(): boolean { function runtimeReady(): boolean {
const gitReady = process.platform !== 'win32' || !!bundledGit() const gitReady = process.platform !== 'win32' || !!bundledGit()
return hermesBinExists() && existsSync(bundledNode()) && gitReady return hermesBinExists() && existsSync(bundledNode()) && gitReady
@@ -230,10 +249,9 @@ async function extractRuntimeArchive(archive: string, targetRoot: string): Promi
await execFileAsync(process.platform === 'win32' ? 'tar.exe' : 'tar', ['-xzf', archive, '-C', tempRoot], { await execFileAsync(process.platform === 'win32' ? 'tar.exe' : 'tar', ['-xzf', archive, '-C', tempRoot], {
windowsHide: true, windowsHide: true,
}) })
for (const required of ['python', 'node']) { const missing = missingRuntimeFiles(tempRoot)
if (!existsSync(join(tempRoot, required))) { if (missing.length > 0) {
throw new Error(`Runtime archive did not contain ${required}/`) throw new Error(`Runtime archive is missing required files: ${missing.map(file => relative(tempRoot, file)).join(', ')}`)
}
} }
rmSync(targetRoot, { recursive: true, force: true }) rmSync(targetRoot, { recursive: true, force: true })
mkdirSync(parent, { recursive: true }) mkdirSync(parent, { recursive: true })
@@ -265,19 +283,23 @@ export async function ensureDesktopRuntime(onProgress?: RuntimeProgressHandler):
const archive = join(dirname(runtimeRoot), `${descriptor.name}.download`) const archive = join(dirname(runtimeRoot), `${descriptor.name}.download`)
console.log(`[runtime] downloading Hermes runtime ${descriptor.name}`) console.log(`[runtime] downloading Hermes runtime ${descriptor.name}`)
onProgress?.({ stage: 'download', message: `Downloading ${descriptor.name}...` }) onProgress?.({ stage: 'download', message: `Downloading ${descriptor.name}...` })
await downloadFile(descriptor.url, archive, onProgress) let archiveSize = 0
if (descriptor.sha256) { try {
onProgress?.({ stage: 'verify', message: 'Verifying Hermes runtime...' }) await downloadFile(descriptor.url, archive, onProgress)
const actual = await sha256File(archive) archiveSize = statSync(archive).size
if (actual !== descriptor.sha256) { if (descriptor.sha256) {
throw new Error(`Runtime checksum mismatch for ${descriptor.name}`) onProgress?.({ stage: 'verify', message: 'Verifying Hermes runtime...' })
const actual = await sha256File(archive)
if (actual !== descriptor.sha256) {
throw new Error(`Runtime checksum mismatch for ${descriptor.name}`)
}
} }
}
onProgress?.({ stage: 'extract', message: 'Extracting Hermes runtime...' }) onProgress?.({ stage: 'extract', message: 'Extracting Hermes runtime...' })
await extractRuntimeArchive(archive, runtimeRoot) await extractRuntimeArchive(archive, runtimeRoot)
const archiveSize = statSync(archive).size } finally {
rmSync(archive, { force: true }) rmSync(archive, { force: true })
}
const manifestPath = join(runtimeRoot, RUNTIME_MANIFEST_NAME) const manifestPath = join(runtimeRoot, RUNTIME_MANIFEST_NAME)
if (!existsSync(manifestPath)) { if (!existsSync(manifestPath)) {
+5 -5
View File
@@ -22,7 +22,7 @@ import {
} from './paths' } from './paths'
const DEFAULT_PORT = 8748 const DEFAULT_PORT = 8748
const DEFAULT_READY_TIMEOUT_MS = 30_000 const DEFAULT_READY_TIMEOUT_MS = 120_000
const AGENT_BRIDGE_STARTED_MARKER = '[bootstrap] agent bridge started' const AGENT_BRIDGE_STARTED_MARKER = '[bootstrap] agent bridge started'
const AGENT_BRIDGE_FAILED_MARKER = '[bootstrap] agent bridge failed to start' const AGENT_BRIDGE_FAILED_MARKER = '[bootstrap] agent bridge failed to start'
const execFileAsync = promisify(execFile) const execFileAsync = promisify(execFile)
@@ -405,10 +405,10 @@ export async function startWebUiServer(port = DEFAULT_PORT): Promise<string> {
}) })
const timeoutMs = readyTimeoutMs() const timeoutMs = readyTimeoutMs()
await Promise.all([ void bridgeStartup.wait(timeoutMs).catch(err => {
waitForReady(port, timeoutMs), console.warn(`[webui] agent bridge was not ready during startup: ${err instanceof Error ? err.message : String(err)}`)
bridgeStartup.wait(timeoutMs), })
]) await waitForReady(port, timeoutMs)
return getServerUrl(port) return getServerUrl(port)
} }