Open bug report

Background nbrowser / UIA session raised a Chrome window over the user's work, and opens during a bridge respawn leave orphan windows

Drew Owens · 11d ago

What happened. On 2026-10-30, between about 11:25 and 11:46 AM CT, an AI thread ("senior-design-proposal") drove a Google Drive PDF in Drew's real Chrome on adom_drew2 to set up an eSignature request. Every window it opened was opened with nbrowser_open_window, which is meant to stay in the background. Drew reported mid-task: "please stop stealing foreground from me." He says this has been happening often lately. nbrowser_events recorded no focus_violation for the session, so nothing logged or reverted the raise.

What the thread called on that window, in order (window hwnd 2035308, nb session prof-send):

  1. nbrowser_open_window with profile: chrome:[email protected].
  2. nbrowser_click, nbrowser_eval and nbrowser_screenshot, repeated.
  3. desktop_find_control and desktop_find_controls (UIA, read-only) with hwnd: 2035308.
  4. desktop_ui_click (UIA Invoke) on the panel's "Request signature" button. The nbrowser_click on that button did nothing, and this did open the dialog.
  5. desktop_ui_set on two signer comboboxes and the message box, plus desktop_ui_click on two suggestion list items.
  6. desktop_ui_get. This one took 6.8 s, failed with uia_not_found ... after wake-retry, and is the call running when Drew complained.

The responses that report it all say "foreground": {"predicted": "background", "raised": false}, but Drew saw the window come forward. My suspects, which I have not confirmed: the UIA wake-retry path, and Invoke and SetValue on Chromium web content.

Second problem in the same session. Twice, nbrowser_open_window ran just as ab respawned the native-browser bridge after an auto-update (0.16.61 to 0.16.63). The call returned ok, but the next verb said "native browser extension not connected". After a wake and a second open, the first windows were still on screen and are now listed as owner: "user": a Gmail search and a second copy of the PDF. The AI can no longer close them without force_close, so they stay on the user's desktop.

Expected.

  • No raise without a foregroundReason. Any raise should be logged as a focus_violation and reverted.
  • An open that fails during a bridge respawn should leave no orphan window behind, or should hand the window back to the session that opened it.

Environment.

  • Adom Bridge 2.1.185 on adom_drew2 (Windows, embedded under Hydrogen).
  • native-browser bridge 0.16.63 at the start of the session, 0.16.64 later; nb nbrowser_status version 0.2.15.
  • Chrome profile [email protected]. The thread ran in the Hydrogen WSL workspace.

Impact. The user was interrupted while working. In his words it is "maddening", and it is making him reluctant to let AIs work in the background.

After the complaint. Two more desktop_ui_click Invokes ("Send request", then "Share anyway") finished the task on the same hwnd. Both returned quickly with no wake-retry. Drew may be able to say whether the window came forward on those.

Filed by Drew's AI agent at his request.

1 Reply

John Lauer · 7d ago

Thanks Drew, this is a clean, well-instrumented report and both problems are real. I read the bridge code against your trace. Here is what I confirmed, split by where the fix actually lives, because one of these is not nb's to fix alone.

Problem 1: the window came forward, nb honestly did not know

Your instinct is right, and the reason nb reported "foreground":{"predicted":"background","raised":false} with no focus_violation is not nb lying, it is nb never being in the loop for that raise.

  • nb's foreground guard (background-default, topmost auto-revert, the focus_violation / foreground_raised events) only wraps nb's OWN window operations, i.e. the nbrowser_* verbs. See the open path at bridge/server.js:2396 and :2512 and announceForeground. That report is accurate for the nbrowser_open_window / nbrowser_click calls: they really did stay background.
  • The raise came from steps 4 and 5 of your trace: desktop_ui_click (UIA Invoke) and desktop_ui_set (SetValue) on the Google Drive web-content controls. Those are ab-core desktop_ui_* verbs the thread called directly against the hwnd. They do not pass through nb at all, so nb never saw them, never predicted anything, and had nothing to revert. That is exactly why nbrowser_events is empty for it.
  • Mechanism: invoking or setting a value on a Chromium web-content accessibility node makes Chromium focus and scroll the element into view, which activates the tab's top-level window. This is Chromium behaviour, not a UIA-wide one. Corroborating data point from the password-export work (#55) this week: UIA Invoke on the native Save common-dialog button (a Win32 #32770 control) presses it in the background and does NOT raise the window, measured on AdomLapper. So "UIA Invoke is focus-neutral" is true for native controls and false for Chromium web content. Your two later Invokes ("Send request", "Share anyway") landed on Google's own modal, which may not re-activate the same way, so they may not have raised. Worth you confirming.

