Skip to content

fix(browser): real-profile auth mirror hangs forever on a locked destination - #105754

Closed
benbarclay wants to merge 1 commit into
mainfrom
fix/browser-auth-mirror-hang
Closed

benbarclay wants to merge 1 commit into
mainfrom
fix/browser-auth-mirror-hang

Conversation

@benbarclay

Copy link
Copy Markdown
Contributor

Symptom

A Desktop session finishes its work, then shows a permanent spinner; every message typed
afterwards piles into the composer queue and is never sent. Only a backend restart clears it.

Root cause

A browser_exec tool call hangs mirroring Chrome's auth DBs, holding the agent's turn open.
Captured live with a SIGUSR2 thread dump:

browser_use_cli.py:542   browser_exec
browser_use_cli.py:492   _route_backend
browser_use_cli.py:481   _resolve_real_profile_cdp
browser_tool_real_profile.py:288  _real_profile_cdp
browser_connect.py:661   snapshot_real_profile
browser_connect.py:428   _mirror_profile_auth
browser_connect.py:409   _copy_auth_file      <-- parked in sqlite3_sleep

The turn never reaches its finally, so _emit_settled_session_info never runs, no
session.info running=false reaches the client, and the composer's queue drain (gated on
busy -> false) never fires.

The destination is the culprit, not the source. Connection.backup() retries a busy
destination internally and ignores the connection's busy timeout, so connect(dst, timeout=5)
cannot bound it. A destination left locked by an earlier hung mirror blocks the next mirror
forever. The tool-level 420s timeout abandons the thread but cannot interrupt a C-level lock
wait, so the lock is never released and every subsequent launch re-hangs — self-perpetuating.

Evidence

observation value
stuck thread, two dumps 24 min apart same TID, 1426/1427 samples in sqlite3_sleep
turn accepted / never finished 15:03:08 accepted, no tui turn finished 46 min later
destination probe OperationalError('database is locked') after 5.2s
mode=ro source URI vs live Chrome hung >20s, timeout=5 never fired
immutable=1 source URI returned in 0.0008s
patched, live Chrome + locked dst True in 0.0006s

Changes

  • Back up into a fresh <dst>.new, then os.replace(). Nothing can hold a file we just
    created, so there is nothing to contend on; the swap stays atomic.
  • Drop the mode=ro source fallback. 8e74666 added immutable=1 to fix this exact hang
    but kept mode=ro as a fallback, leaving the unbounded path one exception away. sqlite's busy
    timeout does not cover lock negotiation, so nothing bounds it. immutable=1 is also
    semantically right: a committed snapshot of a file another process owns. The bounded
    plain-copy fallback is unchanged.

Tests

Three regressions, all mutation-checked — verified failing on base, passing here:

  • test_copy_auth_file_never_opens_the_unbounded_ro_mode
  • test_copy_auth_file_never_backs_up_into_the_live_destination
  • test_copy_auth_file_cleans_up_temp_on_failure

The locked-destination test runs the copy on a worker with a join deadline, so the unfixed
behaviour fails fast rather than hanging the suite (confirmed: the naive version hung a full
pytest run past 300s).

199 passed across tests/tools/test_browser_real_profile.py and tests/tools/test_browser_use_cli.py.

Scope note

This fixes the browser_exec hang. It does not add a general backstop for "a tool wedges in
C code and the turn never settles" — the tool timeout abandons such threads without unwinding
them. That looks worth addressing separately; happy to follow up if you agree.

…ination

A `browser_exec` call could park a thread in `sqlite3_sleep` permanently
while mirroring Chrome's auth DBs, holding the agent's turn open. The turn
never reaches its `finally`, so no `session.info running=false` settle is
emitted and the Desktop composer latches busy — every later message queues
and never sends. Captured live: one thread stuck 24+ minutes across two
dumps, turn accepted at 15:03 with no `tui turn finished` 46 minutes later.

