← Commit history

v1.9.6: persistent circuit breaker kills the ~60s crash-poison respawn loop in recoverSessions; jump-list task icon; dev-skill crash-loop troubleshooting

John Lauer ·e58fd0bce9 · ·parent 90fe748
6 files changed +66−9
CHANGELOG.md+13
@@ -1,5 +1,18 @@ # Changelog — Adom Desktop Puppeteer Bridge +## 1.9.6 — 2026-07-19 — Kill the ~60s respawn loop (crash-poison recovery) + jump-list task icon++Two fixes. (1) THE BIG ONE: pup was respawning every ~60s for hours (AD's lifecycle log showed+spawn-with-no-reap = pup self-crashing, not AD killing it). Root cause: recoverSessions() reconnects+to Chromes from ~/.adom/pup-sessions/*.json on boot; a NATIVE crash during recovery (puppeteer.connect+/ a CDP call) kills the process before the per-session try/catch can delete the file, so the same+poison file re-crashes every boot forever (in-process try/catch and the uncaughtException handler+can't stop a native crash). Fix: a persistent circuit breaker — a recover-attempt marker is written+BEFORE the risky ops; a file that still carries an incomplete attempt next boot is quarantined+(deleted), a clean recovery clears it. One strike quarantines, so any poison file self-heals in a+single crash. (2) The taskbar jump-list toggle now carries the Adom Pup icon (iconPath) instead of+Windows' generic blank-document glyph.+ ## 1.9.5 — 2026-07-19 — Name the Adom wiki in the jump-list toggle (was too generic)  The taskbar toggle read "🔓 Switch to logged-in view" — generic enough to look like it'd apply to
adom-bridge-puppeteer-manifest.json+5−5
@@ -1,10 +1,10 @@ {   "manifest_version": 1,   "name": "puppeteer",-  "version": "1.9.5",-  "url": "https://wiki.adom.inc/download/adom/adom-desktop-puppeteer-bridge/1.9.5/adom-bridge-puppeteer-v1.9.5.zip",-  "sha256": "a88640992e9ae481ccbd4f1f20f8aa78ce3f466fd00d76d36e5aa344fdd0d358",-  "size": 213588,+  "version": "1.9.6",+  "url": "https://wiki.adom.inc/download/adom/adom-desktop-puppeteer-bridge/1.9.6/adom-bridge-puppeteer-v1.9.6.zip",+  "sha256": "da36501f788e968a7d4a617c6f871927cc8d3c4b5200b7b2f04f84f4b3016566",+  "size": 214354,   "verbPrefixes": [     "browser_",     "desktop_recorder_",@@ -12,5 +12,5 @@   ],   "healthEndpoint": "/status",   "statusVerb": "browser_readiness",-  "released_at": "2026-07-19T20:20:00Z"+  "released_at": "2026-07-19T20:45:00Z" }
dev-skills/pup-bridge-dev/SKILL.md+18
@@ -666,6 +666,24 @@ downloads-and-restarts in one step for the UPDATE path; `restart_bridge` is the **Was it AD's fault?** No — AD's reap-on-restart is normal and correct. The wedge was pup's missing launch dedup (#1); AD's 30s relay timeout + the dim LED just made a pup bug VISIBLE. Own it. +**4. The ~60s respawn LOOP (crash-poison session file, fixed v1.9.6 — the deeper root cause).** The+AD build thread read the lifecycle log (`adom-desktop bridge_log_read {name:"puppeteer"}`) and saw+pup being **spawned every ~60s with no reap** — i.e. the process was dying on its OWN and AD's+supervisor kept respawning it, even while idle. Cause: `recoverSessions()` runs on every boot and+reconnects to Chromes named in `~/.adom/pup-sessions/*.json`. If a recovery op crashes pup+**natively** (inside `puppeteer.connect` / a CDP call / a native module), the process dies HARD+BEFORE the per-session `try/catch` can delete that file — so the SAME poison file re-crashes the+next boot, forever. **In-process try/catch cannot stop a native crash**, and pup's+`uncaughtException`/`unhandledRejection` handlers only catch JS throws, so neither helps here. Fix:+a PERSISTENT circuit breaker — `recoverSessions` writes `recoverAttempts:1` into the file BEFORE+the risky ops; a file that still carries an INCOMPLETE attempt on the next boot is QUARANTINED+(deleted, not retried); a clean recovery rewrites the file without the marker; a graceful failure+deletes it. One strike quarantines, so any poison file self-heals in a single crash instead of an+unbounded loop. **The diagnostic that nails this class:** `bridge_log_read` → spawn-with-no-reap+cadence = pup self-crashing, NOT AD killing it (AD added that lifecycle log v1.9.93 for exactly+this). If you see it, suspect a startup/recovery path crashing on poison on-disk state; clearing+`~/.adom/pup-sessions/*` is the manual stop-gap while the breaker ships.+ ## The foreground gate (v1.8.70) — abe-parity dual key, and the AD interception hole  Foregrounding (open `foreground:true`, `browser_raise_os_window`, `browser_focus_window`) requires a
src/BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.9.5\ No newline at end of file+1.9.6\ No newline at end of file
src/bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "puppeteer",   "displayName": "Puppeteer (Chrome for Testing + Native)",-  "version": "1.9.5",+  "version": "1.9.6",   "description": "Drives a browser ALREADY on the machine \u2014 installed Chrome, else Microsoft Edge (every Windows PC ships with Edge) \u2014 launched with a FRESH isolated profile, so the common case has NO ~150 MB Chrome-for-Testing download and it never touches the user's real tabs/logins. Binds its local server to LOOPBACK (127.0.0.1) only, so no Windows Firewall 'allow Node through the firewall' prompt ever pops. Chrome for Testing is fetched only as a LAST RESORT if the machine has no Chromium browser at all. SPAWN-VERIFIED fallback: pup tries each detected browser (Chrome \u2192 Edge \u2192 CfT) and actually launches it, falling through to the next if one won't start (and handling browsers like Edge that relaunch onto another PID), so a corrupt/partial install never hard-fails an open. The browser that launches is cached as the default (persisted across restarts) so it isn't re-probed each time. On spawn the bridge SELF-WARMS: detects browsers + disk, and only if the box has no Chrome/Edge and no cached CfT does it background-fetch CfT so the first open is instant. browser_use pins a specific browser (chrome | edge | cft | auto); {browser:'chrome',install:true} downloads & installs REAL Google Chrome (user-scoped, no admin) on an Edge-only box. browser_readiness (READ-ONLY) reports which browser will be used + the full candidate list + the default + free disk. Every failure returns a specific errorCode + _hint (chrome_install_no_disk, chrome_download_network, chrome_install_permission, chrome_stable_not_detected, \u2026). Launches with FULL browser permissions by default for full debug (strictPermissions:true opts out). Headful automation: open/close windows+tabs, navigate, screenshot, eval JS, record. pup is a GENERIC, non-profile browser only \u2014 every session is a fresh isolated Chrome/Edge process, never the user's real signed-in browser; profile defaults to sessionId. To drive the user's REAL Chrome/Edge (their profile, cookies, SSO), use the Adom browser EXTENSION's nbrowser_* verbs \u2014 a separate bridge, not pup's job.",   "homepage": "https://wiki.adom.inc/adom/adom-desktop",   "author": "Adom Inc.",
src/server.js+28−2
@@ -1071,6 +1071,26 @@ async function recoverSessions() {     // Skip if already in memory     if (sessions.has(sessionId)) continue; +    // v1.9.6: PERSISTENT CRASH CIRCUIT BREAKER. A NATIVE crash during recovery (inside+    // puppeteer.connect / a CDP call / a native module) kills the process HARD — before the+    // per-session try/catch below can delete the file — so the SAME poison session file+    // re-crashes pup on EVERY boot: the ~60s respawn loop John hit 2026-07-19 (AD's lifecycle log+    // showed spawn-with-no-reap every minute; in-process try/catch can't stop a native crash).+    // Fix: persist a recover-attempt marker BEFORE the risky ops. A file that already has an+    // INCOMPLETE recovery attempt (crashed us last boot) is QUARANTINED — deleted, not retried.+    // A recovery that FAILS gracefully deletes its own file (catch below), and a recovery that+    // SUCCEEDS clears the marker (saveSessionFile rewrites without it) — so a leftover marker+    // means "this file crashed recovery," and one strike quarantines it. Bounds any poison file+    // to a single crash instead of an unbounded loop.+    if (Number(sf.recoverAttempts || 0) >= 1) {+      console.error(`Session "${sessionId}" left an INCOMPLETE recovery attempt last boot (likely crashed pup) — QUARANTINING it (deleting the poison file) instead of retrying`);+      deleteSessionFile(sessionId);+      continue;+    }+    try {+      fs.writeFileSync(sessionFilePath(sessionId), JSON.stringify({ ...sf, recoverAttempts: 1 }, null, 2));+    } catch (e) { /* if we can't mark it, still attempt — but the breaker only protects when the mark persists */ }+     // Check if Chrome is still alive     if (!isProcessAlive(pid)) {       console.log(`Session "${sessionId}" (PID ${pid}) is dead — removing stale file`);@@ -1152,6 +1172,9 @@ async function recoverSessions() {       if (be) be.refCount++;       if (!activeSessionId) activeSessionId = sessionId; +      // v1.9.6: recovered CLEANLY — rewrite the file WITHOUT the recover-attempt marker so the+      // circuit breaker doesn't quarantine a healthy session on the next boot.+      try { let u = null; try { u = page.url(); } catch (e) {} saveSessionFile(sessionId, profileName, u); } catch (e) {}       console.log(`Recovered session "${sessionId}" (profile: ${profileName}, URL: ${page.url()})`);     } catch (e) {       console.error(`Failed to recover session "${sessionId}": ${e.message}`);@@ -1762,11 +1785,14 @@ async function updateWikiJumplist(session, sessionId) {   if (session._wikiJumplistView === view) return; // menu already reflects this state   // v1.9.5: the title MUST name the Adom wiki (John: a bare "Switch to logged-in view" reads   // generic, as if it'd work on any site). Lead with "Adom wiki" so the menu is self-describing.+  // v1.9.6: give the task the Adom Pup ICON (iconPath) — without it Windows shows a generic+  // blank-document glyph (John: "why is your icon generic for that menu item?").+  const _taskIcon = pupIconPath();   let tasks = [];   if (view === 'authed') {-    tasks = [{ title: '\u{1F4D6} Adom wiki: switch to public view', description: 'Reload this Adom wiki page logged OUT — exactly what the public/world sees', args: `browser_wiki_set_view {"sessionId":"${sessionId}","view":"public"}` }];+    tasks = [{ title: '\u{1F4D6} Adom wiki: switch to public view', description: 'Reload this Adom wiki page logged OUT — exactly what the public/world sees', iconPath: _taskIcon, args: `browser_wiki_set_view {"sessionId":"${sessionId}","view":"public"}` }];   } else if (view === 'public') {-    tasks = [{ title: '\u{1F513} Adom wiki: switch to logged-in view', description: 'Reload this Adom wiki page SIGNED IN — private source, drafts, owner-only cards', args: `browser_wiki_set_view {"sessionId":"${sessionId}","view":"authed"}` }];+    tasks = [{ title: '\u{1F513} Adom wiki: switch to logged-in view', description: 'Reload this Adom wiki page SIGNED IN — private source, drafts, owner-only cards', iconPath: _taskIcon, args: `browser_wiki_set_view {"sessionId":"${sessionId}","view":"authed"}` }];   }   // The shell ignores a jump list on an unregistered AUMID — ensure it's registered first   // (idempotent; only registers when there IS a menu to show).