openmausbot 0.1.58 → 0.1.60

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.
@@ -11,7 +11,7 @@ import { BOT_PROFILE_LIMITS } from "../shared/bot-profile.js";
11
11
  import { approvalModeFor, supportsApprovalMode, requiresNativeApproval, isEmergencyApprovalDowngrade, isApprovalMode, } from "../shared/approval-mode.js";
12
12
  import { escapeAttribute } from "../src/lib/composer-attachments.js";
13
13
  import { CREDENTIAL_TARGETS, credentialResumeOutcome, credentialIsConfigured, isReusableCredentialRequest, isCredentialTargetId, } from "../shared/credential-request.js";
14
- import { approvalHeldReason, approvalModeForOrigin, autoVerdict, rememberableApprovalKey } from "./auto-approve.js";
14
+ import { HELD_NOTE, approvalHeldNote, approvalHeldReason, approvalModeForOrigin, autoVerdict, rememberableApprovalKey } from "./auto-approve.js";
15
15
  import { requestReview, resolveAutoReviewMode, shouldReview } from "./auto-review.js";
16
16
  import { updateClaudeCli } from "./claude-update.js";
17
17
  import { BrowserCleanupCoordinator, finalizeBrowserCleanupMutation, requireBrowserCleanupAcknowledged, } from "./browser-lifecycle-cleanup.js";
@@ -84,8 +84,8 @@ import { redactSecretsInText } from "./redact.js";
84
84
  import * as vps from "./vps-computer.js";
85
85
  import { RoutineManager } from "./routines.js";
86
86
  import { CalendarCallManager } from "./calendar-calls.js";
87
- import { BUILT_IN_BROWSER_SYSTEM_PROMPT, applyDesktopBrowserConnectionMessage, availableBrowserConnection, browserScreenshot, clearBrowserCapabilities, registerBrowserCapability, revokeBrowserCapability, } from "./browser-connection.js";
88
- import { agentBrowserIntegration, browserEngineEncryptionKey, browserEngineStatus, browserSessionId, describeBrowserEngine, } from "./browser-engine.js";
87
+ import { BUILT_IN_BROWSER_SYSTEM_PROMPT } from "./browser-engine.js";
88
+ import { agentBrowserIntegration, browserEngineEncryptionKey, clearBrowserSessionState, ensureChrome, installAgentBrowserBinary, resolveAgentBrowserBinary, browserEngineStatus, browserSessionId, describeBrowserEngine, } from "./browser-engine.js";
89
89
  import { captureOutsideHumanControl } from "./private-screen-capture.js";
90
90
  import { screenFrameHash, screenTouchingTool, settledFrameIsNews } from "./screen-frame-gate.js";
91
91
  import { RoutineRequestService } from "./routine-requests.js";
@@ -203,9 +203,40 @@ function applyDesktopMutationTokenMessage(raw) {
203
203
  }
204
204
  return true;
205
205
  }
206
+ // Browser data of a deleted bot or profile: the engine's saved session
207
+ // state, cleared here on every host (the desktop no longer owns a browser).
208
+ // The coordinator keeps its durable journal and replay; this is its outbox.
206
209
  const browserCleanup = new BrowserCleanupCoordinator({
207
210
  file: join(DATA_DIR, "browser-cleanups.json"),
208
- send: postDesktopPrivateMessage,
211
+ send: (request) => {
212
+ const status = browserEngineStatus();
213
+ // Guest sessions are throwaway and never saved, so only the bot's own
214
+ // session and shared profile sessions have state to clear.
215
+ const sessions = request.type === "openmausbot:browser-bot-deleted" && request.botId
216
+ ? [browserSessionId(request.botId, "")]
217
+ : request.partitionId
218
+ ? [browserSessionId("", request.partitionId)]
219
+ : [];
220
+ // Failed erasure leaves the committed intent pending for retry; it must
221
+ // never acknowledge that saved state was removed when it was not.
222
+ const work = status.kind === "ready" && sessions.length
223
+ ? Promise.all(sessions.map(async (session) => {
224
+ const ok = await clearBrowserSessionState(status.binaryPath, session, { encryptionKey: browserEngineEncryptionKey() });
225
+ if (!ok)
226
+ console.warn(`browser cleanup: could not clear the engine's saved state for session ${session}; clear it with \`agent-browser --session ${session} state clear --all\``);
227
+ return ok;
228
+ }))
229
+ : Promise.resolve([true]);
230
+ void work.then((results) => results.every(Boolean), (error) => {
231
+ console.warn("browser cleanup: could not clear saved session state", error);
232
+ return false;
233
+ }).then((ok) => {
234
+ browserCleanup.receive({ type: "openmausbot:browser-lifecycle-result", requestId: request.requestId, ok });
235
+ }).catch((error) => {
236
+ console.warn("browser cleanup: could not acknowledge cleanup", error);
237
+ });
238
+ return true;
239
+ },
209
240
  });
210
241
  const phoneSecrets = new PhoneSecretBridge(postDesktopPrivateMessage);