Root cause is the DESTINATION, not the source. `Connection.backup()` retries
a busy destination internally and ignores the connection's busy timeout, so
`sqlite3.connect(dst, timeout=5)` cannot bound it. A destination left locked
by an earlier hung mirror therefore blocks the next mirror forever — and
because the tool-level 420s timeout abandons the thread without interrupting
a C-level lock wait, the lock is never released and every subsequent launch
re-hangs the same way. Self-perpetuating.

Two changes:

- Back up into a fresh `<dst>.new` and `os.replace()` it into place. No other
  process can hold a file we just created, so there is nothing to contend on,
  and the swap stays atomic. Measured against a live Chrome with a
  deliberately locked destination: 0.0006s vs an indefinite hang.
- Drop the `mode=ro` (no `immutable=1`) source fallback. 8e74666 added
  `immutable=1` to fix exactly this hang but left `mode=ro` as a fallback,
  keeping the unbounded path one exception away; sqlite's busy timeout does
  not cover lock negotiation, so nothing bounds it. `immutable=1` is also the
  semantically correct mode — a committed snapshot of a file another process
  owns. The bounded plain-copy fallback is unchanged.

Tests: three regressions, all mutation-checked (fail on base, pass here).
The locked-destination test runs the copy on a worker with a join deadline so
the unfixed behaviour fails fast instead of hanging the suite. 199 passing
across the browser real-profile and CLI suites.
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

૮ >ﻌ< ა ci review

ran on 47d942a — fix(browser): real-profile auth mirror hangs forever on a lo

⚠️ Warnings

OSV vulnerability scan · View job

28 known vulnerabilities found in pinned dependencies.

How to fix:

Review the findings in the Security tab. Update the affected dependencies if a patched version is available.


debug info

CI timings

CI timings · View report · View job

Wall time 5m10s vs 4m50s (+6.9%). 6 job(s) slower, 6 faster, 2 unchanged.

  • OSV scan / Scan lockfiles / osv-scan: +17.0s
  • Python tests / Run tests: +12.0s
  • OS-specific tests / macOS-only tests: -10.0s
  • OS-specific tests / Windows-only tests: +10.0s
  • Check contributors / check-attribution: -9.0s

@benbarclay
benbarclay requested a review from teknium1 September 8, 2026 11:32
@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists tool/browser Browser automation (CDP, Playwright) comp/cli CLI entry point, hermes_cli/, setup wizard labels Sep 8, 2026
@alt-glitch

Copy link
Copy Markdown
Contributor

This was generated by AI during triage.

Related: #96659 (open, bounds the source-side backup and falls through to raw copy) targets the same real-profile auth-mirror hang family; #98249 (merged) covered the source immutable=1 half. This PR addresses the destination-lock side of Connection.backup(). Flagging so the reviewer can compose or pick between the two open fixes.

teknium1 added a commit that referenced this pull request Sep 8, 2026
Preserve Ben Barclay's diagnosis and replace the staging-file approach with
SQLite-coordinated writes and a five-second backup callback deadline. A
main-file replacement can replay an abandoned destination WAL; immutable
source reads can miss committed source WAL. Neither raw copy nor replacement
is safe when the destination is locked.

Refuse unavailable auth databases without raw-copy fallback, retaining the
existing close-browser-and-retry flow. Keep two invariant tests for lock
refusal/recovery and source-versus-destination WAL contents. Convert existing
text masquerading as database fixtures into real SQLite fixtures.

Related: #105754
Related: #96659
@teknium1

teknium1 commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

Landed via #105763 at 13fb5e1. Thanks @benbarclay for the live diagnosis; your original commit and authorship are preserved. The salvage uses bounded SQLite backup in place rather than replacing the main file, avoiding abandoned destination WAL replay and preserving committed source WAL. It refuses locked/unreadable databases without raw-copy fallback. CI is green; the broader local browser sweep passed 686 tests. Linux snapshot/lock/recovery probes passed; native macOS/Desktop incident verification remains unclaimed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/cli CLI entry point, hermes_cli/, setup wizard P2 Medium — degraded but workaround exists tool/browser Browser automation (CDP, Playwright) type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants