@parall/daemon 1.36.0 → 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 (59) hide show
  1. package/bundle/manifest.json +11 -11
  2. package/bundle/parall-browser-pod.js +384 -32
  3. package/bundle/parall-claude-agent.js +426 -51
  4. package/bundle/parall-codex-agent.js +425 -49
  5. package/bundle/parall-daemon.js +33336 -31520
  6. package/dist/browser-pod.d.ts +7 -0
  7. package/dist/browser-pod.d.ts.map +1 -1
  8. package/dist/browser-pod.js +73 -3
  9. package/dist/cli.d.ts.map +1 -1
  10. package/dist/cli.js +2 -2
  11. package/dist/clip-runtime/browser-daemon-env.d.ts +29 -0
  12. package/dist/clip-runtime/browser-daemon-env.d.ts.map +1 -0
  13. package/dist/clip-runtime/browser-daemon-env.js +70 -0
  14. package/dist/clip-runtime/browser-profile-manager.d.ts +34 -5
  15. package/dist/clip-runtime/browser-profile-manager.d.ts.map +1 -1
  16. package/dist/clip-runtime/browser-profile-manager.js +166 -24
  17. package/dist/clip-runtime/browser-profile-pool.d.ts +250 -0
  18. package/dist/clip-runtime/browser-profile-pool.d.ts.map +1 -0
  19. package/dist/clip-runtime/browser-profile-pool.js +581 -0
  20. package/dist/clip-runtime/index.d.ts +2 -1
  21. package/dist/clip-runtime/index.d.ts.map +1 -1
  22. package/dist/clip-runtime/index.js +2 -1
  23. package/dist/clip-runtime/process-manager.d.ts +5 -3
  24. package/dist/clip-runtime/process-manager.d.ts.map +1 -1
  25. package/dist/clip-runtime/subprocess.d.ts +14 -0
  26. package/dist/clip-runtime/subprocess.d.ts.map +1 -1
  27. package/dist/clip-runtime/subprocess.js +49 -0
  28. package/dist/config.d.ts +14 -15
  29. package/dist/config.d.ts.map +1 -1
  30. package/dist/config.js +8 -26
  31. package/dist/daemon-main.d.ts +8 -0
  32. package/dist/daemon-main.d.ts.map +1 -0
  33. package/dist/daemon-main.js +165 -0
  34. package/dist/daemon-paths.d.ts +14 -0
  35. package/dist/daemon-paths.d.ts.map +1 -0
  36. package/dist/daemon-paths.js +26 -0
  37. package/dist/daemon-update-mode.d.ts +8 -0
  38. package/dist/daemon-update-mode.d.ts.map +1 -0
  39. package/dist/daemon-update-mode.js +18 -0
  40. package/dist/index.js +41 -167
  41. package/dist/runtime-bin-resolver.d.ts +4 -0
  42. package/dist/runtime-bin-resolver.d.ts.map +1 -1
  43. package/dist/runtime-bin-resolver.js +40 -7
  44. package/dist/runtime-detector.d.ts +30 -0
  45. package/dist/runtime-detector.d.ts.map +1 -0
  46. package/dist/runtime-detector.js +100 -0
  47. package/dist/supervisor.d.ts +64 -2
  48. package/dist/supervisor.d.ts.map +1 -1
  49. package/dist/supervisor.js +629 -67
  50. package/dist/update-health-gate.d.ts +66 -0
  51. package/dist/update-health-gate.d.ts.map +1 -0
  52. package/dist/update-health-gate.js +93 -0
  53. package/dist/updater-manifest.d.ts +2 -1
  54. package/dist/updater-manifest.d.ts.map +1 -1
  55. package/dist/updater-manifest.js +38 -7
  56. package/dist/updater.d.ts +13 -2
  57. package/dist/updater.d.ts.map +1 -1
  58. package/dist/updater.js +126 -17
  59. package/package.json +6 -6
