← Commit history

v0.16.3 (insiders): nbrowser_close_window replies again: the cleanup branch called .map on close_window's boolean closed field, the throw was swallowed inside the async frame handler and every close timed out at 30s while the window had closed (present since 0.5.1, so on the public tier too). Reply decoration is now best-effort inside a guard so a decoration bug can never eat a reply again (_postError names it instead).

John Lauer ·af3d98c2b5 ·2mo ago ·parent c80207f
6 files changed +44−25
bin/nb-dashboard+1−1
@@ -20,7 +20,7 @@ const { execFile, spawn } = require('child_process'); const fs = require('fs'); const crypto = require('crypto'); -const VERSION = '0.16.2';           // keep in lockstep with package.json (pkg version)+const VERSION = '0.16.3';           // keep in lockstep with package.json (pkg version) // SEND TO AI IS UN-PARKED (John: "yeah, we are re-enabling the send to ai. it seems to be good now"). // The ah build thread certified the crash family dead on the gate-fixed build and the delivery leg is // proven end to end. The park const is DELETED rather than flipped, so no dead branch can resurrect it.
bridge/BRIDGE_VERSION+1−1
@@ -1 +1 @@-0.16.2\ No newline at end of file+0.16.3\ No newline at end of file
bridge/bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "native-browser",   "displayName": "Adom Native Browser",-  "version": "0.16.2",+  "version": "0.16.3",   "expectedExtensionVersion": "0.2.10",   "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.16.2",+  "version": "0.16.3",   "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+39−20
@@ -2620,22 +2620,12 @@ function dispatch(verb, args) {   }); } -async function onHostFrame(frame) {-  // Re-enter the ORIGINATING caller's context: this reply arrives on the extension socket, outside the-  // HTTP request's async scope, so ALS is empty here. Without this, every badge paint / window-identity-  // / caption we fire from the reply path would forward "self" instead of the thread that asked.-  const _p0 = (frame && frame.id != null) ? pending.get(frame.id) : null;-  if (_p0 && _p0.caller) return callerCtx.run(_p0.caller, () => onHostFrameInner(frame));-  return onHostFrameInner(frame);-}-async function onHostFrameInner(frame) {-  if (frame && frame.id != null && pending.has(frame.id)) {-    const p = pending.get(frame.id);-    clearTimeout(p.timer);-    pending.delete(frame.id);-    if (p.startedAt) activityStop(frame.id, p.verb, true, Date.now() - p.startedAt);-    if (frame.ok) {-      let payload = frame.payload || {};+// Everything that happens to a successful reply between the extension answering and the caller+// hearing it: file spill for screenshots/recordings, finder spill, taskbar/badge bookkeeping, the+// activity row, auto-flash, visible-open announcements. Best-effort by contract: the caller in+// onHostFrameInner wraps this in try/catch and ALWAYS resolves the reply (2026-09-03).+async function decorateReply(p, frame, payloadIn, setPayload) {+  let payload = payloadIn;       // Screenshot file-save: huge base64 over the relay times out, so when the caller passes       // toFile:true we write the PNG to a laptop temp file here and return its path instead of the       // base64. Pull it with `adom-bridge pull_file` (binary chunked) - fast + no relay timeout.@@ -2713,7 +2703,12 @@ async function onHostFrameInner(frame) {         }         if (sid) clearBadge(sid); // stop the idle-expiry timer for a closed session         // clear the badge on any spawned windows this close/cleanup removed-        for (const wid of (payload.alsoClosedSpawned || []).concat((payload.closed || []).map((c) => c.windowId).filter(Boolean))) {+        // nbrowser_close_window replies closed:true (a boolean); nbrowser_cleanup replies closed:[{windowId}].+        // Calling .map on the boolean threw here, the throw was swallowed inside the async frame handler,+        // and the reply was never sent: every nbrowser_close_window timed out at ab's 30s while the window+        // had in fact closed (found 2026-09-03; the battery's close row had been passing on a stale build).+        const closedList = Array.isArray(payload.closed) ? payload.closed : [];+        for (const wid of (payload.alsoClosedSpawned || []).concat(closedList.map((c) => c && c.windowId).filter(Boolean))) {           const sh = spawnHwnd.get(wid); if (sh) { taskbar(sh, { overlay: { badge: 'none' }, flash: 'stop' }); spawnHwnd.delete(wid); }         }       }@@ -2734,11 +2729,35 @@ async function onHostFrameInner(frame) {       if (p.crossThreadWarn && payload && typeof payload === 'object') payload._concurrentAgents = p.crossThreadWarn;       const sid = payload && payload.sessionId;       logActivity({ verb: p.verb, sessionId: sid || null, thread: sessionThread.get(sid) || null, profile: p.profileKey || null, ok: payload && payload.ok !== false, errorCode: (payload && payload.errorCode) || null, ...(p._internal ? { internal: true } : {}) });+  setPayload(payload);+}++async function onHostFrame(frame) {+  // Re-enter the ORIGINATING caller's context: this reply arrives on the extension socket, outside the+  // HTTP request's async scope, so ALS is empty here. Without this, every badge paint / window-identity+  // / caption we fire from the reply path would forward "self" instead of the thread that asked.+  const _p0 = (frame && frame.id != null) ? pending.get(frame.id) : null;+  if (_p0 && _p0.caller) return callerCtx.run(_p0.caller, () => onHostFrameInner(frame));+  return onHostFrameInner(frame);+}+async function onHostFrameInner(frame) {+  if (frame && frame.id != null && pending.has(frame.id)) {+    const p = pending.get(frame.id);+    clearTimeout(p.timer);+    pending.delete(frame.id);+    if (p.startedAt) activityStop(frame.id, p.verb, true, Date.now() - p.startedAt);+    if (frame.ok) {+      let payload = frame.payload || {};+      // Any exception in the decoration below used to reject this async handler silently and the caller+      // timed out even though the extension had answered. Decoration is best-effort; the reply is not.+      try { await decorateReply(p, frame, payload, (v) => { payload = v; }); }+      catch (e) { payload._postError = String((e && e.message) || e); }       resolve_ok(p.resolve, payload);-    } else {-      logActivity({ verb: p.verb, sessionId: null, profile: p.profileKey || null, ok: false, errorCode: 'error', error: String(frame.error || '').slice(0, 120) });-      p.resolve({ success: false, error: frame.error || 'unknown error', _hint: frame._hint });+      return;     }+    // frame.ok === false: the extension refused or threw; pass its error and teaching hint through verbatim.+    logActivity({ verb: p.verb, sessionId: null, profile: p.profileKey || null, ok: false, errorCode: 'error', error: String(frame.error || '').slice(0, 120) });+    p.resolve({ success: false, error: frame.error || 'unknown error', _hint: frame._hint });     return;   }   // Unsolicited events are surfaced to the AI via nbrowser_events (the extension buffers them).
package.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "nb-bridge",-  "version": "0.16.2",+  "version": "0.16.3",   "type": "app",   "title": "NB - Native Browser Bridge",   "description": "This package installs the SKILLS into your container so your AI knows how to drive your real signed-in Chrome and Edge; Bridge loads the bridge runtime itself from the release zip.",