@parall/daemon 1.44.0 → 1.46.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 (64) hide show
  1. package/bundle/manifest.json +15 -15
  2. package/bundle/parall-browser-pod.js +29726 -375
  3. package/bundle/parall-channel-exec.js +2 -0
  4. package/bundle/parall-claude-agent.js +26446 -384
  5. package/bundle/parall-codex-agent.js +27490 -1549
  6. package/bundle/parall-daemon.js +31558 -1617
  7. package/bundle/parall-openclaw-agent.js +1 -0
  8. package/dist/browser-pod.d.ts +23 -1
  9. package/dist/browser-pod.d.ts.map +1 -1
  10. package/dist/browser-pod.js +104 -34
  11. package/dist/browser-profile-reconcile.d.ts +21 -0
  12. package/dist/browser-profile-reconcile.d.ts.map +1 -0
  13. package/dist/browser-profile-reconcile.js +188 -0
  14. package/dist/cli.d.ts.map +1 -1
  15. package/dist/cli.js +229 -2
  16. package/dist/clip-runtime/browser-cdp.d.ts +40 -0
  17. package/dist/clip-runtime/browser-cdp.d.ts.map +1 -0
  18. package/dist/clip-runtime/browser-cdp.js +218 -0
  19. package/dist/clip-runtime/browser-profile-manager.d.ts +97 -24
  20. package/dist/clip-runtime/browser-profile-manager.d.ts.map +1 -1
  21. package/dist/clip-runtime/browser-profile-manager.js +316 -182
  22. package/dist/clip-runtime/browser-profile-pool.d.ts +3 -0
  23. package/dist/clip-runtime/browser-profile-pool.d.ts.map +1 -1
  24. package/dist/clip-runtime/browser-profile-pool.js +18 -1
  25. package/dist/clip-runtime/browser-proxy-reconcile.d.ts +77 -0
  26. package/dist/clip-runtime/browser-proxy-reconcile.d.ts.map +1 -0
  27. package/dist/clip-runtime/browser-proxy-reconcile.js +139 -0
  28. package/dist/clip-runtime/browser-proxy-state.d.ts +55 -0
  29. package/dist/clip-runtime/browser-proxy-state.d.ts.map +1 -0
  30. package/dist/clip-runtime/browser-proxy-state.js +149 -0
  31. package/dist/clip-runtime/browser-quiescence.d.ts +71 -0
  32. package/dist/clip-runtime/browser-quiescence.d.ts.map +1 -0
  33. package/dist/clip-runtime/browser-quiescence.js +136 -0
  34. package/dist/clip-runtime/browser-readiness.d.ts +64 -0
  35. package/dist/clip-runtime/browser-readiness.d.ts.map +1 -0
  36. package/dist/clip-runtime/browser-readiness.js +161 -0
  37. package/dist/clip-runtime/browser-state-store.d.ts +13 -2
  38. package/dist/clip-runtime/browser-state-store.d.ts.map +1 -1
  39. package/dist/clip-runtime/browser-state-store.js +15 -6
  40. package/dist/clip-runtime/browser-target-registry.d.ts +143 -0
  41. package/dist/clip-runtime/browser-target-registry.d.ts.map +1 -0
  42. package/dist/clip-runtime/browser-target-registry.js +297 -0
  43. package/dist/clip-runtime/browser-viewer-streamer.d.ts +13 -14
  44. package/dist/clip-runtime/browser-viewer-streamer.d.ts.map +1 -1
  45. package/dist/clip-runtime/browser-viewer-streamer.js +11 -63
  46. package/dist/config.d.ts.map +1 -1
  47. package/dist/daemon-main.d.ts.map +1 -1
  48. package/dist/daemon-main.js +3 -1
  49. package/dist/runtime-bin-resolver.d.ts +7 -1
  50. package/dist/runtime-bin-resolver.d.ts.map +1 -1
  51. package/dist/runtime-bin-resolver.js +57 -22
  52. package/dist/runtimes.d.ts +15 -4
  53. package/dist/runtimes.d.ts.map +1 -1
  54. package/dist/runtimes.js +60 -5
  55. package/dist/supervisor.d.ts +6 -0
  56. package/dist/supervisor.d.ts.map +1 -1
  57. package/dist/supervisor.js +52 -188
  58. package/dist/win-lifecycle.d.ts +96 -0
  59. package/dist/win-lifecycle.d.ts.map +1 -0
  60. package/dist/win-lifecycle.js +229 -0
  61. package/dist/win-service.d.ts +119 -0
  62. package/dist/win-service.d.ts.map +1 -0
  63. package/dist/win-service.js +226 -0
  64. package/package.json +8 -6
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { createRequire as __prllCreateRequire } from 'node:module'; const require = __prllCreateRequire(import.meta.url);
2
3
 
3
4
  // ts/openclaw-agent/dist/index.js
4
5
  import { execFileSync, spawn } from "node:child_process";