@@ -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
@@ -321,10 +376,15 @@ export class BrowserProfileManager {
321
376
  if (!isBrowserAccountInfoUnauthenticatedError(err)) {
322
377
  if (!isBrowserAccountNotFoundError(err))
323
378
  throw err;
379
+ // Resolve the proxy ONLY here, at the single account-creation choke point.
380
+ // A thrown resolve fails closed (no account rather than direct egress for a
381
+ // proxy-configured profile). bb-browser binds the proxy at account_create.
382
+ const proxy = this.opts.resolveProxy ? await this.opts.resolveProxy(account) : null;
324
383
  await this.sendCommand({
325
384
  method: 'account_create',
326
385
  account,
327
386
  ...(accountUrl ? { accountUrl } : {}),
387
+ ...proxyCreateFields(proxy),
328
388
  });
329
389
  accountCreated = true;
330
390
  }
@@ -403,20 +463,39 @@ export class BrowserProfileManager {
403
463
  async ensureDaemon() {
404
464
  if (this.restarting)
405
465
  await this.restarting;
466
+ if (this.stopping)
467
+ throw new Error('browser profile manager is stopping');
406
468
  if (this.daemon &&
407
469
  this.daemon.child.exitCode === null &&
408
470
  this.daemon.child.signalCode === null) {
409
471
  return this.daemon;
410
472
  }
411
- if (this.starting)
412
- 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
+ }
413
484
  this.starting = this.startDaemon();
485
+ const startPromise = this.starting;
414
486
  try {
415
- 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;
416
494
  return this.daemon;
417
495
  }
418
496
  finally {
419
- this.starting = null;
497
+ if (this.starting === startPromise)
498
+ this.starting = null;
420
499
  }
421
500
  }
422
501
  async withDaemonRecovery(operation, label) {
@@ -456,20 +535,67 @@ export class BrowserProfileManager {
456
535
  catch {
457
536
  /* best-effort */
458
537
  }
459
- if (daemon.child.exitCode === null && daemon.child.signalCode === null) {
460
- daemon.child.kill('SIGTERM');
461
- }
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);
462
544
  }
463
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() {
464
567
  const host = '127.0.0.1';
465
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
+ }
466
585
  const token = randomToken();
467
586
  const daemonPath = resolveBbBrowserDaemonPath();
468
- const child = spawn(process.execPath, [daemonPath, '--host', host, '--port', String(port), '--token', token], {
469
- env: {
470
- ...process.env,
471
- BB_BROWSER_HOME: this.opts.homeDir,
472
- },
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),
473
599
  stdio: ['ignore', 'ignore', 'pipe'],
474
600
  });
475
601
  // A spawn/exec failure (ENOENT, EACCES, …) emits 'error' on the child, which
@@ -502,7 +628,11 @@ export class BrowserProfileManager {
502
628
  await sleep(200);
503
629
  }
504
630
  }
505
- 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);
506
636
  throw new Error('bb-browser-daemon did not start in time');
507
637
  }
