← Commit history
Update 6 file(s)
6 files changed
+53−16
bridge/BRIDGE_VERSION+1−1@@ -1 +1 @@-0.1.49+0.1.50
bridge/bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "native-browser", "displayName": "Adom Browser Extension",- "version": "0.1.49",+ "version": "0.1.50", "expectedExtensionVersion": "0.1.3", "description": "Drives the user's REAL signed-in Chrome/Edge. MOST verbs require the Adom extension installed in that browser profile (CDP driving: navigate/click/read/screenshot). TWO verbs need NO extension at all and work on any machine: nbrowser_open_os_window (open a URL in the user's real profile, background+flashing - the OAuth/consent path) and nbrowser_os_windows (enumerate browser windows). Also extension-free: nbrowser_profiles, nbrowser_wake_profile, nbrowser_force_close. The bridge binds an ephemeral loopback port and the native-messaging host pumps stdio <-> bridge.", "homepage": "https://wiki.adom.inc/adom/adom-browser-extension",
bridge/package.json+1−1@@ -1,6 +1,6 @@ { "name": "adom-native-browser-bridge",- "version": "0.1.49",+ "version": "0.1.50", "private": true, "description": "adom-desktop bridge for the Adom native-browser extension (Chrome + Edge)", "main": "server.js",
bridge/server.js+29−2@@ -1347,6 +1347,31 @@ function dispatch(verb, args) { } const extensionStale = staleProfiles.length > 0; const staleDetail = staleProfiles.map((k) => `${k} has ${extensionVersions[k]}`).join(', ');+ // ON-DISK SKEW (Drew Owens, wiki #305, 2026-07-24): the version check above only sees CONNECTED+ // profiles - but a skew OLD ENOUGH to break the native-messaging handshake means the extension+ // NEVER connects, so a connection-based check can never fire (the host just exits: Chrome shows+ // "Native host has exited"). So ALSO read each known profile's extension version straight off disk+ // (Secure Preferences) and flag one that is installed-but-older-than-expected yet NOT connected.+ // This converts the mystery into a one-call, actionable answer.+ // Surface the host's own last exit reason if it wrote one (host-side, wiki #305). Chrome only shows+ // the user "Native host has exited"; the host now records WHY next to the discovery file.+ let hostExit = null;+ try { const he = JSON.parse(fs.readFileSync(path.join(os.homedir(), '.adom', 'bridges', 'native-browser', 'host-exit-reason.json'), 'utf8')); if (he && he.reason && (Date.now() - Date.parse(he.at || 0)) < 10 * 60 * 1000) hostExit = he; } catch {}+ const connectedKeys = new Set(live.map(([k]) => k));+ const staleOnDisk = [];+ if (EXPECTED_EXT) {+ for (const kp of knownProfiles()) {+ if (connectedKeys.has(kp.key)) continue; // connected path already handled above+ let st; try { st = adomExtStateForProfile(kp.browser, kp.profileDir); } catch { continue; }+ if (st && st.installed && st.version && cmpVer(st.version, EXPECTED_EXT) < 0) {+ staleOnDisk.push({ profile: kp.key, installed: st.version, expected: EXPECTED_EXT });+ }+ }+ }+ const hostHint = hostExit ? ` The native host last exited because: "${hostExit.reason}" (host v${hostExit.hostVersion || '?'}, ${hostExit.at}).` : '';+ const skewHint = staleOnDisk.length+ ? ` ⛔ VERSION SKEW (likely the cause of "Native host has exited"): ${staleOnDisk.map((x) => `${x.profile} has extension ${x.installed}`).join(', ')}, but this bridge (${VERSION}) expects extension ${EXPECTED_EXT}. An extension/host that old cannot complete the native-messaging handshake, so the host exits with no message and the SW never connects. FIX: update the extension in that profile to ${EXPECTED_EXT} (re-sync the extension/ + host/ folders per installing-the-extension), then nbrowser_dev_reload {"profile":"<profile>"}. Note: the extension line (${EXPECTED_EXT}) and the skills-package line (a 0.4.x number on the wiki page) are DIFFERENT version streams - match the extension against expectedExtensionVersion, not the pkg version.`+ : ''; return resolve({ success: true, output: JSON.stringify({ ready, state: ready ? 'ready' : 'needs-extension',@@ -1357,14 +1382,16 @@ function dispatch(verb, args) { expectedExtensionVersion: EXPECTED_EXT, // what THIS bridge release ships against (null = not checked) extensionStale, // true if any connected profile runs a KNOWN older build than expected staleProfiles, // the profile keys that are behind (pass one to nbrowser_dev_reload)+ staleOnDisk, // installed-but-old extensions that are TOO old to even connect (the "native host has exited" cause)+ hostExit, // the native host's own last exit reason, if it recorded one (wiki #305) unidentifiedProfiles, // connected but pre-hello (version unknown yet); resolves on next hello chromeInstalled: !!chExe, // is native Google Chrome present on the box? chromeInstallPhase: chExe ? 'ready' : chromeState.phase, // idle|installing|awaiting_uac|ready|failed _hint: extensionStale- ? `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.`+ ? `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 ? `The Adom 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.`- : `The Adom 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). '}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.`,+ : `The Adom 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). '}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, }) }); } // nbrowser_install_chrome - install native Google Chrome for the user (Windows). Non-blocking:
host/native-host.js+16−6@@ -51,6 +51,19 @@ function isLiveHost(pid) { function profileSlug(key) { return String(key || 'default').replace(/[^a-z0-9._@-]+/gi, '_').slice(0, 64) || 'default'; }+// The host talks to Chrome over a stdio pipe, so it can never show the user a message - Chrome only ever+// surfaces "Native host has exited." So on ANY deliberate exit we record WHY to a small file the bridge+// reads (nbrowser_readiness surfaces it), turning a silent code-0 death into a diagnosable reason.+// (Drew Owens, wiki #305, 2026-07-24.)+function exitWithReason(reason, code) {+ try {+ let ver = null; try { ver = require('./package.json').version; } catch {}+ fs.mkdirSync(BRIDGE_DIR, { recursive: true });+ fs.writeFileSync(path.join(BRIDGE_DIR, 'host-exit-reason.json'), JSON.stringify({ reason, hostVersion: ver, pid: process.pid, at: new Date().toISOString() }));+ } catch {}+ try { process.stderr.write(`[native-host] exiting: ${reason}\n`); } catch {}+ process.exit(code || 0);+} let singletonDone = false; function acquireSingleton(profileKey) { if (singletonDone) return; singletonDone = true;@@ -59,8 +72,7 @@ function acquireSingleton(profileKey) { try { const existing = parseInt(String(fs.readFileSync(PIDFILE, 'utf8')).trim(), 10); if (isLiveHost(existing)) {- process.stderr.write(`[native-host] another host (pid ${existing}) already drives profile ${profileSlug(profileKey)} — exiting (singleton)\n`);- process.exit(0);+ exitWithReason(`another host (pid ${existing}) already holds the singleton for profile ${profileSlug(profileKey)}`); } } catch {} // no/garbage pidfile -> we take ownership try { fs.writeFileSync(PIDFILE, String(process.pid)); } catch {}@@ -307,12 +319,10 @@ function readHostPort() { function scheduleReconnect() { if (!everConnected) { if (attempts >= MAX_ATTEMPTS) {- process.stderr.write(`[native-host] bridge unreachable after ${attempts} attempts — exiting\n`);- process.exit(0);+ exitWithReason(`bridge unreachable after ${attempts} attempts (possible version skew: this host cannot complete the handshake with the running bridge)`); } if (Date.now() - startedAt > MAX_UNREACHABLE_MS) {- process.stderr.write(`[native-host] bridge unreachable for >${MAX_UNREACHABLE_MS}ms — exiting\n`);- process.exit(0);+ exitWithReason(`bridge unreachable for >${MAX_UNREACHABLE_MS}ms (possible version skew: this host cannot complete the handshake with the running bridge)`); } } const jitter = Math.floor(Math.random() * 250);
native-browser-bridge-manifest.json+5−5@@ -1,8 +1,8 @@ { "manifest_version": 1,- "version": "0.1.49",- "url": "https://wiki.adom.inc/download/adom/adom-browser-extension/0.1.49/native-browser-bridge-v0.1.49.zip",- "sha256": "3d9b4b0768ebe249f81460d94f3bc44289b1baa88dd96bc9f73d4db96cad4135",- "size": 95934,- "released_at": "2026-07-22T00:00:00Z"+ "version": "0.1.50",+ "url": "https://wiki.adom.inc/download/adom/adom-browser-extension/0.1.50/native-browser-bridge-v0.1.50.zip",+ "sha256": "6d7144fe1911f57cd40a0ea8102ee6e451dece5559f5b88d43d79e7ecaf474f8",+ "size": 97050,+ "released_at": "2026-07-24T00:00:00Z" }