@parall/daemon 1.34.0 → 1.35.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.
@@ -27118,6 +27118,7 @@ var ENDPOINTS = {
27118
27118
  // Org-scoped
27119
27119
  ORG: (orgId) => `${API_BASE}/orgs/${orgId}`,
27120
27120
  ORG_MEMBERS: (orgId) => `${API_BASE}/orgs/${orgId}/members`,
27121
+ TEAMS: (orgId) => `${API_BASE}/orgs/${orgId}/teams`,
27121
27122
  ORG_MEMBERS_ONLINE: (orgId) => `${API_BASE}/orgs/${orgId}/members/online`,
27122
27123
  ORG_MEMBER: (orgId, userId) => `${API_BASE}/orgs/${orgId}/members/${userId}`,
27123
27124
  ORG_MEMBER_CHATS: (orgId, memberId) => `${API_BASE}/orgs/${orgId}/members/${memberId}/chats`,
@@ -27162,6 +27163,7 @@ var ENDPOINTS = {
27162
27163
  AGENT: (orgId, agentId) => `${API_BASE}/orgs/${orgId}/agents/${agentId}`,
27163
27164
  AGENT_API_KEYS: (orgId, agentId) => `${API_BASE}/orgs/${orgId}/agents/${agentId}/api-keys`,
27164
27165
  AGENT_API_KEY: (orgId, agentId, key) => `${API_BASE}/orgs/${orgId}/agents/${agentId}/api-keys/${key}`,
27166
+ AGENT_API_KEY_REGENERATE: (orgId, agentId) => `${API_BASE}/orgs/${orgId}/agents/${agentId}/api-keys/regenerate`,
27165
27167
  AGENT_AVATAR: (orgId, agentId) => `${API_BASE}/orgs/${orgId}/agents/${agentId}/avatar`,
27166
27168
  AGENT_ACTIVITY: (orgId, agentId) => `${API_BASE}/orgs/${orgId}/agents/${agentId}/activity`,
27167
27169
  AGENT_MONITOR: (orgId, agentId) => `${API_BASE}/orgs/${orgId}/agents/${agentId}/monitor`,
@@ -27201,6 +27203,7 @@ var ENDPOINTS = {
27201
27203
  MACHINE_AGENT_WORKSPACE_SETUP: (orgId, machineId, agentId) => `${API_BASE}/orgs/${orgId}/machines/${machineId}/agents/${agentId}/workspace/setup`,
27202
27204
  MACHINE_LLM_SOURCE: (orgId, machineId) => `${API_BASE}/orgs/${orgId}/machines/${machineId}/llm-source`,
27203
27205
  MACHINE_PROVIDER_ENABLED: (orgId, machineId) => `${API_BASE}/orgs/${orgId}/machines/${machineId}/provider-enabled`,
27206
+ MACHINE_CAPABILITIES: (orgId, machineId) => `${API_BASE}/orgs/${orgId}/machines/${machineId}/capabilities`,
27204
27207
  MACHINE_RUNTIME_AUTH: (orgId, machineId) => `${API_BASE}/orgs/${orgId}/machines/${machineId}/runtime-auth`,
27205
27208
  MACHINE_KEYS: (orgId, machineId) => `${API_BASE}/orgs/${orgId}/machines/${machineId}/keys`,
27206
27209
  MACHINE_KEY: (orgId, machineId, keyId) => `${API_BASE}/orgs/${orgId}/machines/${machineId}/keys/${keyId}`,
@@ -27221,6 +27224,12 @@ var ENDPOINTS = {
27221
27224
  MACHINES_ME_AGENT_WORKSPACE_STATE: (agentId) => `${API_BASE}/machines/me/agents/${agentId}/workspace-state`,
27222
27225
  MACHINES_ME_WS_TICKET: `${API_BASE}/machines/me/ws/ticket`,
27223
27226
  MACHINES_ME_BROWSE_RESPONSE: (requestId) => `${API_BASE}/machines/me/browse-response/${requestId}`,
27227
+ // Hosted browser live-viewer control plane (api-server, NOT clip-service):
27228
+ // the web client drives WebRTC signaling + tab nav through VIEWER_COMMAND;
27229
+ // api-server brokers each command to the host daemon via the machine:{id}
27230
+ // request/reply bridge (mirrors filesystem browse), and the daemon replies on
27231
+ // VIEWER_RESPONSE. See docs/engineering-design/hosted-browser-provider-design.md.
27232
+ MACHINES_ME_BROWSER_PROFILE_VIEWER_RESPONSE: (requestId) => `${API_BASE}/machines/me/browser-profiles/viewer-response/${requestId}`,
27224
27233
  // Tasks (org-scoped)
27225
27234
  TASKS: (orgId) => `${API_BASE}/orgs/${orgId}/tasks`,
27226
27235
  TASK: (orgId, taskId) => `${API_BASE}/orgs/${orgId}/tasks/${taskId}`,
@@ -27286,6 +27295,9 @@ var ENDPOINTS = {
27286
27295
  // Wiki Path Scopes (AFCS ACL)
27287
27296
  WIKI_PATH_SCOPES: (orgId, wikiId) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}/path-scopes`,
27288
27297
  WIKI_PATH_SCOPE: (orgId, wikiId, scopeId) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}/path-scopes/${scopeId}`,
27298
+ // Wiki Path Restrictions (AFCS narrowing ACL — private subtrees)
27299
+ WIKI_RESTRICTIONS: (orgId, wikiId) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}/restrictions`,
27300
+ WIKI_RESTRICTION: (orgId, wikiId, restrictionId) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}/restrictions/${restrictionId}`,
27289
27301
  WIKI_ACCESS_STATUS: (orgId, wikiId) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}/access-status`,
27290
27302
  WIKI_ACCESS_REQUESTS: (orgId, wikiId) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}/access-requests`,
27291
27303
  // Wiki History (commits, file commits, blame)
@@ -27351,6 +27363,7 @@ var ENDPOINTS = {
27351
27363
  // Clips (org-scoped, served by clip-service)
27352
27364
  CLIPS: (orgId) => `${CLIP_BASE}/orgs/${orgId}/clips`,
27353
27365
  CLIP: (orgId, clipId) => `${CLIP_BASE}/orgs/${orgId}/clips/${clipId}`,
27366
+ CLIPS_BULK_METADATA: (orgId) => `${CLIP_BASE}/orgs/${orgId}/clips/bulk-metadata`,
27354
27367
  CLIP_AGENTS: (orgId, clipId) => `${CLIP_BASE}/orgs/${orgId}/clips/${clipId}/agents`,
27355
27368
  AGENT_CLIPS: (orgId, agentId) => `${CLIP_BASE}/orgs/${orgId}/agents/${agentId}/clips`,
27356
27369
  AGENT_CLIP: (orgId, agentId, clipId) => `${CLIP_BASE}/orgs/${orgId}/agents/${agentId}/clips/${clipId}`,
@@ -27363,6 +27376,9 @@ var ENDPOINTS = {
27363
27376
  BROWSER_PROFILE_RESET: (orgId, profileId) => `${CLIP_BASE}/orgs/${orgId}/browser-profiles/${profileId}/reset`,
27364
27377
  BROWSER_PROFILE_CONSENTS: (orgId, profileId) => `${CLIP_BASE}/orgs/${orgId}/browser-profiles/${profileId}/consents`,
27365
27378
  BROWSER_PROFILE_CONSENT: (orgId, profileId, clipId) => `${CLIP_BASE}/orgs/${orgId}/browser-profiles/${profileId}/consents/${clipId}`,
27379
+ // Live viewer command — on api-server (API_BASE), not clip-service: it rides
27380
+ // the machine control-plane request/reply bridge that lives in api-server.
27381
+ BROWSER_PROFILE_VIEWER_COMMAND: (orgId, profileId) => `${API_BASE}/orgs/${orgId}/browser-profiles/${profileId}/viewer/command`,
27366
27382
  // Clip registry (global, served by clip-service → Pinix Hub proxy)
27367
27383
  CLIP_REGISTRY: () => `${CLIP_BASE}/registry/clips`
27368
27384
  };
@@ -27439,6 +27455,7 @@ var WS_EVENTS = {
27439
27455
  MACHINE_CONFIG_UPDATED: "machine.config.updated",
27440
27456
  MACHINE_CLIP_SYNC: "machine.clip.sync",
27441
27457
  MACHINE_BROWSER_PROFILE_LIFECYCLE: "machine.browser_profile.lifecycle",
27458
+ MACHINE_BROWSER_PROFILE_VIEWER: "machine.browser_profile.viewer",
27442
27459
  AGENT_NEW_SESSION: "agent.new_session",
27443
27460
  CLIP_CREATED: "clip.created",
27444
27461
  CLIP_REMOVED: "clip.removed",
@@ -27590,7 +27607,10 @@ var ParallClient = class _ParallClient {
27590
27607
  method,
27591
27608
  headers,
27592
27609
  body: body ? JSON.stringify(body) : void 0,
27593
- signal
27610
+ signal,
27611
+ // keepalive lets a request fired during page unload (e.g. the browser
27612
+ // viewer's stream.close on pagehide) outlive the document.
27613
+ keepalive: opts?.keepalive
27594
27614
  });
27595
27615
  } catch (err) {
27596
27616
  throw _ParallClient.normalizeFetchError(err);
@@ -27761,6 +27781,10 @@ var ParallClient = class _ParallClient {
27761
27781
  const res = await this.request("GET", ENDPOINTS.ORG_MEMBERS(orgId));
27762
27782
  return res.data;
27763
27783
  }
27784
+ async getTeams(orgId) {
27785
+ const res = await this.request("GET", ENDPOINTS.TEAMS(orgId));
27786
+ return res.data;
27787
+ }
27764
27788
  async getOnlineMembers(orgId) {
27765
27789
  const res = await this.request("GET", ENDPOINTS.ORG_MEMBERS_ONLINE(orgId));
27766
27790
  return res.user_ids ?? [];
@@ -27999,6 +28023,10 @@ var ParallClient = class _ParallClient {
27999
28023
  async createAgentApiKey(orgId, agentId) {
28000
28024
  return this.request("POST", ENDPOINTS.AGENT_API_KEYS(orgId, agentId));
28001
28025
  }
28026
+ /** Revokes all of the agent's active API keys and mints a replacement. */
28027
+ async regenerateAgentApiKey(orgId, agentId) {
28028
+ return this.request("POST", ENDPOINTS.AGENT_API_KEY_REGENERATE(orgId, agentId));
28029
+ }
28002
28030
  async revokeAgentApiKey(orgId, agentId, key) {
28003
28031
  return this.request("DELETE", ENDPOINTS.AGENT_API_KEY(orgId, agentId, key));
28004
28032
  }
@@ -28170,6 +28198,17 @@ var ParallClient = class _ParallClient {
28170
28198
  provider_enabled: providerEnabled
28171
28199
  });
28172
28200
  }
28201
+ /**
28202
+ * Replace the machine's capability set (admin). Primary use: healing a
28203
+ * machine that registered without `browser_provider` during the capability
28204
+ * migration's rolling-deploy window. Removing `agent_host` is rejected by the
28205
+ * server while agents are attached (409 AGENTS_STILL_ATTACHED).
28206
+ */
28207
+ async patchMachineCapabilities(orgId, machineId, capabilities) {
28208
+ return this.request("PATCH", ENDPOINTS.MACHINE_CAPABILITIES(orgId, machineId), {
28209
+ capabilities
28210
+ });
28211
+ }
28173
28212
  /** Get machine-level runtime auth state. */
28174
28213
  async getMachineRuntimeAuth(orgId, machineId) {
28175
28214
  return this.request("GET", ENDPOINTS.MACHINE_RUNTIME_AUTH(orgId, machineId));
@@ -28260,6 +28299,22 @@ var ParallClient = class _ParallClient {
28260
28299
  async postBrowseResponse(requestId, response) {
28261
28300
  return this.request("POST", ENDPOINTS.MACHINES_ME_BROWSE_RESPONSE(requestId), response);
28262
28301
  }
28302
+ /**
28303
+ * `POST /machines/me/browser-profiles/viewer-response/{requestId}` — daemon
28304
+ * reply to a `machine.browser_profile.viewer` control command. Wakes the
28305
+ * api-server request/reply bridge (mirrors {@link postBrowseResponse}).
28306
+ */
28307
+ async postBrowserProfileViewerResponse(requestId, response) {
28308
+ return this.request("POST", ENDPOINTS.MACHINES_ME_BROWSER_PROFILE_VIEWER_RESPONSE(requestId), response);
28309
+ }
28310
+ /**
28311
+ * `POST /orgs/{orgId}/browser-profiles/{profileId}/viewer/command` — drive the
28312
+ * hosted browser live viewer (WebRTC signaling + tab nav). Authz: profile
28313
+ * owner or org admin. api-server brokers the command to the host daemon.
28314
+ */
28315
+ async browserViewerCommand(orgId, profileId, req, opts) {
28316
+ return this.request("POST", ENDPOINTS.BROWSER_PROFILE_VIEWER_COMMAND(orgId, profileId), req, void 0, false, opts);
28317
+ }
28263
28318
  async resizeMachine(orgId, machineId, spec) {
28264
28319
  return this.request("PATCH", ENDPOINTS.MACHINE_SPEC(orgId, machineId), spec);
28265
28320
  }
@@ -28636,6 +28691,17 @@ var ParallClient = class _ParallClient {
28636
28691
  async deleteWikiPathScope(orgId, wikiId, scopeId) {
28637
28692
  await this.request("DELETE", ENDPOINTS.WIKI_PATH_SCOPE(orgId, wikiId, scopeId));
28638
28693
  }
28694
+ // ---- Wiki Path Restrictions (narrowing ACL — private subtrees) ----
28695
+ async getWikiRestrictions(orgId, wikiId) {
28696
+ const res = await this.request("GET", ENDPOINTS.WIKI_RESTRICTIONS(orgId, wikiId));
28697
+ return res.data;
28698
+ }
28699
+ async createWikiRestriction(orgId, wikiId, data) {
28700
+ return this.request("POST", ENDPOINTS.WIKI_RESTRICTIONS(orgId, wikiId), data);
28701
+ }
28702
+ async deleteWikiRestriction(orgId, wikiId, restrictionId) {
28703
+ await this.request("DELETE", ENDPOINTS.WIKI_RESTRICTION(orgId, wikiId, restrictionId));
28704
+ }
28639
28705
  async getWikiAccessStatus(orgId, wikiId, path15) {
28640
28706
  return this.request("GET", ENDPOINTS.WIKI_ACCESS_STATUS(orgId, wikiId), void 0, path15 ? { path: path15 } : void 0);
28641
28707
  }
@@ -28798,6 +28864,10 @@ var ParallClient = class _ParallClient {
28798
28864
  async updateClip(orgId, clipId, req) {
28799
28865
  return this.request("PATCH", ENDPOINTS.CLIP(orgId, clipId), req);
28800
28866
  }
28867
+ /** Atomically set display_name and/or description on multiple clip instances (application metadata). */
28868
+ async bulkUpdateClipMetadata(orgId, req) {
28869
+ return this.request("POST", ENDPOINTS.CLIPS_BULK_METADATA(orgId), req);
28870
+ }
28801
28871
  async deleteClip(orgId, clipId) {
28802
28872
  await this.request("DELETE", ENDPOINTS.CLIP(orgId, clipId));
28803
28873
  }
@@ -29698,7 +29768,7 @@ async function runCLI(args) {
29698
29768
  init_config();
29699
29769
 
29700
29770
  // ts/daemon/dist/supervisor.js
29701
- import { spawn as spawn5 } from "node:child_process";
29771
+ import { spawn as spawn6 } from "node:child_process";
29702
29772
  import * as fs12 from "node:fs";
29703
29773
  import * as path13 from "node:path";
29704
29774
 
@@ -32001,13 +32071,561 @@ var ClipProvider = class _ClipProvider {
32001
32071
  };
32002
32072
 
32003
32073
  // ts/daemon/dist/clip-runtime/browser-profile-manager.js
32004
- import { spawn as spawn3 } from "node:child_process";
32074
+ import { spawn as spawn4 } from "node:child_process";
32005
32075
  import { randomBytes } from "node:crypto";
32006
32076
  import { existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync7 } from "node:fs";
32007
32077
  import { createRequire } from "node:module";
32008
- import * as net from "node:net";
32009
32078
  import * as path9 from "node:path";
32010
32079
  import { fileURLToPath } from "node:url";
32080
+
32081
+ // ts/daemon/dist/clip-runtime/browser-viewer-streamer.js
32082
+ import { spawn as spawn3 } from "node:child_process";
32083
+
32084
+ // ts/daemon/dist/clip-runtime/subprocess.js
32085
+ import * as net from "node:net";
32086
+ function formatErrorForLog(err) {
32087
+ const message = err instanceof Error ? err.message : String(err);
32088
+ return message.replace(/\s+/g, " ").slice(0, 300);
32089
+ }
32090
+ async function findFreePort() {
32091
+ return new Promise((resolve8, reject) => {
32092
+ const server = net.createServer();
32093
+ server.unref();
32094
+ server.on("error", reject);
32095
+ server.listen(0, "127.0.0.1", () => {
32096
+ const address = server.address();
32097
+ server.close(() => {
32098
+ if (address && typeof address === "object") {
32099
+ resolve8(address.port);
32100
+ } else {
32101
+ reject(new Error("failed to allocate free port"));
32102
+ }
32103
+ });
32104
+ });
32105
+ });
32106
+ }
32107
+ function sleep(ms) {
32108
+ return new Promise((resolve8) => setTimeout(resolve8, ms));
32109
+ }
32110
+
32111
+ // ts/daemon/dist/clip-runtime/browser-viewer-streamer.js
32112
+ var BB_VIEWER_HEALTH_TIMEOUT_MS = 1e4;
32113
+ var BB_VIEWER_COMMAND_TIMEOUT_MS = 15e3;
32114
+ var BB_VIEWER_UNANSWERED_REAP_MS = 9e4;
32115
+ var BB_VIEWER_TERM_GRACE_MS = 5e3;
32116
+ var BrowserViewerStreamer = class {
32117
+ host;
32118
+ // Live bb-viewer (WebRTC streamer) subprocess per profile. One per profile;
32119
+ // see StreamerState. Cleaned up on stream.close, stopProfile, resetProfile,
32120
+ // and stop().
32121
+ streamers = /* @__PURE__ */ new Map();
32122
+ // Set by shutdown(): an in-flight spawnStreamer has no map entry yet, so a
32123
+ // shutdown during its health poll would otherwise let the bb-viewer child
32124
+ // survive daemon stop (on BYOC nothing else reaps it).
32125
+ stopping = false;
32126
+ constructor(host) {
32127
+ this.host = host;
32128
+ }
32129
+ /**
32130
+ * Handle a viewer control command for a profile. Throws on any failure (the
32131
+ * supervisor maps a throw to a `{error:{message}}` reply); never returns a
32132
+ * partial result. `data.input` / `data.turn` / `data.session_id` come from
32133
+ * MachineBrowserProfileViewerData.
32134
+ */
32135
+ async handleViewerCommand(profileId, sessionId, command, input, turn) {
32136
+ if (!profileId)
32137
+ throw new Error("browser profile id is required");
32138
+ switch (command) {
32139
+ case "stream.start":
32140
+ return this.viewerStreamStart(profileId, sessionId, turn);
32141
+ case "stream.answer":
32142
+ return this.viewerStreamAnswer(profileId, sessionId, input);
32143
+ case "stream.close":
32144
+ return this.viewerStreamClose(profileId, sessionId, input);
32145
+ case "stream.switch":
32146
+ return this.viewerStreamSwitch(profileId, sessionId, input);
32147
+ case "close":
32148
+ return this.viewerCloseTab(profileId, sessionId, input);
32149
+ case "tab_list":
32150
+ case "tab_new":
32151
+ case "open":
32152
+ case "reload":
32153
+ case "back":
32154
+ case "forward":
32155
+ return this.viewerForwardNav(profileId, sessionId, command, input);
32156
+ default:
32157
+ throw new Error(`unknown viewer command: ${command}`);
32158
+ }
32159
+ }
32160
+ /**
32161
+ * stream.start — spawn a FRESH bb-viewer for this profile (killing any prior
32162
+ * one), resolve the profile's account-scoped page-target CDP ws URL, and run
32163
+ * the streamer's `connect` command. Records sessionId on the streamer entry.
32164
+ */
32165
+ async viewerStreamStart(profileId, sessionId, turn) {
32166
+ this.killProfileStreamer(profileId);
32167
+ const cdpUrl = await this.host.withDaemonRecovery(() => this.resolveAccountPageCdpUrl(profileId), `viewer stream.start ${profileId}`);
32168
+ await this.bringTargetToFront(cdpUrl);
32169
+ const streamer = await this.spawnStreamer(profileId, sessionId, turn);
32170
+ try {
32171
+ const result = await this.streamerCommand(streamer, "connect", { cdpUrl });
32172
+ streamer.streamedTabId = targetIdFromCdpUrl(cdpUrl);
32173
+ const reapTimer = setTimeout(() => {
32174
+ const current = this.streamers.get(profileId);
32175
+ if (current === streamer && !current.answered) {
32176
+ this.host.log.warn(`[bb-viewer] no stream.answer for ${profileId} within ${BB_VIEWER_UNANSWERED_REAP_MS / 1e3}s; reaping abandoned streamer`);
32177
+ this.killProfileStreamer(profileId);
32178
+ }
32179
+ }, BB_VIEWER_UNANSWERED_REAP_MS);
32180
+ reapTimer.unref?.();
32181
+ return { ...result, streamed_tab_id: streamer.streamedTabId };
32182
+ } catch (err) {
32183
+ this.killProfileStreamer(profileId);
32184
+ throw err;
32185
+ }
32186
+ }
32187
+ /**
32188
+ * stream.answer — apply the client's WebRTC answer to the profile's streamer.
32189
+ * Rejects if the streamer's sessionId no longer matches (a stale viewer
32190
+ * session from a superseded stream.start).
32191
+ */
32192
+ async viewerStreamAnswer(profileId, sessionId, input) {
32193
+ const streamer = this.streamers.get(profileId);
32194
+ if (!streamer)
32195
+ throw new Error("no active viewer streamer for profile");
32196
+ if (streamer.sessionId !== sessionId)
32197
+ throw new Error("stale viewer session");
32198
+ streamer.answered = true;
32199
+ return this.streamerCommand(streamer, "answer", input ?? {});
32200
+ }
32201
+ /**
32202
+ * stream.close — best-effort stop the streamer, then kill + remove it. A
32203
+ * stale session's close (independent HTTP legs can arrive out of order after
32204
+ * a retry) must NOT tear down a successor session's live streamer, so a
32205
+ * mismatched sessionId is ignored. An EMPTY sessionId gets the same stale
32206
+ * treatment — a client closed mid-handshake never received a session id, and
32207
+ * its unscoped close racing the next session's stream.start would kill that
32208
+ * session's live streamer (React StrictMode's dev double-mount exercises
32209
+ * exactly this ordering). The only unscoped close is an explicit
32210
+ * `{force:true}` input, reserved for a server-initiated admin disconnect.
32211
+ */
32212
+ async viewerStreamClose(profileId, sessionId, input) {
32213
+ const streamer = this.streamers.get(profileId);
32214
+ if (streamer) {
32215
+ const force = input?.force === true;
32216
+ if (!force && streamer.sessionId !== sessionId) {
32217
+ return { ok: true, stale: true };
32218
+ }
32219
+ try {
32220
+ await this.streamerCommand(streamer, "stop", {});
32221
+ } catch (err) {
32222
+ this.host.log.warn(`[bb-viewer] stop command failed for ${profileId} (killing anyway): ${formatErrorForLog(err)}`);
32223
+ }
32224
+ this.killProfileStreamer(profileId);
32225
+ }
32226
+ return { ok: true };
32227
+ }
32228
+ /**
32229
+ * stream.switch — point the existing streamer at a different account-scoped
32230
+ * tab. input {tab} is the bb-browser short tab id / target id. Guarded by
32231
+ * sessionId like stream.answer: a superseded session must not steer the
32232
+ * current streamer, and an EMPTY sessionId is rejected the same way — every
32233
+ * real caller has its session id by the time it can switch (it arrives with
32234
+ * the stream.start response), so a session-less switch is by definition not
32235
+ * the current viewer.
32236
+ */
32237
+ async viewerStreamSwitch(profileId, sessionId, input) {
32238
+ const streamer = this.streamers.get(profileId);
32239
+ if (!streamer)
32240
+ throw new Error("no active viewer streamer for profile");
32241
+ if (!sessionId || streamer.sessionId !== sessionId)
32242
+ throw new Error("stale viewer session");
32243
+ const tab = input?.tab;
32244
+ if (tab === void 0 || tab === null || tab === "") {
32245
+ throw new Error("stream.switch requires input.tab");
32246
+ }
32247
+ const cdpUrl = await this.host.withDaemonRecovery(() => this.resolveAccountPageCdpUrl(profileId, tab), `viewer stream.switch ${profileId}`);
32248
+ await this.bringTargetToFront(cdpUrl);
32249
+ const result = await this.streamerCommand(streamer, "switch", { cdpUrl });
32250
+ streamer.streamedTabId = targetIdFromCdpUrl(cdpUrl);
32251
+ return { ...result, streamed_tab_id: streamer.streamedTabId };
32252
+ }
32253
+ /**
32254
+ * close — close an account tab via bb-browser, then, if the closed tab was
32255
+ * the one being streamed, re-point the streamer at the profile's next active
32256
+ * tab. Without the re-switch the streamer stays bound to a destroyed CDP
32257
+ * target and the viewer goes black. Returns `streamed_tab_id` when a
32258
+ * re-switch happened so the client can adopt the new active tab.
32259
+ */
32260
+ async viewerCloseTab(profileId, sessionId, input) {
32261
+ const ref = viewerTabRef(input ?? {});
32262
+ const closedTargetId = ref !== void 0 ? await this.accountTabTargetId(profileId, ref) : await this.accountActivePageTargetId(profileId);
32263
+ const result = await this.viewerForwardNav(profileId, sessionId, "close", {
32264
+ ...input ?? {},
32265
+ tabId: closedTargetId
32266
+ });
32267
+ const streamer = this.streamers.get(profileId);
32268
+ if (!streamer || !streamer.streamedTabId || streamer.streamedTabId !== closedTargetId) {
32269
+ return result;
32270
+ }
32271
+ const cdpUrl = await this.host.withDaemonRecovery(() => this.resolveAccountPageCdpUrl(profileId), `viewer close re-switch ${profileId}`);
32272
+ await this.bringTargetToFront(cdpUrl);
32273
+ const sw = await this.streamerCommand(streamer, "switch", { cdpUrl });
32274
+ streamer.streamedTabId = targetIdFromCdpUrl(cdpUrl);
32275
+ return { ...result, ...sw, streamed_tab_id: streamer.streamedTabId };
32276
+ }
32277
+ /**
32278
+ * Bring a CDP page target to the foreground via Chrome's DevTools HTTP
32279
+ * endpoint (`/json/activate/{targetId}`, same host:port as the CDP ws). Chrome
32280
+ * does not composite backgrounded tabs, so `Page.startScreencast` on a
32281
+ * background tab yields zero frames (black viewer). Best-effort: a failure
32282
+ * must not block the stream. host:port + targetId are parsed from the cdpUrl
32283
+ * (`ws://host:port/devtools/page/<targetId>`) to avoid an extra /status call.
32284
+ */
32285
+ async bringTargetToFront(cdpUrl) {
32286
+ const m = /^ws:\/\/([^/]+)\/devtools\/page\/(.+)$/.exec(cdpUrl);
32287
+ if (!m)
32288
+ return;
32289
+ const [, hostPort, targetId] = m;
32290
+ try {
32291
+ const resp = await fetch(`http://${hostPort}/json/activate/${targetId}`, {
32292
+ method: "GET",
32293
+ signal: AbortSignal.timeout(5e3)
32294
+ });
32295
+ if (!resp.ok) {
32296
+ this.host.log.warn(`[bb-viewer] activate target ${targetId} returned ${resp.status} (continuing)`);
32297
+ }
32298
+ } catch (err) {
32299
+ this.host.log.warn(`[bb-viewer] activate target ${targetId} failed (continuing): ${formatErrorForLog(err)}`);
32300
+ }
32301
+ }
32302
+ /**
32303
+ * Forward a navigation command (tab_list/tab_new/open/reload/back/forward/
32304
+ * close) to bb-browser scoped to the profile's account. This lets the human
32305
+ * in the live viewer navigate and handle OAuth popups directly. Reuses the
32306
+ * same account-scoped sendCommand path the agent invokes use.
32307
+ *
32308
+ * SECURITY — the `account` field alone does NOT scope tab addressing:
32309
+ * bb-browser's ensurePageTarget resolves a client-supplied tab/tabId against
32310
+ * EVERY page target in the shared Chrome (all accounts), and tab_list
32311
+ * returns every tab with `account` as a mere annotation (verified in
32312
+ * @pinixai/bb-browser-pro@0.15.0 dist/daemon.js ensurePageTarget/tab_list).
32313
+ * On a multi-profile host that would let one profile's viewer list,
32314
+ * navigate, and close other profiles' logged-in tabs. So before forwarding:
32315
+ * (a) any tab ref must resolve through accountTabTargetId, which throws on
32316
+ * tabs the profile's account does not own; (b) ref-less tab-addressed
32317
+ * commands are pinned to the profile's own active tab instead of
32318
+ * bb-browser's account-blind global current tab; (c) tab_list output is
32319
+ * filtered to the profile's own rows; and (d) open/tab_new URLs must be
32320
+ * http(s)/about:blank — file:// or chrome:// would read the host
32321
+ * filesystem / browser internals straight into the video stream.
32322
+ */
32323
+ async viewerForwardNav(profileId, sessionId, command, input) {
32324
+ if (command !== "tab_list") {
32325
+ const streamer = this.streamers.get(profileId);
32326
+ if (streamer && streamer.sessionId !== sessionId) {
32327
+ throw new Error("stale viewer session");
32328
+ }
32329
+ }
32330
+ return this.host.withDaemonRecovery(async () => {
32331
+ await this.host.ensureAccount(profileId);
32332
+ const request3 = { ...input ?? {} };
32333
+ if (command === "open" || command === "tab_new") {
32334
+ assertViewerNavigableUrl(request3.url);
32335
+ }
32336
+ const ref = viewerTabRef(request3);
32337
+ if (ref !== void 0) {
32338
+ request3.tabId = await this.accountTabTargetId(profileId, ref);
32339
+ delete request3.tab;
32340
+ } else if (REFLESS_PIN_NAV.has(command)) {
32341
+ request3.tabId = await this.accountActivePageTargetId(profileId);
32342
+ }
32343
+ const result = await this.host.sendBrowserCommand({
32344
+ ...request3,
32345
+ method: command,
32346
+ account: profileId
32347
+ });
32348
+ return command === "tab_list" ? filterTabListToAccount(result, profileId) : result;
32349
+ }, `viewer ${command} ${profileId}`);
32350
+ }
32351
+ /**
32352
+ * Resolve `ws://<cdpHost>:<cdpPort>/devtools/page/<targetId>` for a PAGE
32353
+ * target owned by THIS profile's account — NOT bb-browser's global current
32354
+ * tab (which `getCurrentTabCdpUrl`/`getTabCdpUrl` in the bundled daemon use
32355
+ * via `cdp.currentTargetId`, an account-blind global; see daemon.js
32356
+ * line ~10389/10410). When `tabRef` is given (stream.switch) we resolve that
32357
+ * specific account tab; otherwise the profile's active page tab.
32358
+ *
32359
+ * Evidence from the installed @pinixai/bb-browser-pro@0.15.0 dist/daemon.js:
32360
+ * - GET /status (handleStatus, line ~1659-1668) returns `cdpHost` + `cdpPort`
32361
+ * — the CDP host/port the daemon's Chrome listens on.
32362
+ * - The CDP page ws URL format is `ws://<cdp.host>:<cdp.port>/devtools/page/<page.id>`
32363
+ * (getCurrentTabCdpUrl, line ~10392), where `page.id` is the full CDP
32364
+ * targetId (getTargets maps `t.targetId` → `.id`, line ~2461-2466).
32365
+ * - tab_list (command handler, line ~1041-1059) returns per-tab
32366
+ * `{ tabId: t.id, tab: <shortId>, account: <accountName>, url, ... }`, so
32367
+ * `tabId` is exactly the full targetId for the devtools URL and `account`
32368
+ * is the per-tab attribution we filter on (reused by findAccountTabOnHost).
32369
+ */
32370
+ async resolveAccountPageCdpUrl(profileId, tabRef) {
32371
+ await this.host.ensureAccount(profileId);
32372
+ const { host, port } = await this.host.cdpEndpoint();
32373
+ const targetId = tabRef !== void 0 ? await this.accountTabTargetId(profileId, tabRef) : await this.accountActivePageTargetId(profileId);
32374
+ return `ws://${host}:${port}/devtools/page/${targetId}`;
32375
+ }
32376
+ /**
32377
+ * Full CDP targetId of the profile account's active page tab (the owned tab
32378
+ * Chrome marks active, else the first owned tab). If the account owns no
32379
+ * page tab yet, open `about:blank` for it first (mirrors the agent-invoke
32380
+ * path's "always give a fresh account an owned tab" rule).
32381
+ */
32382
+ async accountActivePageTargetId(profileId) {
32383
+ let targetId = await this.firstAccountPageTargetId(profileId);
32384
+ if (targetId !== void 0)
32385
+ return targetId;
32386
+ await this.host.sendBrowserCommand({
32387
+ method: "tab_new",
32388
+ account: profileId,
32389
+ url: "about:blank"
32390
+ });
32391
+ targetId = await this.firstAccountPageTargetId(profileId);
32392
+ if (targetId === void 0) {
32393
+ throw new Error(`no page tab found for profile account ${profileId}`);
32394
+ }
32395
+ return targetId;
32396
+ }
32397
+ /**
32398
+ * Full CDP targetId (`tabId`) of the account's active page tab: the owned tab
32399
+ * Chrome marks `active` when it belongs to this account, else the first owned
32400
+ * tab (the global active flag is account-blind — another profile's tab may
32401
+ * hold it, which must not leak here).
32402
+ */
32403
+ async firstAccountPageTargetId(profileId) {
32404
+ const tabs = await this.accountTabs(profileId);
32405
+ let firstOwned;
32406
+ for (const t of tabs) {
32407
+ if (t.account !== profileId)
32408
+ continue;
32409
+ const tabId = t.tabId;
32410
+ if (typeof tabId !== "string" || !tabId)
32411
+ continue;
32412
+ if (t.active === true)
32413
+ return tabId;
32414
+ firstOwned ??= tabId;
32415
+ }
32416
+ return firstOwned;
32417
+ }
32418
+ /**
32419
+ * Full CDP targetId for a specific account tab, addressed by the bb-browser
32420
+ * short tab id (`tab`) or full target id (`tabId`). Used by stream.switch.
32421
+ */
32422
+ async accountTabTargetId(profileId, tabRef) {
32423
+ const ref = String(tabRef);
32424
+ const tabs = await this.accountTabs(profileId);
32425
+ for (const t of tabs) {
32426
+ if (t.account !== profileId)
32427
+ continue;
32428
+ const tabId = typeof t.tabId === "string" ? t.tabId : void 0;
32429
+ const shortTab = t.tab === void 0 ? void 0 : String(t.tab);
32430
+ if (tabId && tabId === ref || shortTab && shortTab === ref) {
32431
+ if (!tabId)
32432
+ break;
32433
+ return tabId;
32434
+ }
32435
+ }
32436
+ throw new Error(`tab ${ref} not found for profile account ${profileId}`);
32437
+ }
32438
+ /** `tab_list {account}` rows, typed to the fields we read (tabId/tab/account/active). */
32439
+ async accountTabs(profileId) {
32440
+ const list = await this.host.sendBrowserCommand({ method: "tab_list", account: profileId });
32441
+ return Array.isArray(list.tabs) ? list.tabs : [];
32442
+ }
32443
+ // ---- bb-viewer streamer lifecycle ----
32444
+ /**
32445
+ * Spawn a bb-viewer streamer for the profile and wait until healthy. Binary
32446
+ * is PRLL_BB_VIEWER_BIN (our image sets it to /usr/local/bin/bb-viewer),
32447
+ * defaulting to `bb-viewer` on PATH — we NEVER download it. A free port is
32448
+ * allocated per streamer. Stores the entry in `this.streamers` once healthy.
32449
+ */
32450
+ async spawnStreamer(profileId, sessionId, turn) {
32451
+ if (this.stopping)
32452
+ throw new Error("viewer streamer is shutting down");
32453
+ const bin = process.env.PRLL_BB_VIEWER_BIN ?? "bb-viewer";
32454
+ const port = await findFreePort();
32455
+ const args = ["--api-only", "--port", String(port)];
32456
+ if (turn?.url) {
32457
+ args.push("--turn-url", turn.url, "--turn-user", turn.username ?? "", "--turn-cred", turn.credential ?? "");
32458
+ }
32459
+ const streamerEnv = {};
32460
+ for (const key of ["PATH", "HOME", "LANG", "LC_ALL", "TMPDIR", "DISPLAY"]) {
32461
+ const val = process.env[key];
32462
+ if (val !== void 0)
32463
+ streamerEnv[key] = val;
32464
+ }
32465
+ this.host.log.info(`[bb-viewer] spawning for profile ${profileId} on port ${port}`);
32466
+ const child = spawn3(bin, args, { stdio: ["ignore", "pipe", "pipe"], env: streamerEnv });
32467
+ let childError = null;
32468
+ child.once("error", (err) => {
32469
+ childError = err;
32470
+ });
32471
+ child.once("exit", () => {
32472
+ const current = this.streamers.get(profileId);
32473
+ if (current?.child === child) {
32474
+ this.streamers.delete(profileId);
32475
+ this.host.log.warn(`[bb-viewer] streamer for ${profileId} exited; cleared stale entry`);
32476
+ }
32477
+ });
32478
+ child.stdout?.on("data", (chunk) => {
32479
+ const text = chunk.toString("utf8").trim();
32480
+ if (text)
32481
+ this.host.log.info(`[bb-viewer] ${text}`);
32482
+ });
32483
+ child.stderr?.on("data", (chunk) => {
32484
+ const text = chunk.toString("utf8").trim();
32485
+ if (text)
32486
+ this.host.log.warn(`[bb-viewer] ${text}`);
32487
+ });
32488
+ const deadline = Date.now() + BB_VIEWER_HEALTH_TIMEOUT_MS;
32489
+ while (Date.now() < deadline) {
32490
+ if (this.stopping) {
32491
+ this.killChild(child, "streamer for stopping daemon");
32492
+ throw new Error("viewer streamer is shutting down");
32493
+ }
32494
+ if (childError) {
32495
+ throw childError;
32496
+ }
32497
+ if (child.exitCode !== null || child.signalCode !== null) {
32498
+ throw new Error("bb-viewer exited during startup");
32499
+ }
32500
+ try {
32501
+ const resp = await fetch(`http://127.0.0.1:${port}/health`, {
32502
+ signal: AbortSignal.timeout(2e3)
32503
+ });
32504
+ if (resp.ok) {
32505
+ const streamer = { child, port, sessionId };
32506
+ this.streamers.set(profileId, streamer);
32507
+ this.host.log.info(`[bb-viewer] ready for profile ${profileId} on port ${port}`);
32508
+ return streamer;
32509
+ }
32510
+ } catch {
32511
+ }
32512
+ await sleep(200);
32513
+ }
32514
+ this.killChild(child, "unhealthy streamer");
32515
+ throw new Error("bb-viewer did not become healthy in time");
32516
+ }
32517
+ /**
32518
+ * SIGTERM with a SIGKILL escalation: a bb-viewer that hangs or ignores TERM
32519
+ * would otherwise outlive its teardown holding the port + CDP session (the
32520
+ * shell-level pattern-kill only exists on the hosted pod, not BYOC). The
32521
+ * escalation timer is unref'd so it never holds the daemon open; it is
32522
+ * cleared on exit. Caveat: a daemon process that exits immediately after
32523
+ * shutdown() abandons the timer — acceptable, the TERM was still sent.
32524
+ */
32525
+ killChild(child, label) {
32526
+ if (child.exitCode !== null || child.signalCode !== null)
32527
+ return;
32528
+ try {
32529
+ child.kill("SIGTERM");
32530
+ } catch {
32531
+ return;
32532
+ }
32533
+ const killTimer = setTimeout(() => {
32534
+ if (child.exitCode === null && child.signalCode === null) {
32535
+ this.host.log.warn(`[bb-viewer] ${label} ignored SIGTERM; escalating to SIGKILL`);
32536
+ try {
32537
+ child.kill("SIGKILL");
32538
+ } catch {
32539
+ }
32540
+ }
32541
+ }, BB_VIEWER_TERM_GRACE_MS);
32542
+ killTimer.unref?.();
32543
+ child.once("exit", () => clearTimeout(killTimer));
32544
+ }
32545
+ /**
32546
+ * POST a command to the profile's bb-viewer `/command` endpoint. Parses the
32547
+ * `{result}` / `{error:{message}}` envelope (bb-viewer api.go) and returns
32548
+ * the inner result, throwing the error message on failure.
32549
+ */
32550
+ async streamerCommand(streamer, method, params) {
32551
+ const controller = new AbortController();
32552
+ const timer = setTimeout(() => controller.abort(), BB_VIEWER_COMMAND_TIMEOUT_MS);
32553
+ try {
32554
+ const resp = await fetch(`http://127.0.0.1:${streamer.port}/command`, {
32555
+ method: "POST",
32556
+ headers: { "Content-Type": "application/json" },
32557
+ body: JSON.stringify({ method, params }),
32558
+ signal: controller.signal
32559
+ });
32560
+ const text = await resp.text();
32561
+ const parsed = text ? JSON.parse(text) : {};
32562
+ if (parsed.error) {
32563
+ throw new Error(parsed.error.message || `bb-viewer ${method} failed`);
32564
+ }
32565
+ if (!resp.ok) {
32566
+ throw new Error(`bb-viewer ${method} returned ${resp.status}: ${text}`);
32567
+ }
32568
+ return parsed.result ?? {};
32569
+ } finally {
32570
+ clearTimeout(timer);
32571
+ }
32572
+ }
32573
+ /** Kill + remove the profile's bb-viewer streamer, if any. Idempotent. */
32574
+ killProfileStreamer(profileId) {
32575
+ const streamer = this.streamers.get(profileId);
32576
+ if (!streamer)
32577
+ return;
32578
+ this.streamers.delete(profileId);
32579
+ this.killChild(streamer.child, `streamer for ${profileId}`);
32580
+ }
32581
+ /**
32582
+ * Tear down every profile streamer and refuse new spawns (daemon stop).
32583
+ * Idempotent.
32584
+ */
32585
+ shutdown() {
32586
+ this.stopping = true;
32587
+ for (const profileId of [...this.streamers.keys()]) {
32588
+ this.killProfileStreamer(profileId);
32589
+ }
32590
+ }
32591
+ };
32592
+ function targetIdFromCdpUrl(cdpUrl) {
32593
+ return /\/devtools\/page\/(.+)$/.exec(cdpUrl)?.[1];
32594
+ }
32595
+ var REFLESS_PIN_NAV = /* @__PURE__ */ new Set(["reload", "back", "forward", "close"]);
32596
+ function viewerTabRef(request3) {
32597
+ for (const key of ["tabId", "tab"]) {
32598
+ const v = request3[key];
32599
+ if (typeof v === "string" && v !== "" || typeof v === "number")
32600
+ return String(v);
32601
+ }
32602
+ return void 0;
32603
+ }
32604
+ var VIEWER_NAVIGABLE_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:"]);
32605
+ function assertViewerNavigableUrl(raw) {
32606
+ if (raw === void 0 || raw === null || raw === "")
32607
+ return;
32608
+ if (typeof raw !== "string")
32609
+ throw new Error("url must be a string");
32610
+ if (raw === "about:blank")
32611
+ return;
32612
+ let parsed;
32613
+ try {
32614
+ parsed = new URL(raw);
32615
+ } catch {
32616
+ throw new Error(`invalid url: ${raw}`);
32617
+ }
32618
+ if (!VIEWER_NAVIGABLE_PROTOCOLS.has(parsed.protocol)) {
32619
+ throw new Error(`url scheme not allowed in the live viewer: ${parsed.protocol.replace(/:$/, "")}`);
32620
+ }
32621
+ }
32622
+ function filterTabListToAccount(result, account) {
32623
+ const tabs = Array.isArray(result.tabs) ? result.tabs : [];
32624
+ const owned = tabs.filter((t) => t?.account === account);
32625
+ return { ...result, tabs: owned, activeIndex: owned.findIndex((t) => t?.active === true) };
32626
+ }
32627
+
32628
+ // ts/daemon/dist/clip-runtime/browser-profile-manager.js
32011
32629
  var BB_BROWSER_DAEMON_START_TIMEOUT_MS = 15e3;
32012
32630
  var BrowserCommandError = class extends Error {
32013
32631
  method;
@@ -32027,9 +32645,20 @@ var BrowserProfileManager = class {
32027
32645
  ensuredAccounts = /* @__PURE__ */ new Set();
32028
32646
  ensuringAccounts = /* @__PURE__ */ new Map();
32029
32647
  reportedStatuses = /* @__PURE__ */ new Map();
32648
+ // Live viewer orchestration (bb-viewer streamer subprocesses + viewer nav)
32649
+ // lives in BrowserViewerStreamer; the manager hands it the account-scoped
32650
+ // bb-browser command path via the host interface.
32651
+ viewer;
32030
32652
  constructor(opts) {
32031
32653
  this.opts = opts;
32032
32654
  mkdirSync5(this.opts.homeDir, { recursive: true });
32655
+ this.viewer = new BrowserViewerStreamer({
32656
+ log: this.opts.log,
32657
+ sendBrowserCommand: (request3) => this.sendCommand(request3),
32658
+ ensureAccount: (account) => this.ensureAccount(account),
32659
+ withDaemonRecovery: (operation, label) => this.withDaemonRecovery(operation, label),
32660
+ cdpEndpoint: () => this.cdpEndpoint()
32661
+ });
32033
32662
  }
32034
32663
  async invoke({ profileId, account, command, input }) {
32035
32664
  if (!profileId)
@@ -32096,12 +32725,14 @@ var BrowserProfileManager = class {
32096
32725
  if (!profileId)
32097
32726
  throw new Error("browser profile id is required");
32098
32727
  this.reportedStatuses.delete(profileId);
32728
+ this.viewer.killProfileStreamer(profileId);
32099
32729
  this.reportStatus(profileId, "stopped");
32100
32730
  }
32101
32731
  async resetProfile(profileId) {
32102
32732
  if (!profileId)
32103
32733
  throw new Error("browser profile id is required");
32104
32734
  this.reportedStatuses.delete(profileId);
32735
+ this.viewer.killProfileStreamer(profileId);
32105
32736
  try {
32106
32737
  await this.withDaemonRecovery(() => this.resetProfileOnce(profileId), `reset profile ${profileId}`);
32107
32738
  this.reportStatus(profileId, "stopped");
@@ -32112,6 +32743,7 @@ var BrowserProfileManager = class {
32112
32743
  }
32113
32744
  }
32114
32745
  async stop() {
32746
+ this.viewer.shutdown();
32115
32747
  const daemon = this.daemon;
32116
32748
  this.daemon = null;
32117
32749
  this.starting = null;
@@ -32128,6 +32760,25 @@ var BrowserProfileManager = class {
32128
32760
  daemon.child.kill("SIGTERM");
32129
32761
  }
32130
32762
  }
32763
+ /**
32764
+ * Viewer control command entrypoint — delegates to BrowserViewerStreamer
32765
+ * (browser-viewer-streamer.ts), which owns the per-profile bb-viewer
32766
+ * subprocesses and viewer tab navigation.
32767
+ */
32768
+ async handleViewerCommand(profileId, sessionId, command, input, turn) {
32769
+ return this.viewer.handleViewerCommand(profileId, sessionId, command, input, turn);
32770
+ }
32771
+ /** Read cdpHost/cdpPort from the bb-browser daemon `GET /status`. */
32772
+ async cdpEndpoint() {
32773
+ const daemon = await this.ensureDaemon();
32774
+ const status = await this.post("/status", daemon, void 0, 5e3);
32775
+ const host = status.cdpHost?.trim();
32776
+ const port = status.cdpPort;
32777
+ if (!host || typeof port !== "number" || !Number.isFinite(port)) {
32778
+ throw new Error("bb-browser /status did not report a CDP endpoint");
32779
+ }
32780
+ return { host, port };
32781
+ }
32131
32782
  async prepareOpenProfile(profileId, url) {
32132
32783
  const accountCreated = await this.ensureAccount(profileId, url !== "about:blank" ? url : void 0);
32133
32784
  await this.sendCommand({ method: "tab_list", account: profileId });
@@ -32386,7 +33037,7 @@ var BrowserProfileManager = class {
32386
33037
  const port = await findFreePort();
32387
33038
  const token = randomToken();
32388
33039
  const daemonPath = resolveBbBrowserDaemonPath();
32389
- const child = spawn3(process.execPath, [daemonPath, "--host", host, "--port", String(port), "--token", token], {
33040
+ const child = spawn4(process.execPath, [daemonPath, "--host", host, "--port", String(port), "--token", token], {
32390
33041
  env: {
32391
33042
  ...process.env,
32392
33043
  BB_BROWSER_HOME: this.opts.homeDir
@@ -32500,10 +33151,6 @@ function comparableUrl(url) {
32500
33151
  return null;
32501
33152
  }
32502
33153
  }
32503
- function formatErrorForLog(err) {
32504
- const message = err instanceof Error ? err.message : String(err);
32505
- return message.replace(/\s+/g, " ").slice(0, 300);
32506
- }
32507
33154
  function resolveBbBrowserDaemonPath() {
32508
33155
  const sibling = path9.join(path9.dirname(fileURLToPath(import.meta.url)), "bb-browser-daemon.js");
32509
33156
  if (existsSync8(sibling))
@@ -32518,29 +33165,9 @@ function resolveBbBrowserDaemonPath() {
32518
33165
  }
32519
33166
  return full;
32520
33167
  }
32521
- async function findFreePort() {
32522
- return new Promise((resolve8, reject) => {
32523
- const server = net.createServer();
32524
- server.unref();
32525
- server.on("error", reject);
32526
- server.listen(0, "127.0.0.1", () => {
32527
- const address = server.address();
32528
- server.close(() => {
32529
- if (address && typeof address === "object") {
32530
- resolve8(address.port);
32531
- } else {
32532
- reject(new Error("failed to allocate free port"));
32533
- }
32534
- });
32535
- });
32536
- });
32537
- }
32538
33168
  function randomToken() {
32539
33169
  return randomBytes(16).toString("hex");
32540
33170
  }
32541
- function sleep(ms) {
32542
- return new Promise((resolve8) => setTimeout(resolve8, ms));
32543
- }
32544
33171
 
32545
33172
  // ts/daemon/dist/supervisor.js
32546
33173
  init_config();
@@ -32940,7 +33567,7 @@ function shellQuote(value) {
32940
33567
  }
32941
33568
 
32942
33569
  // ts/daemon/dist/workspace.js
32943
- import { spawn as spawn4 } from "node:child_process";
33570
+ import { spawn as spawn5 } from "node:child_process";
32944
33571
  import { createHash as createHash3 } from "node:crypto";
32945
33572
  import * as fs11 from "node:fs";
32946
33573
  import * as path12 from "node:path";
@@ -33244,7 +33871,7 @@ function runCommand(cmd, args, cwd, timeoutMs = 12e4, env = process.env) {
33244
33871
  clearTimeout(killTimer);
33245
33872
  fn();
33246
33873
  };
33247
- const child = spawn4(cmd, args, { cwd, env, stdio: ["ignore", "pipe", "pipe"] });
33874
+ const child = spawn5(cmd, args, { cwd, env, stdio: ["ignore", "pipe", "pipe"] });
33248
33875
  timeoutTimer = setTimeout(() => {
33249
33876
  timedOut = true;
33250
33877
  child.kill("SIGTERM");
@@ -33397,7 +34024,12 @@ var DaemonSupervisor = class {
33397
34024
  children = /* @__PURE__ */ new Map();
33398
34025
  spawningAgents = /* @__PURE__ */ new Set();
33399
34026
  pendingWorkspaceSetup = /* @__PURE__ */ new Set();
33400
- browserProfileLifecycleQueues = /* @__PURE__ */ new Map();
34027
+ // ONE queue per profile for BOTH lifecycle (open/stop/reset) and viewer
34028
+ // (stream.*/nav) commands. They mutate the same streamer + account state, so
34029
+ // two independent queues would let a stop/reset interleave with an in-flight
34030
+ // stream.start (the kill fires before the streamer registers and misses it,
34031
+ // leaving a live streamer on a profile the server just marked stopped).
34032
+ browserProfileOpQueues = /* @__PURE__ */ new Map();
33401
34033
  workspaceSetupRetryTimers = /* @__PURE__ */ new Map();
33402
34034
  cancelledSpawns = /* @__PURE__ */ new Set();
33403
34035
  ws = null;
@@ -33558,6 +34190,10 @@ var DaemonSupervisor = class {
33558
34190
  this.log.info(`WS: browser profile lifecycle requested \u2014 profile=${data.profile_id} action=${data.action}`);
33559
34191
  void this.enqueueBrowserProfileLifecycle(data);
33560
34192
  });
34193
+ this.ws.on("machine.browser_profile.viewer", (data) => {
34194
+ this.log.info(`WS: browser profile viewer command \u2014 profile=${data.profile_id} command=${data.command}`);
34195
+ this.enqueueBrowserProfileViewer(data);
34196
+ });
33561
34197
  this.ws.onStateChange((state) => {
33562
34198
  if (state === "disconnected" || state === "reconnecting") {
33563
34199
  this.log.warn(`machine WS state: ${state}`);
@@ -33869,17 +34505,45 @@ var DaemonSupervisor = class {
33869
34505
  }
33870
34506
  }
33871
34507
  enqueueBrowserProfileLifecycle(data) {
33872
- const previous = this.browserProfileLifecycleQueues.get(data.profile_id) ?? Promise.resolve();
34508
+ return this.enqueueBrowserProfileOp(data.profile_id, () => this.handleBrowserProfileLifecycle(data));
34509
+ }
34510
+ enqueueBrowserProfileOp(profileId, op) {
34511
+ const previous = this.browserProfileOpQueues.get(profileId) ?? Promise.resolve();
33873
34512
  const next = previous.catch(() => {
33874
- }).then(() => this.handleBrowserProfileLifecycle(data));
33875
- this.browserProfileLifecycleQueues.set(data.profile_id, next);
34513
+ }).then(op);
34514
+ this.browserProfileOpQueues.set(profileId, next);
33876
34515
  void next.finally(() => {
33877
- if (this.browserProfileLifecycleQueues.get(data.profile_id) === next) {
33878
- this.browserProfileLifecycleQueues.delete(data.profile_id);
34516
+ if (this.browserProfileOpQueues.get(profileId) === next) {
34517
+ this.browserProfileOpQueues.delete(profileId);
33879
34518
  }
33880
34519
  });
33881
34520
  return next;
33882
34521
  }
34522
+ /**
34523
+ * Handle one viewer control command and reply via REST. Always answers the
34524
+ * request/reply bridge exactly once ({result} on success, {error:{message}}
34525
+ * on failure) and never throws into the WS event loop — mirrors
34526
+ * handleFilesystemBrowse. Serialized per profile by enqueueBrowserProfileViewer.
34527
+ */
34528
+ async handleBrowserProfileViewer(data) {
34529
+ if (!this.browserProfileManager) {
34530
+ await this.client.postBrowserProfileViewerResponse(data.request_id, {
34531
+ error: { message: "Browser profile runtime is disabled on this daemon" }
34532
+ }).catch((err) => this.log.warn(`browser profile viewer negative ACK failed: ${String(err)}`));
34533
+ return;
34534
+ }
34535
+ try {
34536
+ const result = await this.browserProfileManager.handleViewerCommand(data.profile_id, data.session_id, data.command, data.input, data.turn);
34537
+ await this.client.postBrowserProfileViewerResponse(data.request_id, { result });
34538
+ } catch (err) {
34539
+ const message = err instanceof Error ? err.message : String(err);
34540
+ this.log.warn(`browser profile viewer command failed (profile=${data.profile_id}, command=${data.command}): ${message}`);
34541
+ await this.client.postBrowserProfileViewerResponse(data.request_id, { error: { message } }).catch((replyErr) => this.log.warn(`browser profile viewer error ACK failed: ${String(replyErr)}`));
34542
+ }
34543
+ }
34544
+ enqueueBrowserProfileViewer(data) {
34545
+ void this.enqueueBrowserProfileOp(data.profile_id, () => this.handleBrowserProfileViewer(data));
34546
+ }
33883
34547
  async handleClipSync(_data) {
33884
34548
  await this.reconcileMachineClips();
33885
34549
  }
@@ -34356,7 +35020,7 @@ var DaemonSupervisor = class {
34356
35020
  const env = adapter.buildEnv(baseEnv, state.agentId, state.orgId, state.credential.api_key, dirs, effectiveProviderConfig);
34357
35021
  const spawnCmd = adapter.args.length > 0 ? `${adapter.bin} ${adapter.args.join(" ")}` : adapter.bin;
34358
35022
  this.log.info(`spawning agent ${state.agentId} runtime=${state.runtimeType} cmd=${spawnCmd} (attempt ${state.restartAttempts + 1})`);
34359
- const child = spawn5(adapter.bin, adapter.args, {
35023
+ const child = spawn6(adapter.bin, adapter.args, {
34360
35024
  env,
34361
35025
  stdio: ["ignore", "inherit", "inherit"],
34362
35026
  detached: false