Because the thread invoked desktop_ui_* directly, only ab can guard that path (nb is not a party to the call). Proposed split:

  • ab (I will file on adom/adom-bridge): desktop_ui_click / desktop_ui_set should snapshot the target window's foreground state before the Invoke/SetValue, and if the window was NOT foreground and became topmost as a side effect, re-assert the prior z-order and report it in the result (a raisedAndReverted flag), so a background-driving caller is not silently surfaced. This is the real root fix.
  • nb (I will do here): everywhere nb ITSELF drives web content via UIA inside a composite (autologin, the #55 export, any future one), wrap the ab UIA call with the same before/after foreground check nb already uses for its own windows, revert, and emit a focus_violation on nbrowser_events so the thread learns. That closes the gap for nb-owned flows even before ab lands its fix.
  • Docs/guidance: add a pitfall to the desktop_ui_* guidance and the extension-driving skill that Invoking Chromium web content is not focus-neutral, and that a background-driving thread should expect a raise there until ab guards it.

Problem 2: open raced the respawn, so the hwnd mapping was never persisted

This one is squarely nb's. The order of persistence is the cause:

  • At dispatch, nb persists the session's profile / thread / purpose early (bridge/server.js:3079), before the window exists.
  • The session to hwnd mapping is persisted only on the SUCCESS path, after the extension returns the new window's hwnd (:2527, sessionHwnd.set(...) then persistSessions()).
  • When ab respawned nb mid-call (your 0.16.61 to 0.16.63 auto-update), the extension had already opened the window, but nb died before reaching :2527, so the hwnd was never written to sessions.json. On restart, restoreSessions (:1139) brings back profile/thread/purpose but has no hwnd for the sid. The live window now has no session claiming it, and the origin heuristic (:192, default user-data-dir to owner:"user") reports it as the user's, so the thread can only clear it with force_close. A second open then exists in the same profile, which makes the re-correlation repair (:1203-:1314) ambiguous, so the first window is never reclaimed. That matches your "two windows left, owner: user" exactly.

Proposed nb fixes:

  1. Health-gate the open. Before opening, if the host/extension connection is not healthy or a respawn is in flight, refuse with a retriable bridge_restarting (or ext_not_connected) instead of letting a window be opened that nb is about to forget. Your "call returned ok, next verb said not connected" is the race this closes.
  2. Reclaim on restart by intent, do not default to the user. restoreSessions already has each lost session's profile + purpose; extend it to adopt an unattributed window whose profile and URL match a persisted session opened in the last ~60 s, re-owning it to that sid rather than labelling it owner:"user". An agent window in the user's real profile is the normal case, so the default-user-data-dir heuristic should not win over a matching recent session intent.
  3. Persist the pending open before it happens. Record {sid, profile, url, purpose, openedAt} as a pending-open right before the extension call, so even a crash at the worst moment leaves a breadcrumb for (2) to match against.

Plan

I will take Problem 2 (all three, here in nb) and the nb-side half of Problem 1, and file the ab-core guard for the Problem 1 root cause on adom/adom-bridge, linking back here. Leaving this open until the fixes ship and are verified on a real respawn. One ask for you: can you confirm whether the two later Invokes ("Send request" / "Share anyway") raised the window or not? That tells us whether the raise is specific to web-content nodes vs any Chromium Invoke, and sizes the ab fix.

Log in to reply.