← Commit history

v0.1.7: nbrowser_activity - bounded (600 entries/1h) queryable who-drove-what-when trail + activeSessions summary

John Lauer ·f9585c0a62 ·4mo ago ·parent dba7c51
5 files changed +48−9
bridge/BRIDGE_VERSION+1−1
@@ -1 +1 @@-0.1.6+0.1.7
bridge/bridge.json+2−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "native-browser",   "displayName": "Adom Native Browser (Chrome + Edge)",-  "version": "0.1.6",+  "version": "0.1.7",   "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",@@ -26,6 +26,7 @@     "nbrowser_chrome_status",     "nbrowser_install_chrome",     "nbrowser_ping",+    "nbrowser_activity",     "nbrowser_open_window",     "nbrowser_close_window",     "nbrowser_switch_window",
bridge/package.json+1−1
@@ -1,6 +1,6 @@ {   "name": "adom-native-browser-bridge",-  "version": "0.1.6",+  "version": "0.1.7",   "private": true,   "description": "adom-desktop bridge for the Adom native-browser extension (Chrome + Edge)",   "main": "server.js",
bridge/server.js+40−2
@@ -412,6 +412,21 @@ let lastHwnd = 0;                  // most-recently driven window const sessionHwnd = new Map();     // sessionId -> hwnd (opened windows) const spawnHwnd = new Map();       // chrome windowId -> hwnd (adopted SPAWNED windows: compose/popups) const sessionProfile = new Map();  // sessionId -> profile KEY (pin: verbs on this session route here by default)+const sessionThread = new Map();   // sessionId -> thread label (from registration) — for the activity log+const sessionPurpose = new Map();  // sessionId -> purpose (from registration)++// ── ACTIVITY LOG — a bounded, queryable "who drove what, when" trail so we can answer questions like+// "why did that profile get badged?" without guessing. SIZE- AND TIME-capped (ring + 1h TTL) so it can+// NEVER bloat, consistent with the extension's capped event buffers. In-memory (resets on bridge restart).+const ACTIVITY_LOG = [];+const ACT_LOG_MAX = 600;+const ACT_LOG_TTL_MS = 60 * 60 * 1000; // keep ~1 hour+function logActivity(e) {+  e.ts = Date.now();+  ACTIVITY_LOG.push(e);+  const cutoff = e.ts - ACT_LOG_TTL_MS;+  while (ACTIVITY_LOG.length > ACT_LOG_MAX || (ACTIVITY_LOG.length && ACTIVITY_LOG[0].ts < cutoff)) ACTIVITY_LOG.shift();+} let flashMode = 'quiet';           // quiet | chatty | off const COMELOOK = new Set(['nbrowser_navigate', 'nbrowser_screenshot', 'nbrowser_screenshot_full_res', 'nbrowser_download']); const ACTIVITY = new Set([...COMELOOK, 'nbrowser_reload', 'nbrowser_click', 'nbrowser_double_click', 'nbrowser_right_click', 'nbrowser_type', 'nbrowser_press_key', 'nbrowser_input_dispatch', 'nbrowser_hover', 'nbrowser_eval', 'nbrowser_evaluate', 'nbrowser_open_tab']);@@ -823,6 +838,7 @@ const VERB_META = {   nbrowser_install_chrome: { hint: 'Install NATIVE Google Chrome for the user (Windows). Downloads Google\'s official installer + runs a QUIET user-scoped install (no admin on a normal PC). NON-BLOCKING - returns immediately; poll nbrowser_chrome_status. On a locked-down box it escalates to a notify-first UAC (fires a sticky "Approve now" toast, waits for the tap, then raises the prompt). This is step 1 of getting the AI onto the user\'s REAL browser - after Chrome is in, help them sign into Google + load the extension.', related: ['nbrowser_chrome_status', 'nbrowser_readiness', 'installing-the-extension'], pitfalls: ['non-blocking: do NOT expect Chrome to exist on return - POLL nbrowser_chrome_status until phase:"ready"', 'awaiting_uac means a prompt is on the user\'s desktop - relay it; do not re-trigger install', 'Windows only for now', 'this only INSTALLS Chrome; the user still signs into Google themselves (their credential) - guide them via setting-up-native-chrome'] },   nbrowser_chrome_status: { hint: 'READ-ONLY: is native Google Chrome installed, and how is an in-flight install going? Returns {installed, executablePath, phase, installing, awaitingUac}. phase: idle|installing|awaiting_uac|ready|failed. Poll this after nbrowser_install_chrome. Never installs or spawns.', related: ['nbrowser_install_chrome', 'nbrowser_readiness'], pitfalls: ['awaiting_uac + notifyChannel:true = a toast is on the desktop, keep polling; notifyChannel:false = relay the UAC to the user yourself', 'installed:true means Chrome.exe is present - next help the user sign into Google + load the extension'] },   nbrowser_ping: { hint: 'Round-trip liveness through to the extension.', related: ['nbrowser_status'], pitfalls: ['a pong proves the extension is reachable, NOT that any profile has a drivable window — use nbrowser_status/readiness for that'] },+  nbrowser_activity: { hint: 'Bounded "who drove what, when" activity trail. Query recent verbs + a per-session summary (activeSessions, each with lastAgoMs) to debug things like "why is this profile badged" or "what is running right now". Filter with {sessionId, thread, profile, verb, errorsOnly, sinceMs, limit}.', related: ['nbrowser_events', 'nbrowser_owned', 'nbrowser_status'], pitfalls: ['in-memory + capped (max 600 entries / ~1h) so it RESETS on a bridge restart and can never bloat', 'a session with a large lastAgoMs is idle - its "Adom is driving" badge idle-expires after ~3 min'] },   nbrowser_open_window: { hint: 'Open a NEW background window YOU own and get a sessionId; do ALL your work through that sessionId. REGISTER it: pass sessionId + thread (which AI thread you are) + purpose (human-readable, why) or it is refused (session_unregistered). ' + NO_FG, related: ['nbrowser_navigate', 'nbrowser_screenshot', 'nbrowser_owned', 'nbrowser_close_window'], next: 'nbrowser_navigate', pitfalls: ['REQUIRES sessionId + thread + purpose (registration) — a bare open is refused session_unregistered', 'opens UNFOCUSED in the BACKGROUND - a bare focused:true is IGNORED; only userRequestedForeground:true raises it', 'NEVER fall back to an OS-level launch (Start-Process/msedge/chrome --new-window) - that RAISES it to the foreground; this verb is the polite path and always works in the background', 'always drive a window you opened, never the user\'s existing ones — and close everything you own at task end (nbrowser_owned → nbrowser_close_window)'] },   nbrowser_close_window: { hint: 'Close a window you opened. Pass includeSpawned:true to also close everything that window\'s pages SPAWNED (composes/popups). Clean up your own sessions when done.', related: ['nbrowser_owned', 'nbrowser_cleanup', 'nbrowser_list_windows'], pitfalls: ['⛔ only close a sessionId YOU opened - closing the user\'s window can wipe their live work and other AI threads', 'if the session spawned windows (a page opened a compose/popup), they stay open unless you pass includeSpawned:true or call nbrowser_cleanup'] },   nbrowser_owned: { hint: 'List everything a session owns: opened windows + windows/tabs a page it drove SPAWNED (compose, target=_blank, OAuth popup). Calling it ACKNOWLEDGES the spawns (clears an unacknowledged_spawns block). Use it before ending a task to see what to clean up.', related: ['nbrowser_cleanup', 'nbrowser_close_window', 'nbrowser_list_windows'], next: 'nbrowser_cleanup', pitfalls: ['spawned windows belong to no session until adopted — this is how you find leaked ones (e.g. a Gmail Contacts compose)', 'after seeing spawns, KEEP the ones you need or nbrowser_cleanup the litter'] },@@ -914,6 +930,22 @@ function dispatch(verb, args) {     if (verb === 'nbrowser_help') {       return resolve({ success: true, output: JSON.stringify({ ok: true, bridge: BRIDGE, version: VERSION, extConnected: anyConnected(), flashMode, ...HELP }) });     }+    if (verb === 'nbrowser_activity') {+      // Bounded, queryable "who drove what, when" trail — to debug things like a stray badge.+      const now = Date.now();+      let rows = ACTIVITY_LOG.slice();+      if (args && args.sessionId) rows = rows.filter((r) => r.sessionId === args.sessionId);+      if (args && args.profile) rows = rows.filter((r) => (r.profile || '').includes(String(args.profile)));+      if (args && args.thread) rows = rows.filter((r) => r.thread === args.thread);+      if (args && args.verb) rows = rows.filter((r) => r.verb === args.verb || r.verb === 'nbrowser_' + args.verb);+      if (args && args.errorsOnly) rows = rows.filter((r) => r.ok === false);+      if (args && args.sinceMs) rows = rows.filter((r) => r.ts >= now - Number(args.sinceMs));+      const limit = Math.min(Number(args && args.limit) || 100, 600);+      const entries = rows.slice(-limit).map((r) => ({ agoMs: now - r.ts, verb: r.verb, sessionId: r.sessionId, thread: r.thread, profile: r.profile, ok: r.ok, errorCode: r.errorCode || undefined, error: r.error || undefined }));+      const bySession = {};+      for (const r of rows) { if (!r.sessionId) continue; const s = bySession[r.sessionId] || (bySession[r.sessionId] = { sessionId: r.sessionId, thread: sessionThread.get(r.sessionId) || r.thread, purpose: sessionPurpose.get(r.sessionId) || null, profile: r.profile, count: 0, lastAgoMs: Infinity }); s.count++; s.lastAgoMs = Math.min(s.lastAgoMs, now - r.ts); }+      return resolve({ success: true, output: JSON.stringify({ ok: true, count: entries.length, totalLogged: ACTIVITY_LOG.length, windowMs: ACT_LOG_TTL_MS, activeSessions: Object.values(bySession).sort((a, b) => a.lastAgoMs - b.lastAgoMs), entries, _hint: 'Bounded activity trail (max 600 entries / ~1h, in-memory, resets on bridge restart). Filter: {sessionId, thread, profile, verb, errorsOnly:true, sinceMs, limit}. activeSessions = who drove what + how long ago (lastAgoMs); a large lastAgoMs means that session is idle (its badge should have faded).' }) });+    }     // nbrowser_readiness - READ-ONLY cold-start probe (SDK Cold-start tiers). Never spawns work, never     // installs. This bridge has no heavy runtime (tiny Node), so "readiness" == the Level-2 extension     // being connected. AD's own bridge_readiness aggregates across bridges; this reports THIS one.@@ -989,6 +1021,7 @@ function dispatch(verb, args) {       };       const verbs = [         d('nbrowser_status', 'Is the extension connected? Reports ext id/version + active browser.', {}, { ok: 'bool', ext: 'str', version: 'str', engine: 'str', browser: '"chrome"|"edge"' }, 15),+        d('nbrowser_activity', 'Bounded "who drove what, when" activity trail (max 600 entries / ~1h, in-memory, resets on bridge restart). Returns recent verb entries + a per-session summary. Debug a stray badge or see what is running.', { sessionId: 'optional str', thread: 'optional str', profile: 'optional str', verb: 'optional str', errorsOnly: 'optional bool', sinceMs: 'optional int', limit: 'optional int (<=600)' }, { ok: 'bool', entries: '[{agoMs,verb,sessionId,thread,profile,ok,errorCode}]', activeSessions: '[{sessionId,thread,purpose,count,lastAgoMs}]' }, 15, { example: {} }),         d('nbrowser_readiness', 'READ-ONLY cold-start probe: is the native-browser extension connected + ready to drive? Never spawns work or installs. Use for "is it ready / do I have the browser extension" instead of nbrowser_status. Also flags a STALE extension (loaded build older than this bridge expects) so you fix it up-front instead of hitting "verb not implemented yet" mid-task.', {}, { ready: 'bool', state: '"ready"|"needs-extension"', installing: 'bool', extensionConnected: 'bool', profiles: '[str]', extensionVersions: '{profile:version}', expectedExtensionVersion: 'str|null', extensionStale: 'bool', staleProfiles: '[str]', chromeInstalled: 'bool', chromeInstallPhase: 'str' }, 15, { example: {} }),         d('nbrowser_install_chrome', 'Install NATIVE Google Chrome for the user (Windows). Downloads Google\'s official installer + runs a quiet user-scoped install (no admin on a normal PC); escalates to a notify-first UAC on a locked-down box. NON-BLOCKING - poll nbrowser_chrome_status. Step 1 of putting the AI onto the user\'s real signed-in browser.', { elevate: 'optional "auto"(default) | true (skip quiet, straight to UAC) | false (quiet only, no prompt)' }, { ok: 'bool', installing: 'bool', installed: 'bool?', alreadyInstalled: 'bool?', executablePath: 'str?', statusVerb: 'str' }, 30, { statusVerb: 'nbrowser_chrome_status', example: {} }),         d('nbrowser_chrome_status', 'READ-ONLY: is native Google Chrome installed / how is an install going? Poll after nbrowser_install_chrome.', {}, { installed: 'bool', executablePath: 'str?', phase: '"idle"|"installing"|"awaiting_uac"|"ready"|"failed"', installing: 'bool', awaitingUac: 'bool', errorCode: 'str?' }, 15, { example: {} }),@@ -1271,7 +1304,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 (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); }     // 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);@@ -1361,8 +1394,13 @@ async function onHostFrame(frame) {       }       maybeAutoFlash(p.verb, payload);       if (payload && payload.sessionId && ACTIVITY.has(p.verb)) touchBadge(payload.sessionId, p.profileKey).catch(() => {}); // badge = ACTIVE driving; lazily correlates, idle-expires when commands stop+      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 });       resolve_ok(p.resolve, payload);-    } else p.resolve({ success: false, error: frame.error || 'unknown error', _hint: frame._hint });+    } 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;   }   // Unsolicited events are surfaced to the AI via nbrowser_events (the extension buffers them).
native-browser-bridge-manifest.json+4−4
@@ -1,8 +1,8 @@ {   "manifest_version": 1,-  "version": "0.1.6",-  "url": "https://wiki.adom.inc/download/adom/adom-browser-extension/0.1.6/native-browser-bridge-v0.1.6.bin",-  "sha256": "0d8f72596f87f42c9d8b42797d6c0fc07838c3722bafe17d0b227c24f8a3f105",-  "size": 55297,+  "version": "0.1.7",+  "url": "https://wiki.adom.inc/download/adom/adom-browser-extension/0.1.7/native-browser-bridge-v0.1.7.bin",+  "sha256": "807e82f9886b4168b99b408573c8567077059de367ac0a2cab8d815609247954",+  "size": 56780,   "released_at": "2026-07-14T00:00:00Z" }\ No newline at end of file