@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.
- package/bundle/manifest.json +13 -9
- package/bundle/parall-browser-pod.js +40665 -0
- package/bundle/parall-claude-agent.js +564 -91
- package/bundle/parall-codex-agent.js +559 -90
- package/bundle/parall-daemon.js +1000 -66
- package/dist/browser-pod.d.ts +294 -0
- package/dist/browser-pod.d.ts.map +1 -0
- package/dist/browser-pod.js +765 -0
- package/dist/clip-runtime/browser-profile-manager.d.ts +10 -0
- package/dist/clip-runtime/browser-profile-manager.d.ts.map +1 -1
- package/dist/clip-runtime/browser-profile-manager.js +38 -26
- package/dist/clip-runtime/browser-state-store.d.ts +157 -0
- package/dist/clip-runtime/browser-state-store.d.ts.map +1 -0
- package/dist/clip-runtime/browser-state-store.js +370 -0
- package/dist/clip-runtime/browser-viewer-streamer.d.ts +222 -0
- package/dist/clip-runtime/browser-viewer-streamer.d.ts.map +1 -0
- package/dist/clip-runtime/browser-viewer-streamer.js +691 -0
- package/dist/clip-runtime/clip-provider.d.ts +56 -0
- package/dist/clip-runtime/clip-provider.d.ts.map +1 -1
- package/dist/clip-runtime/clip-provider.js +141 -17
- package/dist/clip-runtime/subprocess.d.ts +11 -0
- package/dist/clip-runtime/subprocess.d.ts.map +1 -0
- package/dist/clip-runtime/subprocess.js +33 -0
- package/dist/supervisor.d.ts +10 -2
- package/dist/supervisor.d.ts.map +1 -1
- package/dist/supervisor.js +61 -19
- package/package.json +11 -8
|
@@ -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;
|
|
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
|
-
//
|
|
222
|
-
//
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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.
|
|
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
|
+
}
|
package/dist/supervisor.d.ts
CHANGED
|
@@ -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
|
|
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;
|
package/dist/supervisor.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"supervisor.d.ts","sourceRoot":"","sources":["../src/supervisor.ts"],"names":[],"mappings":"AAGA,OAAO,EAA8B,KAAK,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACpF,OAAO,
|
|
1
|
+
{"version":3,"file":"supervisor.d.ts","sourceRoot":"","sources":["../src/supervisor.ts"],"names":[],"mappings":"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"}
|
package/dist/supervisor.js
CHANGED
|
@@ -57,7 +57,12 @@ export class DaemonSupervisor {
|
|
|
57
57
|
children = new Map();
|
|
58
58
|
spawningAgents = new Set();
|
|
59
59
|
pendingWorkspaceSetup = new Set();
|
|
60
|
-
|
|
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:
|
|
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
|
-
|
|
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(
|
|
573
|
-
this.
|
|
590
|
+
.then(op);
|
|
591
|
+
this.browserProfileOpQueues.set(profileId, next);
|
|
574
592
|
void next.finally(() => {
|
|
575
|
-
if (this.
|
|
576
|
-
this.
|
|
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
|
-
|
|
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.
|
|
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/
|
|
34
|
-
"@parall/claude-agent": "1.
|
|
35
|
-
"@parall/
|
|
36
|
-
"@parall/
|
|
37
|
-
"@parall/
|
|
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
|
}
|