aboutsummaryrefslogtreecommitdiff
path: root/src/daemon/index.ts
diff options
context:
space:
mode:
authorAman Kumar <145339995+amankumarsingh77@users.noreply.github.com>2026-06-21 00:46:44 +0530
committerGitHub <noreply@github.com>2026-06-21 00:46:44 +0530
commitda5d31af999b728c31ab0169373531cab4543f0c (patch)
tree5d350ad1428ff332372a9c724ac46c964d8ca477 /src/daemon/index.ts
parent8db684d43366d335760e106eae39a74376bd7640 (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.ts114
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);
+});