mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-02 00:05:39 +08:00
Compare commits
2 Commits
fix/slack-
...
fix/window
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7eaf537cff | ||
|
|
02aa1dbb7d |
@@ -1320,12 +1320,12 @@ function unwrapWindowsVenvHermesCommand(command, backendArgs) {
|
||||
if (path.basename(scriptsDir).toLowerCase() !== 'scripts') return null
|
||||
|
||||
const venvRoot = path.dirname(scriptsDir)
|
||||
const python = getNoConsoleVenvPython(venvRoot)
|
||||
const python = getVenvPython(venvRoot)
|
||||
if (!fileExists(python)) return null
|
||||
|
||||
const root = path.dirname(venvRoot)
|
||||
return {
|
||||
label: `existing Hermes no-console Python at ${python}`,
|
||||
label: `existing Hermes Python at ${python}`,
|
||||
command: python,
|
||||
args: ['-m', 'hermes_cli.main', ...backendArgs],
|
||||
bootstrap: false,
|
||||
@@ -1338,7 +1338,6 @@ function unwrapWindowsVenvHermesCommand(command, backendArgs) {
|
||||
// Surfaced so backendSupportsServe() can read this runtime's source for the
|
||||
// `serve` capability check instead of falling back to a heavyweight probe.
|
||||
root,
|
||||
readyFile: true,
|
||||
shell: false
|
||||
}
|
||||
}
|
||||
@@ -1622,62 +1621,24 @@ function getVenvPython(venvRoot) {
|
||||
return path.join(venvRoot, IS_WINDOWS ? path.join('Scripts', 'python.exe') : path.join('bin', 'python'))
|
||||
}
|
||||
|
||||
function readVenvHome(venvRoot) {
|
||||
try {
|
||||
const cfg = fs.readFileSync(path.join(venvRoot, 'pyvenv.cfg'), 'utf8')
|
||||
const match = cfg.match(/^home\s*=\s*(.+?)\s*$/im)
|
||||
return match ? match[1].trim() : null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function getNoConsoleVenvPython(venvRoot) {
|
||||
if (!IS_WINDOWS) return getVenvPython(venvRoot)
|
||||
|
||||
// The venv's ``Scripts\pythonw.exe`` is a uv launcher shim that re-execs the
|
||||
// base console ``python.exe``, allocating a conhost/Windows Terminal window
|
||||
// that CREATE_NO_WINDOW can't suppress. Use the base ``pythonw.exe`` directly;
|
||||
// callers put the venv site-packages on PYTHONPATH so imports still resolve.
|
||||
const baseHome = readVenvHome(venvRoot)
|
||||
if (baseHome) {
|
||||
const basePythonw = path.join(baseHome, 'pythonw.exe')
|
||||
if (fileExists(basePythonw)) return basePythonw
|
||||
}
|
||||
|
||||
return path.join(venvRoot, 'Scripts', 'pythonw.exe')
|
||||
}
|
||||
|
||||
function toNoConsolePython(pythonPath) {
|
||||
if (!IS_WINDOWS || !pythonPath) return pythonPath
|
||||
|
||||
const resolved = String(pythonPath)
|
||||
if (/pythonw\.exe$/i.test(resolved)) return resolved
|
||||
|
||||
if (/python\.exe$/i.test(resolved)) {
|
||||
const pythonw = path.join(path.dirname(resolved), 'pythonw.exe')
|
||||
if (fileExists(pythonw)) return pythonw
|
||||
}
|
||||
|
||||
return pythonPath
|
||||
}
|
||||
|
||||
function applyWindowsNoConsoleSpawnHints(backend) {
|
||||
if (!IS_WINDOWS || !backend?.command) return backend
|
||||
|
||||
const usesHermesModule =
|
||||
backend.kind === 'python' ||
|
||||
(Array.isArray(backend.args) && backend.args[0] === '-m' && backend.args[1] === 'hermes_cli.main')
|
||||
|
||||
if (!usesHermesModule) return backend
|
||||
|
||||
backend.command = toNoConsolePython(backend.command)
|
||||
if (/pythonw\.exe$/i.test(path.basename(String(backend.command || '')))) {
|
||||
backend.readyFile = true
|
||||
}
|
||||
|
||||
return backend
|
||||
}
|
||||
// Windows console-window flashes are governed by the *parent's* console, not by
|
||||
// each child spawn. A GUI-subsystem parent (pythonw.exe) has no console, so every
|
||||
// console-subsystem child it spawns (git, gh, cmd, ...) must allocate its own —
|
||||
// which flashes a window. A console-subsystem parent (python.exe) instead owns a
|
||||
// single console that all of its children inherit, so none of them flash.
|
||||
//
|
||||
// We add no new creationflag: the backend spawn is ALREADY wrapped in
|
||||
// hiddenWindowsChildOptions() (windowsHide: true), but that setting is INERT
|
||||
// against pythonw.exe — a GUI-subsystem process has no console for it to act on.
|
||||
// Launching the backend as the venv's console python.exe is what makes the
|
||||
// existing wrapper load-bearing: with windowsHide the process comes up owning a
|
||||
// *windowless* console (verified at runtime — it has an attachable console whose
|
||||
// window handle is NULL), and its children inherit that one windowless console
|
||||
// instead of each allocating a visible one. This makes "no flashing windows" a
|
||||
// property of the one backend launch rather than a flag that has to be repeated
|
||||
// at every descendant spawn site. Restoring console python also restores stdout,
|
||||
// so the backend announces its port on the normal HERMES_DASHBOARD_READY stdout
|
||||
// line and no ready-file side channel is needed.
|
||||
|
||||
function getVenvSitePackagesEntries(venvRoot) {
|
||||
const entries = []
|
||||
@@ -2899,9 +2860,9 @@ function createPythonBackend(root, label, backendArgs, options = {}) {
|
||||
|
||||
const venvRoot = path.join(root, 'venv')
|
||||
const venvPython = getVenvPython(venvRoot)
|
||||
const command = IS_WINDOWS && fileExists(venvPython) ? getNoConsoleVenvPython(venvRoot) : toNoConsolePython(python)
|
||||
const command = IS_WINDOWS && fileExists(venvPython) ? venvPython : python
|
||||
|
||||
return applyWindowsNoConsoleSpawnHints({
|
||||
return {
|
||||
kind: 'python',
|
||||
label,
|
||||
command,
|
||||
@@ -2914,7 +2875,7 @@ function createPythonBackend(root, label, backendArgs, options = {}) {
|
||||
root,
|
||||
bootstrap: Boolean(options.bootstrap),
|
||||
shell: false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// createActiveBackend — build a backend pointing at ACTIVE_HERMES_ROOT, the
|
||||
@@ -2923,9 +2884,9 @@ function createPythonBackend(root, label, backendArgs, options = {}) {
|
||||
// ensureRuntime() to create / refresh it before launch.
|
||||
function createActiveBackend(backendArgs) {
|
||||
const venvPython = getVenvPython(VENV_ROOT)
|
||||
const command = fileExists(venvPython) ? getNoConsoleVenvPython(VENV_ROOT) : toNoConsolePython(findSystemPython())
|
||||
const command = fileExists(venvPython) ? venvPython : findSystemPython()
|
||||
|
||||
return applyWindowsNoConsoleSpawnHints({
|
||||
return {
|
||||
kind: 'python',
|
||||
label: `Hermes at ${ACTIVE_HERMES_ROOT}`,
|
||||
command,
|
||||
@@ -2938,7 +2899,7 @@ function createActiveBackend(backendArgs) {
|
||||
root: ACTIVE_HERMES_ROOT,
|
||||
bootstrap: true,
|
||||
shell: false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function resolveHermesBackend(backendArgs) {
|
||||
@@ -3045,15 +3006,15 @@ function resolveHermesBackend(backendArgs) {
|
||||
// failure, fall through to step 6 so the bootstrap runner pulls
|
||||
// a uv-managed 3.11 into %LOCALAPPDATA%\hermes\hermes-agent\venv.
|
||||
if (canImportHermesCli(python)) {
|
||||
return applyWindowsNoConsoleSpawnHints({
|
||||
return {
|
||||
kind: 'python',
|
||||
label: `installed hermes_cli module via ${python}`,
|
||||
command: toNoConsolePython(python),
|
||||
command: python,
|
||||
args: ['-m', 'hermes_cli.main', ...backendArgs],
|
||||
bootstrap: false,
|
||||
env: {},
|
||||
shell: false
|
||||
})
|
||||
}
|
||||
}
|
||||
rememberLog(`Ignoring system Python ${python}: hermes_cli is not importable; falling through to bootstrap.`)
|
||||
}
|
||||
@@ -3087,7 +3048,7 @@ function resolveHermesBackend(backendArgs) {
|
||||
async function ensureRuntime(backend) {
|
||||
if (!backend.bootstrap) {
|
||||
await advanceBootProgress('runtime.external', `Using ${backend.label}`, 32)
|
||||
return applyWindowsNoConsoleSpawnHints(backend)
|
||||
return backend
|
||||
}
|
||||
|
||||
// backend.kind === 'bootstrap-needed' means resolveHermesBackend couldn't
|
||||
@@ -3229,7 +3190,7 @@ async function ensureRuntime(backend) {
|
||||
)
|
||||
}
|
||||
|
||||
backend.command = getNoConsoleVenvPython(VENV_ROOT)
|
||||
backend.command = getVenvPython(VENV_ROOT)
|
||||
backend.label = `Hermes at ${ACTIVE_HERMES_ROOT} (venv: ${VENV_ROOT})`
|
||||
updateBootProgress({
|
||||
phase: 'runtime.ready',
|
||||
@@ -3238,7 +3199,7 @@ async function ensureRuntime(backend) {
|
||||
running: true,
|
||||
error: null
|
||||
})
|
||||
return applyWindowsNoConsoleSpawnHints(backend)
|
||||
return backend
|
||||
}
|
||||
|
||||
function fetchJson(url, token, options = {}) {
|
||||
|
||||
@@ -39,34 +39,39 @@ test('desktop background child processes opt into hidden Windows consoles', () =
|
||||
requireHiddenChildOptions(source, /spawn\(\s*py,\s*\['-m', 'hermes_cli\.main', 'uninstall', '--gui-summary'\]/)
|
||||
|
||||
assert.match(source, /function unwrapWindowsVenvHermesCommand\(command, backendArgs\)/)
|
||||
assert.match(source, /existing Hermes no-console Python at/)
|
||||
assert.match(source, /function getNoConsoleVenvPython\(venvRoot\)/)
|
||||
assert.match(source, /function toNoConsolePython\(pythonPath\)/)
|
||||
assert.match(source, /function applyWindowsNoConsoleSpawnHints\(backend\)/)
|
||||
assert.match(source, /function readVenvHome\(venvRoot\)/)
|
||||
assert.match(source, /path\.join\(venvRoot, 'Scripts', 'pythonw\.exe'\)/)
|
||||
assert.match(source, /backendStartFailure/)
|
||||
assert.match(source, /HERMES_DESKTOP_READY_FILE/)
|
||||
assert.match(source, /readyFile: true/)
|
||||
assert.match(source, /function getVenvSitePackagesEntries\(venvRoot\)/)
|
||||
assert.match(source, /path\.join\(venvRoot, 'Lib', 'site-packages'\)/)
|
||||
assert.match(source, /args: \['-m', 'hermes_cli\.main', \.\.\.backendArgs\]/)
|
||||
})
|
||||
|
||||
test('getNoConsoleVenvPython prefers base pythonw over the uv re-exec shim', () => {
|
||||
test('desktop backend launches console python so child consoles are inherited, not pythonw', () => {
|
||||
const source = readElectronFile('main.cjs')
|
||||
const body = source.slice(
|
||||
source.indexOf('function getNoConsoleVenvPython(venvRoot)'),
|
||||
source.indexOf('function getVenvSitePackagesEntries(venvRoot)')
|
||||
|
||||
// The flash fix is structural: the backend runs as a console-subsystem
|
||||
// python.exe under hiddenWindowsChildOptions() (-> CREATE_NO_WINDOW), so it
|
||||
// owns ONE windowless console that every descendant spawn inherits. Launching
|
||||
// it as GUI-subsystem pythonw.exe is what made each child allocate (and flash)
|
||||
// its own console, so the backend command must never be pythonw.
|
||||
assert.doesNotMatch(source, /pythonw\.exe'\)/, 'backend must not be launched via pythonw.exe')
|
||||
assert.doesNotMatch(
|
||||
source,
|
||||
/function getNoConsoleVenvPython\b/,
|
||||
'pythonw-conversion helper should be gone; console python is launched directly'
|
||||
)
|
||||
assert.doesNotMatch(
|
||||
source,
|
||||
/function applyWindowsNoConsoleSpawnHints\b/,
|
||||
'pythonw spawn-hint rewriter should be gone'
|
||||
)
|
||||
|
||||
// The venv Scripts\pythonw.exe re-execs a console python.exe (flashes a
|
||||
// conhost); the base pythonw must be resolved first so it never runs.
|
||||
const baseIdx = body.indexOf('basePythonw')
|
||||
const shimIdx = body.indexOf("'Scripts', 'pythonw.exe'")
|
||||
assert.notEqual(baseIdx, -1, 'base pythonw resolution missing')
|
||||
assert.notEqual(shimIdx, -1, 'venv shim fallback missing')
|
||||
assert.ok(baseIdx < shimIdx, 'base pythonw must be preferred before the venv Scripts shim')
|
||||
// Console python restores stdout, so the port is announced on the normal
|
||||
// HERMES_DASHBOARD_READY stdout line — no ready-file side channel is set.
|
||||
assert.doesNotMatch(source, /readyFile: true/, 'no backend should opt into the pythonw ready-file path')
|
||||
|
||||
// Both desktop backend launches must still go through hiddenWindowsChildOptions
|
||||
// so the single backend console is created windowless.
|
||||
requireHiddenChildOptions(source, /spawn\(\s*backend\.command,\s*backend\.args/)
|
||||
requireHiddenChildOptions(source, /hermesProcess = spawn\(\s*backend\.command,\s*backend\.args/)
|
||||
})
|
||||
|
||||
test('intentional or interactive desktop child processes stay documented', () => {
|
||||
|
||||
Reference in New Issue
Block a user