211
242
  utilityParentPort?.on("message", (event) => {
@@ -219,8 +250,7 @@ utilityParentPort?.on("message", (event) => {
219
250
  return;
220
251
  if (phoneSecrets.receive(message))
221
252
  return;
222
- if (!applyDesktopBrowserConnectionMessage(message))
223
- composio.applyManagedBrokerMessage(message);
253
+ composio.applyManagedBrokerMessage(message);
224
254
  }
225
255
  catch (error) {
226
256
  console.error(`[desktop-sync] rejected private parent message: ${error instanceof Error ? error.message : String(error)}`);
@@ -300,10 +330,8 @@ function revokeInternalCapabilityForProviderEvent(event) {
300
330
  * length; only capabilities for currently active turns are retained. */
301
331
  function authorizedInternalCapability(header) {
302
332
  const got = Buffer.from(Array.isArray(header) ? "" : (header ?? ""));
303
- const now = Date.now();
304
333
  for (const [token, capability] of internalCapabilities) {
305
- if (capability.orphanExpiresAt <= now ||
306
- activeInternalGenerationByThread.get(capability.threadId) !== capability.generation) {
334
+ if (!internalCapabilityIsActive(capability)) {
307
335
  internalCapabilities.delete(token);
308
336
  continue;
309
337
  }
@@ -314,6 +342,12 @@ function authorizedInternalCapability(header) {
314
342
  return null;
315
343
  }
316
344
  function internalCapabilityIsActive(capability) {
345
+ if (capability.localVmTarget) {
346
+ const owner = localVmLeaseFor(capability.localVmTarget).current(localVmOwnerBusy);
347
+ if (localVmThreadTargets.get(capability.threadId) !== capability.localVmTarget ||
348
+ owner?.threadId !== capability.threadId || owner.botId !== capability.botId)
349
+ return false;
350
+ }
317
351
  return (capability.orphanExpiresAt > Date.now() &&
318
352
  activeInternalGenerationByThread.get(capability.threadId) === capability.generation);
319
353
  }
@@ -368,84 +402,6 @@ function agentsIntegration(botId, threadId, depth, skillAuthoring, generation) {
368
402
  },
369
403
  };
370
404
  }
371
- const browserCapabilitiesByThread = new Map();
372
- const pendingBrowserCapabilityRevocations = new Map();
373
- const BROWSER_REVOCATION_RETRY_MS = [250, 1_000, 3_000, 10_000, 30_000];
374
- async function revokeReleasedBrowserCapability(active, attempt = 0) {
375
- const token = active.capability.token;
376
- try {
377
- await revokeBrowserCapability(active.connection, active.capability);
378
- const pending = pendingBrowserCapabilityRevocations.get(token);
379
- if (pending)
380
- clearTimeout(pending.timer);
381
- pendingBrowserCapabilityRevocations.delete(token);
382
- }
383
- catch (error) {
384
- if (Date.now() >= active.capability.expiresAt) {
385
- pendingBrowserCapabilityRevocations.delete(token);
386
- return;
387
- }
388
- if (attempt === 0) {
389
- console.error(`[browser] could not revoke turn capability; retrying until its absolute expiry: ${error instanceof Error ? error.message : String(error)}`);
390
- }
391
- const delay = Math.min(BROWSER_REVOCATION_RETRY_MS[Math.min(attempt, BROWSER_REVOCATION_RETRY_MS.length - 1)], Math.max(1, active.capability.expiresAt - Date.now()));
392
- const timer = setTimeout(() => {
393
- const pending = pendingBrowserCapabilityRevocations.get(token);
394
- if (!pending || pending.timer !== timer)
395
- return;
396
- void revokeReleasedBrowserCapability(active, attempt + 1);
397
- }, delay);
398
- timer.unref?.();
399
- const previous = pendingBrowserCapabilityRevocations.get(token);
400
- if (previous)
401
- clearTimeout(previous.timer);
402
- pendingBrowserCapabilityRevocations.set(token, { active, attempt: attempt + 1, timer });
403
- }
404
- }
405
- async function releaseBrowserCapabilityForThread(threadId, expectedOwnerId) {
406
- const active = browserCapabilitiesByThread.get(threadId);
407
- if (!active || (expectedOwnerId !== undefined && active.ownerId !== expectedOwnerId))
408
- return;
409
- browserCapabilitiesByThread.delete(threadId);
410
- await revokeReleasedBrowserCapability(active);
411
- }
412
- async function releaseBrowserCapabilitiesForBot(botId) {
413
- const threads = [...browserCapabilitiesByThread]
414
- .filter(([, active]) => active.botId === botId)
415
- .map(([threadId]) => threadId);
416
- await Promise.all(threads.map((threadId) => releaseBrowserCapabilityForThread(threadId)));
417
- }
418
- async function releaseAllBrowserCapabilities() {
419
- const active = [...browserCapabilitiesByThread.values()];
420
- browserCapabilitiesByThread.clear();
421
- const connections = new Map();
422
- for (const entry of active) {
423
- connections.set(`${entry.connection.url}:${entry.connection.token}`, entry.connection);
424
- }
425
- for (const pending of pendingBrowserCapabilityRevocations.values()) {
426
- connections.set(`${pending.active.connection.url}:${pending.active.connection.token}`, pending.active.connection);
427
- }
428
- await Promise.all([...connections.values()].map(async (connection) => {
429
- try {
430
- // Master clear is atomic at the host. It also invalidates a token whose
431
- // earlier per-turn revoke timed out, which is essential for feature-off
432
- // and graceful-shutdown boundaries.
433
- await clearBrowserCapabilities(connection);
434
- for (const [token, pending] of pendingBrowserCapabilityRevocations) {
435
- if (pending.active.connection.url === connection.url &&
436
- pending.active.connection.token === connection.token) {
437
- clearTimeout(pending.timer);
438
- pendingBrowserCapabilityRevocations.delete(token);
439
- }
440
- }
441
- }
442
- catch {
443
- await Promise.all(active
444
- .filter((entry) => entry.connection.url === connection.url && entry.connection.token === connection.token)
445
- .map((entry) => revokeReleasedBrowserCapability(entry)));
446
- }
447
- }));
448
- }
449
405
  class DirectTurnSetupCancelled extends Error {
450
406
  }
451
407
  const directTurnDispatchClaims = new Map();
@@ -540,55 +496,10 @@ function cancelDirectTurnDispatch(botId, expectedThreadId) {
540
496
  // start early enough for those old events to settle the replacement.
541
497
  return claim;
542
498
  }
543
- async function browserIntegration(botId, profile, threadId, generation, stillValid = () => true, ownerId = randomUUID()) {
544
- const connection = availableBrowserConnection();
545
- if (!connection)
546
- return engineBrowserIntegration(botId, profile);
547
- const control = controlIntegration(botId, threadId, generation);
548
- // A profile that no longer exists falls back to the bot's own session.
549
- // Canonical ids belong to config/bot references; Electron must receive the
550
- // exact immutable partition inherited from #567 so an upgrade cannot move
551
- // a bot into another account. Guest remains a throwaway partition.
552
- const profileTarget = profile && profile !== "guest"
553
- ? browserProfilePartitionTarget(cfg, profile)
554
- : null;
555
- const partitionId = profile === "guest" ? "guest" : (profileTarget?.partitionId ?? "");
556
- await releaseBrowserCapabilityForThread(threadId);
557
- const capability = await registerBrowserCapability(connection, botId, partitionId);
558
- const active = { botId, ownerId, connection, capability };
559
- // Registration crosses a process boundary. Stop/delete/config changes can
560
- // land while the desktop host is minting the token; revalidate in the same
561
- // event-loop turn that publishes it. If ownership was lost, no agent ever
562
- // receives the bearer and the just-created token is revoked immediately.
563
- if (!stillValid()) {
564
- await revokeReleasedBrowserCapability(active);
565
- return null;
566
- }
567
- browserCapabilitiesByThread.set(threadId, active);
568
- return {
569
- connection,
570
- capability,
571
- profile: partitionId,
572
- integration: {
573
- command: process.execPath,
574
- args: [SPAWNED_PROXIES.browser],
575
- env: {
576
- ...AGENTS_NODE_FLAG,
577
- OMB_BROWSER_URL: connection.url,
578
- OMB_BROWSER_TOKEN: capability.token,
579
- OMB_BROWSER_PROFILE: partitionId,
580
- OMB_BOT_ID: botId,
581
- OMB_CONTROL_URL: control.url,
582
- OMB_CONTROL_TOKEN: control.token,
583
- },
584
- },
585
- };
586
- }
587
- /** No desktop surface (a server, or a Windows desktop): the bot's browser is
588
- * agent-browser, one isolated session per profile or per bot
589
- * (docs/plans/browser-engine.md). Returns null, with the reason logged once,
590
- * when the engine is not on this machine. */
591
- function engineBrowserIntegration(botId, profile) {
499
+ /** The bot's browser for this turn: agent-browser, one isolated session per
500
+ * browser profile or per bot (docs/plans/browser-engine.md). Null, with the
501
+ * reason logged once, when the engine is not on this machine. */
502
+ function browserIntegration(botId, profile) {
592
503
  const status = browserEngineStatus();
593
504
  if (status.kind !== "ready") {
594
505
  if (!engineUnavailableLogged) {
@@ -597,12 +508,11 @@ function engineBrowserIntegration(botId, profile) {
597
508
  }
598
509
  return null;
599
510
  }
511
+ // A profile that no longer exists falls back to the bot's own session.
600
512
  const profileTarget = profile && profile !== "guest" ? browserProfilePartitionTarget(cfg, profile) : null;
601
513
  const partitionId = profile === "guest" ? "guest" : (profileTarget?.partitionId ?? "");
602
514
  const session = browserSessionId(botId, partitionId);
603
515
  return {
604
- connection: null,
605
- capability: null,
606
516
  profile: partitionId,
607
517
  integration: agentBrowserIntegration({
608
518
  binaryPath: status.binaryPath,
@@ -613,13 +523,14 @@ function engineBrowserIntegration(botId, profile) {
613
523
  };
614
524
  }
615
525
  let engineUnavailableLogged = false;
526
+ let browserEngineInstall = null;
527
+ let browserEngineInstallError = null;
616
528
  export function browserEngineSummary() {
617
- if (availableBrowserConnection())
618
- return { kind: "desktop" };
619
529
  const status = browserEngineStatus();
530
+ const progress = { ...(browserEngineInstall ? { installing: true } : {}), ...(browserEngineInstallError ? { installError: browserEngineInstallError } : {}) };
620
531
  return status.kind === "ready"
621
- ? { kind: "headless", version: status.version }
622
- : { kind: "unavailable", reason: status.reason, installable: status.installable };
532
+ ? { kind: "engine", version: status.version, ...progress }
533
+ : { kind: "unavailable", reason: status.reason, installable: status.installable, ...progress };
623
534
  }
624
535
  function phoneIntegration() {
625
536
  const env = { ...AGENTS_NODE_FLAG };
@@ -680,7 +591,7 @@ const routineRequestEnvelopeSchema = z.discriminatedUnion("action", [
680
591
  ...["pause", "resume", "run_now", "delete"].map((action) => z.object({ ...routineRequestSourceSchema, action: z.literal(action), routineId: z.unknown() }).strict()),
681
592
  ]);
682
593
  /** The loopback endpoint a bot's computer proxy polls before acting. */
683
- function controlIntegration(botId, threadId, generation) {
594
+ function controlIntegration(botId, threadId, generation, localVmTarget) {
684
595
  return {
685
596
  url: `http://127.0.0.1:${PORT}/api/internal/computer-control?botId=${encodeURIComponent(botId)}`,
686
597
  token: mintInternalCapability({
@@ -689,6 +600,7 @@ function controlIntegration(botId, threadId, generation) {
689
600
  generation,
690
601
  depth: 0,
691
602
  kind: "computer",
603
+ ...(localVmTarget ? { localVmTarget } : {}),
692
604
  skillAuthoring: false,
693
605
  createdBots: 0,
694
606
  }),
@@ -1959,7 +1871,9 @@ const watchdog = new TurnWatchdog({
1959
1871
  stallMs: TURN_STALL_MS,
1960
1872
  checkMs: 60_000,
1961
1873
  onStall: (turn) => {
1962
- void releaseBrowserCapabilityForThread(turn.threadId);
1874
+ // Room targets carry an invocation identity; only those claims belong
1875
+ // to the room grace cleanup added here.
1876
+ const stalledVmTarget = groupSpeakers.has(turn.threadId) ? localVmThreadTargets.get(turn.threadId) : undefined;
1963
1877
  revokeInternalCapabilitiesForThread(turn.threadId);
1964
1878
  repeats.settle(turn.threadId);
1965
1879
  const bot = store.bot(turn.botId);
@@ -1988,6 +1902,9 @@ const watchdog = new TurnWatchdog({
1988
1902
  retry.unref?.();
1989
1903
  return;
1990
1904
  }
1905
+ if (stalledVmTarget && localVmThreadTargets.get(turn.threadId) === stalledVmTarget) {
1906
+ releaseLocalVmThread(turn.threadId);
1907
+ }
1991
1908
  const group = store.groupByThread(turn.threadId);
1992
1909
  const speaker = groupSpeakers.get(turn.threadId);
1993
1910
  if (group && group.busyBotId === turn.botId && speaker?.botId === turn.botId) {
@@ -2083,7 +2000,6 @@ bus.subscribe((event) => {
2083
2000
  watchdog.setWaitingOnHuman(event.threadId, false);
2084
2001
  else if (event.type === "turn.completed") {
2085
2002
  watchdog.settle(event.threadId);
2086
- void releaseBrowserCapabilityForThread(event.threadId);
2087
2003
  revokeInternalCapabilityForProviderEvent(event);
2088
2004
  }
2089
2005
  else if (event.type === "session.exited") {
@@ -2093,7 +2009,6 @@ bus.subscribe((event) => {
2093
2009
  const directBotBusy = store.botByThread(event.threadId)?.busy === true;
2094
2010
  const roomBusy = Boolean(store.groupByThread(event.threadId)?.busyBotId);
2095
2011
  if (!directBotBusy && !roomBusy) {
2096
- void releaseBrowserCapabilityForThread(event.threadId);
2097
2012
  }
2098
2013
  }
2099
2014
  else
@@ -2552,18 +2467,23 @@ bus.subscribe((event) => {
2552
2467
  // the guards in auto-approve.ts; explicitly acknowledged Full does not.
2553
2468
  const asker = bot ?? (speaker ? store.bot(speaker.botId) : undefined);
2554
2469
  const unattended = permission && asker && event.requestId ? isUnattended(asker.id) : false;
2470
+ const effectiveApprovalMode = asker ? approvalModeForTurn(asker, isInternalTurn(event.threadId)) : "ask";
2555
2471
  const verdict = permission && asker && event.requestId
2556
2472
  ? autoVerdict({
2557
2473
  // the same origin the dispatch used, so a peer-started turn's
2558
2474
  // residual asks are judged as Approve for me here too
2559
- approvalMode: approvalModeForTurn(asker, isInternalTurn(event.threadId)),
2475
+ approvalMode: effectiveApprovalMode,
2560
2476
  autoApprove: false,
2561
2477
  alwaysAllow: asker.alwaysAllow,
2562
2478
  }, event.tool, event.summary, {
2563
2479
  unattended,
2564
2480
  scope: event.approvalScope,
2565
2481
  requiresExplicitApproval: event.requiresExplicitApproval,
2566
- nativeApproval: requiresNativeApproval(event.provider, approvalModeForTurn(asker)),
2482
+ // Antigravity's residual asks must use a peer-started turn's
2483
+ // effective safe Auto mode, not its durable Full grant.
2484
+ // Preserve the existing policy of every other provider.
2485
+ nativeApproval: requiresNativeApproval(event.provider, event.provider === "antigravityAgent"
2486
+ ? effectiveApprovalMode : approvalModeForTurn(asker)),
2567
2487
  })
2568
2488
  : null;
2569
2489
  if (verdict?.approve && asker && event.requestId) {
@@ -2621,9 +2541,12 @@ bus.subscribe((event) => {
2621
2541
  scope: event.approvalScope,
2622
2542
  requiresExplicitApproval: event.requiresExplicitApproval,
2623
2543
  }),
2624
- held: verdict.source === "full-access"
2625
- ? "Full access couldn't deliver this approval."
2626
- : "Approve for me couldn't answer this one.",
2544
+ held: HELD_NOTE[verdict.source === "full-access"
2545
+ ? "approval.held.undeliveredFull"
2546
+ : "approval.held.undelivered"],
2547
+ heldCode: verdict.source === "full-access"
2548
+ ? "approval.held.undeliveredFull"
2549
+ : "approval.held.undelivered",
2627
2550
  approvalScope: event.approvalScope,
2628
2551
  },
2629
2552
  });
@@ -2643,6 +2566,18 @@ bus.subscribe((event) => {
2643
2566
  })();
2644
2567
  break;
2645
2568
  }
2569
+ // A card can outlive the bot record that raised it. Without one there is
2570
+ // no mode to explain, but the sandbox note still applies.
2571
+ const heldContext = {
2572
+ source: verdict?.source,
2573
+ permission,
2574
+ requiresExplicitApproval: event.requiresExplicitApproval,
2575
+ mode: asker ? approvalModeForOrigin(approvalModeFor(asker), { peerInitiated: isInternalTurn(event.threadId) }) : "ask",
2576
+ unattended: Boolean(unattended),
2577
+ fullAccessAvailable: asker && !isInternalTurn(event.threadId)
2578
+ ? supportsApprovalMode(registry.cliTarget(asker.modelSelection.instanceId)?.driverKind, "full")
2579
+ : false,
2580
+ };
2646
2581
  const message = pushMessage({
2647
2582
  role: "bot",
2648
2583
  kind: "options",
@@ -2665,18 +2600,10 @@ bus.subscribe((event) => {
2665
2600
  requiresExplicitApproval: event.requiresExplicitApproval,
2666
2601
  })
2667
2602
  : undefined,
2668
- // A card can outlive the bot record that raised it. Without one
2669
- // there is no mode to explain, but the sandbox note still applies.
2670
- held: approvalHeldReason({
2671
- source: verdict?.source,
2672
- permission,
2673
- requiresExplicitApproval: event.requiresExplicitApproval,
2674
- mode: asker ? approvalModeForOrigin(approvalModeFor(asker), { peerInitiated: isInternalTurn(event.threadId) }) : "ask",
2675
- unattended: Boolean(unattended),
2676
- fullAccessAvailable: asker && !isInternalTurn(event.threadId)
2677
- ? supportsApprovalMode(registry.cliTarget(asker.modelSelection.instanceId)?.driverKind, "full")
2678
- : false,
2679
- }),
2603
+ // The text stays for cards saved before heldCode existed, and for
2604
+ // clients that do not know the key yet.
2605
+ held: approvalHeldReason(heldContext),
2606
+ heldCode: approvalHeldNote(heldContext),
2680
2607
  approvalScope: event.approvalScope,
2681
2608
  },
2682
2609
  });
@@ -3949,13 +3876,7 @@ async function startTurn(botId, text, opts) {
3949
3876
  liveBot.browser !== false &&
3950
3877
  instance.adapter.capabilities.browserMcp === true) {
3951
3878
  const selectedProfile = liveBot.browserProfile;
3952
- browser = await browserIntegration(bot.id, selectedProfile, threadId, dispatchClaimId, () => {
3953
- const current = store.bot(bot.id);
3954
- return (directTurnClaimIsCurrent(bot.id, dispatchClaimId, threadId) &&
3955
- builtInBrowserEnabled(cfg) &&
3956
- current?.browser !== false &&
3957
- current?.browserProfile === selectedProfile);
3958
- }, dispatchClaimId);
3879
+ browser = browserIntegration(bot.id, selectedProfile);
3959
3880
  if (browser)
3960
3881
  integrations.browser = browser.integration;
3961
3882
  }
@@ -4040,10 +3961,6 @@ async function startTurn(botId, text, opts) {
4040
3961
  // after its own turn.completed would never be torn down — it would
4041
3962
  // keep polling the box forever, carrying dead per-turn state. busy
4042
3963
  // is flipped false in the fold, so it is the honest "still running".
4043
- if (!previewCapture && browser?.connection && browser.capability) {
4044
- const { connection, capability } = browser;
4045
- previewCapture = () => browserScreenshot(connection, capability, fetch);
4046
- }
4047
3964
  if (previewCapture && store.bot(bot.id)?.busy) {
4048
3965
  startScreenPoller(bot.id, previewCapture, { screenIsTheWork: instance.driverKind === "boxAgent" });
4049
3966
  }
@@ -4058,7 +3975,6 @@ async function startTurn(botId, text, opts) {
4058
3975
  clearCancelledProviderHandshake(threadId, `direct:${dispatchClaimId}`);
4059
3976
  clearDirectTurnDispatch(bot.id, dispatchClaimId);
4060
3977
  revokeInternalCapabilityGeneration(threadId, dispatchClaimId);
4061
- await releaseBrowserCapabilityForThread(threadId, dispatchClaimId);
4062
3978
  const ownsLatestGeneration = directTurnGenerationByBot.get(bot.id) === dispatchClaimId;
4063
3979
  if (ownsLatestGeneration) {
4064
3980
  releaseLocalVmThread(threadId);
@@ -4222,7 +4138,6 @@ async function interruptRoutineGroupGoal(groupId, threadId, outcome) {
4222
4138
  const bot = speaker ? store.bot(speaker.botId) : undefined;
4223
4139
  cancelGroupTurnOperations(groupId, threadId, outcome);
4224
4140
  revokeInternalCapabilitiesForThread(threadId);
4225
- await releaseBrowserCapabilityForThread(threadId);
4226
4141
  await (bot ? registry.get(bot.modelSelection.instanceId) : undefined)
4227
4142
  ?.adapter.interruptTurn(threadId)
4228
4143
  .catch(() => { });
@@ -4251,7 +4166,6 @@ async function stopBotForEmergencyApprovalDowngrade(botId) {
4251
4166
  revokeInternalCapabilitiesForThread(groupTurn.threadId);
4252
4167
  cancelGroupTurnOperations(groupTurn.group.id, groupTurn.threadId);
4253
4168
  const results = await Promise.allSettled([
4254
- releaseBrowserCapabilityForThread(groupTurn.threadId),
4255
4169
  registry.get(bot.modelSelection.instanceId)?.adapter.interruptTurn(groupTurn.threadId),
4256
4170
  ]);
4257
4171
  closeOpenApprovals(groupTurn.threadId);
@@ -4264,7 +4178,6 @@ async function stopBotForEmergencyApprovalDowngrade(botId) {
4264
4178
  const threadId = directClaim?.threadId ?? bot.threadId;
4265
4179
  revokeInternalCapabilitiesForThread(threadId);
4266
4180
  const results = await Promise.allSettled([
4267
- releaseBrowserCapabilityForThread(threadId),
4268
4181
  registry.get(bot.modelSelection.instanceId)?.adapter.interruptTurn(threadId),
4269
4182
  ]);
4270
4183
  closeOpenApprovals(threadId);
@@ -4318,7 +4231,6 @@ routines = new RoutineManager({
4318
4231
  ? registry.get(bot.modelSelection.instanceId)
4319
4232
  : null;
4320
4233
  try {
4321
- await releaseBrowserCapabilityForThread(threadId);
4322
4234
  await instance?.adapter.interruptTurn(threadId);
4323
4235
  }
4324
4236
  finally {
@@ -4730,6 +4642,15 @@ setupRetry = 0) {
4730
4642
  return true;
4731
4643
  }
4732
4644
  const internalGeneration = beginInternalCapabilityGeneration(threadId);
4645
+ let roomVmTarget = null;
4646
+ let retainRoomVmLease = false;
4647
+ let roomSpeaker;
4648
+ let providerDispatched = false;
4649
+ const releaseRoomVmLease = () => {
4650
+ if (roomVmTarget && localVmThreadTargets.get(threadId) === roomVmTarget)
4651
+ releaseLocalVmThread(threadId);
4652
+ roomVmTarget = null;
4653
+ };
4733
4654
  try {
4734
4655
  const integrations = {};
4735
4656
  const skillAuthoring = skillRecorderEnabled(cfg) &&
@@ -4879,19 +4800,7 @@ setupRetry = 0) {
4879
4800
  readyBot.browser !== false &&
4880
4801
  instance.adapter.capabilities.browserMcp === true) {
4881
4802
  const selectedProfile = readyBot.browserProfile;
4882
- const browser = await browserIntegration(readyBot.id, selectedProfile, threadId, internalGeneration, () => {
4883
- const currentBot = store.bot(readyBot.id);
4884
- const currentGroup = store.group(readyGroup.id);
4885
- const stillOwnsThread = currentGroup?.dm
4886
- ? currentGroup.threadId === threadId
4887
- : Boolean(currentGroup && store.groupTaskByThread(currentGroup.id, threadId));
4888
- return (!isCancelled?.() &&
4889
- stillOwnsThread &&
4890
- currentBot?.busy === true &&
4891
- builtInBrowserEnabled(cfg) &&
4892
- currentBot.browser !== false &&
4893
- currentBot.browserProfile === selectedProfile);
4894
- });
4803
+ const browser = browserIntegration(readyBot.id, selectedProfile);
4895
4804
  if (browser)
4896
4805
  integrations.browser = browser.integration;
4897
4806
  }
@@ -4900,7 +4809,6 @@ setupRetry = 0) {
4900
4809
  // room's setup claim so no provider turn starts after Stop returned.
4901
4810
  const browserReadyBot = store.bot(readyBot.id);
4902
4811
  if (isCancelled?.() || !browserReadyBot || !browserReadyBot.busy) {
4903
- await releaseBrowserCapabilityForThread(threadId);
4904
4812
  if (browserReadyBot?.busy) {
4905
4813
  store.setActivity(browserReadyBot.id, "idle");
4906
4814
  retryDelegationsWaitingOn(browserReadyBot.id);
@@ -4908,7 +4816,44 @@ setupRetry = 0) {
4908
4816
  return false;
4909
4817
  }
4910
4818
  store.patchGroup(readyGroup.id, { busyBotId: bot.id }); // the store's change stream carries the frame
4911
- groupSpeakers.set(threadId, { botId: bot.id, name: bot.name, color: bot.color });
4819
+ roomSpeaker = { botId: bot.id, name: bot.name, color: bot.color };
4820
+ groupSpeakers.set(threadId, roomSpeaker);
4821
+ // Room and Goal turns use the speaker's desktop, never the coordinator's.
4822
+ // Claim the same lease as direct turns before asynchronous VM setup.
4823
+ if (readyBot.computer === "vm") {
4824
+ if (instance.adapter.capabilities.computerMcp !== true || instance.driverKind === "boxAgent") {
4825
+ throw new Error("this model engine cannot use the Local VM");
4826
+ }
4827
+ // A distinct identity fences cleanup even in shared mode on the same room thread.
4828
+ const target = { ...localVmTargetForBot(readyBot.id) };
4829
+ if (localVmImageBusy || localVmModeChangeBusy || localVmLifecycleBusy.has(target.key)) {
4830
+ throw new Error("this Local VM is being started, stopped, or replaced");
4831
+ }
4832
+ if (!localVmLeaseFor(target).claim(threadId, readyBot.id, localVmOwnerBusy)) {
4833
+ throw new Error("this Local VM is already being used by another turn");
4834
+ }
4835
+ roomVmTarget = target;
4836
+ localVmThreadTargets.set(threadId, target);
4837
+ localVmActiveThreads.set(target.key, threadId);
4838
+ localVmIdleFor(target).touch();
4839
+ const setupIsCurrent = () => !isCancelled?.() &&
4840
+ groupSpeakers.get(threadId) === roomSpeaker &&
4841
+ activeInternalGenerationByThread.get(threadId) === internalGeneration &&
4842
+ store.group(readyGroup.id)?.memberIds.includes(readyBot.id) === true &&
4843
+ store.bot(readyBot.id)?.busy === true &&
4844
+ store.group(readyGroup.id)?.busyBotId === readyBot.id;
4845
+ const vm = await readyLocalVmForTurn(readyBot.id, target, setupIsCurrent);
4846
+ if (!setupIsCurrent()) {
4847
+ return false;
4848
+ }
4849
+ if (!vm.ready || !vm.runtime)
4850
+ throw new Error(vm.problem ?? "the Local VM is not ready");
4851
+ const owner = localVmLeaseFor(target).current(localVmOwnerBusy);
4852
+ if (owner?.threadId !== threadId || owner.botId !== readyBot.id) {
4853
+ throw new Error("the Local VM lease expired while preparing the turn");
4854
+ }
4855
+ integrations.localComputer = containerComputerMcp(vm.runtime, controlIntegration(readyBot.id, threadId, internalGeneration, target), target);
4856
+ }
4912
4857
  const roster = readyGroup.memberIds
4913
4858
  .map((id) => store.bot(id))
4914
4859
  .filter((b) => Boolean(b))
@@ -4959,6 +4904,7 @@ setupRetry = 0) {
4959
4904
  store.patchBot(bot.id, { soulDrift: drift.drift });
4960
4905
  }
4961
4906
  const roomSystem = buildSystemPrompt(system, store.bot(bot.id)?.soul ?? bot.soul ?? "", [
4907
+ { id: "computer", label: "Computer", text: computerPrompt(roomVmTarget ? localVmMode(cfg) === "per-bot" ? "vm-private" : "vm-shared" : null) },
4962
4908
  { id: "browser", label: "Browser", text: integrations.browser ? BUILT_IN_BROWSER_SYSTEM_PROMPT : "" },
4963
4909
  { id: "recall", label: "Recall", text: integrations.agents ? SESSION_SEARCH_SYSTEM_PROMPT : "" },
4964
4910
  { id: "section-context", label: "Section context", text: sectionContextSystemPrompt(bot.section) },
@@ -4984,7 +4930,6 @@ setupRetry = 0) {
4984
4930
  if (isCancelled?.() ||
4985
4931
  store.group(group.id)?.busyBotId !== bot.id ||
4986
4932
  store.bot(bot.id)?.busy !== true) {
4987
- await releaseBrowserCapabilityForThread(threadId);
4988
4933
  if (store.group(group.id)?.busyBotId === bot.id) {
4989
4934
  groupSpeakers.delete(threadId);
4990
4935
  store.patchGroup(group.id, { busyBotId: null, unread: true });
@@ -5016,7 +4961,6 @@ setupRetry = 0) {
5016
4961
  let unregisterStall = () => { };
5017
4962
  const deadline = new RoomTurnDeadline(timeoutMinutes, () => {
5018
4963
  abandonProviderTurn();
5019
- void releaseBrowserCapabilityForThread(threadId);
5020
4964
  void instance.adapter.interruptTurn(threadId).catch(() => { });
5021
4965
  store.appendMessage(threadId, {
5022
4966
  role: "bot",
@@ -5068,6 +5012,7 @@ setupRetry = 0) {
5068
5012
  });
5069
5013
  watchdog.watch(threadId, bot.id);
5070
5014
  onProviderHandshakeStarted?.();
5015
+ providerDispatched = true;
5071
5016
  guardTurnDispatch(instance.adapter.sendTurn({
5072
5017
  threadId,
5073
5018
  text,
@@ -5081,7 +5026,6 @@ setupRetry = 0) {
5081
5026
  // Stop may have landed while the adapter was authenticating, before
5082
5027
  // it had an active process for the first interrupt to reach. Now that
5083
5028
  // sendTurn completed setup, revoke again and interrupt the real turn.
5084
- await releaseBrowserCapabilityForThread(threadId);
5085
5029
  await instance.adapter.interruptTurn(threadId).catch(() => { });
5086
5030
  })
5087
5031
  .then((dispatch) => {
@@ -5121,6 +5065,9 @@ setupRetry = 0) {
5121
5065
  // work so a retained proxy from this member cannot act during the next
5122
5066
  // member's generation.
5123
5067
  revokeInternalCapabilityGeneration(threadId, internalGeneration);
5068
+ retainRoomVmLease = outcome === "timed_out" || outcome === "stalled";
5069
+ if (!retainRoomVmLease)
5070
+ releaseRoomVmLease();
5124
5071
  if (orchestration) {
5125
5072
  orchestration.result.replyText = replyText.trim();
5126
5073
  orchestration.result.outcome = outcome;
@@ -5160,6 +5107,7 @@ setupRetry = 0) {
5160
5107
  retry.unref?.();
5161
5108
  return;
5162
5109
  }
5110
+ releaseRoomVmLease();
5163
5111
  const currentGroup = store.group(group.id);
5164
5112
  const speaker = groupSpeakers.get(threadId);
5165
5113
  if (currentGroup?.busyBotId === bot.id && speaker?.botId === bot.id) {
@@ -5195,7 +5143,6 @@ setupRetry = 0) {
5195
5143
  if (outcome === "dispatch_failed") {
5196
5144
  if (skillAuthoring)
5197
5145
  skillAuthoringClaim.claimed = false;
5198
- await releaseBrowserCapabilityForThread(threadId);
5199
5146
  // No turn.completed follows a rejected room dispatch. Anything that was
5200
5147
  // queued while this bot briefly owned the room must be retried now.
5201
5148
  drainQueuedSends();
@@ -5255,10 +5202,38 @@ setupRetry = 0) {
5255
5202
  }
5256
5203
  return true;
5257
5204
  }
5205
+ catch (error) {
5206
+ if (providerDispatched || !roomSpeaker)
5207
+ throw error;
5208
+ const message = error instanceof Error ? error.message : "Local VM setup failed";
5209
+ store.appendMessage(threadId, {
5210
+ role: "bot", kind: "activity",
5211
+ from: { botId: bot.id, name: bot.name, color: bot.color },
5212
+ tool: { name: `error: ${message}`, ok: false },
5213
+ });
5214
+ onDispatchError?.(message);
5215
+ if (orchestration)
5216
+ orchestration.result.outcome = "dispatch_failed";
5217
+ return false;
5218
+ }
5258
5219
  finally {
5259
5220
  // Covers connector/setup failures, cancellation before dispatch, and all
5260
5221
  // other early returns that never produce a provider terminal event.
5261
5222
  revokeInternalCapabilityGeneration(threadId, internalGeneration);
5223
+ if (!retainRoomVmLease)
5224
+ releaseRoomVmLease();
5225
+ if (!providerDispatched && roomSpeaker && groupSpeakers.get(threadId) === roomSpeaker) {
5226
+ groupSpeakers.delete(threadId);
5227
+ if (store.group(group.id)?.busyBotId === bot.id)
5228
+ store.patchGroup(group.id, { busyBotId: null });
5229
+ if (store.bot(bot.id)?.busy) {
5230
+ store.setActivity(bot.id, "idle");
5231
+ retryDelegationsWaitingOn(bot.id);
5232
+ }
5233
+ drainQueuedSends();
5234
+ drainConnectorResumes();
5235
+ drainSecretResumes();
5236
+ }
5262
5237
  }
5263
5238
  }
5264
5239
  async function runGroupGoalStep(args) {
@@ -6493,13 +6468,15 @@ async function localVmPayload(target) {
6493
6468
  * instance cap; creating past it would quietly do what the lifecycle route
6494
6469
  * refuses.
6495
6470
  */
6496
- async function readyLocalVmForTurn(botId, target) {
6471
+ async function readyLocalVmForTurn(botId, target, isCurrent = () => true) {
6497
6472
  let status = await containerComputerStatus(undefined, undefined, target);
6473
+ if (!isCurrent())
6474
+ return status;
6498
6475
  if (status.ready || !localVmRecreatableOnDemand(status))
6499
6476
  return status;
6500
6477
  if (target.key !== SHARED_LOCAL_VM_TARGET.key) {
6501
6478
  const count = await existingPerBotLocalVmCount(status.runtime);
6502
- if (count >= localVmMaxInstances(cfg))
6479
+ if (!isCurrent() || count >= localVmMaxInstances(cfg))
6503
6480
  return status;
6504
6481
  }
6505
6482
  broadcast({ kind: "computer", botId, state: "provisioning" });
@@ -6522,8 +6499,10 @@ async function readyLocalVmForTurn(botId, target) {
6522
6499
  // the turn is the whole point: a person who has been away eight hours should
6523
6500
  // not have to send their message twice.
6524
6501
  const deadline = Date.now() + LOCAL_VM_DESKTOP_WAIT_MS;
6525
- while (!status.ready && status.container === "running" && Date.now() < deadline) {
6502
+ while (isCurrent() && !status.ready && status.container === "running" && Date.now() < deadline) {
6526
6503
  await new Promise((resolve) => setTimeout(resolve, 2_000));
6504
+ if (!isCurrent())
6505
+ break;
6527
6506
  status = await containerComputerStatus(undefined, undefined, target);
6528
6507
  }
6529
6508
  return status;
@@ -6599,7 +6578,6 @@ async function reloadProviders() {
6599
6578
  // one synchronous step before the first teardown await, including room/task
6600
6579
  // threads that are not a bot's default DM.
6601
6580
  revokeAllInternalCapabilities();
6602
- await releaseAllBrowserCapabilities();
6603
6581
  bus.detachAll();
6604
6582
  await registry.disposeAll();
6605
6583
  await registry.load(instanceConfigs(cfg));
@@ -9257,7 +9235,6 @@ const handleRequest = async (req, res) => {
9257
9235
  cancelGroupTurnOperations(group.id, threadId);
9258
9236
  for (const { threadId, instance } of interruptTargets) {
9259
9237
  revokeInternalCapabilitiesForThread(threadId);
9260
- await releaseBrowserCapabilityForThread(threadId);
9261
9238
  await instance?.adapter.interruptTurn(threadId).catch(() => { });
9262
9239
  closeOpenApprovals(threadId);
9263
9240
  }
@@ -9430,7 +9407,7 @@ const handleRequest = async (req, res) => {
9430
9407
  (!supportsApprovalMode(registry.cliTarget(checked.selection.instanceId)?.driverKind, existingApprovalMode) ||
9431
9408
  registry.cliTarget(checked.selection.instanceId)?.driverKind !== registry.cliTarget(existing.modelSelection.instanceId)?.driverKind)) {
9432
9409
  return json(res, 400, {
9433
- error: "Changing providers with elevated permissions requires choosing Ask or Auto first",
9410
+ error: "Changing providers with elevated permissions requires choosing Ask first",
9434
9411
  });
9435
9412
  }
9436
9413
  // patchBot persists first and emits the canonical bot change, which the
@@ -9682,7 +9659,7 @@ const handleRequest = async (req, res) => {
9682
9659
  (!targetSelection || !supportsApprovalMode(registry.cliTarget(targetSelection.instanceId)?.driverKind, requestedApprovalMode) ||
9683
9660
  (existingBot && normalizedSelection && registry.cliTarget(normalizedSelection.instanceId)?.driverKind !== registry.cliTarget(existingBot.modelSelection.instanceId)?.driverKind))) {
9684
9661
  return json(res, 400, {
9685
- error: "This provider does not support the selected approval level, or changing providers requires choosing Ask or Auto first",
9662
+ error: "This provider does not support the selected approval level, or changing providers requires choosing Ask first",
9686
9663
  });
9687
9664
  }
9688
9665
  const requiresPrivateApprovalTransition = ((requestedApprovalMode === "full" || requestedApprovalMode === "custom") &&
@@ -9855,7 +9832,6 @@ const handleRequest = async (req, res) => {
9855
9832
  cancelDirectTurnDispatch(bot.id, routineRun.threadId);
9856
9833
  if (routineRun.threadId) {
9857
9834
  revokeInternalCapabilitiesForThread(routineRun.threadId);
9858
- await releaseBrowserCapabilityForThread(routineRun.threadId);
9859
9835
  }
9860
9836
  await routines.cancelRun(routineRun.id);
9861
9837
  return;
@@ -9865,7 +9841,6 @@ const handleRequest = async (req, res) => {
9865
9841
  if (groupTurn) {
9866
9842
  cancelGroupTurnOperations(groupTurn.group.id, groupTurn.threadId);
9867
9843
  revokeInternalCapabilitiesForThread(groupTurn.threadId);
9868
- await releaseBrowserCapabilityForThread(groupTurn.threadId);
9869
9844
  await instance?.adapter.interruptTurn(groupTurn.threadId).catch(() => { });
9870
9845
  closeOpenApprovals(groupTurn.threadId);
9871
9846
  return;
@@ -9873,7 +9848,6 @@ const handleRequest = async (req, res) => {
9873
9848
  const directClaim = cancelDirectTurnDispatch(bot.id);
9874
9849
  const threadId = directClaim?.threadId ?? bot.threadId;
9875
9850
  revokeInternalCapabilitiesForThread(threadId);
9876
- await releaseBrowserCapabilityForThread(threadId);
9877
9851
  await instance?.adapter.interruptTurn(threadId).catch(() => { });
9878
9852
  closeOpenApprovals(threadId);
9879
9853
  }));
@@ -10000,7 +9974,7 @@ const handleRequest = async (req, res) => {
10000
9974
  // or unreadable journal therefore rejects the delete with the bot and
10001
9975
  // all of its live work untouched. The intent is aborted if a later
10002
9976
  // pre-delete side effect fails, and committed only after Store deletion.
10003
- const browserCleanupRequest = utilityParentPort ? browserCleanup.prepare("bot", bot.id) : null;
9977
+ const browserCleanupRequest = browserCleanup.prepare("bot", bot.id);
10004
9978
  try {
10005
9979
  // a running turn dies with its bot
10006
9980
  const directClaim = cancelDirectTurnDispatch(bot.id);
@@ -10010,7 +9984,6 @@ const handleRequest = async (req, res) => {
10010
9984
  // check is revalidated after its body arrives and must fail closed.
10011
9985
  revokeInternalCapabilitiesForThread(directThreadId);
10012
9986
  directTurnGenerationByBot.delete(bot.id);
10013
- await releaseBrowserCapabilitiesForBot(bot.id);
10014
9987
  await registry.get(bot.modelSelection.instanceId)?.adapter.interruptTurn(directThreadId).catch(() => { });
10015
9988
  closeOpenApprovals(directThreadId);
10016
9989
  // Deletion removes the thread before a late turn.completed can fold
@@ -10771,7 +10744,6 @@ const handleRequest = async (req, res) => {
10771
10744
  cancelDirectTurnDispatch(bot.id, routineRun.threadId ?? expectedThreadId);
10772
10745
  if (routineRun.threadId) {
10773
10746
  revokeInternalCapabilitiesForThread(routineRun.threadId);
10774
- await releaseBrowserCapabilityForThread(routineRun.threadId);
10775
10747
  }
10776
10748
  await routines.cancelRun(routineRun.id);
10777
10749
  return json(res, 200, { ok: true });
@@ -10786,7 +10758,6 @@ const handleRequest = async (req, res) => {
10786
10758
  }
10787
10759
  cancelGroupTurnOperations(busyGroup.group.id, busyGroup.threadId);
10788
10760
  revokeInternalCapabilitiesForThread(busyGroup.threadId);
10789
- await releaseBrowserCapabilityForThread(busyGroup.threadId);
10790
10761
  await instance?.adapter.interruptTurn(busyGroup.threadId).catch(() => { });
10791
10762
  closeOpenApprovals(busyGroup.threadId);
10792
10763
  return json(res, 200, { ok: true });
@@ -10800,7 +10771,6 @@ const handleRequest = async (req, res) => {
10800
10771
  const cancelledDirect = cancelDirectTurnDispatch(bot.id, expectedThreadId);
10801
10772
  const directThreadId = cancelledDirect?.threadId ?? bot.threadId;
10802
10773
  revokeInternalCapabilitiesForThread(directThreadId);
10803
- await releaseBrowserCapabilityForThread(directThreadId);
10804
10774
  await instance?.adapter.interruptTurn(directThreadId).catch(() => { });
10805
10775
  closeOpenApprovals(directThreadId);
10806
10776
  return json(res, 200, { ok: true });
@@ -11088,6 +11058,26 @@ const handleRequest = async (req, res) => {
11088
11058
  if (method === "GET" && path === "/api/health") {
11089
11059
  return json(res, 200, { app: "openmausbot", pid: process.pid, static: Boolean(STATIC_DIR) });
11090
11060
  }
11061
+ // The bots' browser engine: install it on this machine (agent-browser +
11062
+ // a Chrome for Testing, a one-time download), or ask how that is going.
11063
+ // One install at a time; the config frame's browserEngine tells the rest.
11064
+ if (method === "POST" && path === "/api/browser-engine/install") {
11065
+ if (!browserEngineInstall) {
11066
+ const status = browserEngineStatus();
11067
+ if (status.kind === "unavailable" && !status.installable)
11068
+ return json(res, 409, { error: status.reason });
11069
+ browserEngineInstallError = null;
11070
+ browserEngineInstall = (async () => {
11071
+ const binary = resolveAgentBrowserBinary() ?? await installAgentBrowserBinary({ log: (line) => console.log(line) });
11072
+ await ensureChrome(binary, { log: (line) => console.log(line) });
11073
+ })().then(() => { browserEngineInstallError = null; }, (error) => { browserEngineInstallError = error instanceof Error ? error.message : String(error); }).finally(() => {
11074
+ browserEngineInstall = null;
11075
+ broadcast({ kind: "config", ...configStatus() });
11076
+ });
11077
+ broadcast({ kind: "config", ...configStatus() });
11078
+ }
11079
+ return json(res, 202, { installing: true });
11080
+ }
11091
11081
  // Which edition this server runs and why (see server/enterprise.ts). Read-only.
11092
11082
  if (method === "GET" && path === "/api/edition") {
11093
11083
  return json(res, 200, editionStatus());
@@ -11649,13 +11639,11 @@ const handleRequest = async (req, res) => {
11649
11639
  }
11650
11640
  const browserCleanupRequests = [];
11651
11641
  try {
11652
- if (utilityParentPort) {
11653
- for (const profileId of removedBrowserProfileIds) {
11654
- const target = browserProfilePartitionTarget(cfg, profileId);
11655
- if (!target)
11656
- throw new Error(`browser profile cleanup target “${profileId}” is unavailable`);
11657
- browserCleanupRequests.push(browserCleanup.prepare("profile", target.profileId, target.partitionId));
11658
- }
11642
+ for (const profileId of removedBrowserProfileIds) {
11643
+ const target = browserProfilePartitionTarget(cfg, profileId);
11644
+ if (!target)
11645
+ throw new Error(`browser profile cleanup target “${profileId}” is unavailable`);
11646
+ browserCleanupRequests.push(browserCleanup.prepare("profile", target.profileId, target.partitionId));
11659
11647
  }
11660
11648
  }
11661
11649
  catch (error) {
@@ -11763,7 +11751,6 @@ const handleRequest = async (req, res) => {
11763
11751
  let mandatoryError = null;
11764
11752
  if (disablingBuiltInBrowser) {
11765
11753
  try {
11766
- await releaseAllBrowserCapabilities();
11767
11754
  }
11768
11755
  catch (error) {
11769
11756
  mandatoryError = error;
@@ -12241,7 +12228,6 @@ const gracefulShutdown = createGracefulShutdown({
12241
12228
  webhookIngress?.server.close();
12242
12229
  tunnelListener?.close();
12243
12230
  },
12244
- () => releaseAllBrowserCapabilities(),
12245
12231
  () => registry.disposeAll(),
12246
12232
  () => flushAllProfileHistory(),
12247
12233
  ],