← Commit history

0.3.5: readiness means DRIVABLE - pending-* no longer counts as ready; unroutable pending fails fast as extension_resolving instead of hanging

John Lauer ·19781c38af ·2mo ago ·parent eebe98f
5 files changed +32−12
bridge/BRIDGE_VERSION+1−1
@@ -1 +1 @@-0.3.4+0.3.5
bridge/bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "native-browser",   "displayName": "Adom Native Browser",-  "version": "0.3.4",+  "version": "0.3.5",   "expectedExtensionVersion": "0.2.0",   "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",
bridge/package.json+1−1
@@ -1,6 +1,6 @@ {   "name": "adom-native-browser-bridge",-  "version": "0.3.4",+  "version": "0.3.5",   "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",
bridge/server.js+24−4
@@ -105,6 +105,12 @@ function saveBlocklist() { try { fs.writeFileSync(BLOCKFILE, JSON.stringify({ bl function isBlocked(key) { return blocklist.has(key); }  function liveConns() { return [...conns.entries()].filter(([k, c]) => c.sock && !c.sock.destroyed && !isBlocked(k)); }+// A `pending-*` key is a connected extension whose PROFILE IDENTITY has not resolved yet. targetConn+// cannot route to it, so it must never count as "ready" - reporting ready:true off one told the AI to+// drive and every drive then failed 'extension not connected' (or hung 30s with no profile arg).+const isPending = (k) => /^pending-/.test(String(k || ''));+function drivableConns() { return liveConns().filter(([k]) => !isPending(k)); }+function pendingConns() { return liveConns().filter(([k]) => isPending(k)); } function anyConnected() { return liveConns().length > 0; } // Pick the connection an action verb should target: explicit args.profile, else the active profile, // else the only/most-recent live one. Returns null if none drivable (or the target is blocklisted).@@ -138,8 +144,15 @@ function targetConn(args) {     for (const [k, c] of conns) if (k === pin && c.sock && !c.sock.destroyed && !isBlocked(k)) return { conn: c, key: k, pinned: true };     // pinned profile no longer live → fall through to the normal default rather than hard-failing.   }-  const live = liveConns();-  if (!live.length) return null;+  const drivable = drivableConns();+  const live = drivable.length ? drivable : [];+  if (!live.length) {+    // Connected but unresolved (pending-*) is NOT drivable - say so fast instead of forwarding into a+    // socket that will never answer (issue: readiness said ready, drives then hung for 30s).+    const pend = pendingConns();+    if (pend.length) return { unresolved: true, pending: pend.map(([k]) => k) };+    return null;+  }   const act = live.find(([k]) => k === activeProfile);   const [k, c] = act || live[live.length - 1];   return { conn: c, key: k };@@ -1392,7 +1405,8 @@ function dispatch(verb, args) {     // installs. This bridge has no heavy runtime (tiny Node), so "readiness" == the Level-2 extension     // being connected. ab's own bridge_readiness aggregates across bridges; this reports THIS one.     if (verb === 'nbrowser_readiness') {-      const live = liveConns();+      const live = drivableConns();+      const pend = pendingConns();       const ready = live.length > 0;       const chExe = detectChrome();       // Per-profile extension version + stale check. A loaded-unpacked extension does NOT auto-update@@ -1438,7 +1452,8 @@ function dispatch(verb, args) {       const claudeExt = claudeExtAnywhere();       return resolve({ success: true, output: JSON.stringify({         ready,-        state: ready ? 'ready' : 'needs-extension',+        state: ready ? 'ready' : (pend.length ? 'resolving' : 'needs-extension'),+        pendingProfiles: pend.map(([k]) => k),   // connected but identity unresolved: NOT drivable         claudeExtension: claudeExt,             // Claude for Chrome presence (human-fallback awareness; never drivable)         installing: false,         extensionConnected: ready,@@ -1456,6 +1471,8 @@ function dispatch(verb, args) {           ? `STALE EXTENSION: ${staleDetail}; this bridge expects ${EXPECTED_EXT}. New verbs (wait_for/keepalive/download_wait/set_date/totp/robust-click) will return "verb not implemented yet" on the stale profile(s). SELF-HEAL: (1) refresh the unpacked extension dir to the current build (installing-the-extension -> Updating, or re-sync the extension/ folder), then (2) nbrowser_dev_reload {"profile":"<staleProfile>"} - it re-reads disk in 2 passes and reconnects in ~10s. If the on-disk build is ALREADY current, step (2) alone heals it.` + skewHint           : ready           ? `nbe (the Adom Native Browser Extension) is connected (${live.length} profile(s)) and current (v${EXPECTED_EXT || '?'}); drive it with nbrowser_open_window. Read-only probe - it never spawns work or installs.`+          : pend.length+          ? `nbe IS connected (${pend.length} socket(s)) but its PROFILE IDENTITY has not resolved yet (${pend.map(([k]) => k).join(', ')}), so NOTHING is drivable yet - every drive verb will refuse 'extension not connected' until it resolves. This is normal for a few seconds after a browser start. FIX: nbrowser_wake_profile {profile:"chrome:<email>"} (from nbrowser_profiles) forces the resolve, or the user opening any window in that profile completes it. Poll this verb until state:"ready".`           : `nbe (the Adom Native Browser Extension) is NOT connected. ${chExe ? '' : 'This box has NO native Google Chrome (Edge may be present). If the user wants the AI to drive their REAL Chrome, offer nbrowser_install_chrome (installs Google Chrome, mostly no prompt). '}${claudeExt.installed ? `NOTE: the user HAS the Claude for Chrome extension (${claudeExt.profiles.join(', ')}) - you cannot drive it (closed control plane), but the honest interim is real: they can ask Claude in their browser to work a page interactively, while you offer nbe for full automation from any AI thread. ` : ''}It may be not-yet-installed (a Level-2 proactively-offered install: OFFER to install it, then run installing-the-extension - never make them do it) OR installed-but-asleep (the SW sleeps when a profile has no open window). Full onboarding: the setting-up-native-chrome skill. Read-only: never installs or spawns.` + skewHint + hostHint,       }) });     }@@ -2222,6 +2239,9 @@ function dispatch(verb, args) {       sessionProfile.delete(tgt.sessionId); persistSessions();       tgt = targetConn(args); // repin below - a pending-* pin was never a real identity     }+    if (tgt && tgt.unresolved) {+      return resolve({ success: false, errorCode: 'extension_resolving', error: `the extension is connected but its profile identity has not resolved yet (${(tgt.pending || []).join(', ')}), so there is nothing drivable to route to`, _hint: 'This is normal for a few seconds after a browser start. Run nbrowser_profiles to see the real profiles, then nbrowser_wake_profile {profile:"chrome:<email>"} to force the resolve - or just poll nbrowser_readiness until state:"ready" (it reports state:"resolving" while this is true). Failing fast on purpose: forwarding to an unresolved connection used to hang the caller for the full timeout.' });+    }     if (tgt && tgt.mismatch) {       return resolve({ success: false, errorCode: 'profile_mismatch', error: `profile_mismatch: session '${tgt.sessionId}' is pinned to profile '${tgt.pin}', but you called with profile '${tgt.wantKey}'.`, _hint: `Refused so you never act on the WRONG account (personal vs corporate). Drop the profile arg (the session already routes to '${tgt.pin}'), or open a NEW registered session in '${tgt.wantKey}' if you truly meant that account.` });     }
native-browser-bridge-manifest.json+5−5
@@ -1,8 +1,8 @@ {   "manifest_version": 1,-  "version": "0.3.4",-  "url": "https://wiki.adom.inc/download/adom/nb-bridge/0.3.4/native-browser-bridge-v0.3.4.zip",-  "sha256": "3ca007845a04ab9d73ec2327ef9387c090f4cb7cbda336386854b1fb79b1c963",-  "size": 107028,-  "released_at": "2026-08-19T08:24:41Z"+  "version": "0.3.5",+  "url": "https://wiki.adom.inc/download/adom/nb-bridge/0.3.5/native-browser-bridge-v0.3.5.zip",+  "sha256": "65ebfd4ab939f918ba1dacc894a1b73e62575a709a2d68216c64a267ba6ffc55",+  "size": 107771,+  "released_at": "2026-08-19T08:49:54Z" }