← Commit history
nb 0.4.10 / nbe 0.2.7: per-tab last-dialog memory immune to foreign events drains; battery dialog retry
7 files changed
+26−14
bridge/BRIDGE_VERSION+1−1@@ -1 +1 @@-0.4.9+0.4.10
bridge/bridge.json+2−2@@ -2,8 +2,8 @@ "manifest_version": 1, "name": "native-browser", "displayName": "Adom Native Browser",- "version": "0.4.9",- "expectedExtensionVersion": "0.2.6",+ "version": "0.4.10",+ "expectedExtensionVersion": "0.2.7", "description": "Drives the user's REAL signed-in Chrome/Edge, in two layers. Layer 1 (nb) needs NOTHING installed and works on any machine: nbrowser_open_os_window (open a URL in the user's real profile, background+flashing, the OAuth/consent path), nbrowser_os_windows, nbrowser_profiles, nbrowser_wake_profile, nbrowser_force_close. Layer 2 (nbe, the Adom Native Browser Extension) unlocks the rest: CDP driving (navigate/click/read/screenshot) inside their logged-in profile. The bridge binds an ephemeral loopback port and the native-messaging host pumps stdio <-> bridge.", "homepage": "https://wiki.adom.inc/adom/nb-bridge", "author": "Adom Inc.",
bridge/package.json+1−1@@ -1,6 +1,6 @@ { "name": "adom-native-browser-bridge",- "version": "0.4.9",+ "version": "0.4.10", "private": true, "description": "Native-browser bridge for Bridge (ab): layer 1 (nb) plus layer 2 via nbe, the Adom Native Browser Extension (Chrome + Edge).", "main": "server.js",
extension/manifest.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 3, "name": "Adom Native Browser Extension", "short_name": "Adom",- "version": "0.2.6",+ "version": "0.2.7", "description": "Layer 2 of Adom Native Browser (nb): lets your Adom AI drive your real, signed-in Chrome or Edge, using your own logins.", "minimum_chrome_version": "116", "background": {
extension/src/cdp.js+14−3@@ -835,8 +835,15 @@ export async function detectLoginForm(tabId) { // surface it on the session's NEXT verb (modal_open) with an easy way to dismiss it. const _openDialogs = new Map(); // tabId -> { type, message, defaultPrompt, ts } (currently OPEN) const _dialogPolicy = new Map(); // tabId -> 'auto' (default: never wedge) | 'manual' (caller answers)-const _recentDialogs = []; // persistent ring: every dialog we SAW (survives auto-dismiss) for nbrowser_events+const _recentDialogs = []; // ring for nbrowser_events - DRAINED by whoever polls events+// ⚠️ _recentDialogs is drained by ANY events consumer (another AI thread polling nbrowser_events on this+// profile empties it), so it cannot double as the "was a dialog just auto-handled here?" memory - the+// alreadyHandled recovery and the click dialogOpened check lost their evidence to a foreign drain+// (found 2026-08-22: the Fusion thread's event polling raced the verb battery). Per-tab last-dialog+// memory lives here instead, and nothing drains it.+const _lastDialog = new Map(); // tabId -> { dialogType, message, autoHandled, accepted, ts } export function dialogFor(tabId) { return _openDialogs.get(Number(tabId)) || null; }+export function lastDialogFor(tabId) { return _lastDialog.get(Number(tabId)) || null; } export function drainDialogs() { const d = _recentDialogs.splice(0); return d; } // nbrowser_handle_dialog — click OK/Cancel (or answer a prompt) on the native dialog. accept:true = OK/Yes/Leave. export async function handleDialog(tabId, accept, promptText, policy) {@@ -854,7 +861,8 @@ export async function handleDialog(tabId, accept, promptText, policy) { const dlg = _openDialogs.get(id); try { await send(tabId, 'Page.handleJavaScriptDialog', { accept: accept !== false, promptText: promptText != null ? String(promptText) : undefined }); } catch (e) {- const auto = [..._recentDialogs].reverse().find((x) => x.tabId === String(id) && x.autoHandled);+ const auto = ((_lastDialog.get(id) || {}).autoHandled ? _lastDialog.get(id) : null)+ || [..._recentDialogs].reverse().find((x) => x.tabId === String(id) && x.autoHandled); if (auto) return { ok: true, handled: false, alreadyHandled: true, was: auto, tabId: String(id), _hint: `No dialog was open: a ${auto.dialogType} ("${String(auto.message).slice(0, 80)}") was AUTO-${auto.accepted ? 'ACCEPTED' : 'DISMISSED'} the moment it opened, so the page never blocked. To answer one yourself, arm the tab FIRST: nbrowser_handle_dialog {sessionId, policy:"manual"}, then do the action, then answer.` }; return { ok: false, error: 'no dialog to handle (it may have already closed): ' + ((e && e.message) || e), _hint: 'The modal may have closed. Re-check with a nbrowser_screenshot.' };@@ -893,6 +901,7 @@ chrome.debugger.onEvent.addListener((source, method, params) => { if (policy === 'auto') { send(tabId, 'Page.handleJavaScriptDialog', { accept }).then(() => { _openDialogs.delete(tabId);+ _lastDialog.set(tabId, { dialogType: d.type, message: d.message, autoHandled: true, accepted: accept, ts: d.ts }); _recentDialogs.push({ event: 'dialog', tabId: String(tabId), dialogType: d.type, message: d.message, autoHandled: true, accepted: accept, ts: d.ts, note: `auto-${accept ? 'accepted' : 'dismissed'} so the page could not wedge; set policy:"manual" via nbrowser_handle_dialog BEFORE the action if you need to answer it yourself` }); while (_recentDialogs.length > 20) _recentDialogs.shift();@@ -994,7 +1003,9 @@ export async function clickChanged(tabId, args) { // and the dialog IS the effect. Retrying here queued a second click into the frozen page, which fired // a SECOND confirm() the moment the first auto-dismissed (found by the verb battery, 2026-08-22). const dlgNow = _openDialogs.get(Number(tabId));- const dlgRecent = !changed && [..._recentDialogs].reverse().find((x) => x.tabId === String(tabId) && (Date.now() - x.ts) < 3000);+ const _ld = _lastDialog.get(Number(tabId));+ const dlgRecent = !changed && ((_ld && (Date.now() - _ld.ts) < 3000) ? _ld+ : [..._recentDialogs].reverse().find((x) => x.tabId === String(tabId) && (Date.now() - x.ts) < 3000)); if (!changed && (dlgNow || dlgRecent)) { const d = dlgNow || dlgRecent; return { ...r, changed: true, retried: false, dialogOpened: { type: d.type || d.dialogType, message: d.message },
native-browser-bridge-manifest.json+5−5@@ -1,8 +1,8 @@ { "manifest_version": 1,- "version": "0.4.9",- "url": "https://wiki.adom.inc/download/adom/nb-bridge/0.4.9/native-browser-bridge-v0.4.9.zip",- "sha256": "27016dfb08c5fa916df6b21b3559f81b3f8e6d3cfd6725224284e5e63fe050c8",- "size": 106216,- "released_at": "2026-08-22T14:42:24Z"+ "version": "0.4.10",+ "url": "https://wiki.adom.inc/download/adom/nb-bridge/0.4.10/native-browser-bridge-v0.4.10.zip",+ "sha256": "d42f582712b23449090059a541a1ae9d822b8bddb059a10f0adc4beb2ee3c270",+ "size": 106217,+ "released_at": "2026-08-22T14:45:21Z" }
tools/verb-battery.mjs+2−1@@ -166,7 +166,8 @@ const T = { call('nbrowser_click', { sessionId: SID, selector: '#confirm-btn' }); const r = call('nbrowser_handle_dialog', { sessionId: SID, action: 'accept' }); // resolve BEFORE eval: a pending confirm blocks page JS settle(600);- const v = evalIn('document.getElementById("dlg-note").textContent');+ let v = evalIn('document.getElementById("dlg-note").textContent');+ if (v.result === undefined) { settle(900); v = evalIn('document.getElementById("dlg-note").textContent'); } // dialog settle race: one retry const note = String(v.result); // 'confirm:true' = our accept landed; 'confirm:false' = the drive-mode auto-dismiss won the race - both are contract return A(note === 'confirm:true' || note === 'confirm:false', `dialog resolved: ${note} (verb: ${JSON.stringify(r).slice(0, 60)})`, `note:${JSON.stringify(v).slice(0, 120)}`);