508
638
  async post(pathName, daemon, body, timeoutMs = 10_000) {
@@ -618,3 +748,15 @@ function resolveBbBrowserDaemonPath() {
618
748
  function randomToken() {
619
749
  return randomBytes(16).toString('hex');
620
750
  }
751
+ /** Render a proxy config into bb-browser `account_create` fields (camelCase, the
752
+ * bb-browser-pro 0.15 contract). Empty when there is no proxy → direct egress. */
753
+ function proxyCreateFields(proxy) {
754
+ if (!proxy?.server)
755
+ return {};
756
+ const fields = { proxyServer: proxy.server };
757
+ if (proxy.username)
758
+ fields.proxyUsername = proxy.username;
759
+ if (proxy.password)
760
+ fields.proxyPassword = proxy.password;
761
+ return fields;
762
+ }
@@ -0,0 +1,250 @@
1
+ import { type BrowserInvokeRequest, type BrowserInvoker, type BrowserProfileManagerOptions, type BrowserProfileStatus } from './browser-profile-manager.js';
2
+ import type { ViewerTurnConfig } from './browser-viewer-streamer.js';
3
+ /**
4
+ * The subset of BrowserProfileManager the pool drives. Extracted so tests can
5
+ * inject a fake runtime — the real manager spawns bb-browser + Chromium on the
6
+ * first command, which a unit test must not do — via
7
+ * {@link BrowserProfilePoolOptions.createManager}.
8
+ */
9
+ export interface BrowserProfileRuntime {
10
+ invoke(request: BrowserInvokeRequest): Promise<unknown>;
11
+ ensureRuntime(profileId: string, opts?: {
12
+ forceStatusReport?: boolean;
13
+ }): Promise<void>;
14
+ openProfile(profileId: string, startUrl?: string): Promise<void>;
15
+ handleViewerCommand(profileId: string, sessionId: string, command: string, input?: Record<string, unknown>, turn?: ViewerTurnConfig): Promise<Record<string, unknown>>;
16
+ stopProfile(profileId: string): Promise<void>;
17
+ stop(): Promise<void>;
18
+ }
19
+ export interface BrowserProfilePoolOptions {
20
+ /** Parent dir under which each profile gets its own `<baseHomeDir>/<profileId>` home. */
21
+ baseHomeDir: string;
22
+ log: BrowserProfileManagerOptions['log'];
23
+ /** Report a profile's status to the server. Unlike the manager's 3-arg callback,
24
+ * the pool's carries the lifecycle generation the op was acting under (PR #1650),
25
+ * so the server can reject a stale report. The pool injects the generation for
26
+ * manager-driven reports (it owns the per-op generation); supervisor passes it
27
+ * straight to client.reportBrowserProfileStatus. */
28
+ reportStatus?: (profileId: string, status: BrowserProfileStatus, errorMsg?: string, generation?: number) => void;
29
+ /** Per-profile outbound proxy resolver (#1642), threaded to each profile's
30
+ * manager so its isolated Chromium binds its own egress proxy at account_create. */
31
+ resolveProxy?: BrowserProfileManagerOptions['resolveProxy'];
32
+ /**
33
+ * Per-profile runtime factory. Defaults to a real {@link BrowserProfileManager};
34
+ * overridable in tests to avoid spawning a real bb-browser / Chromium.
35
+ */
36
+ createManager?: (opts: BrowserProfileManagerOptions) => BrowserProfileRuntime;
37
+ }
38
+ /**
39
+ * BrowserProfilePool — true per-profile isolation for the BYOC daemon supervisor.
40
+ *
41
+ * WHY THIS EXISTS. A single bb-browser/Chromium shared across profiles isolates
42
+ * only at the bb-browser "account" (CDP BrowserContext) level. That is real for
43
+ * cookies/storage, but bb-browser's COMMAND routing is account-blind: any command
44
+ * issued without an explicit tabId falls through to a single GLOBAL current tab
45
+ * (cdp.currentTargetId, else targets[0]) regardless of the `account` field
46
+ * (bb-browser-pro daemon.js ensurePageTarget). So when the global current tab
47
+ * belongs to profile A, a command meant for profile B executes in A's context —
48
+ * e.g. logging into a second account overwrites the first, and two profiles can
49
+ * never hold two different sessions at once (reproduced on a real Chrome).
50
+ *
51
+ * THE FIX. Give every profile its OWN bb-browser daemon + Chromium + on-disk home
52
+ * (`<baseHomeDir>/<profileId>`). Each Chromium then hosts exactly one profile, so
53
+ * there is no cross-profile global tab to leak into — isolation is enforced by the
54
+ * OS process + user-data-dir boundary, not by remembering to scope every command.
55
+ * The pool owns one {@link BrowserProfileManager} per profile and routes by
56
+ * profileId; an idle (stopped) profile holds no Chromium, so cost scales with the
57
+ * number of CONCURRENTLY-OPEN profiles, not the number that ever existed.
58
+ *
59
+ * NOT used by the hosted browser pod (`browser-pod.ts`): a pod serves exactly one
60
+ * profile, so it keeps using a single BrowserProfileManager directly (and its
61
+ * BrowserStateStore snapshots that manager's flat homeDir to S3 — the pool's
62
+ * per-profile nesting would break that). The pool is supervisor-only.
63
+ *
64
+ * OWNERSHIP. The pool is a per-profile runtime REGISTRY + the per-profile mutex
65
+ * (create / stop / list-active / invoke / viewer, serialized via `enqueue()`). The
66
+ * DESIRED-STATE coordinator is the supervisor: the server (clip-service) is the
67
+ * SSOT for which profiles should be live, and `DaemonSupervisor.reconcileBrowserProfiles`
68
+ * converges the local registry to it (open/ensure desired, stop the rest via
69
+ * `activeProfileIds()`). The pool keeps one bit of lifecycle policy — the `stopped`
70
+ * fence — because invoke (hub channel) and viewer funnel through it and must reject
71
+ * a stale command without a server round-trip.
72
+ */
73
+ export declare class BrowserProfilePool implements BrowserInvoker {
74
+ private readonly opts;
75
+ private readonly managers;
76
+ private readonly opChains;
77
+ private readonly stopped;
78
+ private readonly stopSeq;
79
+ private readonly generations;
80
+ private readonly opGeneration;
81
+ private stopping;
82
+ constructor(opts: BrowserProfilePoolOptions);
83
+ /** Inbound hub browser invoke for a profile — routed to that profile's own daemon. */
84
+ invoke(request: BrowserInvokeRequest): Promise<unknown>;
85
+ private runWithGeneration;
86
+ /**
87
+ * Reconcile-time: ensure the profile's runtime (account + tab) is live. If
88
+ * `sinceSeq` (the fence generation observed when this op was enqueued) is given
89
+ * and a newer stop has been observed since, skip — a stale reconcile/open queued
90
+ * before a stop must not un-fence and revive a profile about to be torn down
91
+ * (the pending stop wins; last-write-wins).
92
+ */
93
+ ensureRuntime(profileId: string, sinceSeq?: number, generation?: number, opts?: {
94
+ forceStatusReport?: boolean;
95
+ }): Promise<void>;
96
+ /** Lifecycle `open` — open (or reuse) the profile's start tab. `sinceSeq`: see ensureRuntime. */
97
+ openProfile(profileId: string, startUrl?: string, sinceSeq?: number, generation?: number): Promise<void>;
98
+ /** True if a newer stop was observed after this revive captured `sinceSeq`. */
99
+ private isStaleRevive;
100
+ /** Live-viewer control command for a profile — routed to its own daemon/streamer. */
101
+ handleViewerCommand(profileId: string, sessionId: string, command: string, input?: Record<string, unknown>, turn?: ViewerTurnConfig): Promise<Record<string, unknown>>;
102
+ /**
103
+ * Lifecycle `stop` (explicit, owner-driven) — stop the profile's viewer + tear
104
+ * down its bb-browser daemon and Chromium to free the resources (an idle profile
105
+ * must not keep a browser running). Confirms `stopped` back to the server: the
106
+ * stop API already set the server row to `stopped`, and this report keeps the
107
+ * daemon authoritative over the actual runtime state (the no-runtime case still
108
+ * confirms). For reconcile-driven cleanup of profiles the server no longer owns
109
+ * here, use {@link releaseRuntime} — it must NOT report.
110
+ */
111
+ stopProfile(profileId: string, generation?: number): Promise<void>;
112
+ /**
113
+ * Reconcile teardown (negative convergence) — release a local runtime the server
114
+ * no longer lists as live on this machine (stopped / deleted / reassigned).
115
+ * Fences + disposes like a stop, but does NOT report status: here the server is
116
+ * the desired-state SSOT, so a daemon-side `stopped` push is semantically wrong
117
+ * (for a profile reassigned to another machine it isn't ours to report — the
118
+ * server's machine-binding check rejects it as 404 anyway) and only emits
119
+ * spurious calls. Local-only cleanup.
120
+ */
121
+ releaseRuntime(profileId: string): Promise<void>;
122
+ /**
123
+ * Synchronously fence a profile (reject late invoke/viewer until an explicit
124
+ * reopen) WITHOUT tearing anything down. The supervisor calls this the moment a
125
+ * stop/reset lifecycle event is received, before the queued stop/reset op runs:
126
+ * a hub invoke — which bypasses the supervisor queue and hits {@link invoke}
127
+ * directly — authorized just before the server row flipped to `stopped` could
128
+ * otherwise arrive in that window and spawn a transient Chromium the stop then
129
+ * immediately tears down. Fencing now rejects it instead. An in-flight invoke
130
+ * already past the fence check is unaffected (the queued stop serializes behind
131
+ * it); open/ensure clears the fence. Idempotent; no-op once stopping.
132
+ */
133
+ fence(profileId: string): void;
134
+ /** The current fence generation for a profile — captured by a revive at enqueue. */
135
+ stopSeqOf(profileId: string): number;
136
+ /**
137
+ * Lifecycle `reset` — wipe the profile's persisted state (cookies, localStorage,
138
+ * IndexedDB, cache) for a clean slate. Removes BOTH the per-profile home AND any
139
+ * unmigrated legacy flat cookie file — a reset done before the profile's first
140
+ * post-upgrade open would otherwise leave the legacy file in place, and the lazy
141
+ * migration would re-import the old login on the next open, silently undoing the
142
+ * reset. Done AFTER the daemon is stopped so Chromium has released its files.
143
+ * Isolated by construction — it can never touch another profile's state.
144
+ * Fail-closed: if the wipe fails (e.g. Chromium still holds a file, or a
145
+ * permission error), report `error` and throw rather than report `stopped` —
146
+ * a reset that left state on disk must not be marked clean, since the next open
147
+ * could restore the old login. Reports `stopped` only on a confirmed wipe.
148
+ */
149
+ resetProfile(profileId: string, generation?: number, resetGeneration?: number): Promise<void>;
150
+ /**
151
+ * Apply a reset the daemon hasn't yet (caught on reconnect when the server's
152
+ * reset_generation is newer than this daemon's recorded one). Same wipe as
153
+ * resetProfile but WITHOUT a status report — the caller drives desired state (an
154
+ * open/ensure reports running for a still-desired profile; a stopped profile is
155
+ * left stopped). Must run BEFORE any open/ensure for the profile so a reopen starts
156
+ * from clean state, never the old cookies (the supervisor enqueues it first).
157
+ */
158
+ applyResetWipe(profileId: string, resetGeneration: number): Promise<void>;
159
+ private doResetWipe;
160
+ /**
161
+ * Reconnect cleanup for a profile the server no longer lists AT ALL (deleted or
162
+ * reassigned away): dispose any runtime and wipe the per-profile home — the old host
163
+ * must not keep its cookies/storage. NO status report (the server is the SSOT and the
164
+ * row is gone there); fences too. Idempotent (a wiped home simply isn't found again).
165
+ */
166
+ wipeAbsent(profileId: string): Promise<void>;
167
+ /**
168
+ * Tear down every profile's daemon (supervisor shutdown). TERMINAL: the
169
+ * supervisor discards the pool after this, so `stopping` stays set forever —
170
+ * manager() refuses any further op. Drain outstanding per-profile chains BEFORE
171
+ * clearing the maps, so a queued op whose `prev` settles only after stop()
172
+ * returns can't slip through manager() and recreate a runtime (orphaning a
173
+ * Chromium past shutdown); `stopping` is the backstop if one still races in.
174
+ */
175
+ stop(): Promise<void>;
176
+ /**
177
+ * Registry view: profile ids that currently hold a live local runtime. The
178
+ * supervisor (the desired-state coordinator) diffs this against the server's
179
+ * desired-live set on full reconcile and stops the extras via stopProfile() —
180
+ * the convergence policy lives there, not here; the pool only owns runtime
181
+ * lifecycle mechanics.
182
+ */
183
+ activeProfileIds(): string[];
184
+ /**
185
+ * Run `op` after every prior operation for this profile settles — a per-profile
186
+ * serialization so invoke / open / stop / reset / viewer never interleave for
187
+ * one profile (different profiles still run in parallel). Bounded: bb-browser
188
+ * commands time out (~30s), so a stop never waits forever behind a hung invoke.
189
+ * Also normalizes a synchronous throw from `op` into a rejected promise.
190
+ *
191
+ * QUEUE CONTRACT. This is the FINAL per-profile mutex — EVERY pool entrypoint
192
+ * (invoke, open, ensure, stop, reset, viewer) goes through it, so it alone
193
+ * guarantees per-profile mutual exclusion. The supervisor's separate
194
+ * `enqueueBrowserProfileOp` is a higher-level ORDERING layer for the ops it
195
+ * originates (WS lifecycle/viewer events + reconcile), not a second mutex. Hub
196
+ * invokes arrive from the clip runtime and do NOT pass through the supervisor
197
+ * queue — only this one. So: do not assume "all profile ops share one queue";
198
+ * they share THIS one.
199
+ */
200
+ private enqueue;
201
+ /**
202
+ * Get (or lazily create) the dedicated manager for a profile. Synchronous so the
203
+ * get-or-create is atomic — the bb-browser daemon itself starts lazily on first
204
+ * command, not here.
205
+ */
206
+ private manager;
207
+ /** Stop a manager and drop it from the map (identity-checked so a concurrent
208
+ * recreate during the async stop is never evicted). */
209
+ private dispose;
210
+ private profileHome;
211
+ /** Legacy shared-layout cookie file for a profile (pre per-profile homes). */
212
+ private legacyAccountFile;
213
+ /**
214
+ * Remove every on-disk trace of a profile: its per-profile home AND any
215
+ * unmigrated legacy flat cookie file — otherwise a later open would re-import
216
+ * the legacy cookies (see resetProfile). Throws if any target could not be
217
+ * removed so the caller can fail the reset closed rather than report a clean
218
+ * slate while state survives on disk.
219
+ */
220
+ private removeProfileState;
221
+ private metaFile;
222
+ /** The reset_generation this daemon has applied for the profile (0 if unknown). */
223
+ appliedResetGeneration(profileId: string): number;
224
+ private writeResetGeneration;
225
+ /**
226
+ * Profile ids that have ANY local state on this host — the reconcile's inventory for
227
+ * finding deleted/reassigned profiles whose residue must be wiped. Union of the two
228
+ * storage layouts this pool owns:
229
+ * - per-profile home dirs `<baseHomeDir>/brp_*`
230
+ * - legacy flat account files `<baseHomeDir>/accounts/brp_*.json` (pre per-profile
231
+ * homes; a profile deleted before its first post-upgrade reopen has ONLY this file
232
+ * and no home dir, so home-only enumeration would miss its login residue)
233
+ * Only names passing the strict brp_ check are returned, and the two layouts are
234
+ * deduped, so reconnect cleanup never touches the `accounts/` dir itself, a hidden
235
+ * dir, `legacy.json`, or any non-profile entry. removeProfileState wipes both layouts.
236
+ */
237
+ localStateProfileIds(): string[];
238
+ /**
239
+ * One-time, lazy migration from the legacy shared layout. Before per-profile
240
+ * homes, all profiles shared one bb-browser home and bb-browser persisted each
241
+ * profile's cookies to `<baseHomeDir>/accounts/<profileId>.json` (account name =
242
+ * profileId). Move that file into the profile's own `accounts/` dir on first use
243
+ * so existing cookie logins survive the upgrade instead of forcing a re-login.
244
+ * Only cookies are migrated — the shared `chrome-data/` was a single comingled
245
+ * Chromium profile that cannot be split per profile, and bb-browser never
246
+ * persisted localStorage/IndexedDB across restarts anyway.
247
+ */
248
+ private migrateFlatProfileState;
249
+ }
250
+ //# sourceMappingURL=browser-profile-pool.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser-profile-pool.d.ts","sourceRoot":"","sources":["../../src/clip-runtime/browser-profile-pool.ts"],"names":[],"mappings":"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"}