← Commit history

nb 0.4.10 / nbe 0.2.7: per-tab last-dialog memory immune to foreign events drains; battery dialog retry

John Lauer ·7c5fa04113 ·2mo ago ·parent efdba2f
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)}`);