fix(browser): retry orcad's agent-browser config write after a failure

This commit is contained in:
Kelvin Amoaba
2026-11-14 00:09:03 +00:00
parent 5b2fe857e0
commit 03361277a4
2 changed files with 49 additions and 40 deletions
@@ -0,0 +1,33 @@
// Both pick the socket directory, so dropping either loses a running helper and its live tabs.
const SOCKET_LOCATION_KEYS = new Set(['AGENT_BROWSER_SOCKET_DIR', 'AGENT_BROWSER_NAMESPACE'])
// No idle timeout: this daemon owns the user’s remote Chromium and all its live tabs.
// agent-browser 0.33+ reads an unset timeout as one hour, so only "0" keeps it unbounded.
export function externalChromiumAgentBrowserEnvironment(options: {
inheritedEnv: NodeJS.ProcessEnv
executablePath: string
profilePath: string
sessionName: string
configPath: string
browserArgs?: readonly string[]
}): NodeJS.ProcessEnv {
// Only the socket location is inherited: a user's AGENT_BROWSER_HEADED or _EXTENSIONS would open a
// visible window. Windows env keys ignore case.
const env = Object.fromEntries(
Object.entries(options.inheritedEnv).filter(([key]) => {
const upper = key.toUpperCase()
return !upper.startsWith('AGENT_BROWSER_') || SOCKET_LOCATION_KEYS.has(upper)
})
)
return {
...env,
AGENT_BROWSER_IDLE_TIMEOUT_MS:
options.inheritedEnv.AGENT_BROWSER_IDLE_TIMEOUT_MS?.trim() || '0',
AGENT_BROWSER_EXECUTABLE_PATH: options.executablePath,
AGENT_BROWSER_PROFILE: options.profilePath,
AGENT_BROWSER_SESSION: options.sessionName,
// An explicit config skips ~/.agent-browser/config.json and ./agent-browser.json.
AGENT_BROWSER_CONFIG: options.configPath,
AGENT_BROWSER_ARGS: options.browserArgs?.join('\n') ?? ''
}
}
@@ -17,6 +17,7 @@ import {
} from '../../../browser/agent-browser/observation/agent-browser-screenshot-arguments'
import { runProcess } from '@orca/process-host'
import { resolveExternalChromiumProfilePath } from './external-chromium-profile-path'
import { externalChromiumAgentBrowserEnvironment } from './external-chromium-agent-browser-environment'
const COMMAND_TIMEOUT_MS = 90_000
const MAX_OUTPUT_BYTES = 50 * 1024 * 1024
@@ -73,40 +74,6 @@ function classifyAgentBrowserError(message: string): string {
return 'browser_error'
}
// Both pick the socket directory, so dropping either loses a running helper and its live tabs.
const SOCKET_LOCATION_KEYS = new Set(['AGENT_BROWSER_SOCKET_DIR', 'AGENT_BROWSER_NAMESPACE'])
// No idle timeout: this daemon owns the user’s remote Chromium and all its live tabs.
// agent-browser 0.33+ reads an unset timeout as one hour, so only "0" keeps it unbounded.
export function externalChromiumAgentBrowserEnvironment(options: {
inheritedEnv: NodeJS.ProcessEnv
executablePath: string
profilePath: string
sessionName: string
configPath: string
browserArgs?: readonly string[]
}): NodeJS.ProcessEnv {
// Only the socket location is inherited: a user's AGENT_BROWSER_HEADED or _EXTENSIONS would open a
// visible window. Windows env keys ignore case.
const env = Object.fromEntries(
Object.entries(options.inheritedEnv).filter(([key]) => {
const upper = key.toUpperCase()
return !upper.startsWith('AGENT_BROWSER_') || SOCKET_LOCATION_KEYS.has(upper)
})
)
return {
...env,
AGENT_BROWSER_IDLE_TIMEOUT_MS:
options.inheritedEnv.AGENT_BROWSER_IDLE_TIMEOUT_MS?.trim() || '0',
AGENT_BROWSER_EXECUTABLE_PATH: options.executablePath,
AGENT_BROWSER_PROFILE: options.profilePath,
AGENT_BROWSER_SESSION: options.sessionName,
// An explicit config skips ~/.agent-browser/config.json and ./agent-browser.json.
AGENT_BROWSER_CONFIG: options.configPath,
AGENT_BROWSER_ARGS: options.browserArgs?.join('\n') ?? ''
}
}
// A profile held by another Chromium also fails ProcessSingleton; only an access failure means unwritable.
function unwritableProfileLockLine(message: string): string | undefined {
return message
@@ -144,12 +111,21 @@ export class ExternalChromiumBrowserSession {
// Empty, so agent-browser's defaults (headless, no extensions) apply instead of a user's config.
private configPath(): Promise<string> {
this.configPathWrite ??= (async () => {
const path = join(this.statePath, 'agent-browser-config.json')
await mkdir(this.statePath, { recursive: true, mode: 0o700 })
await writeFile(path, '{}\n', { mode: 0o600 })
return path
})()
if (!this.configPathWrite) {
const write = (async () => {
const path = join(this.statePath, 'agent-browser-config.json')
await mkdir(this.statePath, { recursive: true, mode: 0o700 })
await writeFile(path, '{}\n', { mode: 0o600 })
return path
})()
// A failed write retries on the next command instead of failing every later one.
write.catch(() => {
if (this.configPathWrite === write) {
this.configPathWrite = null
}
})
this.configPathWrite = write
}
return this.configPathWrite
}