@parall/daemon 1.34.0 → 1.36.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.
@@ -39,6 +39,26 @@ export interface ClipProviderOptions {
39
39
  * re-trigger machine.hello. Optional.
40
40
  */
41
41
  onPersistentFailure?: () => void;
42
+ /**
43
+ * Hosted-browser live-viewer handler. Set ONLY for a hosted browser pod
44
+ * (browser-pod.ts): clip-service relays viewer commands down the
45
+ * ProviderStream and the pod replies over it (design §3.4). Returns the
46
+ * streamer result on success; throws to signal a command-level error (its
47
+ * message is forwarded to the viewer). Absent for ordinary BYOC providers,
48
+ * which never receive ViewerCommands. Callers should serialize their own
49
+ * streamer state if needed — the provider invokes this per inbound command.
50
+ */
51
+ onViewerCommand?: (cmd: {
52
+ profileId: string;
53
+ sessionId: string;
54
+ command: string;
55
+ input?: Record<string, unknown>;
56
+ turn?: {
57
+ url: string;
58
+ username?: string;
59
+ credential?: string;
60
+ };
61
+ }) => Promise<Record<string, unknown>>;
42
62
  }
43
63
  export declare class ClipProvider {
44
64
  private readonly opts;
@@ -52,6 +72,7 @@ export declare class ClipProvider {
52
72
  private statusUnsubscribe;
53
73
  private manifestUnsubscribe;
54
74
  private needsReregister;
75
+ private lastRegisteredKey;
55
76
  private streamGeneration;
56
77
  private static RECONNECT_BASE_MS;
57
78
  private static RECONNECT_MAX_MS;
@@ -69,6 +90,31 @@ export declare class ClipProvider {
69
90
  private clearReconnectTimer;
70
91
  private clearHeartbeatTimer;
71
92
  private startHeartbeat;
93
+ /**
94
+ * Re-register the current clip set on the OPEN stream — the daemon's response
95
+ * to a manifest/clip-set change. The hub overwrites its stored set from this
96
+ * full register (clip-provider-sharing-design §11), so a clip install / wake /
97
+ * sleep no longer requires tearing down and reopening the ProviderStream,
98
+ * which used to drop any invoke in flight on that stream (in-flight requests
99
+ * are keyed by request_id on the hub and survive a clip-set swap).
100
+ *
101
+ * No-op when the stream is not currently writable; needsReregister stays set
102
+ * so the next heartbeat tick (or the post-connect flush) retries.
103
+ *
104
+ * Against an older hub that still rejects an in-stream duplicate register, the
105
+ * write reaches a stream the hub then resets; the resulting stream error runs
106
+ * the normal reconnect path, which re-registers as the first frame — i.e. it
107
+ * degrades to the previous reconnect-to-re-register behavior, so no capability
108
+ * negotiation is needed (deploy the hub first).
109
+ */
110
+ private flushReregister;
111
+ /**
112
+ * Re-register the current clip set on the open stream — public entry for the
113
+ * supervisor when machine clip assignments change. Same in-stream path as a
114
+ * manifest update (no reconnect, in-flight invokes survive). Safe when
115
+ * disconnected: the flag is flushed on the next heartbeat / post-connect.
116
+ */
117
+ reregister(): void;
72
118
  private closeStream;
73
119
  private closeSession;
74
120
  private subscribeStatusChanges;
@@ -76,6 +122,16 @@ export declare class ClipProvider {
76
122
  private handleHubMessage;
77
123
  private handleRegistered;
78
124
  private handleInvokeCommand;
125
+ /**
126
+ * Handle a hosted-browser live-viewer command relayed by clip-service over the
127
+ * stream and reply on the same stream (design §3.4). Always answers the
128
+ * request/reply bridge exactly once — `{result}` on success, `{error:{message}}`
129
+ * on failure — and never throws into the message loop, mirroring the BYOC
130
+ * daemon's handleBrowserProfileViewer (which replies over HTTP instead).
131
+ */
132
+ private handleViewerCommand;
133
+ /** Send a hosted-viewer reply (base64-encoded JSON body) on the stream. */
134
+ private sendViewerReply;
79
135
  private sendRegister;
80
136
  private sendProviderMessage;
81
137
  private buildClipRegistrations;
@@ -1 +1 @@
1
- {"version":3,"file":"clip-provider.d.ts","sourceRoot":"","sources":["../../src/clip-runtime/clip-provider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAGH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AA+I/D,MAAM,WAAW,mBAAmB;IAClC,sDAAsD;IACtD,UAAU,EAAE,MAAM,CAAC;IACnB,6CAA6C;IAC7C,OAAO,EAAE,MAAM,CAAC;IAChB,sEAAsE;IACtE,KAAK,EAAE,MAAM,CAAC;IACd,mDAAmD;IACnD,YAAY,EAAE,MAAM,CAAC;IACrB,yDAAyD;IACzD,WAAW,EAAE,kBAAkB,CAAC;IAChC,mBAAmB;IACnB,GAAG,EAAE;QAAE,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;IACpF;;;;;;OAMG;IACH,mBAAmB,CAAC,EAAE,MAAM,IAAI,CAAC;CAClC;AAED,qBAAa,YAAY;IAwBX,OAAO,CAAC,QAAQ,CAAC,IAAI;IAvBjC,OAAO,CAAC,OAAO,CAAyC;IACxD,OAAO,CAAC,MAAM,CAAwC;IACtD,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,gBAAgB,CAAK;IAC7B,OAAO,CAAC,cAAc,CAA+B;IACrD,OAAO,CAAC,cAAc,CAA+B;IACrD,OAAO,CAAC,iBAAiB,CAA6B;IACtD,OAAO,CAAC,mBAAmB,CAA6B;IACxD,OAAO,CAAC,eAAe,CAAS;IAIhC,OAAO,CAAC,gBAAgB,CAAK;IAE7B,OAAO,CAAC,MAAM,CAAC,iBAAiB,CAAS;IACzC,OAAO,CAAC,MAAM,CAAC,gBAAgB,CAAU;IACzC,OAAO,CAAC,MAAM,CAAC,qBAAqB,CAAU;IAI9C,OAAO,CAAC,MAAM,CAAC,0BAA0B,CAAK;gBAEjB,IAAI,EAAE,mBAAmB;IAEtD,mFAAmF;IAC7E,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAS9B,wCAAwC;IAClC,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAUjC,WAAW,IAAI,OAAO;YAQR,UAAU;IAuExB,OAAO,CAAC,gBAAgB;IASxB,OAAO,CAAC,iBAAiB;IA6BzB,OAAO,CAAC,mBAAmB;IAO3B,OAAO,CAAC,mBAAmB;IAO3B,OAAO,CAAC,cAAc;IAuBtB,OAAO,CAAC,WAAW;IAWnB,OAAO,CAAC,YAAY;IAepB,OAAO,CAAC,sBAAsB;IAyB9B,OAAO,CAAC,wBAAwB;IAWhC,OAAO,CAAC,gBAAgB;IAaxB,OAAO,CAAC,gBAAgB;YAeV,mBAAmB;YA0EnB,YAAY;YAUZ,mBAAmB;IAiBjC,OAAO,CAAC,sBAAsB;IAQ9B,OAAO,CAAC,wBAAwB;IAuBhC,OAAO,CAAC,mBAAmB;IAS3B,OAAO,CAAC,eAAe;IAWvB,OAAO,CAAC,cAAc;CAUvB"}
1
+ {"version":3,"file":"clip-provider.d.ts","sourceRoot":"","sources":["../../src/clip-runtime/clip-provider.ts"],"names":[],"mappings":"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"}
@@ -28,6 +28,17 @@ function encodeEnvelope(msg) {
28
28
  header.writeUInt32BE(json.length, 1);
29
29
  return Buffer.concat([header, json]);
30
30
  }
31
+ /**
32
+ * Canonical key for a clip-registration set — used by flushReregister to skip a
33
+ * re-register that would not change the hub's view. Sorted by alias so set
34
+ * order is irrelevant; each registration's shape is fixed by
35
+ * clipConfigToRegistration, so its JSON is a stable digest of the hub-facing
36
+ * content (commands, version, dependencies, …).
37
+ */
38
+ function registrationKey(clips) {
39
+ const sorted = [...clips].sort((a, b) => (a.alias ?? '').localeCompare(b.alias ?? ''));
40
+ return JSON.stringify(sorted);
41
+ }
31
42
  /**
32
43
  * Incremental envelope decoder. Feed chunks via push(), receive decoded
33
44
  * messages from flush(). Handles partial reads across chunk boundaries.
@@ -66,7 +77,15 @@ export class ClipProvider {
66
77
  heartbeatTimer = null;
67
78
  statusUnsubscribe = null;
68
79
  manifestUnsubscribe = null;
80
+ // Set when the clip manifest/set changed and the hub's copy is stale. Drained
81
+ // by flushReregister(), which re-registers IN-STREAM (no reconnect).
69
82
  needsReregister = false;
83
+ // Canonical key of the clip-registration set last sent to the hub on the
84
+ // CURRENT stream. flushReregister diff-gates against it so a cold-start that
85
+ // re-derives an identical set (the common case — a clip's command surface is
86
+ // stable across spawns) does not emit a redundant re-register. Reset per
87
+ // stream; the first-frame register in openStream re-seeds it.
88
+ lastRegisteredKey = null;
70
89
  // Monotonic stream id, bumped by openStream(). Handlers from a stream that a
71
90
  // newer openStream() has superseded bail in handleDisconnect(gen), so their
72
91
  // late close/end/error can't reconnect against the healthy replacement.
@@ -109,6 +128,9 @@ export class ClipProvider {
109
128
  return;
110
129
  // Bind this stream's handlers to a fresh generation; older streams go stale.
111
130
  const gen = ++this.streamGeneration;
131
+ // New stream → the hub holds nothing until the first-frame register below
132
+ // re-seeds this; clear so a stale key can't suppress that register.
133
+ this.lastRegisteredKey = null;
112
134
  try {
113
135
  this.closeStream();
114
136
  this.closeSession();
@@ -218,25 +240,68 @@ export class ClipProvider {
218
240
  startHeartbeat() {
219
241
  this.clearHeartbeatTimer();
220
242
  this.heartbeatTimer = setInterval(() => {
221
- // If a clip manifest was populated since last heartbeat, reconnect to
222
- // re-register all clips with their now-available commands.
223
- if (this.needsReregister) {
224
- this.needsReregister = false;
225
- this.opts.log.info('[clip-provider] manifest updated reconnecting to re-register clips');
226
- this.connected = false;
227
- this.clearHeartbeatTimer();
228
- this.clearReconnectTimer();
229
- // openStream() bumps the generation before closing the old stream, so its
230
- // late close events fail the gen check (closing inline would re-arm the flap).
231
- void this.openStream();
232
- return;
233
- }
243
+ // A pending manifest/clip-set change re-registers IN-STREAM (no
244
+ // reconnect), so in-flight invokes on this stream survive. This is the
245
+ // fallback for when the immediate flush on the change itself could not
246
+ // write (e.g. it fired mid-(re)connect); the common case re-registers the
247
+ // instant the manifest changes via the manifest listener.
248
+ this.flushReregister();
234
249
  this.sendProviderMessage({ ping: { sentAtUnixMs: Date.now() } }).catch((err) => {
235
250
  this.opts.log.warn(`[clip-provider] heartbeat send failed: ${String(err)}`);
236
251
  });
237
252
  }, ClipProvider.HEARTBEAT_INTERVAL_MS);
238
253
  this.heartbeatTimer.unref?.();
239
254
  }
255
+ /**
256
+ * Re-register the current clip set on the OPEN stream — the daemon's response
257
+ * to a manifest/clip-set change. The hub overwrites its stored set from this
258
+ * full register (clip-provider-sharing-design §11), so a clip install / wake /
259
+ * sleep no longer requires tearing down and reopening the ProviderStream,
260
+ * which used to drop any invoke in flight on that stream (in-flight requests
261
+ * are keyed by request_id on the hub and survive a clip-set swap).
262
+ *
263
+ * No-op when the stream is not currently writable; needsReregister stays set
264
+ * so the next heartbeat tick (or the post-connect flush) retries.
265
+ *
266
+ * Against an older hub that still rejects an in-stream duplicate register, the
267
+ * write reaches a stream the hub then resets; the resulting stream error runs
268
+ * the normal reconnect path, which re-registers as the first frame — i.e. it
269
+ * degrades to the previous reconnect-to-re-register behavior, so no capability
270
+ * negotiation is needed (deploy the hub first).
271
+ */
272
+ flushReregister() {
273
+ if (!this.needsReregister)
274
+ return;
275
+ if (!this.connected || !this.stream || this.stream.closed || this.stream.destroyed)
276
+ return;
277
+ // Diff-gate: a cold-start re-derives the clip set on every wake, but the
278
+ // hub-facing registration is usually identical (a clip's command surface is
279
+ // stable across spawns). Skip the redundant re-register so the hot path
280
+ // (invoke → wake clip → manifest re-read) does no hub work when nothing
281
+ // actually changed. Compare the canonical registration set against what this
282
+ // stream last sent.
283
+ const clips = this.buildClipRegistrations();
284
+ if (registrationKey(clips) === this.lastRegisteredKey) {
285
+ this.needsReregister = false;
286
+ return;
287
+ }
288
+ this.needsReregister = false;
289
+ this.opts.log.info('[clip-provider] re-registering clips in-stream');
290
+ this.sendRegister(clips).catch((err) => {
291
+ this.opts.log.warn(`[clip-provider] in-stream re-register failed: ${String(err)}`);
292
+ this.needsReregister = true; // retry on the next heartbeat tick
293
+ });
294
+ }
295
+ /**
296
+ * Re-register the current clip set on the open stream — public entry for the
297
+ * supervisor when machine clip assignments change. Same in-stream path as a
298
+ * manifest update (no reconnect, in-flight invokes survive). Safe when
299
+ * disconnected: the flag is flushed on the next heartbeat / post-connect.
300
+ */
301
+ reregister() {
302
+ this.needsReregister = true;
303
+ this.flushReregister();
304
+ }
240
305
  closeStream() {
241
306
  if (this.stream) {
242
307
  try {
@@ -279,9 +344,11 @@ export class ClipProvider {
279
344
  this.opts.log.warn(`[clip-provider] status change send failed: ${String(err)}`);
280
345
  });
281
346
  });
282
- // Listen for manifest updates so we can re-register with populated commands
347
+ // Listen for manifest updates so we can re-register with populated commands.
348
+ // Re-register immediately on the open stream; the heartbeat retries if the
349
+ // stream is momentarily not writable.
283
350
  this.manifestUnsubscribe = mgr.addManifestListener(() => {
284
- this.needsReregister = true;
351
+ this.reregister();
285
352
  });
286
353
  }
287
354
  unsubscribeStatusChanges() {
@@ -300,6 +367,9 @@ export class ClipProvider {
300
367
  else if (msg.invokeCommand) {
301
368
  void this.handleInvokeCommand(msg.invokeCommand);
302
369
  }
370
+ else if (msg.viewerCommand) {
371
+ void this.handleViewerCommand(msg.viewerCommand);
372
+ }
303
373
  else if (msg.pong !== undefined) {
304
374
  // Pong is the hub's keepalive ack. Do NOT echo a ping here — the
305
375
  // heartbeat timer (startHeartbeat) is the sole ping driver. Replying to
@@ -313,6 +383,9 @@ export class ClipProvider {
313
383
  this.connected = true;
314
384
  this.reconnectAttempt = 0;
315
385
  this.startHeartbeat();
386
+ // A manifest change that landed during the connect handshake (after the
387
+ // initial register was sent) is flushed now that the stream is writable.
388
+ this.flushReregister();
316
389
  }
317
390
  else {
318
391
  this.opts.log.error(`[clip-provider] registration rejected: ${reg.message}`);
@@ -387,17 +460,68 @@ export class ClipProvider {
387
460
  }).catch(() => { }); // best-effort error response
388
461
  }
389
462
  }
463
+ /**
464
+ * Handle a hosted-browser live-viewer command relayed by clip-service over the
465
+ * stream and reply on the same stream (design §3.4). Always answers the
466
+ * request/reply bridge exactly once — `{result}` on success, `{error:{message}}`
467
+ * on failure — and never throws into the message loop, mirroring the BYOC
468
+ * daemon's handleBrowserProfileViewer (which replies over HTTP instead).
469
+ */
470
+ async handleViewerCommand(cmd) {
471
+ const { requestId } = cmd;
472
+ if (!this.opts.onViewerCommand) {
473
+ // A provider with no live-viewer handler received a viewer command (should
474
+ // not happen — only hosted pods are sent these). Fail the bridge fast.
475
+ await this.sendViewerReply(requestId, {
476
+ error: { message: 'live viewer is not supported by this provider' },
477
+ }).catch(() => { });
478
+ return;
479
+ }
480
+ try {
481
+ let input;
482
+ if (cmd.input) {
483
+ const decoded = Buffer.from(cmd.input, 'base64').toString('utf-8');
484
+ try {
485
+ input = JSON.parse(decoded);
486
+ }
487
+ catch {
488
+ input = undefined;
489
+ }
490
+ }
491
+ const result = await this.opts.onViewerCommand({
492
+ profileId: cmd.profileId,
493
+ sessionId: cmd.sessionId,
494
+ command: cmd.command,
495
+ input,
496
+ turn: cmd.turn,
497
+ });
498
+ await this.sendViewerReply(requestId, { result });
499
+ }
500
+ catch (err) {
501
+ const message = err instanceof Error ? err.message : String(err);
502
+ this.opts.log.warn(`[clip-provider] viewer command failed (profile=${cmd.profileId}, command=${cmd.command}): ${message}`);
503
+ await this.sendViewerReply(requestId, { error: { message } }).catch(() => { });
504
+ }
505
+ }
506
+ /** Send a hosted-viewer reply (base64-encoded JSON body) on the stream. */
507
+ async sendViewerReply(requestId, reply) {
508
+ const payload = Buffer.from(JSON.stringify(reply), 'utf-8').toString('base64');
509
+ await this.sendProviderMessage({ viewerResult: { requestId, payload } });
510
+ }
390
511
  // ---------------------------------------------------------------------------
391
512
  // Sending
392
513
  // ---------------------------------------------------------------------------
393
- async sendRegister() {
394
- const clips = this.buildClipRegistrations();
514
+ async sendRegister(prebuilt) {
515
+ const clips = prebuilt ?? this.buildClipRegistrations();
395
516
  await this.sendProviderMessage({
396
517
  register: {
397
518
  providerName: this.opts.providerName,
398
519
  clips,
399
520
  },
400
521
  });
522
+ // Record what the hub now holds on this stream so flushReregister can
523
+ // diff-gate subsequent cold-start re-registers.
524
+ this.lastRegisteredKey = registrationKey(clips);
401
525
  }
402
526
  async sendProviderMessage(msg) {
403
527
  if (!this.stream || this.stream.closed || this.stream.destroyed) {
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Child-subprocess plumbing shared by the bb-browser daemon manager
3
+ * (browser-profile-manager.ts) and the bb-viewer streamer
4
+ * (browser-viewer-streamer.ts). Moved verbatim out of
5
+ * browser-profile-manager.ts when the viewer block was split into its own
6
+ * file.
7
+ */
8
+ export declare function formatErrorForLog(err: unknown): string;
9
+ export declare function findFreePort(): Promise<number>;
10
+ export declare function sleep(ms: number): Promise<void>;
11
+ //# sourceMappingURL=subprocess.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"subprocess.d.ts","sourceRoot":"","sources":["../../src/clip-runtime/subprocess.ts"],"names":[],"mappings":"AAEA;;;;;;GAMG;AAEH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,CAGtD;AAED,wBAAsB,YAAY,IAAI,OAAO,CAAC,MAAM,CAAC,CAgBpD;AAED,wBAAgB,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE/C"}
@@ -0,0 +1,33 @@
1
+ import * as net from 'node:net';
2
+ /**
3
+ * Child-subprocess plumbing shared by the bb-browser daemon manager
4
+ * (browser-profile-manager.ts) and the bb-viewer streamer
5
+ * (browser-viewer-streamer.ts). Moved verbatim out of
6
+ * browser-profile-manager.ts when the viewer block was split into its own
7
+ * file.
8
+ */
9
+ export function formatErrorForLog(err) {
10
+ const message = err instanceof Error ? err.message : String(err);
11
+ return message.replace(/\s+/g, ' ').slice(0, 300);
12
+ }
13
+ export async function findFreePort() {
14
+ return new Promise((resolve, reject) => {
15
+ const server = net.createServer();
16
+ server.unref();
17
+ server.on('error', reject);
18
+ server.listen(0, '127.0.0.1', () => {
19
+ const address = server.address();
20
+ server.close(() => {
21
+ if (address && typeof address === 'object') {
22
+ resolve(address.port);
23
+ }
24
+ else {
25
+ reject(new Error('failed to allocate free port'));
26
+ }
27
+ });
28
+ });
29
+ });
30
+ }
31
+ export function sleep(ms) {
32
+ return new Promise((resolve) => setTimeout(resolve, ms));
33
+ }
@@ -27,7 +27,7 @@ export declare class DaemonSupervisor {
27
27
  private readonly children;
28
28
  private readonly spawningAgents;
29
29
  private readonly pendingWorkspaceSetup;
30
- private readonly browserProfileLifecycleQueues;
30
+ private readonly browserProfileOpQueues;
31
31
  private readonly workspaceSetupRetryTimers;
32
32
  private readonly cancelledSpawns;
33
33
  private ws;
@@ -69,6 +69,15 @@ export declare class DaemonSupervisor {
69
69
  private handleFilesystemBrowse;
70
70
  private handleBrowserProfileLifecycle;
71
71
  private enqueueBrowserProfileLifecycle;
72
+ private enqueueBrowserProfileOp;
73
+ /**
74
+ * Handle one viewer control command and reply via REST. Always answers the
75
+ * request/reply bridge exactly once ({result} on success, {error:{message}}
76
+ * on failure) and never throws into the WS event loop — mirrors
77
+ * handleFilesystemBrowse. Serialized per profile by enqueueBrowserProfileViewer.
78
+ */
79
+ private handleBrowserProfileViewer;
80
+ private enqueueBrowserProfileViewer;
72
81
  private handleClipSync;
73
82
  private reconcileMachineClips;
74
83
  private reconcileMachineClipsNow;
@@ -80,7 +89,6 @@ export declare class DaemonSupervisor {
80
89
  private clipConfigSignature;
81
90
  private ensureClipInstalled;
82
91
  private readInstalledClipVersion;
83
- private reconnectClipProvider;
84
92
  private handleWorkspaceSetupRequested;
85
93
  private scheduleWorkspaceSetupRetry;
86
94
  private clearWorkspaceSetupRetry;
@@ -1 +1 @@
1
- {"version":3,"file":"supervisor.d.ts","sourceRoot":"","sources":["../src/supervisor.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"supervisor.d.ts","sourceRoot":"","sources":["../src/supervisor.ts"],"names":[],"mappings":"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"}
@@ -57,7 +57,12 @@ export class DaemonSupervisor {
57
57
  children = new Map();
58
58
  spawningAgents = new Set();
59
59
  pendingWorkspaceSetup = new Set();
60
- browserProfileLifecycleQueues = new Map();
60
+ // ONE queue per profile for BOTH lifecycle (open/stop/reset) and viewer
61
+ // (stream.*/nav) commands. They mutate the same streamer + account state, so
62
+ // two independent queues would let a stop/reset interleave with an in-flight
63
+ // stream.start (the kill fires before the streamer registers and misses it,
64
+ // leaving a live streamer on a profile the server just marked stopped).
65
+ browserProfileOpQueues = new Map();
61
66
  workspaceSetupRetryTimers = new Map();
62
67
  cancelledSpawns = new Set();
63
68
  ws = null;
@@ -236,6 +241,10 @@ export class DaemonSupervisor {
236
241
  this.log.info(`WS: browser profile lifecycle requested — profile=${data.profile_id} action=${data.action}`);
237
242
  void this.enqueueBrowserProfileLifecycle(data);
238
243
  });
244
+ this.ws.on('machine.browser_profile.viewer', (data) => {
245
+ this.log.info(`WS: browser profile viewer command — profile=${data.profile_id} command=${data.command}`);
246
+ this.enqueueBrowserProfileViewer(data);
247
+ });
239
248
  this.ws.onStateChange((state) => {
240
249
  if (state === 'disconnected' || state === 'reconnecting') {
241
250
  this.log.warn(`machine WS state: ${state}`);
@@ -391,10 +400,16 @@ export class DaemonSupervisor {
391
400
  for (const profile of profiles) {
392
401
  if (profile.status !== 'running' && profile.status !== 'pending')
393
402
  continue;
403
+ // The daemon supervises only machine-bound (byoc) profiles; hosted
404
+ // profiles (null machine_id) run on the platform pool and are never owned
405
+ // by a daemon machine.
406
+ if (!profile.machine_id)
407
+ continue;
408
+ const machineId = profile.machine_id;
394
409
  try {
395
410
  if (profile.status === 'pending') {
396
411
  await this.enqueueBrowserProfileLifecycle({
397
- machine_id: profile.machine_id,
412
+ machine_id: machineId,
398
413
  profile_id: profile.id,
399
414
  action: 'open',
400
415
  });
@@ -564,20 +579,53 @@ export class DaemonSupervisor {
564
579
  }
565
580
  }
566
581
  enqueueBrowserProfileLifecycle(data) {
567
- const previous = this.browserProfileLifecycleQueues.get(data.profile_id) ?? Promise.resolve();
582
+ return this.enqueueBrowserProfileOp(data.profile_id, () => this.handleBrowserProfileLifecycle(data));
583
+ }
584
+ enqueueBrowserProfileOp(profileId, op) {
585
+ const previous = this.browserProfileOpQueues.get(profileId) ?? Promise.resolve();
568
586
  const next = previous
569
587
  .catch(() => {
570
588
  // Keep the queue moving even if a prior operation failed unexpectedly.
571
589
  })
572
- .then(() => this.handleBrowserProfileLifecycle(data));
573
- this.browserProfileLifecycleQueues.set(data.profile_id, next);
590
+ .then(op);
591
+ this.browserProfileOpQueues.set(profileId, next);
574
592
  void next.finally(() => {
575
- if (this.browserProfileLifecycleQueues.get(data.profile_id) === next) {
576
- this.browserProfileLifecycleQueues.delete(data.profile_id);
593
+ if (this.browserProfileOpQueues.get(profileId) === next) {
594
+ this.browserProfileOpQueues.delete(profileId);
577
595
  }
578
596
  });
579
597
  return next;
580
598
  }
599
+ /**
600
+ * Handle one viewer control command and reply via REST. Always answers the
601
+ * request/reply bridge exactly once ({result} on success, {error:{message}}
602
+ * on failure) and never throws into the WS event loop — mirrors
603
+ * handleFilesystemBrowse. Serialized per profile by enqueueBrowserProfileViewer.
604
+ */
605
+ async handleBrowserProfileViewer(data) {
606
+ if (!this.browserProfileManager) {
607
+ await this.client
608
+ .postBrowserProfileViewerResponse(data.request_id, {
609
+ error: { message: 'Browser profile runtime is disabled on this daemon' },
610
+ })
611
+ .catch((err) => this.log.warn(`browser profile viewer negative ACK failed: ${String(err)}`));
612
+ return;
613
+ }
614
+ try {
615
+ const result = await this.browserProfileManager.handleViewerCommand(data.profile_id, data.session_id, data.command, data.input, data.turn);
616
+ await this.client.postBrowserProfileViewerResponse(data.request_id, { result });
617
+ }
618
+ catch (err) {
619
+ const message = err instanceof Error ? err.message : String(err);
620
+ this.log.warn(`browser profile viewer command failed (profile=${data.profile_id}, command=${data.command}): ${message}`);
621
+ await this.client
622
+ .postBrowserProfileViewerResponse(data.request_id, { error: { message } })
623
+ .catch((replyErr) => this.log.warn(`browser profile viewer error ACK failed: ${String(replyErr)}`));
624
+ }
625
+ }
626
+ enqueueBrowserProfileViewer(data) {
627
+ void this.enqueueBrowserProfileOp(data.profile_id, () => this.handleBrowserProfileViewer(data));
628
+ }
581
629
  async handleClipSync(_data) {
582
630
  await this.reconcileMachineClips();
583
631
  }
@@ -639,7 +687,12 @@ export class DaemonSupervisor {
639
687
  }
640
688
  if (changed) {
641
689
  this.log.info(`machine clips synced — count=${desired.length}`);
642
- await this.reconnectClipProvider();
690
+ // Re-register the new clip set IN-STREAM rather than reconnecting the
691
+ // provider — a clip assign/unassign must not tear down the stream and drop
692
+ // in-flight invokes (the hub overwrites its set from the register; see
693
+ // clip-provider-sharing-design §11). Endpoint changes still rebuild via
694
+ // applyClipProviderState.
695
+ this.clipProvider?.reregister();
643
696
  }
644
697
  }
645
698
  startClipReconcileTimer() {
@@ -774,17 +827,6 @@ export class DaemonSupervisor {
774
827
  }
775
828
  return null;
776
829
  }
777
- async reconnectClipProvider() {
778
- if (!this.clipProvider)
779
- return;
780
- const provider = this.clipProvider;
781
- this.clipProvider = null;
782
- this.connectedClipServiceUrl = null;
783
- await provider
784
- .disconnect()
785
- .catch((err) => this.log.warn(`clip provider reconnect disconnect failed: ${String(err)}`));
786
- await this.applyClipProviderState();
787
- }
788
830
  async handleWorkspaceSetupRequested(agentId) {
789
831
  if (this.spawningAgents.has(agentId)) {
790
832
  this.log.info(`agent ${agentId}: workspace setup already in progress; queueing one restart`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@parall/daemon",
3
- "version": "1.34.0",
3
+ "version": "1.36.0",
4
4
  "description": "Parall local agent runtime — daemon supervisor + bridge runtimes, bundled as standalone JS files",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -16,7 +16,8 @@
16
16
  "parall-daemon": "./bundle/parall-daemon.js",
17
17
  "parall-claude-agent": "./bundle/parall-claude-agent.js",
18
18
  "parall-codex-agent": "./bundle/parall-codex-agent.js",
19
- "parall-openclaw-agent": "./bundle/parall-openclaw-agent.js"
19
+ "parall-openclaw-agent": "./bundle/parall-openclaw-agent.js",
20
+ "parall-browser-pod": "./bundle/parall-browser-pod.js"
20
21
  },
21
22
  "exports": {
22
23
  ".": {
@@ -29,12 +30,13 @@
29
30
  "dist"
30
31
  ],
31
32
  "dependencies": {
33
+ "@aws-sdk/client-s3": "3.984.0",
32
34
  "@pinixai/bb-browser-pro": "0.15.0",
33
- "@parall/agent-core": "1.34.0",
34
- "@parall/claude-agent": "1.34.0",
35
- "@parall/codex-agent": "1.34.0",
36
- "@parall/openclaw-agent": "1.34.0",
37
- "@parall/sdk": "1.34.0"
35
+ "@parall/sdk": "1.36.0",
36
+ "@parall/claude-agent": "1.36.0",
37
+ "@parall/openclaw-agent": "1.36.0",
38
+ "@parall/agent-core": "1.36.0",
39
+ "@parall/codex-agent": "1.36.0"
38
40
  },
39
41
  "devDependencies": {
40
42
  "@types/node": "^22.0.0",
@@ -44,6 +46,7 @@
44
46
  "scripts": {
45
47
  "build": "tsc -b",
46
48
  "bundle": "node ../../scripts/bundle-daemon.mjs",
47
- "start": "node bundle/parall-daemon.js"
49
+ "start": "node bundle/parall-daemon.js",
50
+ "test": "pnpm run build && node --test test/*.test.mjs"
48
51
  }
49
52
  }