← Commit history
bridge/BRIDGE_VERSION+1−1
@@ -1 +1 @@-0.1.16\ No newline at end of file+0.1.17\ 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 (Chrome + Edge)",-  "version": "0.1.16",+  "version": "0.1.17",   "expectedExtensionVersion": "0.1.2",   "description": "Drives the user's REAL signed-in Chrome/Edge via an installed extension (native-messaging transport). Mirrors the puppeteer bridge's verb surface as nbrowser_* but runs inside the actual logged-in profile, so cookies/SSO/saved-logins/captcha human-trust all work. The bridge binds an ephemeral loopback port (PORTS.md compliant) and writes a discovery file; the native-messaging host pumps stdio <-> this bridge; the extension never touches a TCP port.",   "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.16",+  "version": "0.1.17",   "private": true,   "description": "adom-desktop bridge for the Adom native-browser extension (Chrome + Edge)",   "main": "server.js",
bridge/server.js+16−5
@@ -620,7 +620,10 @@ const SESS_FILE = path.join(ICON_DIR, 'sessions.json'); function persistSessions() {   try {     const out = {};-    for (const [sid, hwnd] of sessionHwnd) out[sid] = { hwnd, profile: sessionProfile.get(sid) || null, thread: sessionThread.get(sid) || null, purpose: sessionPurpose.get(sid) || null };+    // Persist every pinned session (profile routing matters across restarts even before an hwnd is+    // captured) plus every hwnd-mapped one.+    for (const [sid, profile] of sessionProfile) out[sid] = { hwnd: sessionHwnd.get(sid) || null, profile, thread: sessionThread.get(sid) || null, purpose: sessionPurpose.get(sid) || null };+    for (const [sid, hwnd] of sessionHwnd) if (!out[sid]) out[sid] = { hwnd, profile: sessionProfile.get(sid) || null, thread: sessionThread.get(sid) || null, purpose: sessionPurpose.get(sid) || null };     fs.writeFileSync(SESS_FILE, JSON.stringify(out));   } catch {} }@@ -628,8 +631,10 @@ function restoreSessions(liveHwnds) {   try {     const j = JSON.parse(fs.readFileSync(SESS_FILE, 'utf8'));     for (const [sid, s] of Object.entries(j)) {-      if (!s || !liveHwnds.has(s.hwnd)) continue; // window is gone - drop the stale entry-      if (!sessionHwnd.has(sid)) sessionHwnd.set(sid, s.hwnd);+      if (!s) continue;+      // hwnd only if that window still exists; profile pin/thread/purpose restore regardless (they+      // keep sid-keyed verbs routing to the RIGHT profile across our restart).+      if (s.hwnd && liveHwnds.has(s.hwnd) && !sessionHwnd.has(sid)) sessionHwnd.set(sid, s.hwnd);       if (s.profile && !sessionProfile.has(sid)) sessionProfile.set(sid, s.profile);       if (s.thread && !sessionThread.has(sid)) sessionThread.set(sid, s.thread);       if (s.purpose && !sessionPurpose.has(sid)) sessionPurpose.set(sid, s.purpose);@@ -686,7 +691,13 @@ function touchBadge(sid, profileKey) {   // sid->hwnd map is persisted + restored at startup, and profile-keyed drives re-correlate via the   // profile's OWN extension (touchProfileBadge -> badgeSpawnsIn), which cannot hit a PWA.   const h = sessionHwnd.get(sid);-  if (!h) return;+  if (!h) {+    // Self-heal: no hwnd for an ACTIVELY-DRIVEN session (e.g. it opened before a bridge restart that+    // predates sessions.json). Ask the profile's own extension for its windows (throttled) and repair.+    const pk = profileKey || sessionProfile.get(sid);+    if (pk) { const now = Date.now(); if ((_profCorrAt.get(pk) || 0) + 30000 <= now) { _profCorrAt.set(pk, now); extQuery(pk, 'nbrowser_list_windows', {}).then((r) => { let pl = r; if (r && typeof r.output === 'string') { try { pl = JSON.parse(r.output); } catch { pl = null; } } if (pl) badgeSpawnsIn(pl, pk).catch(() => {}); }); } }+    return;+  }   if (!badgeOn.has(sid)) applyBadge(sid, h, profileKey); else armBadgeIdle(sid); } function clearBadge(sid) { const t = badgeIdle.get(sid); if (t) clearTimeout(t); badgeIdle.delete(sid); badgeOn.delete(sid); }@@ -1536,7 +1547,7 @@ function dispatch(verb, args) {     };     // PIN the session to whatever profile this window opened in, so every later verb carrying this     // sessionId auto-routes back to the SAME profile (no cross-profile mistakes) even without `profile:`.-    if (verb === 'nbrowser_open_window' && args && args.sessionId) { sessionProfile.set(args.sessionId, tgt.key); if (args.thread) sessionThread.set(args.sessionId, args.thread); if (args.purpose) sessionPurpose.set(args.sessionId, args.purpose); }+    if (verb === 'nbrowser_open_window' && args && args.sessionId) { sessionProfile.set(args.sessionId, tgt.key); if (args.thread) sessionThread.set(args.sessionId, args.thread); if (args.purpose) sessionPurpose.set(args.sessionId, args.purpose); persistSessions(); }     // For open_window, snapshot the Chrome window list BEFORE forwarding so the post-open diff reliably     // catches the new window (the old fire-and-forget snapshot raced the window appearing).     if (verb === 'nbrowser_open_window') chromeHwnds().then((s) => { preOpenHwnds = s; forward(); }).catch(forward);
native-browser-bridge-manifest.json+4−4
@@ -1,8 +1,8 @@ {   "manifest_version": 1,-  "version": "0.1.16",-  "url": "https://wiki.adom.inc/download/adom/adom-browser-extension/0.1.16/native-browser-bridge-v0.1.16.bin",-  "sha256": "07c4460b391a11b16c8b8ff0e95b69ca95242b5b5cfe2a8915366a9374659737",-  "size": 70617,+  "version": "0.1.17",+  "url": "https://wiki.adom.inc/download/adom/adom-browser-extension/0.1.17/native-browser-bridge-v0.1.17.bin",+  "sha256": "6faae072cef04f7042669f1f9f99c72d70ae0768da5c31bdba122ad2c1999059",+  "size": 70919,   "released_at": "2026-07-15T00:00:00Z" }