diff options
| author | Aman Kumar <145339995+amankumarsingh77@users.noreply.github.com> | 2026-06-21 00:46:44 +0530 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-06-21 00:46:44 +0530 |
| commit | da5d31af999b728c31ab0169373531cab4543f0c (patch) | |
| tree | 5d350ad1428ff332372a9c724ac46c964d8ca477 /src/daemon/index.ts | |
| parent | 8db684d43366d335760e106eae39a74376bd7640 (diff) | |
release: v0.6.0 — per-tab isolation, Unix socket daemon, on-demand init (#6)
* feat: Unix socket daemon with auto-reconnect for persistent Chrome CDP
Eliminates the 'Allow Remote Debugging' Chrome prompt that appeared on
every pi session by multiplexing pi instances through a single persistent
daemon process.
Key additions:
- Unix socket IPC (net.createServer + readline JSON framing)
- CdpBridge with ID multiplexer + event router
- DaemonTransport implementing CdpTransport interface
- Auto-spawn with ensureDaemon() (lazy, checks socket)
Fixes since initial implementation:
- Bridge auto-reconnects to Chrome with exponential backoff (1s-60s).
Previously the bridge tried once at startup and permanently failed
if Chrome wasn't ready — causing 'Chrome not connected' on every
browser tool call.
- Daemon spawn uses project-local tsx binary instead of global --import
flag, fixing silent spawn failure (ERR_MODULE_NOT_FOUND).
- Stale ctx crash: persistState in onOwnershipChange wrapped in
try/catch to prevent CDP event consumer crashes.
49 test assertions across 4 test files. Zero TypeScript errors.
* feat: on-demand browser initialization
Browser daemon no longer auto-starts on session_start. Instead:
- session_start creates the client lazily but does NOT connect or spawn
the daemon. Zero Chrome prompts for non-browser pi sessions.
- Browser tool wrapper checks for daemon socket before each call.
Socket missing → clear error: 'Run /browser-setup first'.
Socket exists → proceeds silently (daemon already running).
- /browser-setup is the explicit, one-time init point. It spawns
the daemon and connects to Chrome only when the user opts in.
- SKILL.md updated with Connection Setup instructions telling agents
to direct users to /browser-setup when socket is missing.
* fix: checkChromeRunning on macOS — match by substring not exact
ps -A -o comm= returns full executable paths on modern macOS
(e.g. /Applications/Google Chrome.app/Contents/MacOS/Google Chrome),
not the basename. The old exact-match check (browserProcesses.includes(l))
never matched, causing false 'No browser instance running' errors.
Changed to substring includes() with helper/renderer/crashpad filtering.
* feat: browser_setup as agent-callable tool
The agent can now self-recover from 'Browser harness not initialized'
errors by calling browser_setup directly — no user mediation needed.
- Extracted shared performSetup() from the slash command into setup.ts
- Created src/domains/setup.ts with ensureAlive:false to bypass the
socket guard (this tool IS the setup, can't require itself)
- Registered browser_setup in the tool registry
- Updated SKILL.md: agent calls browser_setup directly, not ask user
- Removed stale test/manual/spawn-test.ts
* release: v0.6.0 — per-tab isolation, Unix socket daemon, on-demand init
Diffstat (limited to 'src/daemon/index.ts')
| -rw-r--r-- | src/daemon/index.ts | 114 |
1 files changed, 114 insertions, 0 deletions
diff --git a/src/daemon/index.ts b/src/daemon/index.ts new file mode 100644 index 0000000..d2c43a8 --- /dev/null +++ b/src/daemon/index.ts @@ -0,0 +1,114 @@ +/** + * Browser daemon entry point — standalone long-lived process. + * + * Owns a single persistent WebSocket to Chrome. Exposes a Unix domain socket + * for pi instances to connect through. One Allow dialog ever (per Chrome + * lifetime), zero repeated prompts. + * + * Run: npx tsx src/daemon/index.ts + */ + +import { createIpcServer } from "./server"; +import { createCdpBridge, type SendToClient } from "./bridge"; +import { DAEMON_IDLE_TIMEOUT_MS } from "./protocol"; + +async function main() { + console.log("[pi-browser-daemon] Starting..."); + + const ipcServer = createIpcServer(); + const cdpBridge = createCdpBridge(); + + // ── Wire IpcServer → CdpBridge ───────────────────────────────────────── + + // When a pi client sends a CDP request, proxy it through the bridge to Chrome. + ipcServer.onMessage((msg, client) => { + if (msg.type !== "request") return; + + const send: SendToClient = (cid, resp) => { + ipcServer.send(cid, resp); + }; + + cdpBridge.handleRequest(msg, client.id, send); + }); + + // When Chrome sends an event, route it to the correct pi client(s). + cdpBridge.onEvent((event, targetClientIds) => { + for (const cid of targetClientIds) { + ipcServer.send(cid, event); + } + }); + + // When a client disconnects, clean up its CDP state. + ipcServer.onDisconnect((client) => { + cdpBridge.removeClient(client.id); + }); + + // When Chrome disconnects, notify all clients. + cdpBridge.onClose(() => { + console.log("[pi-browser-daemon] Chrome disconnected"); + ipcServer.broadcast({ + type: "control", + action: "shutdown", + reason: "chrome_disconnected", + }); + }); + + // ── Idle timeout ─────────────────────────────────────────────────────── + + // The daemon exits after DAEMON_IDLE_TIMEOUT_MS with zero connected clients. + // This prevents zombie processes when pi sessions end and never reconnect. + // The timeout resets whenever a client connects. + let idleTimer: ReturnType<typeof setTimeout> | null = null; + + const resetIdleTimer = (): void => { + if (idleTimer) clearTimeout(idleTimer); + if (ipcServer.clientCount() === 0) { + idleTimer = setTimeout(() => { + console.log("[pi-browser-daemon] Idle timeout — no clients for " + + `${DAEMON_IDLE_TIMEOUT_MS / 60000} minutes. Shutting down.`); + shutdown(); + }, DAEMON_IDLE_TIMEOUT_MS); + } + }; + + const cancelIdleTimer = (): void => { + if (idleTimer) { clearTimeout(idleTimer); idleTimer = null; } + }; + + ipcServer.onConnect(() => { cancelIdleTimer(); }); + ipcServer.onDisconnect(() => { resetIdleTimer(); }); + + // ── Start ────────────────────────────────────────────────────────────── + + try { + await ipcServer.start(); + console.log("[pi-browser-daemon] IPC server listening"); + } catch (e) { + console.error("[pi-browser-daemon] Failed to start IPC server:", e); + process.exit(1); + } + + // Connect to Chrome (auto-retries with backoff in the background). + // The daemon is ready immediately — Chrome may connect later. + await cdpBridge.start(); + + // Start the idle timer (no clients yet at startup). + resetIdleTimer(); + + // ── Graceful shutdown ────────────────────────────────────────────────── + + const shutdown = async () => { + console.log("[pi-browser-daemon] Shutting down..."); + await cdpBridge.stop(); + await ipcServer.stop(); + process.exit(0); + }; + + process.on("SIGINT", shutdown); + process.on("SIGTERM", shutdown); +} + +main().catch((e) => { + console.error("[pi-browser-daemon] Fatal:", e); + process.exit(1); +}); |
