aboutsummaryrefslogtreecommitdiff
path: root/src/daemon/spawn.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/spawn.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/spawn.ts')
-rw-r--r--src/daemon/spawn.ts85
1 files changed, 85 insertions, 0 deletions
diff --git a/src/daemon/spawn.ts b/src/daemon/spawn.ts
new file mode 100644
index 0000000..e9f4d51
--- /dev/null
+++ b/src/daemon/spawn.ts
@@ -0,0 +1,85 @@
+/**
+ * Daemon lifecycle helpers — spawn detection and lazy launch.
+ *
+ * The daemon is a standalone long-lived process that owns the single WebSocket
+ * to Chrome. pi instances connect to it via a Unix domain socket. This module
+ * provides the plumbing to detect a running daemon and spawn one if needed.
+ *
+ * Reference:
+ * - child_process.spawn detached: https://nodejs.org/docs/latest-v22.x/api/child_process.html#optionsdetached
+ */
+
+import { spawn, type ChildProcess } from "node:child_process";
+import { access } from "node:fs/promises";
+import { existsSync } from "node:fs";
+import { fileURLToPath } from "node:url";
+import { dirname, join } from "node:path";
+import { DAEMON_SOCKET_PATH } from "./protocol";
+
+const moduleDir = dirname(fileURLToPath(import.meta.url));
+
+/**
+ * Check whether the daemon is running by testing if its Unix socket exists.
+ * The socket file is created by the daemon on startup and removed on shutdown.
+ */
+export const isDaemonRunning = async (): Promise<boolean> => {
+ try {
+ await access(DAEMON_SOCKET_PATH);
+ return true;
+ } catch {
+ return false;
+ }
+};
+
+/**
+ * Spawn the daemon as a detached child process. The daemon will continue
+ * running after the parent pi process exits.
+ *
+ * The daemon entry point is resolved relative to this module. When running
+ * from source (tsx), the entry is src/daemon/index.ts. When running compiled,
+ * it's dist/daemon/index.js.
+ *
+ * Returns the child process handle (caller should call child.unref()).
+ */
+export const spawnDaemon = (): ChildProcess | null => {
+ // Resolve the daemon entry point and tsx binary.
+ const daemonScript = join(moduleDir, "index.ts");
+ const tsxBin = join(moduleDir, "..", "..", "node_modules", ".bin", "tsx");
+
+ // Use tsx to run the daemon TypeScript source directly.
+ // The spawed process inherits the parent's env so module resolution works.
+ // Fall back to `npx tsx` if the local binary isn't available.
+ const cmd = existsSync(tsxBin) ? tsxBin : "npx";
+ const args = existsSync(tsxBin) ? [daemonScript] : ["tsx", daemonScript];
+
+ const child = spawn(cmd, args, {
+ detached: true,
+ stdio: "ignore",
+ env: { ...process.env },
+ });
+
+ child.unref();
+ return child;
+};
+
+/**
+ * Ensure the daemon is running. If not, spawn it and wait (up to timeoutMs)
+ * for the socket file to appear.
+ *
+ * Returns true if the daemon is running (or was successfully spawned).
+ */
+export const ensureDaemon = async (timeoutMs = 10_000): Promise<boolean> => {
+ if (await isDaemonRunning()) return true;
+
+ const child = spawnDaemon();
+ if (!child) return false;
+
+ // Poll for the socket file to appear
+ const deadline = Date.now() + timeoutMs;
+ while (Date.now() < deadline) {
+ if (await isDaemonRunning()) return true;
+ await new Promise((r) => setTimeout(r, 200));
+ }
+
+ return false;
+};