openmausbot 0.1.58 → 0.1.59
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/index-Cvk0KgFW.js +264 -0
- package/dist/assets/{index-CE7W1AIk.js → index-D914UwuG.js} +1 -1
- package/dist/assets/index-oPMv6a_k.css +1 -0
- package/dist/index.html +2 -2
- package/dist-server/container-mcp.js +1 -2
- package/dist-server/index.js +698 -642
- package/dist-server/proxy-paths.js +0 -1
- package/dist-server/server/auto-approve.js +18 -4
- package/dist-server/server/browser-engine.js +31 -0
- package/dist-server/server/composio.js +43 -21
- package/dist-server/server/container-computer.js +32 -7
- package/dist-server/server/drivers/antigravity-acp.js +1 -2
- package/dist-server/server/drivers/antigravity-runtime.js +2 -2
- package/dist-server/server/drivers/codex-instructions.js +66 -0
- package/dist-server/server/drivers/codex.js +84 -49
- package/dist-server/server/index.js +176 -202
- package/dist-server/server/procs.js +34 -22
- package/dist-server/server/proxy-paths.js +0 -1
- package/dist-server/vps-container-mcp.js +1 -2
- package/package.json +1 -1
- package/dist/assets/index-B433sv8K.css +0 -1
- package/dist/assets/index-r-WPT-Pu.js +0 -264
- package/dist-server/drivers/browser-proxy.js +0 -15037
- package/dist-server/server/browser-connection.js +0 -187
- package/dist-server/server/drivers/browser-proxy.js +0 -520
|
@@ -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
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
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: "
|
|
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
|
-
|
|
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
|
|
@@ -3949,13 +3864,7 @@ async function startTurn(botId, text, opts) {
|
|
|
3949
3864
|
liveBot.browser !== false &&
|
|
3950
3865
|
instance.adapter.capabilities.browserMcp === true) {
|
|
3951
3866
|
const selectedProfile = liveBot.browserProfile;
|
|
3952
|
-
browser =
|
|
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);
|
|
3867
|
+
browser = browserIntegration(bot.id, selectedProfile);
|
|
3959
3868
|
if (browser)
|
|
3960
3869
|
integrations.browser = browser.integration;
|
|
3961
3870
|
}
|
|
@@ -4040,10 +3949,6 @@ async function startTurn(botId, text, opts) {
|
|
|
4040
3949
|
// after its own turn.completed would never be torn down — it would
|
|
4041
3950
|
// keep polling the box forever, carrying dead per-turn state. busy
|
|
4042
3951
|
// 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
3952
|
if (previewCapture && store.bot(bot.id)?.busy) {
|
|
4048
3953
|
startScreenPoller(bot.id, previewCapture, { screenIsTheWork: instance.driverKind === "boxAgent" });
|
|
4049
3954
|
}
|
|
@@ -4058,7 +3963,6 @@ async function startTurn(botId, text, opts) {
|
|
|
4058
3963
|
clearCancelledProviderHandshake(threadId, `direct:${dispatchClaimId}`);
|
|
4059
3964
|
clearDirectTurnDispatch(bot.id, dispatchClaimId);
|
|
4060
3965
|
revokeInternalCapabilityGeneration(threadId, dispatchClaimId);
|
|
4061
|
-
await releaseBrowserCapabilityForThread(threadId, dispatchClaimId);
|
|
4062
3966
|
const ownsLatestGeneration = directTurnGenerationByBot.get(bot.id) === dispatchClaimId;
|
|
4063
3967
|
if (ownsLatestGeneration) {
|
|
4064
3968
|
releaseLocalVmThread(threadId);
|
|
@@ -4222,7 +4126,6 @@ async function interruptRoutineGroupGoal(groupId, threadId, outcome) {
|
|
|
4222
4126
|
const bot = speaker ? store.bot(speaker.botId) : undefined;
|
|
4223
4127
|
cancelGroupTurnOperations(groupId, threadId, outcome);
|
|
4224
4128
|
revokeInternalCapabilitiesForThread(threadId);
|
|
4225
|
-
await releaseBrowserCapabilityForThread(threadId);
|
|
4226
4129
|
await (bot ? registry.get(bot.modelSelection.instanceId) : undefined)
|
|
4227
4130
|
?.adapter.interruptTurn(threadId)
|
|
4228
4131
|
.catch(() => { });
|
|
@@ -4251,7 +4154,6 @@ async function stopBotForEmergencyApprovalDowngrade(botId) {
|
|
|
4251
4154
|
revokeInternalCapabilitiesForThread(groupTurn.threadId);
|
|
4252
4155
|
cancelGroupTurnOperations(groupTurn.group.id, groupTurn.threadId);
|
|
4253
4156
|
const results = await Promise.allSettled([
|
|
4254
|
-
releaseBrowserCapabilityForThread(groupTurn.threadId),
|
|
4255
4157
|
registry.get(bot.modelSelection.instanceId)?.adapter.interruptTurn(groupTurn.threadId),
|
|
4256
4158
|
]);
|
|
4257
4159
|
closeOpenApprovals(groupTurn.threadId);
|
|
@@ -4264,7 +4166,6 @@ async function stopBotForEmergencyApprovalDowngrade(botId) {
|
|
|
4264
4166
|
const threadId = directClaim?.threadId ?? bot.threadId;
|
|
4265
4167
|
revokeInternalCapabilitiesForThread(threadId);
|
|
4266
4168
|
const results = await Promise.allSettled([
|
|
4267
|
-
releaseBrowserCapabilityForThread(threadId),
|
|
4268
4169
|
registry.get(bot.modelSelection.instanceId)?.adapter.interruptTurn(threadId),
|
|
4269
4170
|
]);
|
|
4270
4171
|
closeOpenApprovals(threadId);
|
|
@@ -4318,7 +4219,6 @@ routines = new RoutineManager({
|
|
|
4318
4219
|
? registry.get(bot.modelSelection.instanceId)
|
|
4319
4220
|
: null;
|
|
4320
4221
|
try {
|
|
4321
|
-
await releaseBrowserCapabilityForThread(threadId);
|
|
4322
4222
|
await instance?.adapter.interruptTurn(threadId);
|
|
4323
4223
|
}
|
|
4324
4224
|
finally {
|
|
@@ -4730,6 +4630,15 @@ setupRetry = 0) {
|
|
|
4730
4630
|
return true;
|
|
4731
4631
|
}
|
|
4732
4632
|
const internalGeneration = beginInternalCapabilityGeneration(threadId);
|
|
4633
|
+
let roomVmTarget = null;
|
|
4634
|
+
let retainRoomVmLease = false;
|
|
4635
|
+
let roomSpeaker;
|
|
4636
|
+
let providerDispatched = false;
|
|
4637
|
+
const releaseRoomVmLease = () => {
|
|
4638
|
+
if (roomVmTarget && localVmThreadTargets.get(threadId) === roomVmTarget)
|
|
4639
|
+
releaseLocalVmThread(threadId);
|
|
4640
|
+
roomVmTarget = null;
|
|
4641
|
+
};
|
|
4733
4642
|
try {
|
|
4734
4643
|
const integrations = {};
|
|
4735
4644
|
const skillAuthoring = skillRecorderEnabled(cfg) &&
|
|
@@ -4879,19 +4788,7 @@ setupRetry = 0) {
|
|
|
4879
4788
|
readyBot.browser !== false &&
|
|
4880
4789
|
instance.adapter.capabilities.browserMcp === true) {
|
|
4881
4790
|
const selectedProfile = readyBot.browserProfile;
|
|
4882
|
-
const browser =
|
|
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
|
-
});
|
|
4791
|
+
const browser = browserIntegration(readyBot.id, selectedProfile);
|
|
4895
4792
|
if (browser)
|
|
4896
4793
|
integrations.browser = browser.integration;
|
|
4897
4794
|
}
|
|
@@ -4900,7 +4797,6 @@ setupRetry = 0) {
|
|
|
4900
4797
|
// room's setup claim so no provider turn starts after Stop returned.
|
|
4901
4798
|
const browserReadyBot = store.bot(readyBot.id);
|
|
4902
4799
|
if (isCancelled?.() || !browserReadyBot || !browserReadyBot.busy) {
|
|
4903
|
-
await releaseBrowserCapabilityForThread(threadId);
|
|
4904
4800
|
if (browserReadyBot?.busy) {
|
|
4905
4801
|
store.setActivity(browserReadyBot.id, "idle");
|
|
4906
4802
|
retryDelegationsWaitingOn(browserReadyBot.id);
|
|
@@ -4908,7 +4804,44 @@ setupRetry = 0) {
|
|
|
4908
4804
|
return false;
|
|
4909
4805
|
}
|
|
4910
4806
|
store.patchGroup(readyGroup.id, { busyBotId: bot.id }); // the store's change stream carries the frame
|
|
4911
|
-
|
|
4807
|
+
roomSpeaker = { botId: bot.id, name: bot.name, color: bot.color };
|
|
4808
|
+
groupSpeakers.set(threadId, roomSpeaker);
|
|
4809
|
+
// Room and Goal turns use the speaker's desktop, never the coordinator's.
|
|
4810
|
+
// Claim the same lease as direct turns before asynchronous VM setup.
|
|
4811
|
+
if (readyBot.computer === "vm") {
|
|
4812
|
+
if (instance.adapter.capabilities.computerMcp !== true || instance.driverKind === "boxAgent") {
|
|
4813
|
+
throw new Error("this model engine cannot use the Local VM");
|
|
4814
|
+
}
|
|
4815
|
+
// A distinct identity fences cleanup even in shared mode on the same room thread.
|
|
4816
|
+
const target = { ...localVmTargetForBot(readyBot.id) };
|
|
4817
|
+
if (localVmImageBusy || localVmModeChangeBusy || localVmLifecycleBusy.has(target.key)) {
|
|
4818
|
+
throw new Error("this Local VM is being started, stopped, or replaced");
|
|
4819
|
+
}
|
|
4820
|
+
if (!localVmLeaseFor(target).claim(threadId, readyBot.id, localVmOwnerBusy)) {
|
|
4821
|
+
throw new Error("this Local VM is already being used by another turn");
|
|
4822
|
+
}
|
|
4823
|
+
roomVmTarget = target;
|
|
4824
|
+
localVmThreadTargets.set(threadId, target);
|
|
4825
|
+
localVmActiveThreads.set(target.key, threadId);
|
|
4826
|
+
localVmIdleFor(target).touch();
|
|
4827
|
+
const setupIsCurrent = () => !isCancelled?.() &&
|
|
4828
|
+
groupSpeakers.get(threadId) === roomSpeaker &&
|
|
4829
|
+
activeInternalGenerationByThread.get(threadId) === internalGeneration &&
|
|
4830
|
+
store.group(readyGroup.id)?.memberIds.includes(readyBot.id) === true &&
|
|
4831
|
+
store.bot(readyBot.id)?.busy === true &&
|
|
4832
|
+
store.group(readyGroup.id)?.busyBotId === readyBot.id;
|
|
4833
|
+
const vm = await readyLocalVmForTurn(readyBot.id, target, setupIsCurrent);
|
|
4834
|
+
if (!setupIsCurrent()) {
|
|
4835
|
+
return false;
|
|
4836
|
+
}
|
|
4837
|
+
if (!vm.ready || !vm.runtime)
|
|
4838
|
+
throw new Error(vm.problem ?? "the Local VM is not ready");
|
|
4839
|
+
const owner = localVmLeaseFor(target).current(localVmOwnerBusy);
|
|
4840
|
+
if (owner?.threadId !== threadId || owner.botId !== readyBot.id) {
|
|
4841
|
+
throw new Error("the Local VM lease expired while preparing the turn");
|
|
4842
|
+
}
|
|
4843
|
+
integrations.localComputer = containerComputerMcp(vm.runtime, controlIntegration(readyBot.id, threadId, internalGeneration, target), target);
|
|
4844
|
+
}
|
|
4912
4845
|
const roster = readyGroup.memberIds
|
|
4913
4846
|
.map((id) => store.bot(id))
|
|
4914
4847
|
.filter((b) => Boolean(b))
|
|
@@ -4959,6 +4892,7 @@ setupRetry = 0) {
|
|
|
4959
4892
|
store.patchBot(bot.id, { soulDrift: drift.drift });
|
|
4960
4893
|
}
|
|
4961
4894
|
const roomSystem = buildSystemPrompt(system, store.bot(bot.id)?.soul ?? bot.soul ?? "", [
|
|
4895
|
+
{ id: "computer", label: "Computer", text: computerPrompt(roomVmTarget ? localVmMode(cfg) === "per-bot" ? "vm-private" : "vm-shared" : null) },
|
|
4962
4896
|
{ id: "browser", label: "Browser", text: integrations.browser ? BUILT_IN_BROWSER_SYSTEM_PROMPT : "" },
|
|
4963
4897
|
{ id: "recall", label: "Recall", text: integrations.agents ? SESSION_SEARCH_SYSTEM_PROMPT : "" },
|
|
4964
4898
|
{ id: "section-context", label: "Section context", text: sectionContextSystemPrompt(bot.section) },
|
|
@@ -4984,7 +4918,6 @@ setupRetry = 0) {
|
|
|
4984
4918
|
if (isCancelled?.() ||
|
|
4985
4919
|
store.group(group.id)?.busyBotId !== bot.id ||
|
|
4986
4920
|
store.bot(bot.id)?.busy !== true) {
|
|
4987
|
-
await releaseBrowserCapabilityForThread(threadId);
|
|
4988
4921
|
if (store.group(group.id)?.busyBotId === bot.id) {
|
|
4989
4922
|
groupSpeakers.delete(threadId);
|
|
4990
4923
|
store.patchGroup(group.id, { busyBotId: null, unread: true });
|
|
@@ -5016,7 +4949,6 @@ setupRetry = 0) {
|
|
|
5016
4949
|
let unregisterStall = () => { };
|
|
5017
4950
|
const deadline = new RoomTurnDeadline(timeoutMinutes, () => {
|
|
5018
4951
|
abandonProviderTurn();
|
|
5019
|
-
void releaseBrowserCapabilityForThread(threadId);
|
|
5020
4952
|
void instance.adapter.interruptTurn(threadId).catch(() => { });
|
|
5021
4953
|
store.appendMessage(threadId, {
|
|
5022
4954
|
role: "bot",
|
|
@@ -5068,6 +5000,7 @@ setupRetry = 0) {
|
|
|
5068
5000
|
});
|
|
5069
5001
|
watchdog.watch(threadId, bot.id);
|
|
5070
5002
|
onProviderHandshakeStarted?.();
|
|
5003
|
+
providerDispatched = true;
|
|
5071
5004
|
guardTurnDispatch(instance.adapter.sendTurn({
|
|
5072
5005
|
threadId,
|
|
5073
5006
|
text,
|
|
@@ -5081,7 +5014,6 @@ setupRetry = 0) {
|
|
|
5081
5014
|
// Stop may have landed while the adapter was authenticating, before
|
|
5082
5015
|
// it had an active process for the first interrupt to reach. Now that
|
|
5083
5016
|
// sendTurn completed setup, revoke again and interrupt the real turn.
|
|
5084
|
-
await releaseBrowserCapabilityForThread(threadId);
|
|
5085
5017
|
await instance.adapter.interruptTurn(threadId).catch(() => { });
|
|
5086
5018
|
})
|
|
5087
5019
|
.then((dispatch) => {
|
|
@@ -5121,6 +5053,9 @@ setupRetry = 0) {
|
|
|
5121
5053
|
// work so a retained proxy from this member cannot act during the next
|
|
5122
5054
|
// member's generation.
|
|
5123
5055
|
revokeInternalCapabilityGeneration(threadId, internalGeneration);
|
|
5056
|
+
retainRoomVmLease = outcome === "timed_out" || outcome === "stalled";
|
|
5057
|
+
if (!retainRoomVmLease)
|
|
5058
|
+
releaseRoomVmLease();
|
|
5124
5059
|
if (orchestration) {
|
|
5125
5060
|
orchestration.result.replyText = replyText.trim();
|
|
5126
5061
|
orchestration.result.outcome = outcome;
|
|
@@ -5160,6 +5095,7 @@ setupRetry = 0) {
|
|
|
5160
5095
|
retry.unref?.();
|
|
5161
5096
|
return;
|
|
5162
5097
|
}
|
|
5098
|
+
releaseRoomVmLease();
|
|
5163
5099
|
const currentGroup = store.group(group.id);
|
|
5164
5100
|
const speaker = groupSpeakers.get(threadId);
|
|
5165
5101
|
if (currentGroup?.busyBotId === bot.id && speaker?.botId === bot.id) {
|
|
@@ -5195,7 +5131,6 @@ setupRetry = 0) {
|
|
|
5195
5131
|
if (outcome === "dispatch_failed") {
|
|
5196
5132
|
if (skillAuthoring)
|
|
5197
5133
|
skillAuthoringClaim.claimed = false;
|
|
5198
|
-
await releaseBrowserCapabilityForThread(threadId);
|
|
5199
5134
|
// No turn.completed follows a rejected room dispatch. Anything that was
|
|
5200
5135
|
// queued while this bot briefly owned the room must be retried now.
|
|
5201
5136
|
drainQueuedSends();
|
|
@@ -5255,10 +5190,38 @@ setupRetry = 0) {
|
|
|
5255
5190
|
}
|
|
5256
5191
|
return true;
|
|
5257
5192
|
}
|
|
5193
|
+
catch (error) {
|
|
5194
|
+
if (providerDispatched || !roomSpeaker)
|
|
5195
|
+
throw error;
|
|
5196
|
+
const message = error instanceof Error ? error.message : "Local VM setup failed";
|
|
5197
|
+
store.appendMessage(threadId, {
|
|
5198
|
+
role: "bot", kind: "activity",
|
|
5199
|
+
from: { botId: bot.id, name: bot.name, color: bot.color },
|
|
5200
|
+
tool: { name: `error: ${message}`, ok: false },
|
|
5201
|
+
});
|
|
5202
|
+
onDispatchError?.(message);
|
|
5203
|
+
if (orchestration)
|
|
5204
|
+
orchestration.result.outcome = "dispatch_failed";
|
|
5205
|
+
return false;
|
|
5206
|
+
}
|
|
5258
5207
|
finally {
|
|
5259
5208
|
// Covers connector/setup failures, cancellation before dispatch, and all
|
|
5260
5209
|
// other early returns that never produce a provider terminal event.
|
|
5261
5210
|
revokeInternalCapabilityGeneration(threadId, internalGeneration);
|
|
5211
|
+
if (!retainRoomVmLease)
|
|
5212
|
+
releaseRoomVmLease();
|
|
5213
|
+
if (!providerDispatched && roomSpeaker && groupSpeakers.get(threadId) === roomSpeaker) {
|
|
5214
|
+
groupSpeakers.delete(threadId);
|
|
5215
|
+
if (store.group(group.id)?.busyBotId === bot.id)
|
|
5216
|
+
store.patchGroup(group.id, { busyBotId: null });
|
|
5217
|
+
if (store.bot(bot.id)?.busy) {
|
|
5218
|
+
store.setActivity(bot.id, "idle");
|
|
5219
|
+
retryDelegationsWaitingOn(bot.id);
|
|
5220
|
+
}
|
|
5221
|
+
drainQueuedSends();
|
|
5222
|
+
drainConnectorResumes();
|
|
5223
|
+
drainSecretResumes();
|
|
5224
|
+
}
|
|
5262
5225
|
}
|
|
5263
5226
|
}
|
|
5264
5227
|
async function runGroupGoalStep(args) {
|
|
@@ -6493,13 +6456,15 @@ async function localVmPayload(target) {
|
|
|
6493
6456
|
* instance cap; creating past it would quietly do what the lifecycle route
|
|
6494
6457
|
* refuses.
|
|
6495
6458
|
*/
|
|
6496
|
-
async function readyLocalVmForTurn(botId, target) {
|
|
6459
|
+
async function readyLocalVmForTurn(botId, target, isCurrent = () => true) {
|
|
6497
6460
|
let status = await containerComputerStatus(undefined, undefined, target);
|
|
6461
|
+
if (!isCurrent())
|
|
6462
|
+
return status;
|
|
6498
6463
|
if (status.ready || !localVmRecreatableOnDemand(status))
|
|
6499
6464
|
return status;
|
|
6500
6465
|
if (target.key !== SHARED_LOCAL_VM_TARGET.key) {
|
|
6501
6466
|
const count = await existingPerBotLocalVmCount(status.runtime);
|
|
6502
|
-
if (count >= localVmMaxInstances(cfg))
|
|
6467
|
+
if (!isCurrent() || count >= localVmMaxInstances(cfg))
|
|
6503
6468
|
return status;
|
|
6504
6469
|
}
|
|
6505
6470
|
broadcast({ kind: "computer", botId, state: "provisioning" });
|
|
@@ -6522,8 +6487,10 @@ async function readyLocalVmForTurn(botId, target) {
|
|
|
6522
6487
|
// the turn is the whole point: a person who has been away eight hours should
|
|
6523
6488
|
// not have to send their message twice.
|
|
6524
6489
|
const deadline = Date.now() + LOCAL_VM_DESKTOP_WAIT_MS;
|
|
6525
|
-
while (!status.ready && status.container === "running" && Date.now() < deadline) {
|
|
6490
|
+
while (isCurrent() && !status.ready && status.container === "running" && Date.now() < deadline) {
|
|
6526
6491
|
await new Promise((resolve) => setTimeout(resolve, 2_000));
|
|
6492
|
+
if (!isCurrent())
|
|
6493
|
+
break;
|
|
6527
6494
|
status = await containerComputerStatus(undefined, undefined, target);
|
|
6528
6495
|
}
|
|
6529
6496
|
return status;
|
|
@@ -6599,7 +6566,6 @@ async function reloadProviders() {
|
|
|
6599
6566
|
// one synchronous step before the first teardown await, including room/task
|
|
6600
6567
|
// threads that are not a bot's default DM.
|
|
6601
6568
|
revokeAllInternalCapabilities();
|
|
6602
|
-
await releaseAllBrowserCapabilities();
|
|
6603
6569
|
bus.detachAll();
|
|
6604
6570
|
await registry.disposeAll();
|
|
6605
6571
|
await registry.load(instanceConfigs(cfg));
|
|
@@ -9257,7 +9223,6 @@ const handleRequest = async (req, res) => {
|
|
|
9257
9223
|
cancelGroupTurnOperations(group.id, threadId);
|
|
9258
9224
|
for (const { threadId, instance } of interruptTargets) {
|
|
9259
9225
|
revokeInternalCapabilitiesForThread(threadId);
|
|
9260
|
-
await releaseBrowserCapabilityForThread(threadId);
|
|
9261
9226
|
await instance?.adapter.interruptTurn(threadId).catch(() => { });
|
|
9262
9227
|
closeOpenApprovals(threadId);
|
|
9263
9228
|
}
|
|
@@ -9855,7 +9820,6 @@ const handleRequest = async (req, res) => {
|
|
|
9855
9820
|
cancelDirectTurnDispatch(bot.id, routineRun.threadId);
|
|
9856
9821
|
if (routineRun.threadId) {
|
|
9857
9822
|
revokeInternalCapabilitiesForThread(routineRun.threadId);
|
|
9858
|
-
await releaseBrowserCapabilityForThread(routineRun.threadId);
|
|
9859
9823
|
}
|
|
9860
9824
|
await routines.cancelRun(routineRun.id);
|
|
9861
9825
|
return;
|
|
@@ -9865,7 +9829,6 @@ const handleRequest = async (req, res) => {
|
|
|
9865
9829
|
if (groupTurn) {
|
|
9866
9830
|
cancelGroupTurnOperations(groupTurn.group.id, groupTurn.threadId);
|
|
9867
9831
|
revokeInternalCapabilitiesForThread(groupTurn.threadId);
|
|
9868
|
-
await releaseBrowserCapabilityForThread(groupTurn.threadId);
|
|
9869
9832
|
await instance?.adapter.interruptTurn(groupTurn.threadId).catch(() => { });
|
|
9870
9833
|
closeOpenApprovals(groupTurn.threadId);
|
|
9871
9834
|
return;
|
|
@@ -9873,7 +9836,6 @@ const handleRequest = async (req, res) => {
|
|
|
9873
9836
|
const directClaim = cancelDirectTurnDispatch(bot.id);
|
|
9874
9837
|
const threadId = directClaim?.threadId ?? bot.threadId;
|
|
9875
9838
|
revokeInternalCapabilitiesForThread(threadId);
|
|
9876
|
-
await releaseBrowserCapabilityForThread(threadId);
|
|
9877
9839
|
await instance?.adapter.interruptTurn(threadId).catch(() => { });
|
|
9878
9840
|
closeOpenApprovals(threadId);
|
|
9879
9841
|
}));
|
|
@@ -10000,7 +9962,7 @@ const handleRequest = async (req, res) => {
|
|
|
10000
9962
|
// or unreadable journal therefore rejects the delete with the bot and
|
|
10001
9963
|
// all of its live work untouched. The intent is aborted if a later
|
|
10002
9964
|
// pre-delete side effect fails, and committed only after Store deletion.
|
|
10003
|
-
const browserCleanupRequest =
|
|
9965
|
+
const browserCleanupRequest = browserCleanup.prepare("bot", bot.id);
|
|
10004
9966
|
try {
|
|
10005
9967
|
// a running turn dies with its bot
|
|
10006
9968
|
const directClaim = cancelDirectTurnDispatch(bot.id);
|
|
@@ -10010,7 +9972,6 @@ const handleRequest = async (req, res) => {
|
|
|
10010
9972
|
// check is revalidated after its body arrives and must fail closed.
|
|
10011
9973
|
revokeInternalCapabilitiesForThread(directThreadId);
|
|
10012
9974
|
directTurnGenerationByBot.delete(bot.id);
|
|
10013
|
-
await releaseBrowserCapabilitiesForBot(bot.id);
|
|
10014
9975
|
await registry.get(bot.modelSelection.instanceId)?.adapter.interruptTurn(directThreadId).catch(() => { });
|
|
10015
9976
|
closeOpenApprovals(directThreadId);
|
|
10016
9977
|
// Deletion removes the thread before a late turn.completed can fold
|
|
@@ -10771,7 +10732,6 @@ const handleRequest = async (req, res) => {
|
|
|
10771
10732
|
cancelDirectTurnDispatch(bot.id, routineRun.threadId ?? expectedThreadId);
|
|
10772
10733
|
if (routineRun.threadId) {
|
|
10773
10734
|
revokeInternalCapabilitiesForThread(routineRun.threadId);
|
|
10774
|
-
await releaseBrowserCapabilityForThread(routineRun.threadId);
|
|
10775
10735
|
}
|
|
10776
10736
|
await routines.cancelRun(routineRun.id);
|
|
10777
10737
|
return json(res, 200, { ok: true });
|
|
@@ -10786,7 +10746,6 @@ const handleRequest = async (req, res) => {
|
|
|
10786
10746
|
}
|
|
10787
10747
|
cancelGroupTurnOperations(busyGroup.group.id, busyGroup.threadId);
|
|
10788
10748
|
revokeInternalCapabilitiesForThread(busyGroup.threadId);
|
|
10789
|
-
await releaseBrowserCapabilityForThread(busyGroup.threadId);
|
|
10790
10749
|
await instance?.adapter.interruptTurn(busyGroup.threadId).catch(() => { });
|
|
10791
10750
|
closeOpenApprovals(busyGroup.threadId);
|
|
10792
10751
|
return json(res, 200, { ok: true });
|
|
@@ -10800,7 +10759,6 @@ const handleRequest = async (req, res) => {
|
|
|
10800
10759
|
const cancelledDirect = cancelDirectTurnDispatch(bot.id, expectedThreadId);
|
|
10801
10760
|
const directThreadId = cancelledDirect?.threadId ?? bot.threadId;
|
|
10802
10761
|
revokeInternalCapabilitiesForThread(directThreadId);
|
|
10803
|
-
await releaseBrowserCapabilityForThread(directThreadId);
|
|
10804
10762
|
await instance?.adapter.interruptTurn(directThreadId).catch(() => { });
|
|
10805
10763
|
closeOpenApprovals(directThreadId);
|
|
10806
10764
|
return json(res, 200, { ok: true });
|
|
@@ -11088,6 +11046,26 @@ const handleRequest = async (req, res) => {
|
|
|
11088
11046
|
if (method === "GET" && path === "/api/health") {
|
|
11089
11047
|
return json(res, 200, { app: "openmausbot", pid: process.pid, static: Boolean(STATIC_DIR) });
|
|
11090
11048
|
}
|
|
11049
|
+
// The bots' browser engine: install it on this machine (agent-browser +
|
|
11050
|
+
// a Chrome for Testing, a one-time download), or ask how that is going.
|
|
11051
|
+
// One install at a time; the config frame's browserEngine tells the rest.
|
|
11052
|
+
if (method === "POST" && path === "/api/browser-engine/install") {
|
|
11053
|
+
if (!browserEngineInstall) {
|
|
11054
|
+
const status = browserEngineStatus();
|
|
11055
|
+
if (status.kind === "unavailable" && !status.installable)
|
|
11056
|
+
return json(res, 409, { error: status.reason });
|
|
11057
|
+
browserEngineInstallError = null;
|
|
11058
|
+
browserEngineInstall = (async () => {
|
|
11059
|
+
const binary = resolveAgentBrowserBinary() ?? await installAgentBrowserBinary({ log: (line) => console.log(line) });
|
|
11060
|
+
await ensureChrome(binary, { log: (line) => console.log(line) });
|
|
11061
|
+
})().then(() => { browserEngineInstallError = null; }, (error) => { browserEngineInstallError = error instanceof Error ? error.message : String(error); }).finally(() => {
|
|
11062
|
+
browserEngineInstall = null;
|
|
11063
|
+
broadcast({ kind: "config", ...configStatus() });
|
|
11064
|
+
});
|
|
11065
|
+
broadcast({ kind: "config", ...configStatus() });
|
|
11066
|
+
}
|
|
11067
|
+
return json(res, 202, { installing: true });
|
|
11068
|
+
}
|
|
11091
11069
|
// Which edition this server runs and why (see server/enterprise.ts). Read-only.
|
|
11092
11070
|
if (method === "GET" && path === "/api/edition") {
|
|
11093
11071
|
return json(res, 200, editionStatus());
|
|
@@ -11649,13 +11627,11 @@ const handleRequest = async (req, res) => {
|
|
|
11649
11627
|
}
|
|
11650
11628
|
const browserCleanupRequests = [];
|
|
11651
11629
|
try {
|
|
11652
|
-
|
|
11653
|
-
|
|
11654
|
-
|
|
11655
|
-
|
|
11656
|
-
|
|
11657
|
-
browserCleanupRequests.push(browserCleanup.prepare("profile", target.profileId, target.partitionId));
|
|
11658
|
-
}
|
|
11630
|
+
for (const profileId of removedBrowserProfileIds) {
|
|
11631
|
+
const target = browserProfilePartitionTarget(cfg, profileId);
|
|
11632
|
+
if (!target)
|
|
11633
|
+
throw new Error(`browser profile cleanup target “${profileId}” is unavailable`);
|
|
11634
|
+
browserCleanupRequests.push(browserCleanup.prepare("profile", target.profileId, target.partitionId));
|
|
11659
11635
|
}
|
|
11660
11636
|
}
|
|
11661
11637
|
catch (error) {
|
|
@@ -11763,7 +11739,6 @@ const handleRequest = async (req, res) => {
|
|
|
11763
11739
|
let mandatoryError = null;
|
|
11764
11740
|
if (disablingBuiltInBrowser) {
|
|
11765
11741
|
try {
|
|
11766
|
-
await releaseAllBrowserCapabilities();
|
|
11767
11742
|
}
|
|
11768
11743
|
catch (error) {
|
|
11769
11744
|
mandatoryError = error;
|
|
@@ -12241,7 +12216,6 @@ const gracefulShutdown = createGracefulShutdown({
|
|
|
12241
12216
|
webhookIngress?.server.close();
|
|
12242
12217
|
tunnelListener?.close();
|
|
12243
12218
|
},
|
|
12244
|
-
() => releaseAllBrowserCapabilities(),
|
|
12245
12219
|
() => registry.disposeAll(),
|
|
12246
12220
|
() => flushAllProfileHistory(),
|
|
12247
12221
|
],
|