fix bridge first-turn disconnect during Hermes bootstrap
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
---
|
||||
date: 2026-10-22
|
||||
feature: Agent Bridge startup
|
||||
impact: Prevent first-turn connection loss during Hermes interpreter relaunch
|
||||
commit: pending
|
||||
---
|
||||
# Initialize Hermes before Bridge readiness
|
||||
|
||||
- Commit: pending local validation.
|
||||
- Feature: Agent Bridge process startup, first chat/context estimate.
|
||||
- Cause: importing run_agent after the worker socket was ready invoked Hermes bootstrap, which execed from the selected dependency venv into managed Python and dropped the in-flight request.
|
||||
- Change: run Hermes bootstrap in the manager launch command before starting the broker. Workers inherit the selected interpreter; vendored Python and Socket.IO contracts remain unchanged.
|
||||
- Validation: focused manager and bridge tests, cold-worker context estimates and first chat, production build.
|
||||
|
||||
## Results
|
||||
- Harness and full production build passed.
|
||||
- Focused manager/concurrency suite: 54 passed; two pre-existing no-source tests also fail unchanged on this host because a global Hermes checkout exists.
|
||||
- Cold magic-conch and snowy-owl context estimates succeeded; the first snowy-owl chat completed with OK.
|
||||
- Focused browser chat streaming test passed with one worker. Broad parallel browser run: 3 passed, 8 input-locator failures; no client code changed.
|
||||
@@ -312,6 +312,24 @@ export function resolveAgentBridgeCommand(options: AgentBridgeManagerOptions = {
|
||||
return { command: fallback, argsPrefix: [], agentRoot, hermesHome }
|
||||
}
|
||||
|
||||
export function buildAgentBridgeLaunchArgs(command: BridgeCommand, script: string): string[] {
|
||||
const root = command.agentRoot
|
||||
if (!root || !existsSync(join(root, 'hermes_bootstrap.py'))) {
|
||||
return [...command.argsPrefix, script]
|
||||
}
|
||||
// Bootstrap may exec into Hermes's managed Python. Do that before the broker
|
||||
// forks workers or opens sockets, so the first request survives initialization.
|
||||
// Keep sys.argv[0] as -c until bootstrap returns so its relaunch replays this code.
|
||||
const bootstrap = [
|
||||
'import sys, runpy',
|
||||
'sys.path.insert(0, ' + JSON.stringify(root) + ')',
|
||||
'import hermes_bootstrap',
|
||||
'sys.argv = sys.argv[1:]',
|
||||
"runpy.run_path(sys.argv[0], run_name='__main__')",
|
||||
].join('; ')
|
||||
return [...command.argsPrefix, '-c', bootstrap, script]
|
||||
}
|
||||
|
||||
function bridgeScriptPath(): string {
|
||||
const candidates = [
|
||||
// Built server: dist/server/index.js -> dist/server/agent-bridge/python/hermes_bridge.py
|
||||
@@ -922,7 +940,7 @@ export class AgentBridgeManager {
|
||||
const script = bridgeScriptPath()
|
||||
const command = resolveAgentBridgeCommand(this.options)
|
||||
await this.prepareEndpoint()
|
||||
const args = [...command.argsPrefix, script, '--endpoint', this.endpoint]
|
||||
const args = [...buildAgentBridgeLaunchArgs(command, script), '--endpoint', this.endpoint]
|
||||
const agentRoot = command.agentRoot
|
||||
const hermesHome = command.hermesHome
|
||||
if (agentRoot) args.push('--agent-root', agentRoot)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { execFileSync } from 'child_process'
|
||||
import { EventEmitter } from 'events'
|
||||
import { chmodSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'fs'
|
||||
import { createServer, type Server } from 'net'
|
||||
@@ -113,6 +114,35 @@ describe('agent bridge manager command resolution', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('bootstraps before the bridge starts even when bootstrap replaces the interpreter', async () => {
|
||||
const root = join(tempDir, 'agent root')
|
||||
const script = join(tempDir, 'bridge.py')
|
||||
mkdirSync(root, { recursive: true })
|
||||
writeFileSync(join(root, 'hermes_bootstrap.py'), [
|
||||
'import os, sys',
|
||||
"if os.environ.get('TEST_BRIDGE_BOOTSTRAPPED') != 'yes':",
|
||||
" os.environ['TEST_BRIDGE_BOOTSTRAPPED'] = 'yes'",
|
||||
' os.execv(sys.executable, [sys.executable, *sys.orig_argv[1:]])',
|
||||
].join('\n'))
|
||||
writeFileSync(script, [
|
||||
'import json, os, sys',
|
||||
"assert os.environ.get('TEST_BRIDGE_BOOTSTRAPPED') == 'yes'",
|
||||
'print(json.dumps(sys.argv))',
|
||||
].join('\n'))
|
||||
const { buildAgentBridgeLaunchArgs } = await import('../../packages/server/src/services/hermes/agent-bridge/manager')
|
||||
const args = buildAgentBridgeLaunchArgs({ command: 'python3', argsPrefix: [], agentRoot: root, hermesHome: tempDir }, script)
|
||||
const env = { ...process.env }
|
||||
delete env.TEST_BRIDGE_BOOTSTRAPPED
|
||||
const output = execFileSync(process.platform === 'win32' ? 'python' : 'python3', [...args, '--endpoint', 'ipc:///test.sock'], { env, encoding: 'utf8', timeout: 10000 })
|
||||
expect(JSON.parse(output.trim())).toEqual([script, '--endpoint', 'ipc:///test.sock'])
|
||||
})
|
||||
|
||||
it('preserves legacy launch arguments when Hermes has no bootstrap module', async () => {
|
||||
const { buildAgentBridgeLaunchArgs } = await import('../../packages/server/src/services/hermes/agent-bridge/manager')
|
||||
expect(buildAgentBridgeLaunchArgs({ command: 'uv', argsPrefix: ['run', 'python'], agentRoot: tempDir, hermesHome: tempDir }, 'bridge.py'))
|
||||
.toEqual(['run', 'python', 'bridge.py'])
|
||||
})
|
||||
|
||||
it('injects Web UI OpenRouter attribution into the bridge process env by default', async () => {
|
||||
const { buildAgentBridgeProcessEnv } = await import('../../packages/server/src/services/hermes/agent-bridge/manager')
|
||||
const env = buildAgentBridgeProcessEnv('ipc:///tmp/test.sock', '/tmp/hermes-home', '/tmp/hermes-agent')
|
||||
|
||||
Reference in New Issue
Block a user