@@ -68,6 +68,13 @@ export interface BrowserPodConfig {
68
68
  /** Per-profile outbound proxy (controller-injected via PRLL_BROWSER_PROXY_* env on a
69
69
  * cold pod, or the /assign body on a warm one). null = direct egress. */
70
70
  proxy: BrowserProxyConfig | null;
71
+ /** Proxy readiness probe endpoint (PRLL_BROWSER_PROXY_PROBE_URL; the controller
72
+ * injects `<public api base>/generate_204` via ProxyProbeURLFromAPIBase).
73
+ * MUST be a PUBLIC, proxy-resolvable first-party 204 — never a cluster-internal
74
+ * /health host, which an external residential/datacenter proxy cannot resolve
75
+ * (the probe's whole point is "can we reach the public internet through this
76
+ * proxy"). Zero-byte 204 keeps per-GB proxy cost negligible. */
77
+ proxyProbeUrl: string;
71
78
  }
72
79
  /**
73
80
  * Resolve and validate the pod's runtime config from the environment. Throws on
@@ -76,6 +83,14 @@ export interface BrowserPodConfig {
76
83
  * doomed reconnect loop).
77
84
  */
78
85
  export declare function resolveBrowserPodConfig(env?: NodeJS.ProcessEnv): BrowserPodConfig;
86
+ /** Probe endpoint for proxy readiness (browser-readiness.ts). The probe navigates
87
+ * a sacrificial tab THROUGH the profile's proxy, so the target must be a PUBLIC,
88
+ * proxy-resolvable host — the pod's cluster-internal clip-service (`clipRpcUrl`)
89
+ * could NEVER be resolved by an external residential/datacenter proxy, so it is
90
+ * deliberately not used here. Order: explicit `PRLL_BROWSER_PROXY_PROBE_URL`
91
+ * (controller may inject a per-env Parall-owned public 204), else a well-known
92
+ * public 204 fallback. `clipRpcUrl` is unused now; kept for signature stability. */
93
+ export declare function resolveProxyProbeUrl(env: NodeJS.ProcessEnv, _clipRpcUrl: string): string;
79
94
  /** Resolved warm-pool config — the identity-less env a warm pod boots with. */
