@parall/daemon 1.36.1 → 1.37.0

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.
Files changed (56) hide show
  1. package/bundle/manifest.json +11 -11
  2. package/bundle/parall-browser-pod.js +307 -27
  3. package/bundle/parall-claude-agent.js +363 -50
  4. package/bundle/parall-codex-agent.js +362 -48
  5. package/bundle/parall-daemon.js +33473 -31706
  6. package/dist/cli.d.ts.map +1 -1
  7. package/dist/cli.js +2 -2
  8. package/dist/clip-runtime/browser-daemon-env.d.ts +29 -0
  9. package/dist/clip-runtime/browser-daemon-env.d.ts.map +1 -0
  10. package/dist/clip-runtime/browser-daemon-env.js +70 -0
  11. package/dist/clip-runtime/browser-profile-manager.d.ts +18 -5
  12. package/dist/clip-runtime/browser-profile-manager.d.ts.map +1 -1
  13. package/dist/clip-runtime/browser-profile-manager.js +149 -24
  14. package/dist/clip-runtime/browser-profile-pool.d.ts +250 -0
  15. package/dist/clip-runtime/browser-profile-pool.d.ts.map +1 -0
  16. package/dist/clip-runtime/browser-profile-pool.js +581 -0
  17. package/dist/clip-runtime/index.d.ts +2 -2
  18. package/dist/clip-runtime/index.d.ts.map +1 -1
  19. package/dist/clip-runtime/index.js +2 -1
  20. package/dist/clip-runtime/process-manager.d.ts +5 -3
  21. package/dist/clip-runtime/process-manager.d.ts.map +1 -1
  22. package/dist/clip-runtime/subprocess.d.ts +14 -0
  23. package/dist/clip-runtime/subprocess.d.ts.map +1 -1
  24. package/dist/clip-runtime/subprocess.js +49 -0
  25. package/dist/config.d.ts +14 -15
  26. package/dist/config.d.ts.map +1 -1
  27. package/dist/config.js +8 -26
  28. package/dist/daemon-main.d.ts +8 -0
  29. package/dist/daemon-main.d.ts.map +1 -0
  30. package/dist/daemon-main.js +165 -0
  31. package/dist/daemon-paths.d.ts +14 -0
  32. package/dist/daemon-paths.d.ts.map +1 -0
  33. package/dist/daemon-paths.js +26 -0
  34. package/dist/daemon-update-mode.d.ts +8 -0
  35. package/dist/daemon-update-mode.d.ts.map +1 -0
  36. package/dist/daemon-update-mode.js +18 -0
  37. package/dist/index.js +41 -167
  38. package/dist/runtime-bin-resolver.d.ts +4 -0
  39. package/dist/runtime-bin-resolver.d.ts.map +1 -1
  40. package/dist/runtime-bin-resolver.js +40 -7
  41. package/dist/runtime-detector.d.ts +30 -0
  42. package/dist/runtime-detector.d.ts.map +1 -0
  43. package/dist/runtime-detector.js +100 -0
  44. package/dist/supervisor.d.ts +53 -2
  45. package/dist/supervisor.d.ts.map +1 -1
  46. package/dist/supervisor.js +604 -67
  47. package/dist/update-health-gate.d.ts +66 -0
  48. package/dist/update-health-gate.d.ts.map +1 -0
  49. package/dist/update-health-gate.js +93 -0
  50. package/dist/updater-manifest.d.ts +2 -1
  51. package/dist/updater-manifest.d.ts.map +1 -1
  52. package/dist/updater-manifest.js +38 -7
  53. package/dist/updater.d.ts +13 -2
  54. package/dist/updater.d.ts.map +1 -1
  55. package/dist/updater.js +126 -17
  56. package/package.json +6 -6
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAgTA,wBAAsB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,YAAY,GAAG,SAAS,CAAC,CAuD9E"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAkTA,wBAAsB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,YAAY,GAAG,SAAS,CAAC,CAuD9E"}
package/dist/cli.js CHANGED
@@ -219,11 +219,11 @@ function cmdServiceUninstall() {
219
219
  }
220
220
  }
