pi-browser-harness 0.10.0 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  All notable changes to pi-browser-harness will be documented in this file.
4
4
 
5
+ ## 0.10.1 — 2026-07-26
6
+
7
+ ### Fixed
8
+
9
+ - **Chrome process leak on profile seed failure.** When a profile is pinned and `seedProfileWindow` spawns Chrome via `openProfileWindow`, the spawned child is kept as `detached: true` + `unref()`. If Chromium's ProcessSingleton fails to delegate (user-data-dir mismatch, multiple browsers, Chrome busy/crashed), the spawned process starts a full second browser instance. The sentinel never appears, the seed times out, but the spawned Chrome process was never killed — accumulating zombie browser instances that consume RAM. The fix stores the child process reference, exposes a `kill()` on the returned handle, and calls it on seed timeout.
10
+ - **Unnecessary Chrome re-spawns on every `start()`.** `profileContextId` was unconditionally cleared on every `start()`, forcing a new profile-window launch via ProcessSingleton even when Chrome hadn't restarted and the context was still valid. Now the context is cleared only when the browser UUID in the WebSocket URL changes — which Chrome re-mints on every launch.
11
+ - **`setProfilePin` no longer clears context when the pin hasn't changed.** Loading the same pin from disk on `session_start` was clearing `profileContextId`, forcing yet another Chrome spawn on the next `start()`.
12
+
5
13
  ## 0.10.0 — 2026-07-25
6
14
 
7
15
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-browser-harness",
3
- "version": "0.10.0",
3
+ "version": "0.10.1",
4
4
  "description": "Browser control extension for pi — navigate, click, type, screenshot, and extract data from real Chrome via CDP",
5
5
  "keywords": [
6
6
  "pi-package",
package/src/client.ts CHANGED
@@ -170,11 +170,12 @@ export const createBrowserClient = (opts: BrowserClientOptions): BrowserClient =
170
170
  }
171
171
  const connected = await transport.connect(wsUrl, { timeoutMs: 10_000 });
172
172
  if (!connected.success) return connected;
173
- if (!remote) {
174
- remote = { cdpUrl: wsUrl, browserId: wsUrl.split("/").pop() ?? "unknown" };
173
+
174
+ const newBrowserId: string = wsUrl.split("/").pop() ?? "unknown";
175
+ if (!remote || remote.browserId !== newBrowserId) {
176
+ profileContextId = undefined;
175
177
  }
176
- // A fresh browser run invalidates any context id resolved earlier.
177
- profileContextId = undefined;
178
+ remote = { cdpUrl: wsUrl, browserId: newBrowserId };
178
179
  const seeded = await seedIfPinned();
179
180
  if (!seeded.success) {
180
181
  await transport.close();
@@ -397,9 +398,13 @@ export const createBrowserClient = (opts: BrowserClientOptions): BrowserClient =
397
398
  userDataDir: () => userDataDir,
398
399
  profilePin: () => profilePin,
399
400
  setProfilePin: (pin) => {
401
+ const changed = pin === null
402
+ ? profilePin !== null
403
+ : profilePin === null || profilePin.userDataDir !== pin.userDataDir || profilePin.profileDir !== pin.profileDir;
400
404
  profilePin = pin;
401
- // The old profile's context no longer describes where tabs should go.
402
- profileContextId = undefined;
405
+ if (changed) {
406
+ profileContextId = undefined;
407
+ }
403
408
  },
404
409
  profileContextId: () => profileContextId,
405
410
  setProfileContextId: (contextId) => {
@@ -93,7 +93,7 @@ export const seedProfileWindow = async (
93
93
  return err(cdpError("discovery_failed", `${launched.error} — ${manualFallbackHint(pin)}`));
94
94
  }
95
95
 
96
- const { sentinelUrl, cleanup } = launched.data;
96
+ const { sentinelUrl, cleanup, kill } = launched.data;
97
97
  const deadline = Date.now() + SEED_DEADLINE_MS;
98
98
  let seed: PageTarget | undefined;
99
99
  while (Date.now() < deadline && !seed) {
@@ -102,6 +102,7 @@ export const seedProfileWindow = async (
102
102
  }
103
103
 
104
104
  if (!seed) {
105
+ kill();
105
106
  await cleanup();
106
107
  return err(cdpError(
107
108
  "timeout",
@@ -15,7 +15,7 @@
15
15
  * identification correct when the user opens tabs at the same moment.
16
16
  */
17
17
 
18
- import { spawn } from "node:child_process";
18
+ import { spawn, type ChildProcess } from "node:child_process";
19
19
  import { randomUUID } from "node:crypto";
20
20
  import { rm, writeFile } from "node:fs/promises";
21
21
  import { tmpdir } from "node:os";
@@ -42,6 +42,7 @@ export type ProfileWindowHandle = {
42
42
  readonly sentinelUrl: string;
43
43
  /** Remove the sentinel file. Safe to call more than once. */
44
44
  readonly cleanup: () => Promise<void>;
45
+ readonly kill: () => void;
45
46
  };
46
47
 
47
48
  const SENTINEL_HTML = (token: string): string =>
@@ -68,10 +69,11 @@ export const openProfileWindow = async (
68
69
  if (req.explicitUserDataDir) args.push(`--user-data-dir=${req.explicitUserDataDir}`);
69
70
  args.push(`--profile-directory=${req.profileDir}`, "--new-window", sentinelUrl);
70
71
 
72
+ let child: ChildProcess | null = null;
71
73
  try {
72
74
  // No shell: arguments are passed as an array, so profile names and paths
73
75
  // containing spaces survive intact on every platform.
74
- const child = spawn(req.exePath, args, { detached: true, stdio: "ignore" });
76
+ child = spawn(req.exePath, args, { detached: true, stdio: "ignore" });
75
77
  child.unref();
76
78
  // The launcher process exits as soon as the running browser takes over the
77
79
  // command line; an error here means the binary itself could not be started.
@@ -86,5 +88,11 @@ export const openProfileWindow = async (
86
88
  cleanup: async () => {
87
89
  await rm(file, { force: true }).catch(() => {});
88
90
  },
91
+ kill: () => {
92
+ if (child) {
93
+ try { child.kill("SIGTERM"); } catch {}
94
+ child = null;
95
+ }
96
+ },
89
97
  });
90
98
  };