80
95
  export interface PoolPodConfig {
81
96
  /** Controller-minted pba_ pool bootstrap token; the bearer required on POST /assign. */
@@ -112,6 +127,9 @@ export interface AssignRequest {
112
127
  orgId: string;
113
128
  leaseId: string;
114
129
  generation: number;
130
+ /** State epoch (reset_generation at assignment) — scopes the S3 state key.
131
+ * Absent from an older controller → 0 (legacy un-suffixed key). */
132
+ stateEpoch: number;
115
133
  state?: AssignStateCreds;
116
134
  /** Per-profile outbound proxy (warm-pod counterpart of the cold pod's
117
135
  * PRLL_BROWSER_PROXY_* env). Absent = direct egress. */
@@ -205,10 +223,14 @@ export declare class BrowserPodRuntime {
205
223
  private readonly log;
206
224
  private readonly checkpointer?;
207
225
  private readonly checkpointIntervalMs;
226
+ private readonly quiesce?;
208
227
  private draining;
209
228
  private drainPromise;
210
229
  private checkpointTimer;
211
- constructor(provider: PodProvider, browser: PodBrowser, log: Pick<GatewayLogger, 'info' | 'warn' | 'error'>, checkpointer?: PodStateCheckpointer | undefined, checkpointIntervalMs?: number);
230
+ constructor(provider: PodProvider, browser: PodBrowser, log: Pick<GatewayLogger, 'info' | 'warn' | 'error'>, checkpointer?: PodStateCheckpointer | undefined, checkpointIntervalMs?: number, quiesce?: (() => Promise<{
231
+ quiesced: boolean;
232
+ reason?: string;
233
+ }>) | undefined);
212
234
  /**
213
235
  * Route-readiness: registered to the hub AND not draining. `draining` is checked
214
236
  * first so the readiness gate flips false the instant a drain begins — before
@@ -1 +1 @@
1
- {"version":3,"file":"browser-pod.d.ts","sourceRoot":"","sources":["../src/browser-pod.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"browser-pod.d.ts","sourceRoot":"","sources":["../src/browser-pod.ts"],"names":[],"mappings":"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"}
@@ -52,6 +52,9 @@ import * as path from 'node:path';
52
52
  import { pathToFileURL } from 'node:url';
53
53
  import { createLogger } from '@parall/agent-core';
54
54
  import { BrowserProfileManager, } from './clip-runtime/browser-profile-manager.js';
55
+ import { normalizeProxyConfig } from './clip-runtime/browser-proxy-state.js';
56
+ import { awaitBrowserQuiescence } from './clip-runtime/browser-quiescence.js';
57
+ import { PUBLIC_PROXY_PROBE_FALLBACK_URL } from './clip-runtime/browser-readiness.js';
55
58
  import { BrowserStateStore } from './clip-runtime/browser-state-store.js';
56
59
  import { ClipProvider } from './clip-runtime/clip-provider.js';
57
60
  import { ClipProcessManager } from './clip-runtime/process-manager.js';
@@ -115,11 +118,30 @@ export function resolveBrowserPodConfig(env = process.env) {
115
118
  stateDir,
116
119
  homeDir,
117
120
  proxy: parseProxyEnv(env),
121
+ proxyProbeUrl: resolveProxyProbeUrl(env, clipRpcUrl),
118
122
  };
119
123
  }
124
+ /** Probe endpoint for proxy readiness (browser-readiness.ts). The probe navigates
125
+ * a sacrificial tab THROUGH the profile's proxy, so the target must be a PUBLIC,
126
+ * proxy-resolvable host — the pod's cluster-internal clip-service (`clipRpcUrl`)
127
+ * could NEVER be resolved by an external residential/datacenter proxy, so it is
128
+ * deliberately not used here. Order: explicit `PRLL_BROWSER_PROXY_PROBE_URL`
129
+ * (controller may inject a per-env Parall-owned public 204), else a well-known
130
+ * public 204 fallback. `clipRpcUrl` is unused now; kept for signature stability. */
131
+ export function resolveProxyProbeUrl(env, _clipRpcUrl) {
132
+ const override = env.PRLL_BROWSER_PROXY_PROBE_URL?.trim();
133
+ if (override)
134
+ return override;
135
+ return PUBLIC_PROXY_PROBE_FALLBACK_URL;
136
+ }
120
137
  /** Parse the controller-injected PRLL_BROWSER_PROXY_* env into a proxy config, or
121
138
  * null when no proxy is set (PRLL_BROWSER_PROXY_SERVER absent → direct egress).
122
- * The cold pod gets these as env; the warm pod's /assign writes the same vars. */
139
+ * The cold pod gets these as env; the warm pod's /assign writes the same vars.
140
+ * Validation is the SHARED rule set (browser-proxy-state.ts, mirroring the Go
141
+ * server): orphan/half credentials, credential-less proxies (silently dropped by
142
+ * bb-browser → real-IP egress), SOCKS5+auth, and unknown schemes all FAIL CLOSED
143
+ * here — a misconfigured pod spec or a legacy DB row must crash the pod at boot,
144
+ * never leak traffic past the intended proxy. */
123
145
  function parseProxyEnv(env) {
124
146
  const server = env.PRLL_BROWSER_PROXY_SERVER?.trim();
125
147
  // username + password are credential-like passthrough — kept VERBATIM (never
@@ -127,23 +149,12 @@ function parseProxyEnv(env) {
127
149
  // a provider could legitimately use surrounding whitespace. Empty string → undefined.
128
150
  const username = env.PRLL_BROWSER_PROXY_USERNAME || undefined;
129
151
  const password = env.PRLL_BROWSER_PROXY_PASSWORD || undefined;
130
- if (!server) {
131
- // Orphan credentials with no server must FAIL CLOSED, not silently fall through
132
- // to direct egress: a misconfigured pod spec that drops the server var while
133
- // keeping creds should crash here, not leak traffic past the intended proxy.
134
- if (username !== undefined || password !== undefined) {
135
- throw new Error('browser-pod: PRLL_BROWSER_PROXY_USERNAME/PASSWORD set without PRLL_BROWSER_PROXY_SERVER');
136
- }
137
- return null; // no proxy at all → direct egress
152
+ if (!server && (username !== undefined || password !== undefined)) {
153
+ throw new Error('browser-pod: PRLL_BROWSER_PROXY_USERNAME/PASSWORD set without PRLL_BROWSER_PROXY_SERVER');
138
154
  }
139
- // Reject half-auth at the pod boundary too (the server already enforces the
140
- // user+pass pair). A version skew / bad pod spec should fail fast here, not
141
- // forward incomplete creds to account_create where it becomes a confusing
142
- // runtime auth failure.
143
- if ((username === undefined) !== (password === undefined)) {
144
- throw new Error('browser-pod: PRLL_BROWSER_PROXY_USERNAME and PRLL_BROWSER_PROXY_PASSWORD must be set together');
145
- }
146
- return { server, username, password };
155
+ if (!server)
156
+ return null; // no proxy at all direct egress
157
+ return normalizeProxyConfig({ server, username, password });
147
158
  }
148
159
  /**
149
160
  * Resolve a warm pod's identity-less config. Unlike resolveBrowserPodConfig this
@@ -192,6 +203,9 @@ export function parseAssignRequest(body) {
192
203
  const orgId = str(b.org_id);
193
204
  const leaseId = str(b.lease_id);
194
205
  const generation = typeof b.generation === 'number' && Number.isFinite(b.generation) ? b.generation : 0;
206
+ const stateEpoch = typeof b.state_epoch === 'number' && Number.isSafeInteger(b.state_epoch) && b.state_epoch > 0
207
+ ? b.state_epoch
208
+ : 0;
195
209
  if (!assignmentToken)
196
210
  return { ok: false, error: 'assignment_token required' };
197
211
  if (!assignmentToken.startsWith(ASSIGNMENT_TOKEN_PREFIX)) {
@@ -237,16 +251,19 @@ export function parseAssignRequest(body) {
237
251
  // empty string → undefined. (server is a structured host:port, so it is trimmed.)
238
252
  const username = (typeof p.username === 'string' ? p.username : '') || undefined;
239
253
  const password = (typeof p.password === 'string' ? p.password : '') || undefined;
240
- // Reject half-auth (same invariant the server enforces) before it reaches
241
- // account_create as a confusing runtime auth failure.
242
- if ((username === undefined) !== (password === undefined)) {
243
- return { ok: false, error: 'proxy username and password must be provided together' };
254
+ // Shared rule set (browser-proxy-state.ts, mirrors the Go server): half or
255
+ // missing credentials, SOCKS5+auth, and unknown schemes are rejected before
256
+ // they can reach account_create as a silent drop / confusing runtime failure.
257
+ try {
258
+ proxy = normalizeProxyConfig({ server, username, password }) ?? undefined;
259
+ }
260
+ catch (err) {
261
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
244
262
  }
245
- proxy = { server, username, password };
246
263
  }
247
264
  return {
248
265
  ok: true,
249
- value: { assignmentToken, profileId, orgId, leaseId, generation, state, proxy },
266
+ value: { assignmentToken, profileId, orgId, leaseId, generation, stateEpoch, state, proxy },
250
267
  };
251
268
  }
252
269
  /**
@@ -268,6 +285,7 @@ export function applyAssignmentEnv(env, req) {
268
285
  env.AWS_REGION = req.state.region;
269
286
  env.PRLL_BROWSER_STATE_BUCKET = req.state.bucket;
270
287
  env.PRLL_STATE_GENERATION = String(req.generation);
288
+ env.PRLL_STATE_EPOCH = String(req.stateEpoch);
271
289
  if (req.state.endpoint)
272
290
  env.PRLL_BROWSER_STATE_ENDPOINT = req.state.endpoint;
273
291
  }
@@ -362,18 +380,27 @@ export class BrowserPodRuntime {
362
380
  log;
363
381
  checkpointer;
364
382
  checkpointIntervalMs;
383
+ quiesce;
365
384
  draining = false;
366
385
  drainPromise = null;
367
386
  checkpointTimer = null;
368
387
  constructor(provider, browser, log,
369
388
  // Optional S3 state checkpointer (PR4). Absent => stateless pod (no
370
389
  // hydrate/checkpoint); the drain order then matches the pre-PR4 contract.
371
- checkpointer, checkpointIntervalMs = DEFAULT_CHECKPOINT_INTERVAL_MS) {
390
+ checkpointer, checkpointIntervalMs = DEFAULT_CHECKPOINT_INTERVAL_MS,
391
+ // Chrome-exit barrier before the FINAL checkpoint (browser-quiescence.ts):
392
+ // bb-browser exits without awaiting Chrome, so the drain must independently
393
+ // confirm the user-data-dir is quiet before snapshotting it. Absent (older
394
+ // wiring / stateless pod) => checkpoint immediately after browser.stop() as
395
+ // before. quiesced=false => the final checkpoint is SKIPPED — a possibly-torn
396
+ // snapshot must never be published (bounded loss, structured error).
397
+ quiesce) {
372
398
  this.provider = provider;
373
399
  this.browser = browser;
374
400
  this.log = log;
375
401
  this.checkpointer = checkpointer;
376
402
  this.checkpointIntervalMs = checkpointIntervalMs;
403
+ this.quiesce = quiesce;
377
404
  }
378
405
  /**
379
406
  * Route-readiness: registered to the hub AND not draining. `draining` is checked
@@ -436,17 +463,35 @@ export class BrowserPodRuntime {
436
463
  catch (err) {
437
464
  this.log.warn(`browser stop during drain failed: ${String(err)}`);
438
465
  }
439
- // Final checkpoint AFTER bb-browser has torn down Chromium has flushed and
440
- // released its files, so the snapshot is consistent. A failure here is bounded
441
- // loss (the delta since the last successful checkpoint), logged below —
442
- // durability is best-effort + on-close, with no controller-side verify/repair
443
- // (design §3.2 "simplified durability").
466
+ // Final checkpoint AFTER bb-browser has torn down AND Chrome's exit +
467
+ // write-quiescence are CONFIRMED (bb-browser SIGTERMs Chrome and exits
468
+ // without awaiting it, so browser.stop() returning does not mean the
469
+ // user-data-dir is quiet). An unconfirmed exit SKIPS the upload — a
470
+ // possibly-torn snapshot must never be published as good state; the last
471
+ // periodic checkpoint stands (bounded loss, design §3.2 "simplified
472
+ // durability", no controller-side verify/repair).
444
473
  if (this.checkpointer) {
445
- try {
446
- await this.checkpointer.checkpointFinal();
474
+ let safe = true;
475
+ if (this.quiesce) {
476
+ try {
477
+ const q = await this.quiesce();
478
+ if (!q.quiesced) {
479
+ safe = false;
480
+ this.log.error(`BROWSER_CHECKPOINT_UNSAFE: ${q.reason ?? 'browser did not quiesce'} — skipping the final checkpoint (the last periodic snapshot stands)`);
481
+ }
482
+ }
483
+ catch (err) {
484
+ safe = false;
485
+ this.log.error(`BROWSER_CHECKPOINT_UNSAFE: quiescence check failed (${String(err)}) — skipping the final checkpoint`);
486
+ }
447
487
  }
448
- catch (err) {
449
- this.log.error(`final checkpoint during drain failed: ${String(err)}`);
488
+ if (safe) {
489
+ try {
490
+ await this.checkpointer.checkpointFinal();
491
+ }
492
+ catch (err) {
493
+ this.log.error(`final checkpoint during drain failed: ${String(err)}`);
494
+ }
450
495
  }
451
496
  }
452
497
  })();
@@ -570,6 +615,18 @@ export function buildStateStore(config, env, log) {
570
615
  if (!Number.isSafeInteger(gen) || gen < 0) {
571
616
  throw new Error(`browser state enabled (bucket=${bucket}) but PRLL_STATE_GENERATION="${genRaw}" is not a non-negative safe integer — refusing to run a non-persisted profile (fail closed)`);
572
617
  }
618
+ // Epoch is deliberately fail-OPEN to 0 (unlike the generation): a missing value
619
+ // means an older controller that predates epoch-scoped keys, and 0 IS the
620
+ // legacy key — the correct read for every profile that has never been reset
621
+ // under the new scheme. A garbled value still refuses (misconfiguration).
622
+ const epochRaw = env.PRLL_STATE_EPOCH?.trim();
623
+ let epoch = 0;
624
+ if (epochRaw) {
625
+ epoch = Number(epochRaw);
626
+ if (!Number.isSafeInteger(epoch) || epoch < 0) {
627
+ throw new Error(`browser state enabled (bucket=${bucket}) but PRLL_STATE_EPOCH="${epochRaw}" is not a non-negative safe integer — refusing to run a non-persisted profile (fail closed)`);
628
+ }
629
+ }
573
630
  return new BrowserStateStore({
574
631
  bucket,
575
632
  region: env.AWS_REGION?.trim() || env.PRLL_BROWSER_STATE_REGION?.trim() || '',
@@ -577,6 +634,7 @@ export function buildStateStore(config, env, log) {
577
634
  orgId: config.orgId,
578
635
  profileId: config.profileId,
579
636
  generation: gen,
637
+ epoch,
580
638
  homeDir: config.homeDir,
581
639
  log,
582
640
  });
@@ -632,6 +690,7 @@ export async function preparePodRuntime(config, env, log) {
632
690
  homeDir: config.homeDir,
633
691
  log,
634
692
  resolveProxy: () => config.proxy,
693
+ proxyProbeUrl: config.proxyProbeUrl,
635
694
  });
636
695
  // The process manager hosts no installable clips in pod mode — its only job is
637
696
  // to advertise the synthetic "browser" capability (getProviderClips appends it
@@ -691,7 +750,18 @@ export async function preparePodRuntime(config, env, log) {
691
750
  else {
692
751
  log.info('browser state externalization disabled (no PRLL_BROWSER_STATE_BUCKET) — running stateless');
693
752
  }
694
- return new BrowserPodRuntime(provider, browser, log, checkpointer, resolveCheckpointIntervalMs(env));
753
+ // Chrome-exit barrier before the final checkpoint — only meaningful when a
754
+ // checkpointer exists (its whole purpose is snapshot consistency). The
755
+ // manager's spawned process group is the ownership handle: quiescence waits
756
+ // for (and if needed, signals) exactly that group — never a scanned pid.
757
+ const quiesce = checkpointer
758
+ ? () => awaitBrowserQuiescence({
759
+ pgid: browser.lastKnownProcessGroup(),
760
+ cdpEndpoint: browser.lastKnownCdpEndpoint(),
761
+ log,
762
+ })
763
+ : undefined;
764
+ return new BrowserPodRuntime(provider, browser, log, checkpointer, resolveCheckpointIntervalMs(env), quiesce);
695
765
  }
696
766
  /**
697
767
  * Wire the real clip-runtime stack for a hosted browser pod and run until the
@@ -0,0 +1,21 @@
1
+ import type { GatewayLogger } from '@parall/agent-core';
2
+ import type { ParallClient } from '@parall/sdk';
3
+ import type { BrowserProfilePool } from './clip-runtime/index.js';
4
+ export interface BrowserProfileReconcileContext {
5
+ /** The pool captured by the caller BEFORE the server fetch. */
6
+ pool: BrowserProfilePool;
7
+ /** The supervisor's CURRENT pool — a concurrent stop() may have nulled/replaced it. */
8
+ currentPool: () => BrowserProfilePool | null;
9
+ client: Pick<ParallClient, 'listMachineBrowserProfiles'>;
10
+ log: GatewayLogger;
11
+ /** Serialized per-profile op (stop/reset/release/wipe) — the supervisor's queue. */
12
+ enqueueOp: (profileId: string, op: () => Promise<void>) => Promise<void>;
13
+ /** Serialized per-profile REVIVE (open/ensure), tracked for reconnect fencing. */
14
+ enqueueRevive: (profileId: string, op: () => Promise<void>) => Promise<void>;
15
+ /** Wipe-then-report-error-on-failure, then re-throw so the caller skips the open. */
16
+ wipeBeforeRevive: (pool: BrowserProfilePool, profileId: string, resetGen: number, generation: number | undefined) => Promise<void>;
17
+ /** Snapshot of profiles with a queued-but-not-yet-live revive (pre-fetch). */
18
+ pendingReviveIds: () => Set<string>;
19
+ }
20
+ export declare function reconcileBrowserProfiles(ctx: BrowserProfileReconcileContext): Promise<void>;
21
+ //# sourceMappingURL=browser-profile-reconcile.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser-profile-reconcile.d.ts","sourceRoot":"","sources":["../src/browser-profile-reconcile.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,KAAK,EAAyB,YAAY,EAAE,MAAM,aAAa,CAAC;AACvE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAYlE,MAAM,WAAW,8BAA8B;IAC7C,+DAA+D;IAC/D,IAAI,EAAE,kBAAkB,CAAC;IACzB,uFAAuF;IACvF,WAAW,EAAE,MAAM,kBAAkB,GAAG,IAAI,CAAC;IAC7C,MAAM,EAAE,IAAI,CAAC,YAAY,EAAE,4BAA4B,CAAC,CAAC;IACzD,GAAG,EAAE,aAAa,CAAC;IACnB,oFAAoF;IACpF,SAAS,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACzE,kFAAkF;IAClF,aAAa,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7E,qFAAqF;IACrF,gBAAgB,EAAE,CAChB,IAAI,EAAE,kBAAkB,EACxB,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,GAAG,SAAS,KAC3B,OAAO,CAAC,IAAI,CAAC,CAAC;IACnB,8EAA8E;IAC9E,gBAAgB,EAAE,MAAM,GAAG,CAAC,MAAM,CAAC,CAAC;CACrC;AAED,wBAAsB,wBAAwB,CAAC,GAAG,EAAE,8BAA8B,GAAG,OAAO,CAAC,IAAI,CAAC,CA8LjG"}
@@ -0,0 +1,188 @@
1
+ export async function reconcileBrowserProfiles(ctx) {
2
+ const pool = ctx.pool;
3
+ // Snapshot the locally-live set BEFORE the server fetch. A profile opened via a
4
+ // WS lifecycle event DURING the fetch is NOT in this snapshot, so the negative
5
+ // convergence below (which is stale w.r.t. that open) can never release it.
6
+ // There is no periodic browser reconcile to re-open a wrongly-released profile —
7
+ // it only runs on startup / WS-hello — so a stale release would strand it until
8
+ // the next reconnect.
9
+ const activeBeforeList = new Set(pool.activeProfileIds());
10
+ // Also snapshot revives (open/ensure) queued but not yet a live manager: such a
11
+ // stale open is invisible to activeProfileIds() and would otherwise drain after a
12
+ // missed offline stop/delete and start a runtime the server no longer wants.
13
+ const pendingReviveBeforeList = ctx.pendingReviveIds();
14
+ // And ALL local profile state — per-profile homes AND legacy flat account files
15
+ // (the pool owns that storage layout). State with NO server row is a
16
+ // deleted/reassigned profile whose cookies/storage must be wiped from this host;
17
+ // a pre-upgrade profile never reopened after migration exists only as a legacy flat
18
+ // file, so home-only enumeration would miss it. Pre-fetch so state created during
19
+ // the fetch is left alone.
20
+ const localStateBeforeList = new Set(pool.localStateProfileIds());
21
+ let profiles;
22
+ try {
23
+ profiles = await ctx.client.listMachineBrowserProfiles();
24
+ }
25
+ catch (err) {
26
+ ctx.log.warn(`browser profile reconcile: list failed: ${String(err)}`);
27
+ return;
28
+ }
29
+ // The pool must still be the same instance after the await — stop() may have
30
+ // nulled/replaced it during a concurrent shutdown; bail rather than act on a
31
+ // torn-down pool (also avoids NPE-ing through Promise.allSettled into warnings).
32
+ if (ctx.currentPool() !== pool)
33
+ return;
34
+ // Converge local runtimes to the server's desired-live set (the server is the
35
+ // SSOT). The supervisor is the desired-state coordinator; the pool is the
36
+ // per-profile runtime registry. Every op here goes through
37
+ // enqueueBrowserProfileOp so all supervisor-originated profile work shares one
38
+ // ordering layer (the pool's own queue is the final per-profile mutex).
39
+ const desiredLive = new Set();
40
+ const serverIds = new Set(); // every byoc profile the server still lists (any status)
41
+ const serverResetGen = new Map();
42
+ const ops = [];
43
+ // reset_generation (server) is the SSOT for reset intent; the daemon's per-profile
44
+ // sidecar (read via pool.appliedResetGeneration) is only a local ack of "already
45
+ // wiped up to this generation". A missed reset (server newer than the local ack) must
46
+ // wipe local state BEFORE any open/ensure, so a reopen starts clean and never
47
+ // resurrects the cookies the reset cleared. Returns the server reset_generation to
48
+ // apply, or null if nothing is missed. Pure (no side effects).
49
+ const missedResetGen = (profileId) => {
50
+ const gen = serverResetGen.get(profileId) ?? 0;
51
+ return gen > pool.appliedResetGeneration(profileId) ? gen : null;
52
+ };
53
+ // Negative-path helper (a stopped profile with a missed reset): fence synchronously
54
+ // then enqueue a STANDALONE wipe. No revive follows, so a wipe failure simply leaves
55
+ // the profile fenced and the sidecar unwritten → the next reconcile retries (the
56
+ // SSOT reset_generation is still unapplied locally). Fail-closed by construction.
57
+ const enqueueResetWipeIfMissed = (profileId) => {
58
+ const gen = missedResetGen(profileId);
59
+ if (gen === null)
60
+ return false;
61
+ // Fence SYNCHRONOUSLY before the async wipe — same contract as the WS stop/reset
62
+ // path. On a machine-WS reconnect whose clip-provider stream stays live, a hub
63
+ // invoke could otherwise run on the stale runtime in the gap before the wipe op
64
+ // starts. (In the negative loop this re-asserts an already-set fence within the same
65
+ // synchronous iteration — atomic, no revive can observe an intermediate generation.)
66
+ pool.fence(profileId);
67
+ ops.push(ctx.enqueueOp(profileId, () => pool.applyResetWipe(profileId, gen)));
68
+ return true;
69
+ };
70
+ for (const profile of profiles) {
71
+ // The daemon supervises only machine-bound (byoc) profiles; hosted profiles
72
+ // (null machine_id) run on the platform pool and are never owned by a daemon.
73
+ if (!profile.machine_id)
74
+ continue;
75
+ serverIds.add(profile.id);
76
+ serverResetGen.set(profile.id, profile.reset_generation ?? 0);
77
+ if (profile.status !== 'running' && profile.status !== 'pending')
78
+ continue;
79
+ desiredLive.add(profile.id);
80
+ // A missed reset must wipe SUCCESSFULLY before this profile is revived. Fence
81
+ // synchronously now (reject hub invokes in the gap before the queued wipe runs).
82
+ const wipeGen = missedResetGen(profile.id);
83
+ if (wipeGen !== null)
84
+ pool.fence(profile.id);
85
+ // Capture the fence generation AFTER any fence bump so a stop arriving while the op
86
+ // waits makes it skip as stale; carry the lifecycle generation so the daemon's
87
+ // `running` report is fenced server-side. Drive desired state through the CAPTURED
88
+ // pool (not enqueueBrowserProfileLifecycle, which re-reads this.browserProfilePool
89
+ // and would negative-ACK `error` for a pending profile if stop() nulled the field).
90
+ const sinceSeq = pool.stopSeqOf(profile.id);
91
+ const generation = profile.lifecycle_generation;
92
+ // forceStatusReport ONLY for a `pending` row: the daemon's last report may already be
93
+ // `running` (Chromium survived the WS drop), so a deduped repeat would leave the
94
+ // server stuck at `pending`. A `running` row keeps the normal dedup.
95
+ const forceStatusReport = profile.status === 'pending';
96
+ ops.push(ctx.enqueueRevive(profile.id, async () => {
97
+ // Reset-before-open, FAIL-CLOSED: a missed reset must wipe successfully BEFORE
98
+ // the revive un-fences and reopens. If applyResetWipe throws, the await
99
+ // propagates and ensureRuntime is SKIPPED — the profile stays fenced (the wipe
100
+ // fenced it and the applied-reset sidecar was NOT written), so the next reconcile
101
+ // retries because the SSOT reset_generation is still unapplied locally. This
102
+ // ordering is expressed locally; the enqueueBrowserProfileOp queue contract (a
103
+ // rejected op does not block later ops) is intentionally unchanged. A failed
104
+ // wipe reports `error` (visible) and re-throws here, so ensureRuntime is skipped.
105
+ if (wipeGen !== null)
106
+ await ctx.wipeBeforeRevive(pool, profile.id, wipeGen, generation);
107
+ // Reconnect recovery is liveness-only: ensure the runtime is live (account +
108
+ // tab) for both pending and running. The original open's `start_url` is NOT
109
+ // replayed (it rides only the live lifecycle event, not persisted); a `pending`
110
+ // profile whose open event the daemon missed comes up at the default page rather
111
+ // than a targetless `about:blank`. Persisting start_url is a separate follow-up.
112
+ await pool.ensureRuntime(profile.id, sinceSeq, generation, { forceStatusReport });
113
+ }));
114
+ }
115
+ // Negative convergence over everything locally known but NOT desired — active
116
+ // runtimes, queued revives (open/ensure not yet a manager), and local state (homes +
117
+ // legacy flat files) — all from the PRE-FETCH snapshots, so a profile that appeared
118
+ // DURING the fetch is left alone (no periodic reconcile would undo a wrong teardown).
119
+ const candidates = new Set([
120
+ ...activeBeforeList,
121
+ ...pendingReviveBeforeList,
122
+ ...localStateBeforeList,
123
+ ]);
124
+ for (const profileId of candidates) {
125
+ if (desiredLive.has(profileId))
126
+ continue;
127
+ const isActive = activeBeforeList.has(profileId);
128
+ const hasLocalState = localStateBeforeList.has(profileId);
129
+ // Pending-only revive (queued open/ensure, no runtime, no on-disk state at the
130
+ // pre-fetch snapshot): the synchronous fence alone neutralizes the stale open (it
131
+ // bumps stopSeq, so the queued revive skips as stale). Do NOT AWAIT a teardown — it
132
+ // would block behind the possibly-stuck queue and stall the whole reconcile (the
133
+ // queued-stale-open deadlock).
134
+ if (!isActive && !hasLocalState) {
135
+ pool.fence(profileId);
136
+ // Race: a pending-only revive can COMPLETE during the list fetch (create a manager
137
+ // + on-disk state) BEFORE this fence — the snapshot is then stale, so the fence
138
+ // alone leaves an orphaned runtime/disk the negative loop never converged. Converge
139
+ // it FIRE-AND-FORGET (NOT in `ops`: a still-stuck revive queue must not hang the
140
+ // reconcile; it runs once the queue drains — after the fenced open skips or
141
+ // completes — and is a no-op if nothing was created):
142
+ // - deleted/reassigned → wipeAbsent: the old host must keep no session data.
143
+ // - present + missed reset → applyResetWipe: the completed open may have applied
144
+ // only its event's (older) reset_generation, not the current one.
145
+ // - present + stopped → releaseRuntime: free the orphaned Chromium; a plain
146
+ // stop keeps its cookies on disk.
147
+ let cleanup;
148
+ if (!serverIds.has(profileId)) {
149
+ cleanup = pool.wipeAbsent(profileId);
150
+ }
151
+ else {
152
+ const wipeGen = missedResetGen(profileId);
153
+ cleanup =
154
+ wipeGen !== null
155
+ ? pool.applyResetWipe(profileId, wipeGen)
156
+ : pool.releaseRuntime(profileId);
157
+ }
158
+ void cleanup.catch((err) => ctx.log.warn(`browser profile pending-revive cleanup failed for ${profileId}: ${String(err)}`));
159
+ continue;
160
+ }
161
+ if (!serverIds.has(profileId)) {
162
+ // Absent from the server list → deleted/reassigned → fence + wipe local state (the
163
+ // old host must not keep cookies/storage). No status report — server is the SSOT.
164
+ pool.fence(profileId);
165
+ ops.push(ctx.enqueueOp(profileId, () => pool.wipeAbsent(profileId)));
166
+ }
167
+ else if (enqueueResetWipeIfMissed(profileId)) {
168
+ // Present (stopped) with a missed reset → fenced synchronously + wipe enqueued.
169
+ }
170
+ else if (isActive) {
171
+ // Present (stopped) but the runtime is still live → fence + release the runtime
172
+ // only; a plain stop keeps its cookies on disk.
173
+ pool.fence(profileId);
174
+ ops.push(ctx.enqueueOp(profileId, () => pool.releaseRuntime(profileId)));
175
+ }
176
+ // else: present + stopped + on-disk state + no runtime + no missed reset → LEAVE
177
+ // ALONE (do NOT fence). Its stopped-fence is already held — the start-sequence
178
+ // pre-fence on a fresh daemon, or the persisted pool state on a WS reconnect — so a
179
+ // hub invoke is still rejected. Re-fencing here would bump stopSeq and drop an `open`
180
+ // that arrived during the list fetch (it captured an older sinceSeq, and the stale
181
+ // `stopped` snapshot doesn't reflect it yet), stranding the server at `pending`.
182
+ }
183
+ for (const result of await Promise.allSettled(ops)) {
184
+ if (result.status === 'rejected') {
185
+ ctx.log.warn(`browser profile reconcile op failed: ${String(result.reason)}`);
186
+ }
187
+ }
188
+ }
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
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"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAmkBA,wBAAsB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,YAAY,GAAG,SAAS,CAAC,CAuD9E"}