221
221
  async function cmdUpdate(checkOnly) {
222
- const { resolveClaudeDaemonConfig, resolveBundleDir } = await import('./config.js');
222
+ const { resolveClaudeDaemonConfig, resolveBundleDir, resolveUpdateSigningEnabled } = await import('./config.js');
223
223
  const { DaemonUpdater } = await import('./updater.js');
224
224
  const config = resolveClaudeDaemonConfig(process.env);
225
225
  const bundleDir = resolveBundleDir(process.env);
226
- const signingEnabled = process.env.PRLL_DAEMON_SIGNING_DISABLED !== '1';
226
+ const signingEnabled = resolveUpdateSigningEnabled(process.env);
227
227
  const updater = new DaemonUpdater(bundleDir, config.updateCdnUrl, {
228
228
  info: (msg) => console.log(msg),
229
229
  warn: (msg) => console.warn(msg),
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Build the env for a spawned bb-browser daemon: inherit the parent env (so the
3
+ * Chromium it launches keeps PATH / HOME / DISPLAY / XDG / locale / proxy — the
4
+ * hosted browser-docker pod runs Chrome under Xvfb and depends on these), set
5
+ * BB_BROWSER_HOME, and strip the **platform credentials the daemon/pod carries**
6
+ * before they reach the child — bb-browser uses none of them and the Chromium it
7
+ * launches runs untrusted web content, so credentials must never land in its env:
8
+ * - LLM/provider creds (`ANTHROPIC_*` / `OPENAI_*` / `PRLL_CLAUDE_ALLOW_API_KEY`,
9
+ * via the maintained `clearAllProviderCreds` SSOT);
10
+ * - the machine key (`PRLL_API_KEY=mck_`, BYOC control plane);
11
+ * - hosted-pod creds injected by the browser-profile-controller (browser-pod.ts):
12
+ * the platform assignment / pool tokens (`PRLL_ASSIGNMENT_TOKEN`,
13
+ * `PRLL_POOL_TOKEN`, `pba_`) and the S3 browser-state STS creds (`AWS_*`).
14
+ * It also drops the hosted browser-state config the controller injects alongside
15
+ * those creds (`AWS_REGION`, `PRLL_BROWSER_STATE_BUCKET`, `PRLL_STATE_GENERATION`,
16
+ * `PRLL_BROWSER_STATE_ENDPOINT`): bb-browser never reads it, so dropping it shrinks
17
+ * the env surface handed to a process running untrusted content. (Unrelated BYOC
18
+ * host env is out of scope.)
19
+ *
20
+ * ts/daemon/AGENTS.md requires the credential env to be overridden before spawning
21
+ * a child, and with one bb-browser per profile the exposure would otherwise be
22
+ * multiplied N×. Denylist rather than allowlist: the SAME spawn feeds the Xvfb
23
+ * Chromium in the hosted pod, whose full env surface can't be exercised locally, so
24
+ * a too-narrow allowlist would silently break it — a denylist cannot. The hosted
25
+ * keys below are a safe no-op on a BYOC host (its env never carries them). Exported
26
+ * for unit testing.
27
+ */
28
+ export declare function buildBrowserDaemonEnv(baseEnv: NodeJS.ProcessEnv, homeDir: string): NodeJS.ProcessEnv;
29
+ //# sourceMappingURL=browser-daemon-env.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser-daemon-env.d.ts","sourceRoot":"","sources":["../../src/clip-runtime/browser-daemon-env.ts"],"names":[],"mappings":"AAsCA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,MAAM,CAAC,UAAU,EAC1B,OAAO,EAAE,MAAM,GACd,MAAM,CAAC,UAAU,CAOnB"}
@@ -0,0 +1,70 @@
1
+ import { clearAllProviderCreds } from '@parall/agent-core';
2
+ // Env sanitization for the spawned bb-browser daemon child (and the Chromium it
3
+ // launches, which runs untrusted web content). Split out of browser-profile-manager
4
+ // to keep that file under the size threshold; this is a distinct concern (env
5
+ // scrubbing) from the daemon/account/tab lifecycle.
6
+ /** Env vars stripped from the bb-browser child — the platform credentials it must
7
+ * never carry, plus hosted browser-state config it doesn't use (in addition to the
8
+ * provider creds scrubbed by clearAllProviderCreds). See buildBrowserDaemonEnv. */
9
+ const BROWSER_DAEMON_STRIPPED_ENV = [
10
+ 'PRLL_API_KEY', // machine key (mck_) — BYOC + agent control plane
11
+ 'PRLL_PROVIDER_CONFIG', // JSON ProviderConfig blob — may carry BYO LLM api keys
12
+ 'PRLL_ASSIGNMENT_TOKEN', // hosted pod platform assignment token (pba_)
13
+ 'PRLL_POOL_TOKEN', // hosted warm-pool bootstrap token (pba_)
14
+ 'AWS_ACCESS_KEY_ID', // hosted S3 browser-state STS creds (browser-pod.ts)
15
+ 'AWS_SECRET_ACCESS_KEY',
16
+ 'AWS_SESSION_TOKEN',
17
+ // AWS credential-RESOLUTION vars: the SDK derives creds from these too (IRSA
18
+ // web-identity, ECS/EKS container creds, named profile, on-disk cred/config
19
+ // files), so a child inheriting them could assume the pod's role even without
20
+ // the static keys above. bb-browser uses none of them.
21
+ 'AWS_WEB_IDENTITY_TOKEN_FILE',
22
+ 'AWS_ROLE_ARN',
23
+ 'AWS_ROLE_SESSION_NAME',
24
+ 'AWS_CONTAINER_CREDENTIALS_RELATIVE_URI',
25
+ 'AWS_CONTAINER_CREDENTIALS_FULL_URI',
26
+ 'AWS_CONTAINER_AUTHORIZATION_TOKEN',
27
+ 'AWS_PROFILE',
28
+ 'AWS_SHARED_CREDENTIALS_FILE',
29
+ 'AWS_CONFIG_FILE',
30
+ 'AWS_REGION', // hosted browser-state config — bb-browser does not use it
31
+ 'PRLL_BROWSER_STATE_REGION', // browser-pod AWS_REGION fallback — same config family
32
+ 'PRLL_BROWSER_STATE_BUCKET',
33
+ 'PRLL_STATE_GENERATION',
34
+ 'PRLL_BROWSER_STATE_ENDPOINT',
35
+ ];
36
+ /**
37
+ * Build the env for a spawned bb-browser daemon: inherit the parent env (so the
38
+ * Chromium it launches keeps PATH / HOME / DISPLAY / XDG / locale / proxy — the
39
+ * hosted browser-docker pod runs Chrome under Xvfb and depends on these), set
40
+ * BB_BROWSER_HOME, and strip the **platform credentials the daemon/pod carries**
41
+ * before they reach the child — bb-browser uses none of them and the Chromium it
42
+ * launches runs untrusted web content, so credentials must never land in its env:
43
+ * - LLM/provider creds (`ANTHROPIC_*` / `OPENAI_*` / `PRLL_CLAUDE_ALLOW_API_KEY`,
44
+ * via the maintained `clearAllProviderCreds` SSOT);
45
+ * - the machine key (`PRLL_API_KEY=mck_`, BYOC control plane);
46
+ * - hosted-pod creds injected by the browser-profile-controller (browser-pod.ts):
47
+ * the platform assignment / pool tokens (`PRLL_ASSIGNMENT_TOKEN`,
48
+ * `PRLL_POOL_TOKEN`, `pba_`) and the S3 browser-state STS creds (`AWS_*`).
49
+ * It also drops the hosted browser-state config the controller injects alongside
50
+ * those creds (`AWS_REGION`, `PRLL_BROWSER_STATE_BUCKET`, `PRLL_STATE_GENERATION`,
51
+ * `PRLL_BROWSER_STATE_ENDPOINT`): bb-browser never reads it, so dropping it shrinks
52
+ * the env surface handed to a process running untrusted content. (Unrelated BYOC
53
+ * host env is out of scope.)
54
+ *
55
+ * ts/daemon/AGENTS.md requires the credential env to be overridden before spawning
56
+ * a child, and with one bb-browser per profile the exposure would otherwise be
57
+ * multiplied N×. Denylist rather than allowlist: the SAME spawn feeds the Xvfb
58
+ * Chromium in the hosted pod, whose full env surface can't be exercised locally, so
59
+ * a too-narrow allowlist would silently break it — a denylist cannot. The hosted
60
+ * keys below are a safe no-op on a BYOC host (its env never carries them). Exported
61
+ * for unit testing.
62
+ */
63
+ export function buildBrowserDaemonEnv(baseEnv, homeDir) {
64
+ const env = { ...baseEnv, BB_BROWSER_HOME: homeDir };
65
+ clearAllProviderCreds(env); // strips ANTHROPIC_* / OPENAI_* / PRLL_CLAUDE_ALLOW_API_KEY
66
+ for (const key of BROWSER_DAEMON_STRIPPED_ENV) {
67
+ delete env[key];
68
+ }
69
+ return env;
70
+ }
@@ -1,5 +1,5 @@
1
1
  import { type ViewerTurnConfig } from './browser-viewer-streamer.js';
2
- type BrowserProfileStatus = 'running' | 'stopped' | 'error';
2
+ export type BrowserProfileStatus = 'running' | 'stopped' | 'error';
3
3
  /** Per-profile outbound proxy (BYO provider credentials) bound at bb-browser
4
4
  * account_create. server is host:port (optional http/https/socks5 scheme);
5
5
  * username/password authenticate (CDP Fetch.authRequired). Geo/session targeting
@@ -9,7 +9,7 @@ export interface BrowserProxyConfig {
9
9
  username?: string;
10
10
  password?: string;
11
11
  }
12
- interface BrowserProfileManagerOptions {
12
+ export interface BrowserProfileManagerOptions {
13
13
  homeDir: string;
14
14
  log: {
15
15
  info(msg: string): void;
@@ -25,24 +25,37 @@ interface BrowserProfileManagerOptions {
25
25
  * IP for a proxy-configured profile). */
26
26
  resolveProxy?: (profileId: string) => Promise<BrowserProxyConfig | null> | BrowserProxyConfig | null;
27
27
  }
28
- interface BrowserInvokeRequest {
28
+ export interface BrowserInvokeRequest {
29
29
  profileId: string;
30
30
  account: string;
31
31
  command: string;
32
32
  input: unknown;
33
33
  }
34
+ /**
35
+ * The host-side browser invoke surface ClipProcessManager depends on. Both
36
+ * BrowserProfileManager (one bb-browser, multi-account — used by the hosted
37
+ * browser pod) and BrowserProfilePool (one bb-browser per profile — used by the
38
+ * BYOC daemon supervisor for true per-profile isolation) satisfy it, so the
39
+ * synthetic "browser" capability routes through either without caring which.
40
+ */
41
+ export interface BrowserInvoker {
42
+ invoke(request: BrowserInvokeRequest): Promise<unknown>;
43
+ }
34
44
  export declare class BrowserProfileManager {
35
45
  private readonly opts;
36
46
  private daemon;
37
47
  private starting;
38
48
  private restarting;
49
+ private stopping;
39
50
  private readonly ensuredAccounts;
40
51
  private readonly ensuringAccounts;
41
52
  private readonly reportedStatuses;
42
53
  private readonly viewer;
43
54
  constructor(opts: BrowserProfileManagerOptions);
44
55
  invoke({ profileId, account, command, input }: BrowserInvokeRequest): Promise<unknown>;
45
- ensureRuntime(profileId: string): Promise<void>;
56
+ ensureRuntime(profileId: string, opts?: {
57
+ forceStatusReport?: boolean;
58
+ }): Promise<void>;
46
59
  openProfile(profileId: string, startUrl?: string): Promise<void>;
47
60
  stopProfile(profileId: string): Promise<void>;
48
61
  resetProfile(profileId: string): Promise<void>;
@@ -90,7 +103,7 @@ export declare class BrowserProfileManager {
90
103
  private restartDaemon;
91
104
  private restartDaemonOnce;
92
105
  private startDaemon;
106
+ private startDaemonAttempt;
93
107
  private post;
94
108
  }
95
- export {};
96
109
  //# sourceMappingURL=browser-profile-manager.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"browser-profile-manager.d.ts","sourceRoot":"","sources":["../../src/clip-runtime/browser-profile-manager.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"browser-profile-manager.d.ts","sourceRoot":"","sources":["../../src/clip-runtime/browser-profile-manager.ts"],"names":[],"mappings":"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"}
@@ -4,9 +4,13 @@ import { existsSync, mkdirSync, readFileSync } from 'node:fs';
4
4
  import { createRequire } from 'node:module';
5
5
  import * as path from 'node:path';
6
6
  import { fileURLToPath } from 'node:url';
7
+ import { buildBrowserDaemonEnv } from './browser-daemon-env.js';
7
8
  import { BrowserViewerStreamer } from './browser-viewer-streamer.js';
8
- import { findFreePort, formatErrorForLog, sleep } from './subprocess.js';
9
+ import { findFreePort, formatErrorForLog, sleep, waitForChildExit } from './subprocess.js';
9
10
  const BB_BROWSER_DAEMON_START_TIMEOUT_MS = 15_000;
11
+ // Retries for a transient port collision when several daemons boot in parallel
12
+ // (probe-and-close port allocation is TOCTOU); each retry re-rolls fresh ports.
13
+ const BB_BROWSER_DAEMON_START_ATTEMPTS = 3;
10
14
  class BrowserCommandError extends Error {
11
15
  method;
12
16
  account;
@@ -22,6 +26,11 @@ export class BrowserProfileManager {
22
26
  daemon = null;
23
27
  starting = null;
24
28
  restarting = null;
29
+ // Set by stop() and never reset (stop() is terminal — the pool disposes the
30
+ // manager and the hosted pod exits). Makes a startDaemon() that resolves after
31
+ // stop() began un-adoptable, so a late child can't be reassigned to this.daemon
32
+ // and escape teardown.
33
+ stopping = false;
25
34
  ensuredAccounts = new Set();
26
35
  ensuringAccounts = new Map();
27
36
  reportedStatuses = new Map();
@@ -77,9 +86,19 @@ export class BrowserProfileManager {
77
86
  throw err;
78
87
  }
79
88
  }
80
- async ensureRuntime(profileId) {
89
+ async ensureRuntime(profileId, opts = {}) {
81
90
  if (!profileId)
82
91
  throw new Error('browser profile id is required');
92
+ // forceStatusReport is set by the reconnect reconcile for a `pending` row: the server
93
+ // flipped the row to `pending` (an `open` API call) while the daemon was disconnected
94
+ // but its Chromium stayed alive, so our last report was already `running`. A deduped
95
+ // repeat would be swallowed and leave the server stuck at `pending` — clear the marker
96
+ // (as openProfile / stopProfile do) so this recovery report is authoritative for both
97
+ // `running` and `error`. A plain (non-forced) ensure keeps the dedup so a `running`
98
+ // row's chatty re-confirmations are still suppressed.
99
+ if (opts.forceStatusReport) {
100
+ this.reportedStatuses.delete(profileId);
101
+ }
83
102
  try {
84
103
  await this.withDaemonRecovery(() => this.ensureAccount(profileId), `ensure runtime for ${profileId}`);
85
104
  this.reportStatus(profileId, 'running');
@@ -140,25 +159,61 @@ export class BrowserProfileManager {
140
159
  }
141
160
  }
142
161
  async stop() {
162
+ this.stopping = true;
143
163
  // Tear down all per-profile bb-viewer streamers before the bb-browser daemon.
144
164
  this.viewer.shutdown();
165
+ // Capture an IN-FLIGHT startup too: a startDaemon() resolving after we snapshot
166
+ // this.daemon would otherwise produce a fresh child this stop never kills. The
167
+ // ensureDaemon stopping-guard refuses to ADOPT it (won't set this.daemon), but
168
+ // only we can shut it down — so await the startup here and tear down whatever it
169
+ // produced. `daemon` and `starting` are mutually exclusive (ensureDaemon sets
170
+ // this.daemon then nulls this.starting in finally), so at most one is set.
171
+ const inflight = this.starting;
172
+ // An in-flight restartDaemonOnce() has already cleared this.daemon/this.starting
173
+ // and is awaiting the PREVIOUS child's waitForChildExit. Join it so stop() does
174
+ // not return — letting the hosted pod's drain checkpoint run — before that child
175
+ // (and its Chromium) has actually exited and flushed state. The stopping guard
176
+ // set above prevents its post-restart retry from spawning a fresh daemon.
177
+ const restarting = this.restarting;
145
178
  const daemon = this.daemon;
146
179
  this.daemon = null;
147
180
  this.starting = null;
148
181
  this.ensuredAccounts.clear();
149
182
  this.ensuringAccounts.clear();
150
183
  this.reportedStatuses.clear();
151
- if (!daemon)
152
- return;
153
- try {
154
- await this.post('/shutdown', daemon, undefined, 3_000);
155
- }
156
- catch {
157
- /* best-effort */
184
+ if (restarting) {
185
+ try {
186
+ await restarting;
187
+ }
188
+ catch {
189
+ /* restart failure is already logged by restartDaemonOnce */
190
+ }
158
191
  }
159
- if (daemon.child.exitCode === null && daemon.child.signalCode === null) {
160
- daemon.child.kill('SIGTERM');
192
+ let started = null;
193
+ if (inflight) {
194
+ try {
195
+ started = await inflight;
196
+ }
197
+ catch {
198
+ /* startup failed — nothing to tear down */
199
+ }
161
200
  }
201
+ // Wait for each child (and its Chromium) to ACTUALLY exit before returning: the
202
+ // same per-profile home is reused right after — the pool reopens/resets it, or a
203
+ // recovery restart spawns a new daemon on it — so returning while Chromium is
204
+ // still tearing down would race a new Chromium / a state wipe on the same
205
+ // user-data-dir.
206
+ await Promise.all([daemon, started]
207
+ .filter((d) => d !== null)
208
+ .map(async (d) => {
209
+ try {
210
+ await this.post('/shutdown', d, undefined, 3_000);
211
+ }
212
+ catch {
213
+ /* best-effort */
214
+ }
215
+ await waitForChildExit(d.child, this.opts.log);
216
+ }));
162
217
  }
163
218
  /**
164
219
  * Viewer control command entrypoint — delegates to BrowserViewerStreamer
@@ -408,20 +463,39 @@ export class BrowserProfileManager {
408
463
  async ensureDaemon() {
409
464
  if (this.restarting)
410
465
  await this.restarting;
466
+ if (this.stopping)
467
+ throw new Error('browser profile manager is stopping');
411
468
  if (this.daemon &&
412
469
  this.daemon.child.exitCode === null &&
413
470
  this.daemon.child.signalCode === null) {
414
471
  return this.daemon;
415
472
  }
416
- if (this.starting)
417
- return this.starting;
473
+ if (this.starting) {
474
+ // A concurrent caller shares the in-flight start, but must re-check the stopping
475
+ // guard after awaiting it — exactly like the owner below. Otherwise stop() beginning
476
+ // mid-start would hand this waiter the freshly-started daemon that stop() is tearing
477
+ // down, and it would post a command into teardown (the resurrect race, via the
478
+ // waiter path). Fail closed instead.
479
+ const started = await this.starting;
480
+ if (this.stopping)
481
+ throw new Error('browser profile manager is stopping');
482
+ return started;
483
+ }
418
484
  this.starting = this.startDaemon();
485
+ const startPromise = this.starting;
419
486
  try {
420
- this.daemon = await this.starting;
487
+ const started = await startPromise;
488
+ // stop() may have begun while we were starting; it captured this same promise
489
+ // and tears the child down. Do NOT adopt it as this.daemon, or it would escape
490
+ // teardown — exactly the resurrect race this guard closes.
491
+ if (this.stopping)
492
+ throw new Error('browser profile manager is stopping');
493
+ this.daemon = started;
421
494
  return this.daemon;
422
495
  }
423
496
  finally {
424
- this.starting = null;
497
+ if (this.starting === startPromise)
498
+ this.starting = null;
425
499
  }
426
500
  }
427
501
  async withDaemonRecovery(operation, label) {
@@ -461,20 +535,67 @@ export class BrowserProfileManager {
461
535
  catch {
462
536
  /* best-effort */
463
537
  }
464
- if (daemon.child.exitCode === null && daemon.child.signalCode === null) {
465
- daemon.child.kill('SIGTERM');
466
- }
538
+ // Wait for the bb-browser child (and its Chromium) to ACTUALLY exit before
539
+ // returning: the same per-profile home is reused right after — the pool
540
+ // reopens/resets it, or a recovery restart spawns a new daemon on it — so
541
+ // returning while Chromium is still tearing down would race a new Chromium /
542
+ // a state wipe against the old one on the same user-data-dir.
543
+ await waitForChildExit(daemon.child, this.opts.log);
467
544
  }
468
545
  async startDaemon() {
546
+ // findFreePort() probes by binding :0 then closing, so when the supervisor
547
+ // boots several profiles' bb-browser daemons in parallel two can grab the same
548
+ // just-freed port and the loser's Chromium fails to bind. Retry with fresh
549
+ // ports so a transient collision self-heals instead of stranding the profile
550
+ // until the next reconcile. A port-collision attempt fails fast (the child
551
+ // exits on EADDRINUSE), so the retries don't stack the full start timeout.
552
+ let lastErr;
553
+ for (let attempt = 1; attempt <= BB_BROWSER_DAEMON_START_ATTEMPTS; attempt++) {
554
+ try {
555
+ return await this.startDaemonAttempt();
556
+ }
557
+ catch (err) {
558
+ lastErr = err;
559
+ if (attempt < BB_BROWSER_DAEMON_START_ATTEMPTS) {
560
+ this.opts.log.warn(`[bb-browser] daemon start attempt ${attempt} failed; retrying with fresh ports: ${String(err)}`);
561
+ }
562
+ }
563
+ }
564
+ throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));
565
+ }
566
+ async startDaemonAttempt() {
469
567
  const host = '127.0.0.1';
470
568
  const port = await findFreePort();
569
+ // Each bb-browser daemon launches its OWN Chromium. bb-browser defaults the
570
+ // CDP port to 9222, so running more than one daemon per host (the BYOC
571
+ // supervisor's BrowserProfilePool spawns one bb-browser per profile for
572
+ // hard isolation) would collide on 9222 → only the first profile's Chrome
573
+ // starts. Bind a free CDP port per daemon; every consumer reads the live
574
+ // port from GET /status (cdpEndpoint), so nothing depends on 9222.
575
+ // findFreePort() probes by binding :0 then closing, so two back-to-back calls
576
+ // can hand back the SAME ephemeral port — the HTTP and CDP ports must differ,
577
+ // so re-roll the CDP port if it collides with the HTTP port.
578
+ let cdpPort = await findFreePort();
579
+ for (let i = 0; cdpPort === port && i < 5; i++) {
580
+ cdpPort = await findFreePort();
581
+ }
582
+ if (cdpPort === port) {
583
+ throw new Error('bb-browser: could not allocate distinct HTTP and CDP ports');
584
+ }
471
585
  const token = randomToken();
472
586
  const daemonPath = resolveBbBrowserDaemonPath();
473
- const child = spawn(process.execPath, [daemonPath, '--host', host, '--port', String(port), '--token', token], {
474
- env: {
475
- ...process.env,
476
- BB_BROWSER_HOME: this.opts.homeDir,
477
- },
587
+ const child = spawn(process.execPath, [
588
+ daemonPath,
589
+ '--host',
590
+ host,
591
+ '--port',
592
+ String(port),
593
+ '--token',
594
+ token,
595
+ '--cdp-port',
596
+ String(cdpPort),
597
+ ], {
598
+ env: buildBrowserDaemonEnv(process.env, this.opts.homeDir),
478
599
  stdio: ['ignore', 'ignore', 'pipe'],
479
600
  });
480
601
  // A spawn/exec failure (ENOENT, EACCES, …) emits 'error' on the child, which
@@ -507,7 +628,11 @@ export class BrowserProfileManager {
507
628
  await sleep(200);
508
629
  }
509
630
  }
510
- child.kill('SIGTERM');
631
+ // Await the timed-out child's exit before failing: a retry reuses the SAME
632
+ // per-profile home, so returning while this Chromium is still shutting down
633
+ // would race the next startup against it on the same user-data-dir (same
634
+ // guarantee as stop/restart). waitForChildExit sends SIGTERM, then SIGKILL.
635
+ await waitForChildExit(child, this.opts.log);
511
636
  throw new Error('bb-browser-daemon did not start in time');
512
637
  }
513
638
  async post(pathName, daemon, body, timeoutMs = 10_000) {