@parall/daemon 1.35.0 → 1.36.1
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 +40737 -0
- package/bundle/parall-claude-agent.js +472 -13
- package/bundle/parall-codex-agent.js +472 -13
- package/bundle/parall-daemon.js +351 -32
- package/dist/browser-pod.d.ts +301 -0
- package/dist/browser-pod.d.ts.map +1 -0
- package/dist/browser-pod.js +835 -0
- package/dist/clip-runtime/browser-profile-manager.d.ts +16 -0
- package/dist/clip-runtime/browser-profile-manager.d.ts.map +1 -1
- package/dist/clip-runtime/browser-profile-manager.js +17 -0
- 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 +35 -1
- package/dist/clip-runtime/browser-viewer-streamer.d.ts.map +1 -1
- package/dist/clip-runtime/browser-viewer-streamer.js +97 -1
- 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/index.d.ts +1 -0
- package/dist/clip-runtime/index.d.ts.map +1 -1
- package/dist/supervisor.d.ts +11 -1
- package/dist/supervisor.d.ts.map +1 -1
- package/dist/supervisor.js +38 -13
- package/package.json +11 -8
package/bundle/parall-daemon.js
CHANGED
|
@@ -27256,6 +27256,17 @@ var ENDPOINTS = {
|
|
|
27256
27256
|
SCHEDULE_CANCEL: (orgId, id) => `${API_BASE}/orgs/${orgId}/schedules/${id}/cancel`,
|
|
27257
27257
|
SCHEDULE_RUNS: (orgId, id) => `${API_BASE}/orgs/${orgId}/schedules/${id}/runs`,
|
|
27258
27258
|
SCHEDULE_RUN: (orgId, runId) => `${API_BASE}/orgs/${orgId}/schedule_runs/${runId}`,
|
|
27259
|
+
// External triggers (org-scoped incoming integration primitive)
|
|
27260
|
+
EXTERNAL_CONNECTIONS: (orgId) => `${API_BASE}/orgs/${orgId}/external-connections`,
|
|
27261
|
+
EXTERNAL_CONNECTION: (orgId, connectionId) => `${API_BASE}/orgs/${orgId}/external-connections/${connectionId}`,
|
|
27262
|
+
EXTERNAL_CONNECTION_INGRESS_TOKEN_REGENERATE: (orgId, connectionId) => `${API_BASE}/orgs/${orgId}/external-connections/${connectionId}/ingress-token/regenerate`,
|
|
27263
|
+
EXTERNAL_TRIGGER_SCHEMA: (orgId, connectionId) => `${API_BASE}/orgs/${orgId}/external-connections/${connectionId}/trigger-schema`,
|
|
27264
|
+
EXTERNAL_INGRESS_EVENTS: (orgId) => `${API_BASE}/orgs/${orgId}/external-ingress-events`,
|
|
27265
|
+
EXTERNAL_INGRESS_EVENT: (orgId, eventId) => `${API_BASE}/orgs/${orgId}/external-ingress-events/${eventId}`,
|
|
27266
|
+
EXTERNAL_TRIGGERS: (orgId) => `${API_BASE}/orgs/${orgId}/external-triggers`,
|
|
27267
|
+
EXTERNAL_TRIGGER: (orgId, triggerId) => `${API_BASE}/orgs/${orgId}/external-triggers/${triggerId}`,
|
|
27268
|
+
EXTERNAL_TRIGGER_RUNS: (orgId) => `${API_BASE}/orgs/${orgId}/external-trigger-runs`,
|
|
27269
|
+
EXTERNAL_TRIGGER_RUN: (orgId, runId) => `${API_BASE}/orgs/${orgId}/external-trigger-runs/${runId}`,
|
|
27259
27270
|
// Invitations (org-scoped, admin)
|
|
27260
27271
|
ORG_INVITATIONS: (orgId) => `${API_BASE}/orgs/${orgId}/invitations`,
|
|
27261
27272
|
ORG_INVITATION: (orgId, invId) => `${API_BASE}/orgs/${orgId}/invitations/${invId}`,
|
|
@@ -27337,6 +27348,7 @@ var ENDPOINTS = {
|
|
|
27337
27348
|
// References (org-scoped)
|
|
27338
27349
|
REFS_RESOLVE: (orgId) => `${API_BASE}/orgs/${orgId}/refs/resolve`,
|
|
27339
27350
|
REFS_BACKLINKS: (orgId) => `${API_BASE}/orgs/${orgId}/refs/backlinks`,
|
|
27351
|
+
REFS_GRAPH: (orgId) => `${API_BASE}/orgs/${orgId}/refs/graph`,
|
|
27340
27352
|
REFS_CHECK: (orgId) => `${API_BASE}/orgs/${orgId}/refs/check`,
|
|
27341
27353
|
// Platform config (agent-scoped, not org-scoped)
|
|
27342
27354
|
PLATFORM_CONFIG: `${API_BASE}/agents/platform-config`,
|
|
@@ -27465,6 +27477,7 @@ var WS_EVENTS = {
|
|
|
27465
27477
|
// ts/sdk/dist/client.js
|
|
27466
27478
|
var ParallClient = class _ParallClient {
|
|
27467
27479
|
baseUrl;
|
|
27480
|
+
wikiBaseUrl;
|
|
27468
27481
|
token;
|
|
27469
27482
|
onTokenExpired;
|
|
27470
27483
|
getRefreshToken;
|
|
@@ -27516,12 +27529,22 @@ var ParallClient = class _ParallClient {
|
|
|
27516
27529
|
}
|
|
27517
27530
|
constructor(options = {}) {
|
|
27518
27531
|
this.baseUrl = options.baseUrl ?? "";
|
|
27532
|
+
this.wikiBaseUrl = options.wikiBaseUrl ?? this.baseUrl;
|
|
27519
27533
|
this.token = options.token ?? null;
|
|
27520
27534
|
this.onTokenExpired = options.onTokenExpired;
|
|
27521
27535
|
this.getRefreshToken = options.getRefreshToken;
|
|
27522
27536
|
this.setTokens = options.setTokens;
|
|
27523
27537
|
this.swimlaneName = options.swimlaneName;
|
|
27524
27538
|
}
|
|
27539
|
+
/**
|
|
27540
|
+
* Pick the origin for a request path: wiki-service base for `/wiki/v1`
|
|
27541
|
+
* endpoints, api base for everything else. The path itself (from ENDPOINTS)
|
|
27542
|
+
* is authoritative, so wiki vs api routing can't drift from how a caller
|
|
27543
|
+
* happens to invoke the client.
|
|
27544
|
+
*/
|
|
27545
|
+
baseUrlFor(path15) {
|
|
27546
|
+
return path15.startsWith(WIKI_BASE) ? this.wikiBaseUrl : this.baseUrl;
|
|
27547
|
+
}
|
|
27525
27548
|
setToken(token) {
|
|
27526
27549
|
this.token = token;
|
|
27527
27550
|
}
|
|
@@ -27586,7 +27609,7 @@ var ParallClient = class _ParallClient {
|
|
|
27586
27609
|
if (!retried) {
|
|
27587
27610
|
await this.ensureFreshToken(path15);
|
|
27588
27611
|
}
|
|
27589
|
-
let url = `${this.
|
|
27612
|
+
let url = `${this.baseUrlFor(path15)}${path15}`;
|
|
27590
27613
|
if (query) {
|
|
27591
27614
|
const params = new URLSearchParams();
|
|
27592
27615
|
for (const [key, value] of Object.entries(query)) {
|
|
@@ -27658,7 +27681,7 @@ var ParallClient = class _ParallClient {
|
|
|
27658
27681
|
void _drop;
|
|
27659
27682
|
let res;
|
|
27660
27683
|
try {
|
|
27661
|
-
res = await fetch(`${this.
|
|
27684
|
+
res = await fetch(`${this.baseUrlFor(path15)}${path15}`, {
|
|
27662
27685
|
method,
|
|
27663
27686
|
headers,
|
|
27664
27687
|
body,
|
|
@@ -27818,6 +27841,21 @@ var ParallClient = class _ParallClient {
|
|
|
27818
27841
|
q.limit = String(params.limit);
|
|
27819
27842
|
return this.request("GET", ENDPOINTS.ORG_MEMBER_TASKS(orgId, memberId), void 0, q);
|
|
27820
27843
|
}
|
|
27844
|
+
// Auto-paginated variant of getMemberTasks: fetches ALL pending tasks
|
|
27845
|
+
// (todo + in_progress) assigned to a member, including subtasks (the
|
|
27846
|
+
// endpoint does not filter parent_id). Powers the CLI `tasks assigned`
|
|
27847
|
+
// command so an agent answering "what's on X's plate" sees the full
|
|
27848
|
+
// backlog, not just the first page.
|
|
27849
|
+
async getMemberTasksAll(orgId, memberId) {
|
|
27850
|
+
const all = [];
|
|
27851
|
+
let cursor;
|
|
27852
|
+
do {
|
|
27853
|
+
const res = await this.getMemberTasks(orgId, memberId, { cursor, limit: 100 });
|
|
27854
|
+
all.push(...res.data);
|
|
27855
|
+
cursor = res.has_more ? res.next_cursor : void 0;
|
|
27856
|
+
} while (cursor);
|
|
27857
|
+
return all;
|
|
27858
|
+
}
|
|
27821
27859
|
// ---- Invitations ----
|
|
27822
27860
|
async createInvitation(orgId, email, role) {
|
|
27823
27861
|
return this.request("POST", ENDPOINTS.ORG_INVITATIONS(orgId), { email, role });
|
|
@@ -28247,7 +28285,8 @@ var ParallClient = class _ParallClient {
|
|
|
28247
28285
|
const res = await this.request("GET", ENDPOINTS.MACHINES_ME_CLIPS);
|
|
28248
28286
|
return res.data;
|
|
28249
28287
|
}
|
|
28250
|
-
/** `GET /machines/me/browser-profiles` — browser profiles hosted by this machine.
|
|
28288
|
+
/** `GET /machines/me/browser-profiles` — browser profiles hosted by this machine.
|
|
28289
|
+
* Returns the daemon DTO (carries proxy_password for bb-browser replay). */
|
|
28251
28290
|
async listMachineBrowserProfiles() {
|
|
28252
28291
|
const res = await this.request("GET", ENDPOINTS.MACHINES_ME_BROWSER_PROFILES);
|
|
28253
28292
|
return res.data;
|
|
@@ -28407,7 +28446,7 @@ var ParallClient = class _ParallClient {
|
|
|
28407
28446
|
* Returns null when the server responds with 304 (config unchanged).
|
|
28408
28447
|
*/
|
|
28409
28448
|
async getPlatformConfig(currentVersion) {
|
|
28410
|
-
const url = `${this.
|
|
28449
|
+
const url = `${this.baseUrlFor(ENDPOINTS.PLATFORM_CONFIG)}${ENDPOINTS.PLATFORM_CONFIG}`;
|
|
28411
28450
|
const extra = {};
|
|
28412
28451
|
if (currentVersion !== void 0) {
|
|
28413
28452
|
extra["If-None-Match"] = currentVersion;
|
|
@@ -28573,6 +28612,55 @@ var ParallClient = class _ParallClient {
|
|
|
28573
28612
|
async getScheduleRun(orgId, runId) {
|
|
28574
28613
|
return this.request("GET", ENDPOINTS.SCHEDULE_RUN(orgId, runId));
|
|
28575
28614
|
}
|
|
28615
|
+
// ---- External triggers (org-scoped) ----
|
|
28616
|
+
async createExternalConnection(orgId, input) {
|
|
28617
|
+
return this.request("POST", ENDPOINTS.EXTERNAL_CONNECTIONS(orgId), input);
|
|
28618
|
+
}
|
|
28619
|
+
async listExternalConnections(orgId, filters) {
|
|
28620
|
+
return this.request("GET", ENDPOINTS.EXTERNAL_CONNECTIONS(orgId), void 0, filters);
|
|
28621
|
+
}
|
|
28622
|
+
async getExternalConnection(orgId, connectionId) {
|
|
28623
|
+
return this.request("GET", ENDPOINTS.EXTERNAL_CONNECTION(orgId, connectionId));
|
|
28624
|
+
}
|
|
28625
|
+
async updateExternalConnection(orgId, connectionId, patch) {
|
|
28626
|
+
return this.request("PATCH", ENDPOINTS.EXTERNAL_CONNECTION(orgId, connectionId), patch);
|
|
28627
|
+
}
|
|
28628
|
+
async regenerateExternalConnectionIngressToken(orgId, connectionId) {
|
|
28629
|
+
return this.request("POST", ENDPOINTS.EXTERNAL_CONNECTION_INGRESS_TOKEN_REGENERATE(orgId, connectionId));
|
|
28630
|
+
}
|
|
28631
|
+
async deleteExternalConnection(orgId, connectionId) {
|
|
28632
|
+
return this.request("DELETE", ENDPOINTS.EXTERNAL_CONNECTION(orgId, connectionId));
|
|
28633
|
+
}
|
|
28634
|
+
async getExternalTriggerSchema(orgId, connectionId) {
|
|
28635
|
+
return this.request("GET", ENDPOINTS.EXTERNAL_TRIGGER_SCHEMA(orgId, connectionId));
|
|
28636
|
+
}
|
|
28637
|
+
async listExternalIngressEvents(orgId, filters) {
|
|
28638
|
+
return this.request("GET", ENDPOINTS.EXTERNAL_INGRESS_EVENTS(orgId), void 0, filters);
|
|
28639
|
+
}
|
|
28640
|
+
async getExternalIngressEvent(orgId, eventId) {
|
|
28641
|
+
return this.request("GET", ENDPOINTS.EXTERNAL_INGRESS_EVENT(orgId, eventId));
|
|
28642
|
+
}
|
|
28643
|
+
async createExternalTrigger(orgId, input) {
|
|
28644
|
+
return this.request("POST", ENDPOINTS.EXTERNAL_TRIGGERS(orgId), input);
|
|
28645
|
+
}
|
|
28646
|
+
async listExternalTriggers(orgId, filters) {
|
|
28647
|
+
return this.request("GET", ENDPOINTS.EXTERNAL_TRIGGERS(orgId), void 0, filters);
|
|
28648
|
+
}
|
|
28649
|
+
async getExternalTrigger(orgId, triggerId) {
|
|
28650
|
+
return this.request("GET", ENDPOINTS.EXTERNAL_TRIGGER(orgId, triggerId));
|
|
28651
|
+
}
|
|
28652
|
+
async updateExternalTrigger(orgId, triggerId, patch) {
|
|
28653
|
+
return this.request("PATCH", ENDPOINTS.EXTERNAL_TRIGGER(orgId, triggerId), patch);
|
|
28654
|
+
}
|
|
28655
|
+
async deleteExternalTrigger(orgId, triggerId) {
|
|
28656
|
+
return this.request("DELETE", ENDPOINTS.EXTERNAL_TRIGGER(orgId, triggerId));
|
|
28657
|
+
}
|
|
28658
|
+
async listExternalTriggerRuns(orgId, filters) {
|
|
28659
|
+
return this.request("GET", ENDPOINTS.EXTERNAL_TRIGGER_RUNS(orgId), void 0, filters);
|
|
28660
|
+
}
|
|
28661
|
+
async getExternalTriggerRun(orgId, runId) {
|
|
28662
|
+
return this.request("GET", ENDPOINTS.EXTERNAL_TRIGGER_RUN(orgId, runId));
|
|
28663
|
+
}
|
|
28576
28664
|
// ---- Wikis (org-scoped) ----
|
|
28577
28665
|
async createWiki(orgId, data) {
|
|
28578
28666
|
return this.request("POST", ENDPOINTS.WIKIS(orgId), data);
|
|
@@ -28587,8 +28675,35 @@ var ParallClient = class _ParallClient {
|
|
|
28587
28675
|
async getWikiTree(orgId, wikiId, params) {
|
|
28588
28676
|
return this.request("GET", ENDPOINTS.WIKI_TREE(orgId, wikiId), void 0, params);
|
|
28589
28677
|
}
|
|
28678
|
+
/**
|
|
28679
|
+
* Resolve a server-returned, host-relative media URL (a wiki `signed_url`
|
|
28680
|
+
* like `/wiki/v1/signed/files?token=...`) against this client's base origin,
|
|
28681
|
+
* so it can be dropped straight into a browser `<img>`/`<video>`/`<iframe>`
|
|
28682
|
+
* `src`.
|
|
28683
|
+
*
|
|
28684
|
+
* wiki-service returns these relative on purpose — it doesn't know its own
|
|
28685
|
+
* public origin. A relative `src` resolves against the *page* origin, which
|
|
28686
|
+
* only works when the page and wiki-service share an origin (local dev:
|
|
28687
|
+
* same-origin + Next.js `/wiki/*` proxy). In deployed envs the app
|
|
28688
|
+
* (app.parall.com) and wiki-service (api.parall.com) are different origins,
|
|
28689
|
+
* so `app.parall.com/wiki/v1/signed/files` hits the SPA's own `/wiki/[...]`
|
|
28690
|
+
* catch-all route — an `<iframe>` then recursively renders the whole app
|
|
28691
|
+
* instead of the file. Prefixing with the wiki base (the exact origin every
|
|
28692
|
+
* wiki API request already uses — `baseUrlFor` resolves `/wiki/v1` paths to
|
|
28693
|
+
* `wikiBaseUrl`) makes the URL absolute against the origin that actually
|
|
28694
|
+
* serves the bytes. An empty base (local dev, same-origin proxy) leaves it
|
|
28695
|
+
* relative, preserving the proxy path.
|
|
28696
|
+
*/
|
|
28697
|
+
absoluteMediaUrl(url) {
|
|
28698
|
+
if (/^https?:\/\//i.test(url))
|
|
28699
|
+
return url;
|
|
28700
|
+
return `${this.baseUrlFor(url)}${url}`;
|
|
28701
|
+
}
|
|
28590
28702
|
async getWikiBlob(orgId, wikiId, params) {
|
|
28591
|
-
|
|
28703
|
+
const blob = await this.request("GET", ENDPOINTS.WIKI_BLOB(orgId, wikiId), void 0, params);
|
|
28704
|
+
if (blob.signed_url)
|
|
28705
|
+
blob.signed_url = this.absoluteMediaUrl(blob.signed_url);
|
|
28706
|
+
return blob;
|
|
28592
28707
|
}
|
|
28593
28708
|
async getWikiNodeSections(orgId, wikiId, params) {
|
|
28594
28709
|
return this.request("GET", ENDPOINTS.WIKI_NODE_SECTIONS(orgId, wikiId), void 0, params);
|
|
@@ -28678,7 +28793,10 @@ var ParallClient = class _ParallClient {
|
|
|
28678
28793
|
* token — don't leak it.
|
|
28679
28794
|
*/
|
|
28680
28795
|
async getWikiFilePreviewUrl(orgId, wikiId, params) {
|
|
28681
|
-
|
|
28796
|
+
const res = await this.request("POST", ENDPOINTS.WIKI_FILE_PREVIEW_URL(orgId, wikiId), params);
|
|
28797
|
+
if (res.url)
|
|
28798
|
+
res.url = this.absoluteMediaUrl(res.url);
|
|
28799
|
+
return res;
|
|
28682
28800
|
}
|
|
28683
28801
|
// ---- Wiki Path Scopes (AFCS ACL) ----
|
|
28684
28802
|
async getWikiPathScopes(orgId, wikiId) {
|
|
@@ -28755,6 +28873,16 @@ var ParallClient = class _ParallClient {
|
|
|
28755
28873
|
async getBacklinks(orgId, params) {
|
|
28756
28874
|
return this.request("GET", ENDPOINTS.REFS_BACKLINKS(orgId), void 0, params);
|
|
28757
28875
|
}
|
|
28876
|
+
/**
|
|
28877
|
+
* Bounded multi-hop walk of the prll:// reference graph around `uri`. `uri`
|
|
28878
|
+
* must be an entity-level prll:// URI — a refined URI (path/query/fragment) is
|
|
28879
|
+
* rejected with 400 UNSUPPORTED_REFINED_URI. `depth` is clamped server-side to
|
|
28880
|
+
* [1, 4]; breadth (node/edge counts) is capped server-side and surfaced via
|
|
28881
|
+
* `truncated`. ACL is applied per hop (chat membership + wiki path scope).
|
|
28882
|
+
*/
|
|
28883
|
+
async getRefsGraph(orgId, params) {
|
|
28884
|
+
return this.request("GET", ENDPOINTS.REFS_GRAPH(orgId), void 0, params);
|
|
28885
|
+
}
|
|
28758
28886
|
async checkBrokenRefs(orgId) {
|
|
28759
28887
|
return this.request("GET", ENDPOINTS.REFS_CHECK(orgId));
|
|
28760
28888
|
}
|
|
@@ -28894,6 +29022,9 @@ var ParallClient = class _ParallClient {
|
|
|
28894
29022
|
const resp = await this.request("GET", ENDPOINTS.CLIP_ONLINE(orgId));
|
|
28895
29023
|
return resp.data;
|
|
28896
29024
|
}
|
|
29025
|
+
/** Org-wide browser-profile discovery list. Returns the sanitized
|
|
29026
|
+
* {@link BrowserProfileListItem} shape (not the full domain model), each row
|
|
29027
|
+
* carrying a per-viewer `can_open` control hint. */
|
|
28897
29028
|
async listBrowserProfiles(orgId) {
|
|
28898
29029
|
const resp = await this.request("GET", ENDPOINTS.BROWSER_PROFILES(orgId));
|
|
28899
29030
|
return resp.data;
|
|
@@ -31692,6 +31823,10 @@ function encodeEnvelope2(msg) {
|
|
|
31692
31823
|
header.writeUInt32BE(json.length, 1);
|
|
31693
31824
|
return Buffer.concat([header, json]);
|
|
31694
31825
|
}
|
|
31826
|
+
function registrationKey(clips) {
|
|
31827
|
+
const sorted = [...clips].sort((a, b) => (a.alias ?? "").localeCompare(b.alias ?? ""));
|
|
31828
|
+
return JSON.stringify(sorted);
|
|
31829
|
+
}
|
|
31695
31830
|
var EnvelopeDecoder2 = class _EnvelopeDecoder {
|
|
31696
31831
|
buf = Buffer.alloc(0);
|
|
31697
31832
|
push(chunk) {
|
|
@@ -31727,7 +31862,15 @@ var ClipProvider = class _ClipProvider {
|
|
|
31727
31862
|
heartbeatTimer = null;
|
|
31728
31863
|
statusUnsubscribe = null;
|
|
31729
31864
|
manifestUnsubscribe = null;
|
|
31865
|
+
// Set when the clip manifest/set changed and the hub's copy is stale. Drained
|
|
31866
|
+
// by flushReregister(), which re-registers IN-STREAM (no reconnect).
|
|
31730
31867
|
needsReregister = false;
|
|
31868
|
+
// Canonical key of the clip-registration set last sent to the hub on the
|
|
31869
|
+
// CURRENT stream. flushReregister diff-gates against it so a cold-start that
|
|
31870
|
+
// re-derives an identical set (the common case — a clip's command surface is
|
|
31871
|
+
// stable across spawns) does not emit a redundant re-register. Reset per
|
|
31872
|
+
// stream; the first-frame register in openStream re-seeds it.
|
|
31873
|
+
lastRegisteredKey = null;
|
|
31731
31874
|
// Monotonic stream id, bumped by openStream(). Handlers from a stream that a
|
|
31732
31875
|
// newer openStream() has superseded bail in handleDisconnect(gen), so their
|
|
31733
31876
|
// late close/end/error can't reconnect against the healthy replacement.
|
|
@@ -31768,6 +31911,7 @@ var ClipProvider = class _ClipProvider {
|
|
|
31768
31911
|
if (this.stopped)
|
|
31769
31912
|
return;
|
|
31770
31913
|
const gen = ++this.streamGeneration;
|
|
31914
|
+
this.lastRegisteredKey = null;
|
|
31771
31915
|
try {
|
|
31772
31916
|
this.closeStream();
|
|
31773
31917
|
this.closeSession();
|
|
@@ -31866,21 +32010,57 @@ var ClipProvider = class _ClipProvider {
|
|
|
31866
32010
|
startHeartbeat() {
|
|
31867
32011
|
this.clearHeartbeatTimer();
|
|
31868
32012
|
this.heartbeatTimer = setInterval(() => {
|
|
31869
|
-
|
|
31870
|
-
this.needsReregister = false;
|
|
31871
|
-
this.opts.log.info("[clip-provider] manifest updated \u2014 reconnecting to re-register clips");
|
|
31872
|
-
this.connected = false;
|
|
31873
|
-
this.clearHeartbeatTimer();
|
|
31874
|
-
this.clearReconnectTimer();
|
|
31875
|
-
void this.openStream();
|
|
31876
|
-
return;
|
|
31877
|
-
}
|
|
32013
|
+
this.flushReregister();
|
|
31878
32014
|
this.sendProviderMessage({ ping: { sentAtUnixMs: Date.now() } }).catch((err) => {
|
|
31879
32015
|
this.opts.log.warn(`[clip-provider] heartbeat send failed: ${String(err)}`);
|
|
31880
32016
|
});
|
|
31881
32017
|
}, _ClipProvider.HEARTBEAT_INTERVAL_MS);
|
|
31882
32018
|
this.heartbeatTimer.unref?.();
|
|
31883
32019
|
}
|
|
32020
|
+
/**
|
|
32021
|
+
* Re-register the current clip set on the OPEN stream — the daemon's response
|
|
32022
|
+
* to a manifest/clip-set change. The hub overwrites its stored set from this
|
|
32023
|
+
* full register (clip-provider-sharing-design §11), so a clip install / wake /
|
|
32024
|
+
* sleep no longer requires tearing down and reopening the ProviderStream,
|
|
32025
|
+
* which used to drop any invoke in flight on that stream (in-flight requests
|
|
32026
|
+
* are keyed by request_id on the hub and survive a clip-set swap).
|
|
32027
|
+
*
|
|
32028
|
+
* No-op when the stream is not currently writable; needsReregister stays set
|
|
32029
|
+
* so the next heartbeat tick (or the post-connect flush) retries.
|
|
32030
|
+
*
|
|
32031
|
+
* Against an older hub that still rejects an in-stream duplicate register, the
|
|
32032
|
+
* write reaches a stream the hub then resets; the resulting stream error runs
|
|
32033
|
+
* the normal reconnect path, which re-registers as the first frame — i.e. it
|
|
32034
|
+
* degrades to the previous reconnect-to-re-register behavior, so no capability
|
|
32035
|
+
* negotiation is needed (deploy the hub first).
|
|
32036
|
+
*/
|
|
32037
|
+
flushReregister() {
|
|
32038
|
+
if (!this.needsReregister)
|
|
32039
|
+
return;
|
|
32040
|
+
if (!this.connected || !this.stream || this.stream.closed || this.stream.destroyed)
|
|
32041
|
+
return;
|
|
32042
|
+
const clips = this.buildClipRegistrations();
|
|
32043
|
+
if (registrationKey(clips) === this.lastRegisteredKey) {
|
|
32044
|
+
this.needsReregister = false;
|
|
32045
|
+
return;
|
|
32046
|
+
}
|
|
32047
|
+
this.needsReregister = false;
|
|
32048
|
+
this.opts.log.info("[clip-provider] re-registering clips in-stream");
|
|
32049
|
+
this.sendRegister(clips).catch((err) => {
|
|
32050
|
+
this.opts.log.warn(`[clip-provider] in-stream re-register failed: ${String(err)}`);
|
|
32051
|
+
this.needsReregister = true;
|
|
32052
|
+
});
|
|
32053
|
+
}
|
|
32054
|
+
/**
|
|
32055
|
+
* Re-register the current clip set on the open stream — public entry for the
|
|
32056
|
+
* supervisor when machine clip assignments change. Same in-stream path as a
|
|
32057
|
+
* manifest update (no reconnect, in-flight invokes survive). Safe when
|
|
32058
|
+
* disconnected: the flag is flushed on the next heartbeat / post-connect.
|
|
32059
|
+
*/
|
|
32060
|
+
reregister() {
|
|
32061
|
+
this.needsReregister = true;
|
|
32062
|
+
this.flushReregister();
|
|
32063
|
+
}
|
|
31884
32064
|
closeStream() {
|
|
31885
32065
|
if (this.stream) {
|
|
31886
32066
|
try {
|
|
@@ -31915,7 +32095,7 @@ var ClipProvider = class _ClipProvider {
|
|
|
31915
32095
|
});
|
|
31916
32096
|
});
|
|
31917
32097
|
this.manifestUnsubscribe = mgr.addManifestListener(() => {
|
|
31918
|
-
this.
|
|
32098
|
+
this.reregister();
|
|
31919
32099
|
});
|
|
31920
32100
|
}
|
|
31921
32101
|
unsubscribeStatusChanges() {
|
|
@@ -31932,6 +32112,8 @@ var ClipProvider = class _ClipProvider {
|
|
|
31932
32112
|
this.handleRegistered(msg.registerResponse);
|
|
31933
32113
|
} else if (msg.invokeCommand) {
|
|
31934
32114
|
void this.handleInvokeCommand(msg.invokeCommand);
|
|
32115
|
+
} else if (msg.viewerCommand) {
|
|
32116
|
+
void this.handleViewerCommand(msg.viewerCommand);
|
|
31935
32117
|
} else if (msg.pong !== void 0) {
|
|
31936
32118
|
}
|
|
31937
32119
|
}
|
|
@@ -31941,6 +32123,7 @@ var ClipProvider = class _ClipProvider {
|
|
|
31941
32123
|
this.connected = true;
|
|
31942
32124
|
this.reconnectAttempt = 0;
|
|
31943
32125
|
this.startHeartbeat();
|
|
32126
|
+
this.flushReregister();
|
|
31944
32127
|
} else {
|
|
31945
32128
|
this.opts.log.error(`[clip-provider] registration rejected: ${reg.message}`);
|
|
31946
32129
|
this.stopped = true;
|
|
@@ -31994,17 +32177,64 @@ var ClipProvider = class _ClipProvider {
|
|
|
31994
32177
|
});
|
|
31995
32178
|
}
|
|
31996
32179
|
}
|
|
32180
|
+
/**
|
|
32181
|
+
* Handle a hosted-browser live-viewer command relayed by clip-service over the
|
|
32182
|
+
* stream and reply on the same stream (design §3.4). Always answers the
|
|
32183
|
+
* request/reply bridge exactly once — `{result}` on success, `{error:{message}}`
|
|
32184
|
+
* on failure — and never throws into the message loop, mirroring the BYOC
|
|
32185
|
+
* daemon's handleBrowserProfileViewer (which replies over HTTP instead).
|
|
32186
|
+
*/
|
|
32187
|
+
async handleViewerCommand(cmd) {
|
|
32188
|
+
const { requestId } = cmd;
|
|
32189
|
+
if (!this.opts.onViewerCommand) {
|
|
32190
|
+
await this.sendViewerReply(requestId, {
|
|
32191
|
+
error: { message: "live viewer is not supported by this provider" }
|
|
32192
|
+
}).catch(() => {
|
|
32193
|
+
});
|
|
32194
|
+
return;
|
|
32195
|
+
}
|
|
32196
|
+
try {
|
|
32197
|
+
let input;
|
|
32198
|
+
if (cmd.input) {
|
|
32199
|
+
const decoded = Buffer.from(cmd.input, "base64").toString("utf-8");
|
|
32200
|
+
try {
|
|
32201
|
+
input = JSON.parse(decoded);
|
|
32202
|
+
} catch {
|
|
32203
|
+
input = void 0;
|
|
32204
|
+
}
|
|
32205
|
+
}
|
|
32206
|
+
const result = await this.opts.onViewerCommand({
|
|
32207
|
+
profileId: cmd.profileId,
|
|
32208
|
+
sessionId: cmd.sessionId,
|
|
32209
|
+
command: cmd.command,
|
|
32210
|
+
input,
|
|
32211
|
+
turn: cmd.turn
|
|
32212
|
+
});
|
|
32213
|
+
await this.sendViewerReply(requestId, { result });
|
|
32214
|
+
} catch (err) {
|
|
32215
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
32216
|
+
this.opts.log.warn(`[clip-provider] viewer command failed (profile=${cmd.profileId}, command=${cmd.command}): ${message}`);
|
|
32217
|
+
await this.sendViewerReply(requestId, { error: { message } }).catch(() => {
|
|
32218
|
+
});
|
|
32219
|
+
}
|
|
32220
|
+
}
|
|
32221
|
+
/** Send a hosted-viewer reply (base64-encoded JSON body) on the stream. */
|
|
32222
|
+
async sendViewerReply(requestId, reply) {
|
|
32223
|
+
const payload = Buffer.from(JSON.stringify(reply), "utf-8").toString("base64");
|
|
32224
|
+
await this.sendProviderMessage({ viewerResult: { requestId, payload } });
|
|
32225
|
+
}
|
|
31997
32226
|
// ---------------------------------------------------------------------------
|
|
31998
32227
|
// Sending
|
|
31999
32228
|
// ---------------------------------------------------------------------------
|
|
32000
|
-
async sendRegister() {
|
|
32001
|
-
const clips = this.buildClipRegistrations();
|
|
32229
|
+
async sendRegister(prebuilt) {
|
|
32230
|
+
const clips = prebuilt ?? this.buildClipRegistrations();
|
|
32002
32231
|
await this.sendProviderMessage({
|
|
32003
32232
|
register: {
|
|
32004
32233
|
providerName: this.opts.providerName,
|
|
32005
32234
|
clips
|
|
32006
32235
|
}
|
|
32007
32236
|
});
|
|
32237
|
+
this.lastRegisteredKey = registrationKey(clips);
|
|
32008
32238
|
}
|
|
32009
32239
|
async sendProviderMessage(msg) {
|
|
32010
32240
|
if (!this.stream || this.stream.closed || this.stream.destroyed) {
|
|
@@ -32113,12 +32343,27 @@ var BB_VIEWER_HEALTH_TIMEOUT_MS = 1e4;
|
|
|
32113
32343
|
var BB_VIEWER_COMMAND_TIMEOUT_MS = 15e3;
|
|
32114
32344
|
var BB_VIEWER_UNANSWERED_REAP_MS = 9e4;
|
|
32115
32345
|
var BB_VIEWER_TERM_GRACE_MS = 5e3;
|
|
32346
|
+
function isLoopbackBindHost(host) {
|
|
32347
|
+
return host === "127.0.0.1" || host === "localhost" || host === "::1" || host === "[::1]";
|
|
32348
|
+
}
|
|
32349
|
+
function kickHitsCurrentSession(requested, currentSessionId) {
|
|
32350
|
+
if (currentSessionId === void 0)
|
|
32351
|
+
return false;
|
|
32352
|
+
return requested === void 0 || requested === currentSessionId;
|
|
32353
|
+
}
|
|
32116
32354
|
var BrowserViewerStreamer = class {
|
|
32117
32355
|
host;
|
|
32118
32356
|
// Live bb-viewer (WebRTC streamer) subprocess per profile. One per profile;
|
|
32119
32357
|
// see StreamerState. Cleaned up on stream.close, stopProfile, resetProfile,
|
|
32120
32358
|
// and stop().
|
|
32121
32359
|
streamers = /* @__PURE__ */ new Map();
|
|
32360
|
+
// The most-recently server-side-kicked viewer session per profile (design §3.6
|
|
32361
|
+
// PR7). After a kick the streamer is gone, so the session-staleness guards
|
|
32362
|
+
// (which key off a live streamer) can no longer reject the kicked viewer's
|
|
32363
|
+
// straggler nav commands — this map does. Reset whenever the profile's streamer
|
|
32364
|
+
// is (re)killed via killProfileStreamer, so a fresh stream.start clears it. One
|
|
32365
|
+
// entry per profile (overwritten per kick); bounded, no leak.
|
|
32366
|
+
kickedSessions = /* @__PURE__ */ new Map();
|
|
32122
32367
|
// Set by shutdown(): an in-flight spawnStreamer has no map entry yet, so a
|
|
32123
32368
|
// shutdown during its health poll would otherwise let the bb-viewer child
|
|
32124
32369
|
// survive daemon stop (on BYOC nothing else reaps it).
|
|
@@ -32135,6 +32380,9 @@ var BrowserViewerStreamer = class {
|
|
|
32135
32380
|
async handleViewerCommand(profileId, sessionId, command, input, turn) {
|
|
32136
32381
|
if (!profileId)
|
|
32137
32382
|
throw new Error("browser profile id is required");
|
|
32383
|
+
if (command !== "stream.start" && sessionId && this.kickedSessions.get(profileId) === sessionId) {
|
|
32384
|
+
throw new Error("viewer session was terminated");
|
|
32385
|
+
}
|
|
32138
32386
|
switch (command) {
|
|
32139
32387
|
case "stream.start":
|
|
32140
32388
|
return this.viewerStreamStart(profileId, sessionId, turn);
|
|
@@ -32144,6 +32392,8 @@ var BrowserViewerStreamer = class {
|
|
|
32144
32392
|
return this.viewerStreamClose(profileId, sessionId, input);
|
|
32145
32393
|
case "stream.switch":
|
|
32146
32394
|
return this.viewerStreamSwitch(profileId, sessionId, input);
|
|
32395
|
+
case "kick":
|
|
32396
|
+
return this.viewerKick(profileId, sessionId, input);
|
|
32147
32397
|
case "close":
|
|
32148
32398
|
return this.viewerCloseTab(profileId, sessionId, input);
|
|
32149
32399
|
case "tab_list":
|
|
@@ -32157,6 +32407,31 @@ var BrowserViewerStreamer = class {
|
|
|
32157
32407
|
throw new Error(`unknown viewer command: ${command}`);
|
|
32158
32408
|
}
|
|
32159
32409
|
}
|
|
32410
|
+
/**
|
|
32411
|
+
* kick — server-side per-viewer disconnect (design §3.6 PR7). Terminates ONE
|
|
32412
|
+
* viewer's WebRTC session: stop its bb-viewer streamer (killing the process
|
|
32413
|
+
* tears down the peer connection + datachannel) and reject the kicked session's
|
|
32414
|
+
* subsequent commands — while bb-browser + Chromium (the agent's live browser)
|
|
32415
|
+
* keep running. This is explicitly DISTINCT from stopping the pod / profile,
|
|
32416
|
+
* which would kill the agent's browser too; a viewer session is a sub-session of
|
|
32417
|
+
* the profile lease, so a kick does NOT release the lease.
|
|
32418
|
+
*
|
|
32419
|
+
* Targets `input.session_id` if given, else the current streamer's session.
|
|
32420
|
+
* Idempotent: kicking with no live streamer still records the kicked session.
|
|
32421
|
+
*/
|
|
32422
|
+
viewerKick(profileId, sessionId, input) {
|
|
32423
|
+
const streamer = this.streamers.get(profileId);
|
|
32424
|
+
const requested = typeof input?.session_id === "string" ? input.session_id : void 0;
|
|
32425
|
+
const target = requested || streamer?.sessionId || sessionId;
|
|
32426
|
+
const killsCurrent = kickHitsCurrentSession(requested, streamer?.sessionId);
|
|
32427
|
+
if (killsCurrent) {
|
|
32428
|
+
this.killProfileStreamer(profileId);
|
|
32429
|
+
}
|
|
32430
|
+
if (target)
|
|
32431
|
+
this.kickedSessions.set(profileId, target);
|
|
32432
|
+
this.host.log.info(`[bb-viewer] kicked viewer session ${target || "(none)"} for profile ${profileId}; pod + agent browser keep running`);
|
|
32433
|
+
return target ? { ok: true, kicked: true, session_id: target } : { ok: true, kicked: true };
|
|
32434
|
+
}
|
|
32160
32435
|
/**
|
|
32161
32436
|
* stream.start — spawn a FRESH bb-viewer for this profile (killing any prior
|
|
32162
32437
|
* one), resolve the profile's account-scoped page-target CDP ws URL, and run
|
|
@@ -32453,6 +32728,13 @@ var BrowserViewerStreamer = class {
|
|
|
32453
32728
|
const bin = process.env.PRLL_BB_VIEWER_BIN ?? "bb-viewer";
|
|
32454
32729
|
const port = await findFreePort();
|
|
32455
32730
|
const args = ["--api-only", "--port", String(port)];
|
|
32731
|
+
const bindHost = process.env.PRLL_BB_VIEWER_HOST?.trim();
|
|
32732
|
+
if (bindHost) {
|
|
32733
|
+
if (!isLoopbackBindHost(bindHost)) {
|
|
32734
|
+
throw new Error(`PRLL_BB_VIEWER_HOST must be loopback (127.0.0.1, localhost, ::1, [::1]) \u2014 refusing to bind bb-viewer's unauthenticated /command to ${bindHost}`);
|
|
32735
|
+
}
|
|
32736
|
+
args.push("--host", bindHost);
|
|
32737
|
+
}
|
|
32456
32738
|
if (turn?.url) {
|
|
32457
32739
|
args.push("--turn-url", turn.url, "--turn-user", turn.username ?? "", "--turn-cred", turn.credential ?? "");
|
|
32458
32740
|
}
|
|
@@ -32570,8 +32852,14 @@ var BrowserViewerStreamer = class {
|
|
|
32570
32852
|
clearTimeout(timer);
|
|
32571
32853
|
}
|
|
32572
32854
|
}
|
|
32573
|
-
/**
|
|
32855
|
+
/**
|
|
32856
|
+
* Kill + remove the profile's bb-viewer streamer, if any. Idempotent. Also
|
|
32857
|
+
* clears any kick marker for the profile: a (re)kill establishes a clean
|
|
32858
|
+
* streamer slot, so a subsequent stream.start starts un-kicked. viewerKick
|
|
32859
|
+
* re-records the kicked session AFTER calling this.
|
|
32860
|
+
*/
|
|
32574
32861
|
killProfileStreamer(profileId) {
|
|
32862
|
+
this.kickedSessions.delete(profileId);
|
|
32575
32863
|
const streamer = this.streamers.get(profileId);
|
|
32576
32864
|
if (!streamer)
|
|
32577
32865
|
return;
|
|
@@ -32908,10 +33196,12 @@ var BrowserProfileManager = class {
|
|
|
32908
33196
|
if (!isBrowserAccountInfoUnauthenticatedError(err)) {
|
|
32909
33197
|
if (!isBrowserAccountNotFoundError(err))
|
|
32910
33198
|
throw err;
|
|
33199
|
+
const proxy = this.opts.resolveProxy ? await this.opts.resolveProxy(account) : null;
|
|
32911
33200
|
await this.sendCommand({
|
|
32912
33201
|
method: "account_create",
|
|
32913
33202
|
account,
|
|
32914
|
-
...accountUrl ? { accountUrl } : {}
|
|
33203
|
+
...accountUrl ? { accountUrl } : {},
|
|
33204
|
+
...proxyCreateFields(proxy)
|
|
32915
33205
|
});
|
|
32916
33206
|
accountCreated = true;
|
|
32917
33207
|
}
|
|
@@ -33168,6 +33458,16 @@ function resolveBbBrowserDaemonPath() {
|
|
|
33168
33458
|
function randomToken() {
|
|
33169
33459
|
return randomBytes(16).toString("hex");
|
|
33170
33460
|
}
|
|
33461
|
+
function proxyCreateFields(proxy) {
|
|
33462
|
+
if (!proxy?.server)
|
|
33463
|
+
return {};
|
|
33464
|
+
const fields = { proxyServer: proxy.server };
|
|
33465
|
+
if (proxy.username)
|
|
33466
|
+
fields.proxyUsername = proxy.username;
|
|
33467
|
+
if (proxy.password)
|
|
33468
|
+
fields.proxyPassword = proxy.password;
|
|
33469
|
+
return fields;
|
|
33470
|
+
}
|
|
33171
33471
|
|
|
33172
33472
|
// ts/daemon/dist/supervisor.js
|
|
33173
33473
|
init_config();
|
|
@@ -34095,7 +34395,8 @@ var DaemonSupervisor = class {
|
|
|
34095
34395
|
log: this.log,
|
|
34096
34396
|
reportStatus: (profileId, status, errorMsg) => {
|
|
34097
34397
|
this.client.reportBrowserProfileStatus(profileId, status, errorMsg).catch((err) => this.log.warn(`browser profile status report failed: ${String(err)}`));
|
|
34098
|
-
}
|
|
34398
|
+
},
|
|
34399
|
+
resolveProxy: (profileId) => this.resolveBrowserProfileProxy(profileId)
|
|
34099
34400
|
});
|
|
34100
34401
|
this.clipManager = new ClipProcessManager({
|
|
34101
34402
|
clipsDir: path13.join(this.config.rootStateDir, "clips"),
|
|
@@ -34343,10 +34644,13 @@ var DaemonSupervisor = class {
|
|
|
34343
34644
|
for (const profile of profiles) {
|
|
34344
34645
|
if (profile.status !== "running" && profile.status !== "pending")
|
|
34345
34646
|
continue;
|
|
34647
|
+
if (!profile.machine_id)
|
|
34648
|
+
continue;
|
|
34649
|
+
const machineId = profile.machine_id;
|
|
34346
34650
|
try {
|
|
34347
34651
|
if (profile.status === "pending") {
|
|
34348
34652
|
await this.enqueueBrowserProfileLifecycle({
|
|
34349
|
-
machine_id:
|
|
34653
|
+
machine_id: machineId,
|
|
34350
34654
|
profile_id: profile.id,
|
|
34351
34655
|
action: "open"
|
|
34352
34656
|
});
|
|
@@ -34358,6 +34662,30 @@ var DaemonSupervisor = class {
|
|
|
34358
34662
|
}
|
|
34359
34663
|
}
|
|
34360
34664
|
}
|
|
34665
|
+
/**
|
|
34666
|
+
* Resolve a profile's outbound proxy for bb-browser account creation (BYOC).
|
|
34667
|
+
* Fetched fresh — account creation is rare (once per profile until reset), so a
|
|
34668
|
+
* proxy edit applies on the next account_create. The mck_-authed machine list is
|
|
34669
|
+
* the only path that carries proxy_password (for replay to bb-browser).
|
|
34670
|
+
*
|
|
34671
|
+
* Fails closed: a fetch failure or a vanished profile THROWS, so the manager never
|
|
34672
|
+
* creates an account that would egress from the host's real IP for a
|
|
34673
|
+
* proxy-configured profile (the caller reports `error` and retries next tick).
|
|
34674
|
+
*/
|
|
34675
|
+
async resolveBrowserProfileProxy(profileId) {
|
|
34676
|
+
const profiles = await this.client.listMachineBrowserProfiles();
|
|
34677
|
+
const profile = profiles.find((p) => p.id === profileId);
|
|
34678
|
+
if (!profile) {
|
|
34679
|
+
throw new Error(`browser profile ${profileId} not found; refusing to create a bb-browser account without its proxy config`);
|
|
34680
|
+
}
|
|
34681
|
+
if (!profile.proxy_server)
|
|
34682
|
+
return null;
|
|
34683
|
+
return {
|
|
34684
|
+
server: profile.proxy_server,
|
|
34685
|
+
username: profile.proxy_username ?? void 0,
|
|
34686
|
+
password: profile.proxy_password ?? void 0
|
|
34687
|
+
};
|
|
34688
|
+
}
|
|
34361
34689
|
// ---- Flat layout migration (self-hosted → daemon) ----
|
|
34362
34690
|
/**
|
|
34363
34691
|
* Detects a legacy flat state layout (no agents/ subdir) and migrates it
|
|
@@ -34602,7 +34930,7 @@ var DaemonSupervisor = class {
|
|
|
34602
34930
|
}
|
|
34603
34931
|
if (changed) {
|
|
34604
34932
|
this.log.info(`machine clips synced \u2014 count=${desired.length}`);
|
|
34605
|
-
|
|
34933
|
+
this.clipProvider?.reregister();
|
|
34606
34934
|
}
|
|
34607
34935
|
}
|
|
34608
34936
|
startClipReconcileTimer() {
|
|
@@ -34731,15 +35059,6 @@ var DaemonSupervisor = class {
|
|
|
34731
35059
|
}
|
|
34732
35060
|
return null;
|
|
34733
35061
|
}
|
|
34734
|
-
async reconnectClipProvider() {
|
|
34735
|
-
if (!this.clipProvider)
|
|
34736
|
-
return;
|
|
34737
|
-
const provider = this.clipProvider;
|
|
34738
|
-
this.clipProvider = null;
|
|
34739
|
-
this.connectedClipServiceUrl = null;
|
|
34740
|
-
await provider.disconnect().catch((err) => this.log.warn(`clip provider reconnect disconnect failed: ${String(err)}`));
|
|
34741
|
-
await this.applyClipProviderState();
|
|
34742
|
-
}
|
|
34743
35062
|
async handleWorkspaceSetupRequested(agentId) {
|
|
34744
35063
|
if (this.spawningAgents.has(agentId)) {
|
|
34745
35064
|
this.log.info(`agent ${agentId}: workspace setup already in progress; queueing one restart`);
|