Cold-start auto-launch fails: daemon exits before the chrome-not-running rescue branch can fire

Author: hnevkyCreated Sep 10, 2026Updated Sep 13, 2026

Summary

When Chrome has zero running processes (a true cold start — not just "CDP port not up yet"), browser-harness fails outright with daemon default didn't come up, instead of auto-launching Chrome as advertised since v0.1.8 ("If no Chromium-family browser is running, the harness launches one for you").

Confirmed regression between 0.1.9 and 0.1.13 (0.1.9 auto-launched correctly in this exact scenario, verified twice against real automated worker processes before upgrading; 0.1.13 fails immediately, reproduced 3 times in a row with a clean environment — Chrome fully quit, no stale singleton lock files, fresh daemon log each time).

Environment

  • macOS (Darwin 25.6.0)
  • browser-harness 0.1.13 (pypi install)
  • Python 3.11.15

Reproduction

osascript -e 'quit app "Google Chrome"'
# confirm zero Chrome processes: ps aux | grep "Google Chrome.app/Contents/MacOS/Google Chrome "
rm -f ~/.config/browser-harness/tmp/bu-default.log
browser-harness <<'PY'
new_tab("https://example.com")
print(page_info())
PY

Actual: immediate failure —

browser-harness: daemon default didn't come up -- check ~/.config/browser-harness/tmp/bu-default.log

with bu-default.log containing exactly one line:

fatal: chrome-not-running: no supported Chromium-family browser is running -- start Chrome, then retry

No "Chrome isn't running — launching it" message is ever printed, and no Chrome process is spawned.

Expected: the harness launches Chrome automatically (per the v0.1.8 auto-launch feature) and proceeds normally.

Root cause (traced in the installed package source)

In browser_harness/daemon.py, the daemon subprocess's own connect-wait loop:

python
if now >= next_liveness_check:
    if not supported_browser_running():
        raise RuntimeError(
            "chrome-not-running: no supported Chromium-family browser is running -- start Chrome, then retry"
        )
    next_liveness_check = now + 2

raises and lets the daemon subprocess exit almost immediately when Chrome isn't running at all (no 30s grace — the first liveness check trips it).

Back in browser_harness/admin.py, the higher-level orchestrator that's supposed to catch this and auto-launch Chrome only does so on this branch:

python
if local and launched_browser is None and _chrome_not_running(msg):
    # Chrome is closed — launch the browser and retry
    restart_daemon(name)
    launched_browser = _launch_browser()
    ...

But that branch is only reached if the daemon subprocess is still alive while its log reports chrome-not-running (i.e. pending_died is False). Since the daemon subprocess actually exits immediately in the true-cold-start case, the orchestrator instead takes the earlier pending_died branch:

python
if local and pending_died:
    ...
    if permission_wait:
        raise RuntimeError("permission-blocked: ...")
    continue   # <-- just respawns the SAME daemon again, up to 3x, never calling _launch_browser()

permission_wait is False for this message (it only checks for handshake-wait / permission-popup prefixes), so it falls through to continue, which just retries spawning the daemon from scratch — without ever calling _launch_browser(). After 3 identical failed attempts it gives up with daemon default didn't come up.

Auto-launch still works fine for the other chrome-not-running-ish case (Chrome running but CDP port not up yet, e.g. right after a fresh launch by something else) — that path polls _log_tail while the daemon subprocess is still looping, so the daemon-alive branch is reached correctly. It's specifically the "daemon dies immediately because zero Chrome processes exist" case that breaks the auto-launch rescue.

Suggested fix

Either:

  • Have the daemon.py connect-wait loop not immediately exit on the very first failed liveness check when Chrome is fully absent — give the orchestrator's own launch-and-retry a chance to run first, or
  • Have admin.py's pending_died branch also check _chrome_not_running(msg) and call _launch_browser() before falling through to a bare continue, matching what the still-alive branch already does.

Also observed (may be unrelated, noting for completeness)

browser-harness --doctor's "chrome running" check (_chrome_running() in admin.py) does a naive case-insensitive substring match against ps -A -o comm= output for names like "chrome". This produces false positives against unrelated processes that merely contain "chrome" in their path, e.g. Electron apps' own chrome_crashpad_handler helper (observed here from an unrelated app, not Google Chrome itself). Doctor reported chrome running: ok while the real Google Chrome.app process count was zero. Worth a more precise check (e.g. matching on the actual browser binary path/bundle id) if this hasn't already been fixed elsewhere.

Source: browser-use/browser-harness