@cmmd-center/forge 0.13.29 → 0.13.31
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/{BunHttpServer-C0oPP_3A.cjs → BunHttpServer-CXndtc1j.cjs} +2 -2
- package/dist/{BunHttpServer-FbbQmsl_.mjs → BunHttpServer-DjilmGOM.mjs} +2 -2
- package/dist/{BunPTY-uyreNRGp.cjs → BunPTY-BiTlUj63.cjs} +1 -1
- package/dist/{BunPTY-DkAIzLIx.mjs → BunPTY-Csgwhbi5.mjs} +1 -1
- package/dist/{BunServices-DC7nhA3w.mjs → BunServices-BZKzHdrG.mjs} +1 -1
- package/dist/{BunServices-DkenwD7S.cjs → BunServices-DsWG7VM1.cjs} +1 -1
- package/dist/{NodeHttpServer-Cjd3bFrD.cjs → NodeHttpServer-B4dCKqYY.cjs} +1 -1
- package/dist/{NodeHttpServer-BJ4l15f-.mjs → NodeHttpServer-Ds3Gp1Gp.mjs} +1 -1
- package/dist/{NodePTY-BehazUMi.cjs → NodePTY-BJ7874MO.cjs} +1 -1
- package/dist/{NodePTY-BhezYy9H.mjs → NodePTY-DgXWLtWp.mjs} +1 -1
- package/dist/{NodeSqliteClient-CfsdC3AB.cjs → NodeSqliteClient-CPsJxxdD.cjs} +1 -1
- package/dist/{NodeSqliteClient-md8xd9hY.mjs → NodeSqliteClient-D29w60zZ.mjs} +1 -1
- package/dist/{SqliteClient-BKB695RF.mjs → SqliteClient-Bgo_s5sA.mjs} +1 -1
- package/dist/{SqliteClient-O1ABIngw.cjs → SqliteClient-k0pbfeIC.cjs} +1 -1
- package/dist/bin.cjs +1879 -2378
- package/dist/bin.mjs +1876 -2375
- package/dist/client/assets/{DiffPanel-CsYeFcEd.js → DiffPanel-rxdARnAn.js} +2 -2
- package/dist/client/assets/{DiffPanel.logic-BhrQueD8.js → DiffPanel.logic-pioWZtAT.js} +2 -2
- package/dist/client/assets/{DiffPanelShell-CssAtWUC.js → DiffPanelShell-CJre8MyV.js} +3 -3
- package/dist/client/assets/{DiffWorkerPoolProvider-C0iUCn9m.js → DiffWorkerPoolProvider-BoinSMK1.js} +2 -2
- package/dist/client/assets/{PullRequestCodePanel-O-XZJVnK.js → PullRequestCodePanel-BR0xA-je.js} +2 -2
- package/dist/client/assets/{Virtualizer-Z2t67-w7.js → Virtualizer-H0mi-vRc.js} +2 -2
- package/dist/client/assets/{index-AMVPSS6N.js → index-DQtjHxYt.js} +6 -6
- package/dist/client/index.html +4 -4
- package/dist/{execAsync-BUcaFQ1D.cjs → execAsync--H9ZZOiC.cjs} +1 -1
- package/dist/{execAsync-BNNdR88o.mjs → execAsync-BJ4g1UuR.mjs} +1 -1
- package/dist/forge-build.json +1 -1
- package/dist/{getMachineId-bsd-3FzKrfVS.cjs → getMachineId-bsd-CJJWgsvW.cjs} +2 -2
- package/dist/{getMachineId-bsd-BuYwT36r.mjs → getMachineId-bsd-CvzjzhCJ.mjs} +2 -2
- package/dist/{getMachineId-darwin-RZKhGQ6C.mjs → getMachineId-darwin-BkjZ6NK7.mjs} +2 -2
- package/dist/{getMachineId-darwin-J2wNbebX.cjs → getMachineId-darwin-hZeIAq-t.cjs} +2 -2
- package/dist/{getMachineId-linux-BDLTrARB.cjs → getMachineId-linux-B4Pn5gp9.cjs} +1 -1
- package/dist/{getMachineId-linux-D5HSP0rH.mjs → getMachineId-linux-BaBxzaf3.mjs} +1 -1
- package/dist/{getMachineId-unsupported-DRh5MFwr.cjs → getMachineId-unsupported-B3jtoOL0.cjs} +1 -1
- package/dist/{getMachineId-unsupported-IaplPZnC.mjs → getMachineId-unsupported-EhCzrOph.mjs} +1 -1
- package/dist/{getMachineId-win-D220ursp.mjs → getMachineId-win-BFwtIsaZ.mjs} +2 -2
- package/dist/{getMachineId-win-BA8dHcjg.cjs → getMachineId-win-Bb4lJE8e.cjs} +2 -2
- package/dist/{open-D8mGyWmt.mjs → open-2-reBrpv.mjs} +1 -1
- package/dist/{open-BAsPVCWX.cjs → open-CghsF25U.cjs} +1 -1
- package/dist/playwright-backend-BONxTCSA.cjs +643 -0
- package/dist/playwright-backend-DVQOhqRO.mjs +642 -0
- package/dist/settle-B6Rn65UV.cjs +28 -0
- package/dist/settle-BJcmpbvq.mjs +23 -0
- package/package.json +1 -1
package/dist/bin.mjs
CHANGED
|
@@ -13,6 +13,7 @@ import { r as wrapper_default, t as Mime_default } from "./Mime-Dq9IoNiV.mjs";
|
|
|
13
13
|
import { a as diag, c as SpanKind$1, d as TraceFlags, f as ROOT_CONTEXT, i as propagation, l as isSpanContextValid, m as DiagLogLevel, n as init_esm$1, o as context$1, p as createContextKey, r as trace, s as SpanStatusCode, t as esm_exports$1 } from "./esm-wLIF-u3N.mjs";
|
|
14
14
|
import { a as verifyCmmdAuthRuntimeTicket, c as verifyCmmdRuntimeTicket, d as runtimeGrantToken, f as setRuntimeGrantToken, i as sendRuntimeControlHeartbeat, l as verifyCmmdRuntimeTicketRefreshProof, n as bootstrapRuntimeControlAuthorityFromNarrowGrant, o as verifyCmmdClaudeReadinessTicket, p as createReadOnceFileTokenAccessor, r as readRuntimeControlAuthority, s as verifyCmmdClaudeUsageTicket, t as bootstrapRuntimeControlAuthority, u as verifyCmmdScopedRuntimePayload } from "./runtimeControlAuthority-DpRIoWqP.mjs";
|
|
15
15
|
import { n as PtySpawnError, t as PtyAdapter } from "./PTY-BzdbZ0Ym.mjs";
|
|
16
|
+
import { t as settleValueWithin } from "./settle-BJcmpbvq.mjs";
|
|
16
17
|
import { createRequire } from "node:module";
|
|
17
18
|
import * as HE from "node:child_process";
|
|
18
19
|
import { execFile, execFileSync, spawn, spawnSync } from "node:child_process";
|
|
@@ -73,7 +74,6 @@ import z$ from "http2";
|
|
|
73
74
|
import { access, appendFile, chmod as chmod$1, copyFile, link as link$1, lstat as lstat$1, mkdir as mkdir$1, open, readFile as readFile$2, readdir as readdir$2, readlink, realpath as realpath$1, rename as rename$1, rm as rm$1, rmdir, stat as stat$1, symlink, unlink as unlink$1, writeFile as writeFile$1 } from "fs/promises";
|
|
74
75
|
import { createInterface as createInterface$1 } from "readline";
|
|
75
76
|
import { AsyncLocalStorage as AsyncLocalStorage$1 } from "async_hooks";
|
|
76
|
-
import { chromium } from "playwright";
|
|
77
77
|
import { createSocket } from "node:dgram";
|
|
78
78
|
import { createSocket as createSocket$1 } from "dgram";
|
|
79
79
|
import { setTimeout as setTimeout$1 } from "timers/promises";
|
|
@@ -52897,19 +52897,19 @@ var require_getMachineId = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
52897
52897
|
async function getMachineId() {
|
|
52898
52898
|
if (!getMachineIdImpl) switch (process$3.platform) {
|
|
52899
52899
|
case "darwin":
|
|
52900
|
-
getMachineIdImpl = (await import("./getMachineId-darwin-
|
|
52900
|
+
getMachineIdImpl = (await import("./getMachineId-darwin-BkjZ6NK7.mjs").then((m) => /* @__PURE__ */ __toESM(m.default))).getMachineId;
|
|
52901
52901
|
break;
|
|
52902
52902
|
case "linux":
|
|
52903
|
-
getMachineIdImpl = (await import("./getMachineId-linux-
|
|
52903
|
+
getMachineIdImpl = (await import("./getMachineId-linux-BaBxzaf3.mjs").then((m) => /* @__PURE__ */ __toESM(m.default))).getMachineId;
|
|
52904
52904
|
break;
|
|
52905
52905
|
case "freebsd":
|
|
52906
|
-
getMachineIdImpl = (await import("./getMachineId-bsd-
|
|
52906
|
+
getMachineIdImpl = (await import("./getMachineId-bsd-CvzjzhCJ.mjs").then((m) => /* @__PURE__ */ __toESM(m.default))).getMachineId;
|
|
52907
52907
|
break;
|
|
52908
52908
|
case "win32":
|
|
52909
|
-
getMachineIdImpl = (await import("./getMachineId-win-
|
|
52909
|
+
getMachineIdImpl = (await import("./getMachineId-win-BFwtIsaZ.mjs").then((m) => /* @__PURE__ */ __toESM(m.default))).getMachineId;
|
|
52910
52910
|
break;
|
|
52911
52911
|
default:
|
|
52912
|
-
getMachineIdImpl = (await import("./getMachineId-unsupported-
|
|
52912
|
+
getMachineIdImpl = (await import("./getMachineId-unsupported-EhCzrOph.mjs").then((m) => /* @__PURE__ */ __toESM(m.default))).getMachineId;
|
|
52913
52913
|
break;
|
|
52914
52914
|
}
|
|
52915
52915
|
return getMachineIdImpl();
|
|
@@ -65937,7 +65937,7 @@ function normalizeNumberish(value) {
|
|
|
65937
65937
|
}
|
|
65938
65938
|
//#endregion
|
|
65939
65939
|
//#region package.json
|
|
65940
|
-
var version$1 = "0.13.
|
|
65940
|
+
var version$1 = "0.13.31";
|
|
65941
65941
|
//#endregion
|
|
65942
65942
|
//#region src/sentry.ts
|
|
65943
65943
|
const SERVER_APP_NAME = "forge-server";
|
|
@@ -93516,7 +93516,7 @@ const launchDetached = (launch) => gen(function* () {
|
|
|
93516
93516
|
});
|
|
93517
93517
|
const OpenLive = effect(Open, gen(function* () {
|
|
93518
93518
|
const open = yield* tryPromise({
|
|
93519
|
-
try: () => import("./open-
|
|
93519
|
+
try: () => import("./open-2-reBrpv.mjs"),
|
|
93520
93520
|
catch: (cause) => new OpenError$1({
|
|
93521
93521
|
message: "failed to load browser opener",
|
|
93522
93522
|
cause
|
|
@@ -100800,7 +100800,7 @@ var BrowserManager = class extends Service$1()("forge/browser/Services/Manager/B
|
|
|
100800
100800
|
const DEFAULT_PORT_MIN = 5200;
|
|
100801
100801
|
const DEFAULT_PORT_MAX = 5999;
|
|
100802
100802
|
var ThreadDevRuntime = class extends Service$1()("forge/threadDevRuntime/ThreadDevRuntime") {};
|
|
100803
|
-
function nowIso$
|
|
100803
|
+
function nowIso$7() {
|
|
100804
100804
|
return (/* @__PURE__ */ new Date()).toISOString();
|
|
100805
100805
|
}
|
|
100806
100806
|
function previewUrlForPort(port) {
|
|
@@ -100827,7 +100827,7 @@ const ThreadDevRuntimeLive = effect(ThreadDevRuntime, gen(function* () {
|
|
|
100827
100827
|
allocatedPort: port,
|
|
100828
100828
|
status: "allocated",
|
|
100829
100829
|
previewUrl: previewUrlForPort(port),
|
|
100830
|
-
updatedAt: nowIso$
|
|
100830
|
+
updatedAt: nowIso$7()
|
|
100831
100831
|
};
|
|
100832
100832
|
yield* update$4(byThreadId, (current) => {
|
|
100833
100833
|
const next = new Map(current);
|
|
@@ -100849,7 +100849,7 @@ const ThreadDevRuntimeLive = effect(ThreadDevRuntime, gen(function* () {
|
|
|
100849
100849
|
next.set(input.threadId, {
|
|
100850
100850
|
...existing,
|
|
100851
100851
|
status: "released",
|
|
100852
|
-
updatedAt: nowIso$
|
|
100852
|
+
updatedAt: nowIso$7()
|
|
100853
100853
|
});
|
|
100854
100854
|
return next;
|
|
100855
100855
|
});
|
|
@@ -100875,7 +100875,7 @@ const ThreadDevRuntimeLive = effect(ThreadDevRuntime, gen(function* () {
|
|
|
100875
100875
|
...patch.terminalId !== void 0 ? { terminalId: patch.terminalId } : {},
|
|
100876
100876
|
...patch.previewUrl !== void 0 ? { previewUrl: patch.previewUrl } : {},
|
|
100877
100877
|
...patch.tunnelId !== void 0 ? { tunnelId: patch.tunnelId } : {},
|
|
100878
|
-
updatedAt: nowIso$
|
|
100878
|
+
updatedAt: nowIso$7()
|
|
100879
100879
|
};
|
|
100880
100880
|
yield* update$4(byThreadId, (current) => {
|
|
100881
100881
|
const next = new Map(current);
|
|
@@ -118053,8 +118053,8 @@ effectDiscard(runMigrations());
|
|
|
118053
118053
|
//#endregion
|
|
118054
118054
|
//#region src/persistence/Layers/Sqlite.ts
|
|
118055
118055
|
const defaultSqliteClientLoaders = {
|
|
118056
|
-
bun: () => import("./SqliteClient-
|
|
118057
|
-
node: () => import("./NodeSqliteClient-
|
|
118056
|
+
bun: () => import("./SqliteClient-Bgo_s5sA.mjs"),
|
|
118057
|
+
node: () => import("./NodeSqliteClient-D29w60zZ.mjs")
|
|
118058
118058
|
};
|
|
118059
118059
|
const makeRuntimeSqliteLayer = fn$1("makeRuntimeSqliteLayer")(function* (config) {
|
|
118060
118060
|
const loader = defaultSqliteClientLoaders[process.versions.bun !== void 0 ? "bun" : "node"];
|
|
@@ -230852,7 +230852,7 @@ const OpenCodeRuntimeLive = effect(OpenCodeRuntime, makeOpenCodeRuntime).pipe(pr
|
|
|
230852
230852
|
//#endregion
|
|
230853
230853
|
//#region src/provider/Layers/OpenCodeAdapter.ts
|
|
230854
230854
|
const PROVIDER$5 = "opencode";
|
|
230855
|
-
const nowIso$
|
|
230855
|
+
const nowIso$6 = map$3(now, formatIso);
|
|
230856
230856
|
/**
|
|
230857
230857
|
* Map a tagged OpenCodeRuntimeError produced by {@link runOpenCodeSdk} into
|
|
230858
230858
|
* the adapter-boundary `ProviderAdapterRequestError`. SDK-method-level call
|
|
@@ -230879,7 +230879,7 @@ const toProcessError = (threadId, cause) => new ProviderAdapterProcessError({
|
|
|
230879
230879
|
});
|
|
230880
230880
|
const buildEventBase = (input) => gen(function* () {
|
|
230881
230881
|
const uuid = yield* nextUUIDv4;
|
|
230882
|
-
const createdAt = input.createdAt ?? (yield* nowIso$
|
|
230882
|
+
const createdAt = input.createdAt ?? (yield* nowIso$6);
|
|
230883
230883
|
return {
|
|
230884
230884
|
eventId: EventId.make(uuid),
|
|
230885
230885
|
provider: PROVIDER$5,
|
|
@@ -231038,7 +231038,7 @@ function sessionErrorMessage(error) {
|
|
|
231038
231038
|
}
|
|
231039
231039
|
function updateProviderSession(context, patch, options) {
|
|
231040
231040
|
return gen(function* () {
|
|
231041
|
-
const updatedAt = yield* nowIso$
|
|
231041
|
+
const updatedAt = yield* nowIso$6;
|
|
231042
231042
|
const nextSession = {
|
|
231043
231043
|
...context.session,
|
|
231044
231044
|
...patch,
|
|
@@ -231160,7 +231160,7 @@ function makeOpenCodeAdapter(openCodeSettings, options) {
|
|
|
231160
231160
|
if (("properties" in event ? event.properties.sessionID : void 0) !== context.openCodeSessionId) return;
|
|
231161
231161
|
const turnId = context.activeTurnId;
|
|
231162
231162
|
yield* writeNativeEventBestEffort(context.session.threadId, {
|
|
231163
|
-
observedAt: yield* nowIso$
|
|
231163
|
+
observedAt: yield* nowIso$6,
|
|
231164
231164
|
event: {
|
|
231165
231165
|
provider: PROVIDER$5,
|
|
231166
231166
|
threadId: context.session.threadId,
|
|
@@ -231476,7 +231476,7 @@ function makeOpenCodeAdapter(openCodeSettings, options) {
|
|
|
231476
231476
|
yield* close(started.sessionScope, void_$3).pipe(ignore);
|
|
231477
231477
|
return raceWinner.session;
|
|
231478
231478
|
}
|
|
231479
|
-
const createdAt = yield* nowIso$
|
|
231479
|
+
const createdAt = yield* nowIso$6;
|
|
231480
231480
|
const session = {
|
|
231481
231481
|
provider: PROVIDER$5,
|
|
231482
231482
|
status: "ready",
|
|
@@ -238648,7 +238648,7 @@ function filterEvents(events, options) {
|
|
|
238648
238648
|
});
|
|
238649
238649
|
return options.limit === void 0 ? filtered : filtered.slice(-options.limit);
|
|
238650
238650
|
}
|
|
238651
|
-
function nowIso$
|
|
238651
|
+
function nowIso$5() {
|
|
238652
238652
|
return (/* @__PURE__ */ new Date()).toISOString();
|
|
238653
238653
|
}
|
|
238654
238654
|
//#endregion
|
|
@@ -238663,7 +238663,7 @@ var BrowserActionEventRecorder = class {
|
|
|
238663
238663
|
type: "browser.action",
|
|
238664
238664
|
action,
|
|
238665
238665
|
...this.sessionFields(),
|
|
238666
|
-
timestamp: nowIso$
|
|
238666
|
+
timestamp: nowIso$5()
|
|
238667
238667
|
});
|
|
238668
238668
|
}
|
|
238669
238669
|
async result(action) {
|
|
@@ -238672,7 +238672,7 @@ var BrowserActionEventRecorder = class {
|
|
|
238672
238672
|
action,
|
|
238673
238673
|
ok: true,
|
|
238674
238674
|
...this.sessionFields(),
|
|
238675
|
-
timestamp: nowIso$
|
|
238675
|
+
timestamp: nowIso$5()
|
|
238676
238676
|
});
|
|
238677
238677
|
}
|
|
238678
238678
|
async error(action, error) {
|
|
@@ -238681,7 +238681,7 @@ var BrowserActionEventRecorder = class {
|
|
|
238681
238681
|
action,
|
|
238682
238682
|
error: error instanceof Error ? error.message : String(error),
|
|
238683
238683
|
...this.sessionFields(),
|
|
238684
|
-
timestamp: nowIso$
|
|
238684
|
+
timestamp: nowIso$5()
|
|
238685
238685
|
});
|
|
238686
238686
|
}
|
|
238687
238687
|
sessionFields() {
|
|
@@ -238689,24 +238689,6 @@ var BrowserActionEventRecorder = class {
|
|
|
238689
238689
|
}
|
|
238690
238690
|
};
|
|
238691
238691
|
//#endregion
|
|
238692
|
-
//#region ../../node_modules/.bun/@cmmd-center+agent-browser@0.6.2+69e331b0475d65dc/node_modules/@cmmd-center/agent-browser/dist/core/settle.js
|
|
238693
|
-
async function settleValueWithin(promise, timeoutMs) {
|
|
238694
|
-
let timer;
|
|
238695
|
-
try {
|
|
238696
|
-
return await Promise.race([promise.then((value) => ({
|
|
238697
|
-
status: "fulfilled",
|
|
238698
|
-
value
|
|
238699
|
-
}), (error) => ({
|
|
238700
|
-
error,
|
|
238701
|
-
status: "rejected"
|
|
238702
|
-
})), new Promise((resolve) => {
|
|
238703
|
-
timer = setTimeout(() => resolve({ status: "timed-out" }), timeoutMs);
|
|
238704
|
-
})]);
|
|
238705
|
-
} finally {
|
|
238706
|
-
clearTimeout(timer);
|
|
238707
|
-
}
|
|
238708
|
-
}
|
|
238709
|
-
//#endregion
|
|
238710
238692
|
//#region ../../node_modules/.bun/@cmmd-center+agent-browser@0.6.2+69e331b0475d65dc/node_modules/@cmmd-center/agent-browser/dist/safety/policy.js
|
|
238711
238693
|
var DefaultSafetyPolicy = class {
|
|
238712
238694
|
evaluate(action) {
|
|
@@ -238921,7 +238903,7 @@ var AgentBrowser = class {
|
|
|
238921
238903
|
async clickAction(action) {
|
|
238922
238904
|
await this.run({
|
|
238923
238905
|
kind: "click",
|
|
238924
|
-
selector: locatorActionLabel
|
|
238906
|
+
selector: locatorActionLabel(action)
|
|
238925
238907
|
}, async (tab) => {
|
|
238926
238908
|
await tab.click(action);
|
|
238927
238909
|
});
|
|
@@ -238933,7 +238915,7 @@ var AgentBrowser = class {
|
|
|
238933
238915
|
async doubleClickAction(action) {
|
|
238934
238916
|
await this.run({
|
|
238935
238917
|
kind: "click",
|
|
238936
|
-
selector: locatorActionLabel
|
|
238918
|
+
selector: locatorActionLabel(action)
|
|
238937
238919
|
}, async (tab) => {
|
|
238938
238920
|
await tab.doubleClick(action);
|
|
238939
238921
|
});
|
|
@@ -238945,7 +238927,7 @@ var AgentBrowser = class {
|
|
|
238945
238927
|
async fillAction(action, value) {
|
|
238946
238928
|
await this.run({
|
|
238947
238929
|
kind: "fill",
|
|
238948
|
-
selector: locatorActionLabel
|
|
238930
|
+
selector: locatorActionLabel(action),
|
|
238949
238931
|
value
|
|
238950
238932
|
}, async (tab) => {
|
|
238951
238933
|
await tab.fill(action, value);
|
|
@@ -238958,7 +238940,7 @@ var AgentBrowser = class {
|
|
|
238958
238940
|
async typeAction(action, value) {
|
|
238959
238941
|
await this.run({
|
|
238960
238942
|
kind: "fill",
|
|
238961
|
-
selector: locatorActionLabel
|
|
238943
|
+
selector: locatorActionLabel(action),
|
|
238962
238944
|
value
|
|
238963
238945
|
}, async (tab) => {
|
|
238964
238946
|
await tab.type(action, value);
|
|
@@ -238971,7 +238953,7 @@ var AgentBrowser = class {
|
|
|
238971
238953
|
async pressAction(action, key) {
|
|
238972
238954
|
await this.run({
|
|
238973
238955
|
kind: "press",
|
|
238974
|
-
selector: locatorActionLabel
|
|
238956
|
+
selector: locatorActionLabel(action),
|
|
238975
238957
|
key
|
|
238976
238958
|
}, async (tab) => {
|
|
238977
238959
|
await tab.press(action, key);
|
|
@@ -238984,7 +238966,7 @@ var AgentBrowser = class {
|
|
|
238984
238966
|
async selectAction(action, value) {
|
|
238985
238967
|
await this.run({
|
|
238986
238968
|
kind: "select",
|
|
238987
|
-
selector: locatorActionLabel
|
|
238969
|
+
selector: locatorActionLabel(action),
|
|
238988
238970
|
value
|
|
238989
238971
|
}, async (tab) => {
|
|
238990
238972
|
await tab.select(action, value);
|
|
@@ -238997,7 +238979,7 @@ var AgentBrowser = class {
|
|
|
238997
238979
|
async checkAction(action, checked) {
|
|
238998
238980
|
await this.run({
|
|
238999
238981
|
kind: "click",
|
|
239000
|
-
selector: locatorActionLabel
|
|
238982
|
+
selector: locatorActionLabel(action),
|
|
239001
238983
|
value: String(checked)
|
|
239002
238984
|
}, async (tab) => {
|
|
239003
238985
|
await tab.check(action, checked);
|
|
@@ -239202,7 +239184,7 @@ var AgentBrowser = class {
|
|
|
239202
239184
|
await this.options.events?.record({
|
|
239203
239185
|
type: "browser.safety.confirmation.requested",
|
|
239204
239186
|
...request,
|
|
239205
|
-
createdAt: nowIso$
|
|
239187
|
+
createdAt: nowIso$5()
|
|
239206
239188
|
});
|
|
239207
239189
|
const result = await this.resolveConfirmation(request);
|
|
239208
239190
|
const approved = typeof result === "boolean" ? result : result.approved;
|
|
@@ -239210,7 +239192,7 @@ var AgentBrowser = class {
|
|
|
239210
239192
|
type: "browser.safety.confirmation.resolved",
|
|
239211
239193
|
approved,
|
|
239212
239194
|
requestId: request.requestId,
|
|
239213
|
-
resolvedAt: nowIso$
|
|
239195
|
+
resolvedAt: nowIso$5(),
|
|
239214
239196
|
...this.events.sessionFields()
|
|
239215
239197
|
});
|
|
239216
239198
|
if (!approved) throw new BrowserSafetyError(decision.reason ?? "Browser action was not confirmed.", action, {
|
|
@@ -239233,7 +239215,7 @@ function locatorAction(selector, timeoutMs) {
|
|
|
239233
239215
|
timeoutMs
|
|
239234
239216
|
};
|
|
239235
239217
|
}
|
|
239236
|
-
function locatorActionLabel
|
|
239218
|
+
function locatorActionLabel(action) {
|
|
239237
239219
|
return action.selector ?? action.query?.kind ?? "locator";
|
|
239238
239220
|
}
|
|
239239
239221
|
//#endregion
|
|
@@ -240340,778 +240322,1392 @@ function isPromiseLike(value) {
|
|
|
240340
240322
|
return !!value && typeof value.then === "function";
|
|
240341
240323
|
}
|
|
240342
240324
|
//#endregion
|
|
240343
|
-
//#region
|
|
240344
|
-
|
|
240345
|
-
|
|
240346
|
-
|
|
240347
|
-
|
|
240348
|
-
|
|
240349
|
-
|
|
240350
|
-
|
|
240351
|
-
|
|
240352
|
-
|
|
240353
|
-
|
|
240354
|
-
|
|
240355
|
-
|
|
240356
|
-
|
|
240357
|
-
|
|
240358
|
-
|
|
240359
|
-
|
|
240360
|
-
|
|
240361
|
-
|
|
240362
|
-
|
|
240363
|
-
|
|
240364
|
-
|
|
240365
|
-
|
|
240366
|
-
|
|
240367
|
-
|
|
240368
|
-
|
|
240369
|
-
|
|
240370
|
-
|
|
240371
|
-
|
|
240372
|
-
|
|
240373
|
-
|
|
240374
|
-
|
|
240375
|
-
|
|
240376
|
-
|
|
240377
|
-
|
|
240378
|
-
|
|
240379
|
-
|
|
240380
|
-
|
|
240381
|
-
|
|
240382
|
-
|
|
240383
|
-
|
|
240384
|
-
|
|
240325
|
+
//#region src/sprite/spriteTaskHold.ts
|
|
240326
|
+
/**
|
|
240327
|
+
* Keeps a Fly.io Sprite awake for the duration of an agent turn.
|
|
240328
|
+
*
|
|
240329
|
+
* ── why this exists ─────────────────────────────────────────────────────────
|
|
240330
|
+
* A sprite's container cgroup is frozen within ~1 second of the last inbound
|
|
240331
|
+
* request. Processes are paused mid-instruction, not killed, and resume when
|
|
240332
|
+
* traffic returns. Measured on a throwaway sprite with a 1 Hz ticker
|
|
240333
|
+
* (`pocs/sprite-lifecycle-spike/`):
|
|
240334
|
+
*
|
|
240335
|
+
* detached, nothing connected 2 ticks / 121 s = 1.7%
|
|
240336
|
+
* with a task hold 90 ticks / 90 s = 100%
|
|
240337
|
+
*
|
|
240338
|
+
* So an agent turn that nobody is watching effectively stops. fly.io names our
|
|
240339
|
+
* previous approach — relying on a running process after disconnection — as an
|
|
240340
|
+
* anti-pattern, and points at the Tasks API instead. See
|
|
240341
|
+
* https://docs.sprites.dev/keeping-sprites-running/ and
|
|
240342
|
+
* `docs/specs/2026-07-30-sprites-platform-ground-truth.md` §1.
|
|
240343
|
+
*
|
|
240344
|
+
* Note that Services (the platform supervisor) do NOT solve this: "a Sprite with
|
|
240345
|
+
* ten services defined still pauses when it goes idle." Supervision and
|
|
240346
|
+
* wakefulness are separate primitives, and this module is only the second one.
|
|
240347
|
+
*
|
|
240348
|
+
* ── why it lives in the guest ───────────────────────────────────────────────
|
|
240349
|
+
* The Tasks API is guest-only. `GET https://api.sprites.dev/v1/sprites/{n}/tasks`
|
|
240350
|
+
* answers 404; the socket at /.sprite/api.sock is the only surface. CMMD cannot
|
|
240351
|
+
* take this hold on our behalf, which is why the env vars below are exported by
|
|
240352
|
+
* CMMD's provider and consumed here.
|
|
240353
|
+
*
|
|
240354
|
+
* ── the ordering rules ──────────────────────────────────────────────────────
|
|
240355
|
+
* 1. The refresher MUST be stopped before the DELETE is issued. A refresher that
|
|
240356
|
+
* outlives the delete re-acquires the hold on its next tick; that was
|
|
240357
|
+
* previously measured leaking 37 renewals over 18 minutes, pinning a sprite
|
|
240358
|
+
* running — and billing — 33 minutes after its work had ended.
|
|
240359
|
+
* 2. Stopping the interval only cancels FUTURE ticks. A PUT already on the wire
|
|
240360
|
+
* must be JOINED before the DELETE, or it lands afterwards and recreates the
|
|
240361
|
+
* hold for another full expiry window. fly.io's reference worker cancels and
|
|
240362
|
+
* awaits its heartbeat for the same reason.
|
|
240363
|
+
* 3. `release` can arrive while `acquire` is still awaiting its own PUT —
|
|
240364
|
+
* `releaseAll` runs from runtime drain and the shutdown finalizer,
|
|
240365
|
+
* concurrently with the provider event stream. The hold is registered before
|
|
240366
|
+
* that await (so a duplicate turn.started cannot double-acquire), so release
|
|
240367
|
+
* can remove the map entry first; `acquire` must then NOT install a
|
|
240368
|
+
* refresher. One attached to an unmapped hold is unreachable by release,
|
|
240369
|
+
* releaseThread and releaseAll alike, and re-PUTs every 60 s for the life of
|
|
240370
|
+
* the process — an unbounded pin.
|
|
240371
|
+
*
|
|
240372
|
+
* All three are pinned by tests.
|
|
240373
|
+
*
|
|
240374
|
+
* Pure and dependency-free on purpose, for the same reason as
|
|
240375
|
+
* `provider/claudeCredentialContainment.ts`: this rule is cost-critical and must
|
|
240376
|
+
* stay directly unit-testable without booting the Effect runtime.
|
|
240377
|
+
*/
|
|
240378
|
+
/** Set by CMMD's Sprites provider on every project sprite. Absent elsewhere. */
|
|
240379
|
+
const SPRITE_TASK_HOLD_SOCKET_ENV = "FORGE_CMMD_SPRITE_TASK_HOLD_SOCKET";
|
|
240380
|
+
const SPRITE_TASK_HOLD_REFRESH_MS = 6e4;
|
|
240381
|
+
/**
|
|
240382
|
+
* Per-request deadline for the guest-socket calls. Must stay strictly under
|
|
240383
|
+
* SPRITE_TASK_HOLD_REFRESH_MS: the manager serializes refreshes, so a PUT that
|
|
240384
|
+
* never settles turns every later tick into a no-op and the hold expires
|
|
240385
|
+
* mid-turn. Bounding the call means a hung socket settles as a rejection, the
|
|
240386
|
+
* next 60 s tick retries, and release is not wedged joining it. From the last
|
|
240387
|
+
* good refresh there are still four attempts (+60/+120/+180/+240 s) before the
|
|
240388
|
+
* 300 s expiry. 10 s matches the `-m 10` cap CMMD already puts on every curl in
|
|
240389
|
+
* its own hold preamble; the guest-side manager was the one path left unbounded.
|
|
240390
|
+
*/
|
|
240391
|
+
const SPRITE_TASK_HOLD_REQUEST_TIMEOUT_MS = 1e4;
|
|
240392
|
+
/**
|
|
240393
|
+
* Resolve the hold configuration from the process environment.
|
|
240394
|
+
*
|
|
240395
|
+
* Returns null on every runtime that is not a project sprite — desktop-local and
|
|
240396
|
+
* browser runtimes have no `/.sprite/api.sock`, so the whole feature no-ops
|
|
240397
|
+
* there rather than erroring.
|
|
240398
|
+
*/
|
|
240399
|
+
function resolveSpriteTaskHoldConfig(env = process.env) {
|
|
240400
|
+
const socketPath = env[SPRITE_TASK_HOLD_SOCKET_ENV]?.trim();
|
|
240401
|
+
if (socketPath === void 0 || socketPath.length === 0) return null;
|
|
240385
240402
|
return {
|
|
240386
|
-
|
|
240387
|
-
|
|
240388
|
-
...options.hasNotText === void 0 ? {} : { hasNotText: options.hasNotText },
|
|
240389
|
-
...options.hasText === void 0 ? {} : { hasText: options.hasText }
|
|
240403
|
+
socketPath,
|
|
240404
|
+
namePrefix: env["FORGE_CMMD_SPRITE_TASK_HOLD_PREFIX"]?.trim() ?? ""
|
|
240390
240405
|
};
|
|
240391
240406
|
}
|
|
240392
|
-
|
|
240393
|
-
|
|
240394
|
-
|
|
240395
|
-
|
|
240396
|
-
|
|
240397
|
-
|
|
240398
|
-
|
|
240399
|
-
|
|
240400
|
-
|
|
240401
|
-
|
|
240402
|
-
|
|
240403
|
-
constructor(options = {}) {
|
|
240404
|
-
this.options = options;
|
|
240405
|
-
}
|
|
240406
|
-
info() {
|
|
240407
|
-
return {
|
|
240408
|
-
id: "playwright",
|
|
240409
|
-
kind: "playwright",
|
|
240410
|
-
name: this.options.name ?? "Playwright Chromium"
|
|
240411
|
-
};
|
|
240412
|
-
}
|
|
240413
|
-
async close() {
|
|
240414
|
-
/* v8 ignore next -- defensive cleanup for already-closed Playwright contexts. */
|
|
240415
|
-
await this.context?.close().catch(() => void 0);
|
|
240416
|
-
await this.browser?.close();
|
|
240417
|
-
this.browser = void 0;
|
|
240418
|
-
this.context = void 0;
|
|
240407
|
+
/**
|
|
240408
|
+
* Hold names must be unique per turn: several git worktrees inside one sprite
|
|
240409
|
+
* can run turns concurrently, and each has to be released independently.
|
|
240410
|
+
*/
|
|
240411
|
+
function spriteTaskHoldName(namePrefix, turnId) {
|
|
240412
|
+
return `${namePrefix}${turnId}`;
|
|
240413
|
+
}
|
|
240414
|
+
const DEFAULT_TIMER = {
|
|
240415
|
+
setInterval: (handler, ms) => setInterval(handler, ms),
|
|
240416
|
+
clearInterval: (handle) => {
|
|
240417
|
+
clearInterval(handle);
|
|
240419
240418
|
}
|
|
240420
|
-
|
|
240421
|
-
|
|
240422
|
-
|
|
240419
|
+
};
|
|
240420
|
+
/**
|
|
240421
|
+
* Tracks one hold per active turn.
|
|
240422
|
+
*
|
|
240423
|
+
* Every method is failure-tolerant by design. Acquiring a hold is an
|
|
240424
|
+
* optimisation, not a precondition: if it fails the turn still runs, just at the
|
|
240425
|
+
* degraded speed it has today. Failing the turn instead would convert a billing
|
|
240426
|
+
* inefficiency into an outage.
|
|
240427
|
+
*/
|
|
240428
|
+
var SpriteTaskHoldManager = class {
|
|
240429
|
+
#config;
|
|
240430
|
+
#transport;
|
|
240431
|
+
#timer;
|
|
240432
|
+
#onError;
|
|
240433
|
+
#holds = /* @__PURE__ */ new Map();
|
|
240434
|
+
constructor(options) {
|
|
240435
|
+
this.#config = options.config;
|
|
240436
|
+
this.#transport = options.transport;
|
|
240437
|
+
this.#timer = options.timer ?? DEFAULT_TIMER;
|
|
240438
|
+
this.#onError = options.onError ?? (() => {});
|
|
240423
240439
|
}
|
|
240424
|
-
|
|
240425
|
-
|
|
240426
|
-
|
|
240427
|
-
return page ? this.toTab(page) : void 0;
|
|
240440
|
+
/** Turn ids that currently hold the sprite awake. Test/diagnostic surface. */
|
|
240441
|
+
activeTurnIds() {
|
|
240442
|
+
return [...this.#holds.keys()];
|
|
240428
240443
|
}
|
|
240429
|
-
async
|
|
240430
|
-
|
|
240431
|
-
|
|
240432
|
-
|
|
240433
|
-
|
|
240434
|
-
|
|
240435
|
-
|
|
240436
|
-
|
|
240437
|
-
|
|
240438
|
-
|
|
240439
|
-
|
|
240440
|
-
|
|
240441
|
-
|
|
240442
|
-
|
|
240443
|
-
if (this.options.userDataDir && !this.context) {
|
|
240444
|
-
this.context = await chromium.launchPersistentContext(this.options.userDataDir, {
|
|
240445
|
-
...this.options.launchOptions,
|
|
240446
|
-
...this.options.contextOptions,
|
|
240447
|
-
headless: this.options.headless ?? false
|
|
240448
|
-
});
|
|
240449
|
-
this.browser = this.context.browser() ?? void 0;
|
|
240450
|
-
return this.context;
|
|
240451
|
-
}
|
|
240452
|
-
/* v8 ignore next -- requires a live externally managed Chrome CDP endpoint */
|
|
240453
|
-
if (!this.browser && this.options.cdpEndpoint) {
|
|
240454
|
-
/* v8 ignore start -- requires a live externally managed Chrome CDP endpoint */
|
|
240455
|
-
this.browser = await chromium.connectOverCDP(this.options.cdpEndpoint);
|
|
240456
|
-
this.context = this.browser.contexts()[0] ?? await this.browser.newContext(this.options.contextOptions);
|
|
240457
|
-
return this.context;
|
|
240458
|
-
}
|
|
240459
|
-
/* v8 ignore next -- headed/default launch is covered by browser parity smoke. */
|
|
240460
|
-
if (!this.browser) this.browser = await chromium.launch({
|
|
240461
|
-
...this.options.launchOptions,
|
|
240462
|
-
headless: this.options.headless ?? false
|
|
240463
|
-
});
|
|
240464
|
-
if (!this.context) this.context = await this.browser.newContext(this.options.contextOptions);
|
|
240465
|
-
return this.context;
|
|
240466
|
-
}
|
|
240467
|
-
toTab(page) {
|
|
240468
|
-
this.registerPage(page);
|
|
240469
|
-
return new PlaywrightTabHandle(page, this.idFor(page), this.logsFor(page));
|
|
240470
|
-
}
|
|
240471
|
-
async preparePage(page) {
|
|
240472
|
-
this.registerPage(page);
|
|
240473
|
-
const bounds = this.options.windowBounds;
|
|
240474
|
-
if (!bounds) return;
|
|
240475
|
-
await settleValueWithin(page.setViewportSize({
|
|
240476
|
-
width: bounds.width,
|
|
240477
|
-
height: bounds.height
|
|
240478
|
-
}), TAB_CONTROL_TIMEOUT_MS);
|
|
240479
|
-
const sessionResult = await settleValueWithin(page.context().newCDPSession(page), TAB_CONTROL_TIMEOUT_MS);
|
|
240480
|
-
/* v8 ignore next -- Playwright CDP attachment failures are bounded and covered by the shared timeout primitive. */
|
|
240481
|
-
if (sessionResult.status !== "fulfilled") return;
|
|
240482
|
-
const session = sessionResult.value;
|
|
240444
|
+
async acquire(turnId, threadId = null) {
|
|
240445
|
+
if (this.#holds.has(turnId)) return;
|
|
240446
|
+
const name = spriteTaskHoldName(this.#config.namePrefix, turnId);
|
|
240447
|
+
const hold = {
|
|
240448
|
+
name,
|
|
240449
|
+
threadId,
|
|
240450
|
+
refresher: null,
|
|
240451
|
+
inFlight: /* @__PURE__ */ new Set(),
|
|
240452
|
+
released: false
|
|
240453
|
+
};
|
|
240454
|
+
this.#holds.set(turnId, hold);
|
|
240455
|
+
const put = this.#transport.put(name, 300);
|
|
240456
|
+
const tracked = put.then(() => {}, () => {});
|
|
240457
|
+
hold.inFlight.add(tracked);
|
|
240483
240458
|
try {
|
|
240484
|
-
|
|
240485
|
-
|
|
240486
|
-
|
|
240487
|
-
|
|
240488
|
-
|
|
240489
|
-
height: bounds.height,
|
|
240490
|
-
left: bounds.left ?? 0,
|
|
240491
|
-
top: bounds.top ?? 0,
|
|
240492
|
-
width: bounds.width,
|
|
240493
|
-
windowState: "normal"
|
|
240494
|
-
},
|
|
240495
|
-
windowId: windowResult.value.windowId
|
|
240496
|
-
}), TAB_CONTROL_TIMEOUT_MS);
|
|
240497
|
-
} finally {
|
|
240498
|
-
await settleValueWithin(session.detach(), TAB_CONTROL_TIMEOUT_MS);
|
|
240499
|
-
}
|
|
240500
|
-
}
|
|
240501
|
-
registerPage(page) {
|
|
240502
|
-
this.idFor(page);
|
|
240503
|
-
this.logsFor(page);
|
|
240504
|
-
if (this.registeredPages.has(page)) return;
|
|
240505
|
-
this.registeredPages.add(page);
|
|
240506
|
-
page.on("console", (message) => {
|
|
240507
|
-
this.logsFor(page).push(toConsoleLog(message, page.url()));
|
|
240508
|
-
});
|
|
240509
|
-
}
|
|
240510
|
-
idFor(page) {
|
|
240511
|
-
let id = this.pageIds.get(page);
|
|
240512
|
-
if (!id) {
|
|
240513
|
-
id = `tab_${randomUUID()}`;
|
|
240514
|
-
this.pageIds.set(page, id);
|
|
240459
|
+
await put;
|
|
240460
|
+
} catch (cause) {
|
|
240461
|
+
hold.inFlight.delete(tracked);
|
|
240462
|
+
this.#onError(`failed to acquire sprite task hold for turn ${turnId}`, cause);
|
|
240463
|
+
if (hold.released || this.#holds.get(turnId) !== hold) return;
|
|
240515
240464
|
}
|
|
240516
|
-
|
|
240465
|
+
hold.inFlight.delete(tracked);
|
|
240466
|
+
if (hold.released || this.#holds.get(turnId) !== hold) return;
|
|
240467
|
+
hold.refresher = this.#timer.setInterval(() => {
|
|
240468
|
+
if (hold.inFlight.size > 0) return;
|
|
240469
|
+
const refresh = this.#transport.put(name, 300).catch((cause) => this.#onError(`failed to refresh sprite task hold for turn ${turnId}`, cause));
|
|
240470
|
+
hold.inFlight.add(refresh);
|
|
240471
|
+
refresh.finally(() => hold.inFlight.delete(refresh));
|
|
240472
|
+
}, SPRITE_TASK_HOLD_REFRESH_MS);
|
|
240517
240473
|
}
|
|
240518
|
-
|
|
240519
|
-
|
|
240520
|
-
if (!
|
|
240521
|
-
|
|
240522
|
-
|
|
240474
|
+
async release(turnId) {
|
|
240475
|
+
const hold = this.#holds.get(turnId);
|
|
240476
|
+
if (!hold) return;
|
|
240477
|
+
hold.released = true;
|
|
240478
|
+
if (hold.refresher !== null) {
|
|
240479
|
+
this.#timer.clearInterval(hold.refresher);
|
|
240480
|
+
hold.refresher = null;
|
|
240523
240481
|
}
|
|
240524
|
-
|
|
240525
|
-
|
|
240526
|
-
};
|
|
240527
|
-
var PlaywrightTabHandle = class {
|
|
240528
|
-
page;
|
|
240529
|
-
id;
|
|
240530
|
-
logEntries;
|
|
240531
|
-
constructor(page, id, logEntries) {
|
|
240532
|
-
this.page = page;
|
|
240533
|
-
this.id = id;
|
|
240534
|
-
this.logEntries = logEntries;
|
|
240535
|
-
}
|
|
240536
|
-
async info() {
|
|
240537
|
-
const [title, history] = await Promise.all([settleValueWithin(this.page.title(), TAB_METADATA_TIMEOUT_MS), settleValueWithin(this.historyState(), TAB_METADATA_TIMEOUT_MS)]);
|
|
240538
|
-
const base = title.status !== "fulfilled" ? {
|
|
240539
|
-
id: this.id,
|
|
240540
|
-
url: this.page.url()
|
|
240541
|
-
} : {
|
|
240542
|
-
id: this.id,
|
|
240543
|
-
title: title.value,
|
|
240544
|
-
url: this.page.url()
|
|
240545
|
-
};
|
|
240546
|
-
/* v8 ignore next -- metadata timeout fallback is covered through AgentBrowser's bounded observation contract. */
|
|
240547
|
-
return history.status === "fulfilled" ? {
|
|
240548
|
-
...base,
|
|
240549
|
-
...history.value
|
|
240550
|
-
} : base;
|
|
240551
|
-
}
|
|
240552
|
-
async historyState() {
|
|
240553
|
-
const sessionResult = await settleValueWithin(this.page.context().newCDPSession(this.page), TAB_METADATA_TIMEOUT_MS);
|
|
240554
|
-
/* v8 ignore next -- Playwright CDP attachment failures are bounded and covered by the shared timeout primitive. */
|
|
240555
|
-
if (sessionResult.status !== "fulfilled") return {};
|
|
240556
|
-
const session = sessionResult.value;
|
|
240482
|
+
this.#holds.delete(turnId);
|
|
240483
|
+
if (hold.inFlight.size > 0) await Promise.all(hold.inFlight);
|
|
240557
240484
|
try {
|
|
240558
|
-
|
|
240559
|
-
|
|
240560
|
-
|
|
240561
|
-
const history = historyResult.value;
|
|
240562
|
-
return {
|
|
240563
|
-
canGoBack: history.currentIndex > 0,
|
|
240564
|
-
canGoForward: history.currentIndex < history.entries.length - 1
|
|
240565
|
-
};
|
|
240566
|
-
} finally {
|
|
240567
|
-
await settleValueWithin(session.detach(), TAB_METADATA_TIMEOUT_MS);
|
|
240485
|
+
await this.#transport.delete(hold.name);
|
|
240486
|
+
} catch (cause) {
|
|
240487
|
+
this.#onError(`failed to release sprite task hold for turn ${turnId}`, cause);
|
|
240568
240488
|
}
|
|
240569
240489
|
}
|
|
240570
|
-
|
|
240571
|
-
|
|
240490
|
+
/**
|
|
240491
|
+
* Release every hold belonging to one thread.
|
|
240492
|
+
*
|
|
240493
|
+
* `session.exited` names only the thread whose provider process died, and a
|
|
240494
|
+
* dead session's turn will never emit `turn.completed`. Releasing by thread —
|
|
240495
|
+
* rather than releasing everything — is what keeps a crash in one worktree
|
|
240496
|
+
* from silently dropping the holds of the other turns running beside it.
|
|
240497
|
+
*/
|
|
240498
|
+
async releaseThread(threadId) {
|
|
240499
|
+
const turnIds = [...this.#holds.entries()].filter(([, hold]) => hold.threadId === threadId).map(([turnId]) => turnId);
|
|
240500
|
+
await Promise.all(turnIds.map((turnId) => this.release(turnId)));
|
|
240572
240501
|
}
|
|
240573
|
-
|
|
240574
|
-
|
|
240502
|
+
/**
|
|
240503
|
+
* Release everything. Runtime drain and server shutdown must call this, or a
|
|
240504
|
+
* drained sprite keeps billing until each hold expires.
|
|
240505
|
+
*/
|
|
240506
|
+
async releaseAll() {
|
|
240507
|
+
await Promise.all([...this.#holds.keys()].map((turnId) => this.release(turnId)));
|
|
240575
240508
|
}
|
|
240576
|
-
|
|
240577
|
-
|
|
240509
|
+
};
|
|
240510
|
+
/**
|
|
240511
|
+
* HTTP-over-unix-socket transport for the guest API.
|
|
240512
|
+
*
|
|
240513
|
+
* Bun's fetch takes a `unix` option; the URL host is ignored by the socket but
|
|
240514
|
+
* still has to parse, which is why it is the literal `http://sprite` the
|
|
240515
|
+
* platform docs use.
|
|
240516
|
+
*/
|
|
240517
|
+
function makeSpriteTaskHoldTransport(config, fetchImpl = fetch) {
|
|
240518
|
+
const call = async (path, init) => {
|
|
240519
|
+
const controller = new AbortController();
|
|
240520
|
+
const deadline = setTimeout(() => controller.abort(), SPRITE_TASK_HOLD_REQUEST_TIMEOUT_MS);
|
|
240578
240521
|
try {
|
|
240579
|
-
await
|
|
240580
|
-
|
|
240581
|
-
|
|
240522
|
+
const response = await fetchImpl(`http://sprite/v1/tasks${path}`, {
|
|
240523
|
+
...init,
|
|
240524
|
+
signal: controller.signal,
|
|
240525
|
+
unix: config.socketPath
|
|
240526
|
+
});
|
|
240527
|
+
if (!response.ok) throw new Error(`sprite task API responded ${response.status}`);
|
|
240582
240528
|
} finally {
|
|
240583
|
-
|
|
240529
|
+
clearTimeout(deadline);
|
|
240584
240530
|
}
|
|
240531
|
+
};
|
|
240532
|
+
return {
|
|
240533
|
+
put: (name, expireSeconds) => call(`/${encodeURIComponent(name)}`, {
|
|
240534
|
+
method: "PUT",
|
|
240535
|
+
headers: { "content-type": "application/json" },
|
|
240536
|
+
body: JSON.stringify({ expire: `${expireSeconds}s` })
|
|
240537
|
+
}),
|
|
240538
|
+
delete: (name) => call(`/${encodeURIComponent(name)}`, { method: "DELETE" })
|
|
240539
|
+
};
|
|
240540
|
+
}
|
|
240541
|
+
/**
|
|
240542
|
+
* Build a manager for this process, or null when the runtime is not a sprite.
|
|
240543
|
+
* Callers treat null as "this feature does not apply here".
|
|
240544
|
+
*/
|
|
240545
|
+
function makeSpriteTaskHoldManager(env = process.env, onError) {
|
|
240546
|
+
const config = resolveSpriteTaskHoldConfig(env);
|
|
240547
|
+
if (config === null) return null;
|
|
240548
|
+
return new SpriteTaskHoldManager({
|
|
240549
|
+
config,
|
|
240550
|
+
transport: makeSpriteTaskHoldTransport(config),
|
|
240551
|
+
...onError ? { onError } : {}
|
|
240552
|
+
});
|
|
240553
|
+
}
|
|
240554
|
+
//#endregion
|
|
240555
|
+
//#region src/browser/AgentBrowserHostBridgeCommands.ts
|
|
240556
|
+
const DEFAULT_AGENT_BROWSER_VIEWPORT = {
|
|
240557
|
+
width: 1280,
|
|
240558
|
+
height: 720
|
|
240559
|
+
};
|
|
240560
|
+
const ABSOLUTE_WEB_URL_PATTERN = /^[a-z][a-z\d+\-.]*:\/\//i;
|
|
240561
|
+
const SPECIAL_URL_PATTERN = /^(about|blob|chrome|data|devtools|file):/i;
|
|
240562
|
+
const LOCAL_URL_LIKE_PATTERN = /^(localhost|127\.0\.0\.1|\[::1\]|::1|[a-z0-9.-]+\.localhost)(?::\d+)?(?:[/?#].*)?$/i;
|
|
240563
|
+
function nowIso$4() {
|
|
240564
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
240565
|
+
}
|
|
240566
|
+
function normalizeUserVisibleBrowserUrl(url) {
|
|
240567
|
+
const trimmed = url.trim();
|
|
240568
|
+
if (trimmed.length === 0) return trimmed;
|
|
240569
|
+
if (ABSOLUTE_WEB_URL_PATTERN.test(trimmed) || SPECIAL_URL_PATTERN.test(trimmed) || trimmed.startsWith("//")) return trimmed;
|
|
240570
|
+
if (LOCAL_URL_LIKE_PATTERN.test(trimmed)) return `http://${trimmed}`;
|
|
240571
|
+
return `https://${trimmed}`;
|
|
240572
|
+
}
|
|
240573
|
+
function isAgentBrowserBoundedControlCommand(command) {
|
|
240574
|
+
switch (command.type) {
|
|
240575
|
+
case "click":
|
|
240576
|
+
case "clickPoint":
|
|
240577
|
+
case "pressKey":
|
|
240578
|
+
case "resetViewport":
|
|
240579
|
+
case "scroll":
|
|
240580
|
+
case "setViewport":
|
|
240581
|
+
case "type":
|
|
240582
|
+
case "typeText":
|
|
240583
|
+
case "wheel": return true;
|
|
240584
|
+
default: return false;
|
|
240585
240585
|
}
|
|
240586
|
-
|
|
240587
|
-
|
|
240588
|
-
|
|
240589
|
-
|
|
240590
|
-
|
|
240591
|
-
|
|
240592
|
-
|
|
240593
|
-
|
|
240594
|
-
|
|
240595
|
-
|
|
240596
|
-
|
|
240597
|
-
|
|
240598
|
-
|
|
240599
|
-
|
|
240600
|
-
...options.quality === void 0 ? {} : { quality: options.quality }
|
|
240601
|
-
});
|
|
240602
|
-
if (options.maxBytes !== void 0 && bytes.byteLength > options.maxBytes) throw new Error(`Browser screenshot exceeded maxBytes=${options.maxBytes}. size=${bytes.byteLength}`);
|
|
240603
|
-
return bytes;
|
|
240604
|
-
}
|
|
240605
|
-
async setViewport(size) {
|
|
240606
|
-
await this.page.setViewportSize({
|
|
240607
|
-
width: Math.round(size.width),
|
|
240608
|
-
height: Math.round(size.height)
|
|
240609
|
-
});
|
|
240586
|
+
}
|
|
240587
|
+
function snapshotAfterTimedOutControl(snapshot, command) {
|
|
240588
|
+
switch (command.type) {
|
|
240589
|
+
case "setViewport": return {
|
|
240590
|
+
...snapshot,
|
|
240591
|
+
viewport: command.viewport,
|
|
240592
|
+
updatedAt: nowIso$4()
|
|
240593
|
+
};
|
|
240594
|
+
case "resetViewport": return {
|
|
240595
|
+
...snapshot,
|
|
240596
|
+
viewport: DEFAULT_AGENT_BROWSER_VIEWPORT,
|
|
240597
|
+
updatedAt: nowIso$4()
|
|
240598
|
+
};
|
|
240599
|
+
default: return snapshot;
|
|
240610
240600
|
}
|
|
240611
|
-
|
|
240612
|
-
|
|
240613
|
-
|
|
240614
|
-
|
|
240615
|
-
|
|
240601
|
+
}
|
|
240602
|
+
const HOST_COMMAND_MAPPERS = {
|
|
240603
|
+
activateTab: (command) => ({
|
|
240604
|
+
type: "activateTab",
|
|
240605
|
+
tabId: command.tabId
|
|
240606
|
+
}),
|
|
240607
|
+
click: (command) => ({
|
|
240608
|
+
type: "click",
|
|
240609
|
+
tabId: command.tabId,
|
|
240610
|
+
selector: command.selector
|
|
240611
|
+
}),
|
|
240612
|
+
clickPoint: (command) => ({
|
|
240613
|
+
type: "clickPoint",
|
|
240614
|
+
tabId: command.tabId,
|
|
240615
|
+
x: command.x,
|
|
240616
|
+
y: command.y,
|
|
240617
|
+
...command.button !== void 0 ? { button: command.button } : {},
|
|
240618
|
+
...command.clickCount !== void 0 ? { clickCount: command.clickCount } : {}
|
|
240619
|
+
}),
|
|
240620
|
+
closeTab: (command) => ({
|
|
240621
|
+
type: "closeTab",
|
|
240622
|
+
tabId: command.tabId
|
|
240623
|
+
}),
|
|
240624
|
+
consoleLogs: (command) => ({
|
|
240625
|
+
type: "consoleLogs",
|
|
240626
|
+
...command.tabId !== void 0 ? { tabId: command.tabId } : {},
|
|
240627
|
+
...command.limit !== void 0 ? { limit: command.limit } : {},
|
|
240628
|
+
...command.levels !== void 0 ? { levels: command.levels } : {}
|
|
240629
|
+
}),
|
|
240630
|
+
goBack: (command) => ({
|
|
240631
|
+
type: "goBack",
|
|
240632
|
+
tabId: command.tabId
|
|
240633
|
+
}),
|
|
240634
|
+
goForward: (command) => ({
|
|
240635
|
+
type: "goForward",
|
|
240636
|
+
tabId: command.tabId
|
|
240637
|
+
}),
|
|
240638
|
+
nameSession: (command) => ({
|
|
240639
|
+
type: "nameSession",
|
|
240640
|
+
name: command.name
|
|
240641
|
+
}),
|
|
240642
|
+
navigate: (command) => ({
|
|
240643
|
+
type: "navigate",
|
|
240644
|
+
url: normalizeUserVisibleBrowserUrl(command.url),
|
|
240645
|
+
...command.tabId !== void 0 ? { tabId: command.tabId } : {}
|
|
240646
|
+
}),
|
|
240647
|
+
newTab: (command) => ({
|
|
240648
|
+
type: "newTab",
|
|
240649
|
+
...command.url !== void 0 ? { url: normalizeUserVisibleBrowserUrl(command.url) } : {},
|
|
240650
|
+
...command.makeActive !== void 0 ? { makeActive: command.makeActive } : {}
|
|
240651
|
+
}),
|
|
240652
|
+
pressKey: (command) => ({
|
|
240653
|
+
type: "pressKey",
|
|
240654
|
+
tabId: command.tabId,
|
|
240655
|
+
key: command.key
|
|
240656
|
+
}),
|
|
240657
|
+
reload: (command) => ({
|
|
240658
|
+
type: "reload",
|
|
240659
|
+
tabId: command.tabId
|
|
240660
|
+
}),
|
|
240661
|
+
resetViewport: () => ({ type: "resetViewport" }),
|
|
240662
|
+
screenshot: (command) => ({
|
|
240663
|
+
type: "screenshot",
|
|
240664
|
+
tabId: command.tabId,
|
|
240665
|
+
...command.fullPage !== void 0 ? { fullPage: command.fullPage } : {}
|
|
240666
|
+
}),
|
|
240667
|
+
scroll: (command) => ({
|
|
240668
|
+
type: "scroll",
|
|
240669
|
+
tabId: command.tabId,
|
|
240670
|
+
scrollX: 0,
|
|
240671
|
+
scrollY: command.toTop === true ? -900 : 900,
|
|
240672
|
+
...command.x !== void 0 ? { x: command.x } : {},
|
|
240673
|
+
...command.y !== void 0 ? { y: command.y } : {}
|
|
240674
|
+
}),
|
|
240675
|
+
setViewport: (command) => ({
|
|
240676
|
+
type: "setViewport",
|
|
240677
|
+
viewport: command.viewport
|
|
240678
|
+
}),
|
|
240679
|
+
setVisibility: (command) => ({
|
|
240680
|
+
type: "setVisibility",
|
|
240681
|
+
visibility: command.visibility
|
|
240682
|
+
}),
|
|
240683
|
+
snapshot: (command) => ({
|
|
240684
|
+
type: "snapshot",
|
|
240685
|
+
tabId: command.tabId
|
|
240686
|
+
}),
|
|
240687
|
+
type: (command) => ({
|
|
240688
|
+
type: "type",
|
|
240689
|
+
tabId: command.tabId,
|
|
240690
|
+
selector: command.selector,
|
|
240691
|
+
text: command.text,
|
|
240692
|
+
...command.append !== void 0 ? { append: command.append } : {}
|
|
240693
|
+
}),
|
|
240694
|
+
typeText: (command) => ({
|
|
240695
|
+
type: "typeText",
|
|
240696
|
+
tabId: command.tabId,
|
|
240697
|
+
text: command.text,
|
|
240698
|
+
...command.pressEnter !== void 0 ? { pressEnter: command.pressEnter } : {}
|
|
240699
|
+
}),
|
|
240700
|
+
wheel: (command) => ({
|
|
240701
|
+
type: "wheel",
|
|
240702
|
+
tabId: command.tabId,
|
|
240703
|
+
deltaX: command.deltaX,
|
|
240704
|
+
deltaY: command.deltaY,
|
|
240705
|
+
...command.x !== void 0 ? { x: command.x } : {},
|
|
240706
|
+
...command.y !== void 0 ? { y: command.y } : {}
|
|
240707
|
+
})
|
|
240708
|
+
};
|
|
240709
|
+
function toHostCommand(command) {
|
|
240710
|
+
return HOST_COMMAND_MAPPERS[command.type](command);
|
|
240711
|
+
}
|
|
240712
|
+
function commandCaptureTabId(command) {
|
|
240713
|
+
switch (command.type) {
|
|
240714
|
+
case "snapshot":
|
|
240715
|
+
case "screenshot": return command.tabId;
|
|
240716
|
+
default: return;
|
|
240616
240717
|
}
|
|
240617
|
-
|
|
240618
|
-
|
|
240718
|
+
}
|
|
240719
|
+
//#endregion
|
|
240720
|
+
//#region src/browser/AgentBrowserHostBridgeDisplay.ts
|
|
240721
|
+
const REMOTE_WEBRTC_UNAVAILABLE_DETAIL = "Remote WebRTC browser display is not available: the current Agent Browser runtime exposes tab control but does not provide a media transport.";
|
|
240722
|
+
const REMOTE_WEBRTC_RELAY_REQUIRED_DETAIL = "Remote WebRTC browser display requires a TURN relay in cloud mode. Configure FORGE_BROWSER_WEBRTC_ICE_SERVERS with authenticated turn: or turns: URLs.";
|
|
240723
|
+
function remoteWebRtcUnavailableError(_input) {
|
|
240724
|
+
return new BrowserError({ detail: REMOTE_WEBRTC_UNAVAILABLE_DETAIL });
|
|
240725
|
+
}
|
|
240726
|
+
function remoteWebRtcRelayRequiredError(_input) {
|
|
240727
|
+
return new BrowserError({ detail: REMOTE_WEBRTC_RELAY_REQUIRED_DETAIL });
|
|
240728
|
+
}
|
|
240729
|
+
function agentBrowserDisplayForRuntimeMode(input) {
|
|
240730
|
+
if (input.mode !== "desktop") {
|
|
240731
|
+
const hasRelay = hasTurnRelay(input.iceServers ?? []);
|
|
240732
|
+
return {
|
|
240733
|
+
transport: "remote-webrtc",
|
|
240734
|
+
status: "degraded",
|
|
240735
|
+
reason: input.requiresRelay === true && !hasRelay ? "network_unreachable" : "transport_unavailable",
|
|
240736
|
+
detail: input.requiresRelay === true && !hasRelay ? REMOTE_WEBRTC_RELAY_REQUIRED_DETAIL : "Remote WebRTC browser display is not attached yet.",
|
|
240737
|
+
...input.viewport ? { viewport: input.viewport } : {},
|
|
240738
|
+
...input.iceServers && input.iceServers.length > 0 ? { iceServers: input.iceServers } : {},
|
|
240739
|
+
reconnect: {
|
|
240740
|
+
status: "ready",
|
|
240741
|
+
attempt: 0
|
|
240742
|
+
}
|
|
240743
|
+
};
|
|
240619
240744
|
}
|
|
240620
|
-
|
|
240621
|
-
|
|
240622
|
-
|
|
240623
|
-
|
|
240624
|
-
|
|
240625
|
-
|
|
240745
|
+
return {
|
|
240746
|
+
transport: "native-embedded",
|
|
240747
|
+
status: "connected",
|
|
240748
|
+
...input.viewport ? { viewport: input.viewport } : {},
|
|
240749
|
+
reconnect: {
|
|
240750
|
+
status: "ready",
|
|
240751
|
+
attempt: 0
|
|
240626
240752
|
}
|
|
240753
|
+
};
|
|
240754
|
+
}
|
|
240755
|
+
function hasTurnRelay(iceServers) {
|
|
240756
|
+
return browserWebRtcIceServersIncludeRelay(iceServers);
|
|
240757
|
+
}
|
|
240758
|
+
async function resolveDisplayIceServers(options) {
|
|
240759
|
+
return options?.resolveDisplayIceServers ? await options.resolveDisplayIceServers() : options?.displayIceServers ?? [];
|
|
240760
|
+
}
|
|
240761
|
+
function webRtcDisplayRequiresRelay(serverConfig) {
|
|
240762
|
+
return serverConfig.mode === "web" && serverConfig.devUrl === void 0;
|
|
240763
|
+
}
|
|
240764
|
+
//#endregion
|
|
240765
|
+
//#region src/browser/AgentBrowserHostBridgeProjection.ts
|
|
240766
|
+
const DEFAULT_URL$1 = "about:blank";
|
|
240767
|
+
const DEFAULT_RENDER_MODE = "interactive";
|
|
240768
|
+
const DEFAULT_AGENT_BROWSER_DISPLAY = {
|
|
240769
|
+
transport: "remote-webrtc",
|
|
240770
|
+
status: "connected",
|
|
240771
|
+
reconnect: {
|
|
240772
|
+
status: "ready",
|
|
240773
|
+
attempt: 0
|
|
240627
240774
|
}
|
|
240628
|
-
|
|
240629
|
-
|
|
240630
|
-
|
|
240631
|
-
|
|
240632
|
-
|
|
240633
|
-
|
|
240634
|
-
|
|
240635
|
-
|
|
240636
|
-
|
|
240637
|
-
|
|
240638
|
-
|
|
240639
|
-
|
|
240640
|
-
|
|
240641
|
-
|
|
240642
|
-
|
|
240643
|
-
|
|
240644
|
-
|
|
240645
|
-
|
|
240646
|
-
|
|
240647
|
-
|
|
240648
|
-
|
|
240649
|
-
|
|
240650
|
-
|
|
240651
|
-
|
|
240652
|
-
|
|
240653
|
-
|
|
240654
|
-
|
|
240655
|
-
|
|
240656
|
-
|
|
240657
|
-
|
|
240658
|
-
|
|
240659
|
-
|
|
240660
|
-
|
|
240661
|
-
|
|
240662
|
-
|
|
240663
|
-
|
|
240664
|
-
|
|
240665
|
-
|
|
240666
|
-
|
|
240667
|
-
|
|
240668
|
-
|
|
240669
|
-
|
|
240670
|
-
|
|
240671
|
-
|
|
240672
|
-
|
|
240673
|
-
|
|
240674
|
-
|
|
240675
|
-
|
|
240676
|
-
|
|
240677
|
-
|
|
240678
|
-
|
|
240679
|
-
|
|
240680
|
-
|
|
240681
|
-
|
|
240682
|
-
|
|
240683
|
-
|
|
240684
|
-
|
|
240685
|
-
|
|
240686
|
-
|
|
240687
|
-
|
|
240688
|
-
|
|
240689
|
-
|
|
240690
|
-
|
|
240691
|
-
|
|
240692
|
-
|
|
240693
|
-
|
|
240694
|
-
|
|
240695
|
-
|
|
240696
|
-
|
|
240697
|
-
|
|
240698
|
-
|
|
240699
|
-
|
|
240700
|
-
|
|
240701
|
-
|
|
240702
|
-
|
|
240703
|
-
|
|
240704
|
-
|
|
240705
|
-
|
|
240706
|
-
|
|
240707
|
-
|
|
240775
|
+
};
|
|
240776
|
+
function nowIso$3() {
|
|
240777
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
240778
|
+
}
|
|
240779
|
+
function toForgeTab(input) {
|
|
240780
|
+
return {
|
|
240781
|
+
tabId: BrowserTabId.make(input.tab.id),
|
|
240782
|
+
restoreKey: input.tab.id,
|
|
240783
|
+
...input.tab.title ? { title: input.tab.title } : {},
|
|
240784
|
+
url: input.tab.url ?? DEFAULT_URL$1,
|
|
240785
|
+
isActive: input.tab.isActive,
|
|
240786
|
+
canGoBack: input.tab.canGoBack ?? false,
|
|
240787
|
+
canGoForward: input.tab.canGoForward ?? false,
|
|
240788
|
+
renderMode: DEFAULT_RENDER_MODE,
|
|
240789
|
+
status: "live",
|
|
240790
|
+
...input.tab.navigationStatus !== void 0 ? { navigationStatus: input.tab.navigationStatus } : {},
|
|
240791
|
+
...input.tab.requestedUrl !== void 0 ? { requestedUrl: input.tab.requestedUrl } : {},
|
|
240792
|
+
...input.tab.navigationError !== void 0 ? { navigationError: input.tab.navigationError } : {},
|
|
240793
|
+
updatedAt: input.updatedAt
|
|
240794
|
+
};
|
|
240795
|
+
}
|
|
240796
|
+
function toForgeSession(input) {
|
|
240797
|
+
return {
|
|
240798
|
+
browserId: BrowserId.make(input.snapshot.browserId),
|
|
240799
|
+
threadId: input.threadId,
|
|
240800
|
+
environmentId: EnvironmentId.make(input.environmentId ?? DEFAULT_BROWSER_ENVIRONMENT_ID),
|
|
240801
|
+
userId: BrowserUserId.make(input.userId ?? "local-user"),
|
|
240802
|
+
agentId: input.agentId ?? null,
|
|
240803
|
+
ownerRole: input.ownerRole ?? "lead",
|
|
240804
|
+
name: input.snapshot.name,
|
|
240805
|
+
visibility: input.snapshot.visibility,
|
|
240806
|
+
viewport: input.snapshot.viewport,
|
|
240807
|
+
display: input.display ?? DEFAULT_AGENT_BROWSER_DISPLAY,
|
|
240808
|
+
sessionId: BrowserSessionId.make(input.snapshot.sessionId),
|
|
240809
|
+
...input.controlState ? {
|
|
240810
|
+
sessionEpoch: input.controlState.sessionEpoch,
|
|
240811
|
+
lastAppliedControlSequence: input.controlState.lastAppliedControlSequence
|
|
240812
|
+
} : {},
|
|
240813
|
+
activeTabId: input.snapshot.activeTabId ? BrowserTabId.make(input.snapshot.activeTabId) : null,
|
|
240814
|
+
tabs: input.snapshot.tabs.map((tab) => toForgeTab({
|
|
240815
|
+
tab,
|
|
240816
|
+
updatedAt: input.snapshot.updatedAt
|
|
240817
|
+
})),
|
|
240818
|
+
updatedAt: input.snapshot.updatedAt
|
|
240819
|
+
};
|
|
240820
|
+
}
|
|
240821
|
+
function toStreamEvent(input) {
|
|
240822
|
+
const base = {
|
|
240823
|
+
eventId: EventId.make(crypto.randomUUID()),
|
|
240824
|
+
browserId: input.browserId,
|
|
240825
|
+
threadId: input.threadId,
|
|
240826
|
+
environmentId: EnvironmentId.make(input.environmentId),
|
|
240827
|
+
userId: BrowserUserId.make(input.userId),
|
|
240828
|
+
agentId: input.agentId,
|
|
240829
|
+
...input.controlState ? { sessionEpoch: input.controlState.sessionEpoch } : {},
|
|
240830
|
+
createdAt: nowIso$3()
|
|
240831
|
+
};
|
|
240832
|
+
switch (input.event.type) {
|
|
240833
|
+
case "session.started": return {
|
|
240834
|
+
...base,
|
|
240835
|
+
type: "session.started",
|
|
240836
|
+
payload: { snapshot: toForgeSession({
|
|
240837
|
+
snapshot: input.event.payload.snapshot,
|
|
240838
|
+
threadId: input.threadId,
|
|
240839
|
+
environmentId: input.environmentId,
|
|
240840
|
+
userId: input.userId,
|
|
240841
|
+
agentId: input.agentId,
|
|
240842
|
+
ownerRole: input.ownerRole,
|
|
240843
|
+
display: input.display,
|
|
240844
|
+
controlState: input.controlState
|
|
240845
|
+
}) }
|
|
240846
|
+
};
|
|
240847
|
+
case "session.updated": return {
|
|
240848
|
+
...base,
|
|
240849
|
+
type: input.event.type,
|
|
240850
|
+
payload: { snapshot: toForgeSession({
|
|
240851
|
+
snapshot: input.event.payload.snapshot,
|
|
240852
|
+
threadId: input.threadId,
|
|
240853
|
+
environmentId: input.environmentId,
|
|
240854
|
+
userId: input.userId,
|
|
240855
|
+
agentId: input.agentId,
|
|
240856
|
+
ownerRole: input.ownerRole,
|
|
240857
|
+
display: input.display,
|
|
240858
|
+
controlState: input.controlState
|
|
240859
|
+
}) }
|
|
240860
|
+
};
|
|
240861
|
+
case "session.closed": return {
|
|
240862
|
+
...base,
|
|
240863
|
+
type: "session.closed",
|
|
240864
|
+
payload: { sessionId: BrowserSessionId.make(input.event.payload.sessionId) }
|
|
240865
|
+
};
|
|
240866
|
+
case "tab.closed": return {
|
|
240867
|
+
...base,
|
|
240868
|
+
type: "tab.closed",
|
|
240869
|
+
payload: {
|
|
240870
|
+
sessionId: BrowserSessionId.make(input.event.sessionId),
|
|
240871
|
+
tabId: BrowserTabId.make(input.event.payload.tabId)
|
|
240708
240872
|
}
|
|
240709
|
-
|
|
240710
|
-
|
|
240711
|
-
|
|
240712
|
-
|
|
240713
|
-
|
|
240714
|
-
|
|
240715
|
-
|
|
240716
|
-
|
|
240717
|
-
|
|
240718
|
-
|
|
240719
|
-
|
|
240720
|
-
|
|
240721
|
-
let current = node;
|
|
240722
|
-
while (current && current !== document.body && current !== document.documentElement && parts.length < 5) {
|
|
240723
|
-
let part = current.tagName.toLowerCase();
|
|
240724
|
-
const name = current.getAttribute("name");
|
|
240725
|
-
if (name) part += `[name="${cssString(name)}"]`;
|
|
240726
|
-
else {
|
|
240727
|
-
const className = Array.from(current.classList).find(Boolean);
|
|
240728
|
-
if (className) part += `.${cssEscape(className)}`;
|
|
240729
|
-
}
|
|
240730
|
-
const parent = current.parentElement;
|
|
240731
|
-
if (parent) {
|
|
240732
|
-
const siblings = Array.from(parent.children).filter((sibling) => sibling.tagName === current?.tagName);
|
|
240733
|
-
if (siblings.length > 1) part += `:nth-of-type(${siblings.indexOf(current) + 1})`;
|
|
240734
|
-
}
|
|
240735
|
-
parts.unshift(part);
|
|
240736
|
-
const candidate = parts.join(" > ");
|
|
240737
|
-
if (document.querySelectorAll(candidate).length === 1) {
|
|
240738
|
-
candidates.push(candidate);
|
|
240739
|
-
break;
|
|
240740
|
-
}
|
|
240741
|
-
current = parent;
|
|
240742
|
-
}
|
|
240743
|
-
const fallback = parts.join(" > ") || node.tagName.toLowerCase();
|
|
240744
|
-
candidates.push(fallback);
|
|
240745
|
-
return Array.from(new Set(candidates));
|
|
240873
|
+
};
|
|
240874
|
+
case "snapshot.ready": return {
|
|
240875
|
+
...base,
|
|
240876
|
+
type: "snapshot.ready",
|
|
240877
|
+
payload: {
|
|
240878
|
+
sessionId: BrowserSessionId.make(input.event.sessionId),
|
|
240879
|
+
tabId: BrowserTabId.make(input.event.payload.tabId),
|
|
240880
|
+
captureId: BrowserCaptureId.make(input.event.payload.captureId ?? crypto.randomUUID()),
|
|
240881
|
+
capturedAt: input.event.payload.capturedAt,
|
|
240882
|
+
url: input.event.payload.url ?? DEFAULT_URL$1,
|
|
240883
|
+
viewport: input.event.payload.viewport,
|
|
240884
|
+
content: input.event.payload.content
|
|
240746
240885
|
}
|
|
240747
|
-
|
|
240748
|
-
|
|
240886
|
+
};
|
|
240887
|
+
case "screenshot.ready": return {
|
|
240888
|
+
...base,
|
|
240889
|
+
type: "screenshot.ready",
|
|
240890
|
+
payload: {
|
|
240891
|
+
sessionId: BrowserSessionId.make(input.event.sessionId),
|
|
240892
|
+
tabId: BrowserTabId.make(input.event.payload.tabId),
|
|
240893
|
+
captureId: BrowserCaptureId.make(input.event.payload.captureId ?? crypto.randomUUID()),
|
|
240894
|
+
capturedAt: input.event.payload.capturedAt,
|
|
240895
|
+
url: input.event.payload.url ?? DEFAULT_URL$1,
|
|
240896
|
+
viewport: input.event.payload.viewport,
|
|
240897
|
+
imageDataUrl: input.event.payload.imageDataUrl
|
|
240749
240898
|
}
|
|
240750
|
-
|
|
240751
|
-
|
|
240899
|
+
};
|
|
240900
|
+
case "console.logs.ready": return {
|
|
240901
|
+
...base,
|
|
240902
|
+
type: "console.logs.ready",
|
|
240903
|
+
payload: {
|
|
240904
|
+
sessionId: BrowserSessionId.make(input.event.sessionId),
|
|
240905
|
+
tabId: BrowserTabId.make(input.event.payload.tabId),
|
|
240906
|
+
logs: input.event.payload.logs.map((log) => ({
|
|
240907
|
+
level: log.level === "warn" ? "warning" : log.level === "log" ? "info" : log.level,
|
|
240908
|
+
message: log.message,
|
|
240909
|
+
createdAt: log.timestamp
|
|
240910
|
+
}))
|
|
240752
240911
|
}
|
|
240753
|
-
}
|
|
240754
|
-
|
|
240755
|
-
|
|
240756
|
-
|
|
240757
|
-
|
|
240758
|
-
|
|
240759
|
-
|
|
240760
|
-
|
|
240761
|
-
|
|
240762
|
-
|
|
240763
|
-
|
|
240764
|
-
|
|
240765
|
-
|
|
240766
|
-
|
|
240767
|
-
const locator = this.locatorFor(action);
|
|
240768
|
-
await assertSingle(locator.count(), locatorActionLabel(action));
|
|
240769
|
-
await locator.dblclick(timeoutOptions(action.timeoutMs));
|
|
240770
|
-
}
|
|
240771
|
-
async fill(action, value) {
|
|
240772
|
-
const locator = this.locatorFor(action);
|
|
240773
|
-
await assertSingle(locator.count(), locatorActionLabel(action));
|
|
240774
|
-
await locator.fill(value, timeoutOptions(action.timeoutMs));
|
|
240775
|
-
}
|
|
240776
|
-
async type(action, value) {
|
|
240777
|
-
const locator = this.locatorFor(action);
|
|
240778
|
-
await assertSingle(locator.count(), locatorActionLabel(action));
|
|
240779
|
-
await locator.pressSequentially(value, timeoutOptions(action.timeoutMs));
|
|
240780
|
-
}
|
|
240781
|
-
async press(action, key) {
|
|
240782
|
-
const locator = this.locatorFor(action);
|
|
240783
|
-
await assertSingle(locator.count(), locatorActionLabel(action));
|
|
240784
|
-
await locator.press(key, timeoutOptions(action.timeoutMs));
|
|
240785
|
-
}
|
|
240786
|
-
async select(action, value) {
|
|
240787
|
-
const locator = this.locatorFor(action);
|
|
240788
|
-
await assertSingle(locator.count(), locatorActionLabel(action));
|
|
240789
|
-
await locator.selectOption(value, timeoutOptions(action.timeoutMs));
|
|
240790
|
-
}
|
|
240791
|
-
async check(action, checked) {
|
|
240792
|
-
const locator = this.locatorFor(action);
|
|
240793
|
-
await assertSingle(locator.count(), locatorActionLabel(action));
|
|
240794
|
-
await locator.setChecked(checked, timeoutOptions(action.timeoutMs));
|
|
240795
|
-
}
|
|
240796
|
-
async text(action) {
|
|
240797
|
-
const locator = this.locatorFor(action);
|
|
240798
|
-
await assertSingle(locator.count(), locatorActionLabel(action));
|
|
240799
|
-
return locator.textContent(timeoutOptions(action.timeoutMs));
|
|
240800
|
-
}
|
|
240801
|
-
async innerText(action) {
|
|
240802
|
-
const locator = this.locatorFor(action);
|
|
240803
|
-
await assertSingle(locator.count(), locatorActionLabel(action));
|
|
240804
|
-
return locator.innerText(timeoutOptions(action.timeoutMs));
|
|
240805
|
-
}
|
|
240806
|
-
async allTextContents(action) {
|
|
240807
|
-
return this.locatorFor(action).allTextContents();
|
|
240808
|
-
}
|
|
240809
|
-
async attribute(action, name) {
|
|
240810
|
-
const locator = this.locatorFor(action);
|
|
240811
|
-
await assertSingle(locator.count(), locatorActionLabel(action));
|
|
240812
|
-
return locator.getAttribute(name, timeoutOptions(action.timeoutMs));
|
|
240813
|
-
}
|
|
240814
|
-
async isVisible(action) {
|
|
240815
|
-
const locator = this.locatorFor(action);
|
|
240816
|
-
await assertSingle(locator.count(), locatorActionLabel(action));
|
|
240817
|
-
return locator.isVisible(timeoutOptions(action.timeoutMs));
|
|
240818
|
-
}
|
|
240819
|
-
async isEnabled(action) {
|
|
240820
|
-
const locator = this.locatorFor(action);
|
|
240821
|
-
await assertSingle(locator.count(), locatorActionLabel(action));
|
|
240822
|
-
return locator.isEnabled(timeoutOptions(action.timeoutMs));
|
|
240823
|
-
}
|
|
240824
|
-
async locatorCount(action) {
|
|
240825
|
-
return this.locatorFor(action).count();
|
|
240826
|
-
}
|
|
240827
|
-
async waitForLocator(action, state) {
|
|
240828
|
-
await this.locatorFor(action).waitFor({
|
|
240829
|
-
...state === void 0 ? {} : { state },
|
|
240830
|
-
...timeoutOptions(action.timeoutMs)
|
|
240831
|
-
});
|
|
240832
|
-
}
|
|
240833
|
-
async cuaMove(point) {
|
|
240834
|
-
await this.page.mouse.move(point.x, point.y);
|
|
240835
|
-
}
|
|
240836
|
-
async cuaClick(point) {
|
|
240837
|
-
await this.page.mouse.click(point.x, point.y, { button: mouseButton(point.button) });
|
|
240838
|
-
}
|
|
240839
|
-
async cuaDoubleClick(point) {
|
|
240840
|
-
await this.page.mouse.dblclick(point.x, point.y);
|
|
240841
|
-
}
|
|
240842
|
-
async cuaDrag(path) {
|
|
240843
|
-
const [start, ...rest] = path;
|
|
240844
|
-
if (!start) return;
|
|
240845
|
-
await this.page.mouse.move(start.x, start.y);
|
|
240846
|
-
await this.page.mouse.down();
|
|
240847
|
-
for (const point of rest) await this.page.mouse.move(point.x, point.y);
|
|
240848
|
-
await this.page.mouse.up();
|
|
240849
|
-
}
|
|
240850
|
-
async cuaScroll(input) {
|
|
240851
|
-
await this.page.mouse.move(input.x, input.y);
|
|
240852
|
-
await this.page.mouse.wheel(input.scrollX, input.scrollY);
|
|
240853
|
-
}
|
|
240854
|
-
async cuaType(text) {
|
|
240855
|
-
await this.page.keyboard.type(text);
|
|
240856
|
-
}
|
|
240857
|
-
async cuaKeypress(keys) {
|
|
240858
|
-
await this.page.keyboard.press(keys.join("+"));
|
|
240859
|
-
}
|
|
240860
|
-
async visibleDom() {
|
|
240861
|
-
return this.page.evaluate(`(() => {
|
|
240862
|
-
let nextId = Number(document.documentElement.getAttribute("data-agent-browser-next-node-id") || "1");
|
|
240863
|
-
function walk(element) {
|
|
240864
|
-
const rect = element.getBoundingClientRect();
|
|
240865
|
-
const style = window.getComputedStyle(element);
|
|
240866
|
-
if (rect.width <= 0 || rect.height <= 0 || style.display === "none" || style.visibility === "hidden") {
|
|
240867
|
-
return undefined;
|
|
240868
|
-
}
|
|
240869
|
-
let nodeId = element.getAttribute("data-agent-browser-node-id");
|
|
240870
|
-
if (!nodeId) {
|
|
240871
|
-
nodeId = String(nextId++);
|
|
240872
|
-
element.setAttribute("data-agent-browser-node-id", nodeId);
|
|
240873
|
-
}
|
|
240874
|
-
const tagName = element.tagName.toLowerCase();
|
|
240875
|
-
const visibleText = (element.textContent ?? "").replace(/\s+/g, " ").trim().slice(0, 120) || undefined;
|
|
240876
|
-
const role = element.getAttribute("role") ?? undefined;
|
|
240877
|
-
const ariaName = element.getAttribute("aria-label") ?? undefined;
|
|
240878
|
-
const children = Array.from(element.children).map(walk).filter(Boolean);
|
|
240879
|
-
return {
|
|
240880
|
-
...(ariaName ? { ariaName } : {}),
|
|
240881
|
-
boundingBox: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
|
|
240882
|
-
...(children.length ? { children } : {}),
|
|
240883
|
-
nodeId,
|
|
240884
|
-
preview: (tagName + (ariaName ? ' "' + ariaName + '"' : "") + (visibleText ? " " + visibleText : "")).slice(0, 180),
|
|
240885
|
-
...(role ? { role } : {}),
|
|
240886
|
-
tagName,
|
|
240887
|
-
...(visibleText ? { visibleText } : {})
|
|
240888
|
-
};
|
|
240889
|
-
}
|
|
240890
|
-
document.documentElement.setAttribute("data-agent-browser-next-node-id", String(nextId));
|
|
240891
|
-
return walk(document.body) ?? { nodeId: "body", preview: "body", tagName: "body" };
|
|
240892
|
-
})()`);
|
|
240893
|
-
}
|
|
240894
|
-
async domClick(nodeId) {
|
|
240895
|
-
await this.byNodeId(nodeId).click();
|
|
240896
|
-
}
|
|
240897
|
-
async domDoubleClick(nodeId) {
|
|
240898
|
-
await this.byNodeId(nodeId).dblclick();
|
|
240899
|
-
}
|
|
240900
|
-
async domScroll(input) {
|
|
240901
|
-
if (input.nodeId) {
|
|
240902
|
-
/* v8 ignore next -- browser-executed DOM callback runs outside Node coverage */
|
|
240903
|
-
await this.byNodeId(input.nodeId).evaluate((element, delta) => element.scrollBy(delta.x, delta.y), {
|
|
240904
|
-
x: input.x,
|
|
240905
|
-
y: input.y
|
|
240906
|
-
});
|
|
240907
|
-
return;
|
|
240908
|
-
}
|
|
240909
|
-
await this.page.mouse.wheel(input.x, input.y);
|
|
240910
|
-
}
|
|
240911
|
-
async domType(text) {
|
|
240912
|
-
await this.page.keyboard.type(text);
|
|
240913
|
-
}
|
|
240914
|
-
async domKeypress(keys) {
|
|
240915
|
-
await this.page.keyboard.press(keys.join("+"));
|
|
240916
|
-
}
|
|
240917
|
-
async clipboardReadText() {
|
|
240918
|
-
/* v8 ignore next -- browser-executed callback runs outside Node coverage */
|
|
240919
|
-
return this.page.evaluate(() => navigator.clipboard.readText());
|
|
240920
|
-
}
|
|
240921
|
-
async clipboardWriteText(text) {
|
|
240922
|
-
/* v8 ignore next -- browser-executed callback runs outside Node coverage */
|
|
240923
|
-
await this.page.evaluate((value) => navigator.clipboard.writeText(value), text);
|
|
240924
|
-
}
|
|
240925
|
-
async logs(limit = 100) {
|
|
240926
|
-
return this.logEntries.slice(-limit);
|
|
240912
|
+
};
|
|
240913
|
+
case "action.failed": return {
|
|
240914
|
+
...base,
|
|
240915
|
+
type: "action.failed",
|
|
240916
|
+
payload: {
|
|
240917
|
+
commandType: input.event.payload.commandType,
|
|
240918
|
+
message: input.event.payload.message,
|
|
240919
|
+
...input.event.payload.tabId ? { tabId: BrowserTabId.make(input.event.payload.tabId) } : {}
|
|
240920
|
+
}
|
|
240921
|
+
};
|
|
240922
|
+
case "dom.visible.ready":
|
|
240923
|
+
case "element.info.ready":
|
|
240924
|
+
case "element.screenshot.ready":
|
|
240925
|
+
case "clipboard.text.ready": return null;
|
|
240927
240926
|
}
|
|
240928
|
-
|
|
240929
|
-
|
|
240927
|
+
}
|
|
240928
|
+
function toCommandResult(input) {
|
|
240929
|
+
return {
|
|
240930
|
+
ok: input.result.ok,
|
|
240931
|
+
snapshot: toForgeSession({
|
|
240932
|
+
snapshot: input.result.snapshot,
|
|
240933
|
+
threadId: input.command.threadId,
|
|
240934
|
+
environmentId: input.command.environmentId,
|
|
240935
|
+
userId: input.command.userId,
|
|
240936
|
+
display: input.display,
|
|
240937
|
+
controlState: input.controlState
|
|
240938
|
+
}),
|
|
240939
|
+
sessionEpoch: input.controlState.sessionEpoch,
|
|
240940
|
+
acknowledgedControlSequence: input.acknowledgedControlSequence,
|
|
240941
|
+
...input.result.detail ? { detail: input.result.detail } : {},
|
|
240942
|
+
...input.latestCapture ? { latestCapture: input.latestCapture } : {}
|
|
240943
|
+
};
|
|
240944
|
+
}
|
|
240945
|
+
function toCommandLatestCapture(input) {
|
|
240946
|
+
const tabId = commandCaptureTabId(input.command.command);
|
|
240947
|
+
if (tabId === void 0 || input.event.type !== "snapshot.ready" && input.event.type !== "screenshot.ready" || input.event.sessionId !== BrowserSessionId.make(input.command.threadId) || input.event.payload.tabId !== tabId) return;
|
|
240948
|
+
const mapped = toStreamEvent({
|
|
240949
|
+
event: input.event,
|
|
240950
|
+
threadId: input.command.threadId,
|
|
240951
|
+
browserId: input.browserId,
|
|
240952
|
+
environmentId: input.command.environmentId ?? DEFAULT_BROWSER_ENVIRONMENT_ID,
|
|
240953
|
+
userId: input.command.userId ?? "local-user",
|
|
240954
|
+
agentId: null,
|
|
240955
|
+
ownerRole: "lead",
|
|
240956
|
+
display: input.display,
|
|
240957
|
+
controlState: input.controlState
|
|
240958
|
+
});
|
|
240959
|
+
return mapped?.type === "snapshot.ready" || mapped?.type === "screenshot.ready" ? mapped : void 0;
|
|
240960
|
+
}
|
|
240961
|
+
//#endregion
|
|
240962
|
+
//#region src/browser/AgentBrowserHostBridgeShared.ts
|
|
240963
|
+
const DEFAULT_AGENT_BROWSER_ID = "embedded";
|
|
240964
|
+
const DEFAULT_URL = "about:blank";
|
|
240965
|
+
const isBrowserError = is$1(BrowserError);
|
|
240966
|
+
function nowIso$2() {
|
|
240967
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
240968
|
+
}
|
|
240969
|
+
function sessionIdForThread(threadId) {
|
|
240970
|
+
return BrowserSessionId.make(threadId);
|
|
240971
|
+
}
|
|
240972
|
+
function browserIdFrom(input) {
|
|
240973
|
+
return BrowserId.make(input.browserId ?? "embedded");
|
|
240974
|
+
}
|
|
240975
|
+
function toBrowserError(error, fallback) {
|
|
240976
|
+
console.warn("[browser] Agent Browser host bridge operation failed", {
|
|
240977
|
+
operation: fallback,
|
|
240978
|
+
error
|
|
240979
|
+
});
|
|
240980
|
+
if (isBrowserError(error)) return error;
|
|
240981
|
+
if (error instanceof Error && error.message.trim().length > 0) return new BrowserError({ detail: error.message.trim() });
|
|
240982
|
+
return new BrowserError({ detail: fallback });
|
|
240983
|
+
}
|
|
240984
|
+
//#endregion
|
|
240985
|
+
//#region src/browser/AgentBrowserHostBridgeTimeouts.ts
|
|
240986
|
+
const DEFAULT_AGENT_BROWSER_CONTROL_COMMAND_TIMEOUT_MS = 1500;
|
|
240987
|
+
const DEFAULT_AGENT_BROWSER_FAST_COMMAND_TIMEOUT_MS = 1e4;
|
|
240988
|
+
const DEFAULT_AGENT_BROWSER_NAVIGATION_COMMAND_TIMEOUT_MS = 9e4;
|
|
240989
|
+
const DEFAULT_AGENT_BROWSER_CAPTURE_COMMAND_TIMEOUT_MS = 9e4;
|
|
240990
|
+
const DEFAULT_AGENT_BROWSER_BRIDGE_OPEN_TIMEOUT_MS = 9e4;
|
|
240991
|
+
const DEFAULT_AGENT_BROWSER_BRIDGE_CLOSE_TIMEOUT_MS = 3e4;
|
|
240992
|
+
const DEFAULT_AGENT_BROWSER_PLAYWRIGHT_BACKEND_CLOSE_TIMEOUT_MS = 1e4;
|
|
240993
|
+
function positiveTimeoutMs(value) {
|
|
240994
|
+
if (!value) return;
|
|
240995
|
+
const parsed = Number.parseInt(value, 10);
|
|
240996
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : void 0;
|
|
240997
|
+
}
|
|
240998
|
+
function agentBrowserCommandTimeoutMs(command, env = process.env) {
|
|
240999
|
+
const explicitCommandTimeout = positiveTimeoutMs(env.FORGE_AGENT_BROWSER_BRIDGE_COMMAND_TIMEOUT_MS);
|
|
241000
|
+
if (explicitCommandTimeout !== void 0) return explicitCommandTimeout;
|
|
241001
|
+
if (isAgentBrowserBoundedControlCommand(command)) return positiveTimeoutMs(env.FORGE_AGENT_BROWSER_BRIDGE_CONTROL_TIMEOUT_MS) ?? DEFAULT_AGENT_BROWSER_CONTROL_COMMAND_TIMEOUT_MS;
|
|
241002
|
+
switch (command.type) {
|
|
241003
|
+
case "screenshot":
|
|
241004
|
+
case "snapshot": return positiveTimeoutMs(env.FORGE_AGENT_BROWSER_BRIDGE_CAPTURE_TIMEOUT_MS) ?? DEFAULT_AGENT_BROWSER_CAPTURE_COMMAND_TIMEOUT_MS;
|
|
241005
|
+
case "newTab":
|
|
241006
|
+
case "navigate":
|
|
241007
|
+
case "goBack":
|
|
241008
|
+
case "goForward":
|
|
241009
|
+
case "reload": return DEFAULT_AGENT_BROWSER_NAVIGATION_COMMAND_TIMEOUT_MS;
|
|
241010
|
+
default: return DEFAULT_AGENT_BROWSER_FAST_COMMAND_TIMEOUT_MS;
|
|
240930
241011
|
}
|
|
240931
|
-
|
|
240932
|
-
|
|
241012
|
+
}
|
|
241013
|
+
function agentBrowserCloseTimeoutMs(env = process.env) {
|
|
241014
|
+
return positiveTimeoutMs(env.FORGE_AGENT_BROWSER_BRIDGE_CLOSE_TIMEOUT_MS) ?? DEFAULT_AGENT_BROWSER_BRIDGE_CLOSE_TIMEOUT_MS;
|
|
241015
|
+
}
|
|
241016
|
+
function agentBrowserOpenTimeoutMs(env = process.env) {
|
|
241017
|
+
return positiveTimeoutMs(env.FORGE_AGENT_BROWSER_BRIDGE_OPEN_TIMEOUT_MS) ?? DEFAULT_AGENT_BROWSER_BRIDGE_OPEN_TIMEOUT_MS;
|
|
241018
|
+
}
|
|
241019
|
+
function agentBrowserFastTimeoutMs(env = process.env) {
|
|
241020
|
+
return positiveTimeoutMs(env.FORGE_AGENT_BROWSER_BRIDGE_COMMAND_TIMEOUT_MS) ?? DEFAULT_AGENT_BROWSER_FAST_COMMAND_TIMEOUT_MS;
|
|
241021
|
+
}
|
|
241022
|
+
function agentBrowserPlaywrightBackendCloseTimeoutMs(env = process.env) {
|
|
241023
|
+
return positiveTimeoutMs(env.FORGE_AGENT_BROWSER_PLAYWRIGHT_BACKEND_CLOSE_TIMEOUT_MS) ?? DEFAULT_AGENT_BROWSER_PLAYWRIGHT_BACKEND_CLOSE_TIMEOUT_MS;
|
|
241024
|
+
}
|
|
241025
|
+
async function withAgentBrowserBridgeTimeout(label, promise, timeoutMs) {
|
|
241026
|
+
let timeout;
|
|
241027
|
+
try {
|
|
241028
|
+
return await Promise.race([promise, new Promise((_, reject) => {
|
|
241029
|
+
timeout = setTimeout(() => {
|
|
241030
|
+
reject(/* @__PURE__ */ new Error(`${label} timed out after ${Math.ceil(timeoutMs / 1e3)}s.`));
|
|
241031
|
+
}, timeoutMs);
|
|
241032
|
+
})]);
|
|
241033
|
+
} finally {
|
|
241034
|
+
if (timeout !== void 0) clearTimeout(timeout);
|
|
240933
241035
|
}
|
|
240934
|
-
};
|
|
240935
|
-
function locatorActionLabel(action) {
|
|
240936
|
-
/* v8 ignore next -- valid public calls always supply selector or query; malformed calls are rejected downstream. */
|
|
240937
|
-
return action.selector ?? action.query?.kind ?? "locator";
|
|
240938
241036
|
}
|
|
240939
|
-
function
|
|
240940
|
-
|
|
240941
|
-
|
|
240942
|
-
|
|
241037
|
+
async function openAgentBrowserSessionReady(bridge, input) {
|
|
241038
|
+
let resolveStarted;
|
|
241039
|
+
const started = new Promise((resolve) => {
|
|
241040
|
+
resolveStarted = resolve;
|
|
241041
|
+
});
|
|
241042
|
+
const unsubscribe = await bridge.subscribe({
|
|
241043
|
+
sessionId: input.sessionId,
|
|
241044
|
+
...input.browserId ? { browserId: input.browserId } : {}
|
|
241045
|
+
}, (event) => {
|
|
241046
|
+
if (event.type === "session.started" && event.sessionId === input.sessionId) resolveStarted(event.payload.snapshot);
|
|
241047
|
+
});
|
|
241048
|
+
try {
|
|
241049
|
+
return await Promise.race([bridge.openSession(input), started]);
|
|
241050
|
+
} finally {
|
|
241051
|
+
unsubscribe();
|
|
241052
|
+
}
|
|
240943
241053
|
}
|
|
240944
|
-
function
|
|
240945
|
-
|
|
241054
|
+
function isAgentBrowserBridgeTimeoutError(error) {
|
|
241055
|
+
const message = [
|
|
241056
|
+
isBrowserError(error) ? error.detail : void 0,
|
|
241057
|
+
error instanceof Error ? error.message : void 0,
|
|
241058
|
+
error instanceof Error ? error.stack : void 0,
|
|
241059
|
+
typeof error === "string" ? error : void 0,
|
|
241060
|
+
(() => {
|
|
241061
|
+
try {
|
|
241062
|
+
return JSON.stringify(error);
|
|
241063
|
+
} catch {
|
|
241064
|
+
return;
|
|
241065
|
+
}
|
|
241066
|
+
})(),
|
|
241067
|
+
String(error)
|
|
241068
|
+
].filter((part) => typeof part === "string" && part.length > 0).join("\n");
|
|
241069
|
+
return /timed out after \d+s\./.test(message);
|
|
240946
241070
|
}
|
|
240947
|
-
function
|
|
240948
|
-
|
|
241071
|
+
async function closeAgentBrowserBackendWithTimeout(closeBackend, env = process.env) {
|
|
241072
|
+
try {
|
|
241073
|
+
await withAgentBrowserBridgeTimeout("Agent Browser Playwright backend close", closeBackend(), agentBrowserPlaywrightBackendCloseTimeoutMs(env));
|
|
241074
|
+
} catch (error) {
|
|
241075
|
+
if (!isAgentBrowserBridgeTimeoutError(error)) throw error;
|
|
241076
|
+
console.warn("[browser] Agent Browser Playwright backend close timed out; continuing", { timeoutMs: agentBrowserPlaywrightBackendCloseTimeoutMs(env) });
|
|
241077
|
+
}
|
|
240949
241078
|
}
|
|
240950
|
-
|
|
241079
|
+
//#endregion
|
|
241080
|
+
//#region src/browser/browserControlProtocol.ts
|
|
241081
|
+
function resolveBrowserControlDecision(state, metadata) {
|
|
241082
|
+
const hasSessionEpoch = metadata.sessionEpoch !== void 0;
|
|
241083
|
+
const hasControlSequence = metadata.controlSequence !== void 0;
|
|
241084
|
+
const expectedControlSequence = state.lastAppliedControlSequence + 1;
|
|
241085
|
+
if (!hasSessionEpoch && !hasControlSequence) return {
|
|
241086
|
+
type: "apply",
|
|
241087
|
+
controlSequence: expectedControlSequence
|
|
241088
|
+
};
|
|
241089
|
+
if (!hasSessionEpoch || !hasControlSequence) return {
|
|
241090
|
+
type: "reject",
|
|
241091
|
+
reason: "incomplete_metadata",
|
|
241092
|
+
expectedSessionEpoch: state.sessionEpoch,
|
|
241093
|
+
expectedControlSequence,
|
|
241094
|
+
acknowledgedControlSequence: state.lastAppliedControlSequence
|
|
241095
|
+
};
|
|
241096
|
+
if (metadata.sessionEpoch !== state.sessionEpoch) return {
|
|
241097
|
+
type: "reject",
|
|
241098
|
+
reason: "stale_epoch",
|
|
241099
|
+
expectedSessionEpoch: state.sessionEpoch,
|
|
241100
|
+
acknowledgedControlSequence: state.lastAppliedControlSequence
|
|
241101
|
+
};
|
|
241102
|
+
if (metadata.controlSequence <= state.lastAppliedControlSequence) return {
|
|
241103
|
+
type: "duplicate",
|
|
241104
|
+
acknowledgedControlSequence: state.lastAppliedControlSequence
|
|
241105
|
+
};
|
|
241106
|
+
if (metadata.controlSequence !== expectedControlSequence) return {
|
|
241107
|
+
type: "reject",
|
|
241108
|
+
reason: "sequence_gap",
|
|
241109
|
+
expectedControlSequence,
|
|
241110
|
+
acknowledgedControlSequence: state.lastAppliedControlSequence
|
|
241111
|
+
};
|
|
240951
241112
|
return {
|
|
240952
|
-
|
|
240953
|
-
|
|
240954
|
-
saveAs: (path) => download.saveAs(path),
|
|
240955
|
-
suggestedFilename: () => download.suggestedFilename(),
|
|
240956
|
-
url: () => download.url()
|
|
241113
|
+
type: "apply",
|
|
241114
|
+
controlSequence: metadata.controlSequence
|
|
240957
241115
|
};
|
|
240958
241116
|
}
|
|
240959
|
-
|
|
240960
|
-
|
|
240961
|
-
|
|
241117
|
+
function browserControlRejectionDetail(decision) {
|
|
241118
|
+
switch (decision.reason) {
|
|
241119
|
+
case "stale_epoch": return `Browser session is stale. Expected epoch ${decision.expectedSessionEpoch}.`;
|
|
241120
|
+
case "sequence_gap": return `Browser control sequence is out of order. Expected ${decision.expectedControlSequence}.`;
|
|
241121
|
+
case "incomplete_metadata": return "Browser control ordering requires both sessionEpoch and controlSequence.";
|
|
241122
|
+
}
|
|
240962
241123
|
}
|
|
240963
|
-
|
|
241124
|
+
//#endregion
|
|
241125
|
+
//#region src/browser/AgentBrowserHostBridgeManagerCommands.ts
|
|
241126
|
+
async function duplicateControlResult(input) {
|
|
241127
|
+
const snapshot = await withAgentBrowserBridgeTimeout("Agent Browser duplicate command session read", input.bridge.getSession({
|
|
241128
|
+
sessionId: input.hostSessionId,
|
|
241129
|
+
...input.commandInput.browserId ? { browserId: input.commandInput.browserId } : {}
|
|
241130
|
+
}), agentBrowserFastTimeoutMs());
|
|
241131
|
+
if (!snapshot) throw new BrowserError({ detail: "Browser session not found." });
|
|
241132
|
+
const controlState = input.state.controlStateFor(input.hostSessionId);
|
|
240964
241133
|
return {
|
|
240965
|
-
|
|
240966
|
-
|
|
240967
|
-
|
|
240968
|
-
|
|
241134
|
+
ok: true,
|
|
241135
|
+
snapshot: toForgeSession({
|
|
241136
|
+
snapshot,
|
|
241137
|
+
threadId: input.commandInput.threadId,
|
|
241138
|
+
environmentId: input.commandInput.environmentId,
|
|
241139
|
+
userId: input.commandInput.userId,
|
|
241140
|
+
display: input.state.displayForSnapshot(snapshot),
|
|
241141
|
+
controlState
|
|
241142
|
+
}),
|
|
241143
|
+
sessionEpoch: controlState.sessionEpoch,
|
|
241144
|
+
acknowledgedControlSequence: input.acknowledgedControlSequence,
|
|
241145
|
+
detail: "Browser control was already applied"
|
|
241146
|
+
};
|
|
241147
|
+
}
|
|
241148
|
+
async function dispatchHostCommand(input) {
|
|
241149
|
+
try {
|
|
241150
|
+
return await withAgentBrowserBridgeTimeout(`Agent Browser command ${input.commandInput.command.type}`, input.bridge.command({
|
|
241151
|
+
...input.hostIdentity,
|
|
241152
|
+
command: toHostCommand(input.commandInput.command)
|
|
241153
|
+
}), agentBrowserCommandTimeoutMs(input.commandInput.command));
|
|
241154
|
+
} catch (error) {
|
|
241155
|
+
if (!(isAgentBrowserBridgeTimeoutError(error) && isAgentBrowserBoundedControlCommand(input.commandInput.command) && input.preCommandSnapshot !== null)) throw error;
|
|
241156
|
+
console.warn("[browser] Agent Browser control command timed out after dispatch", {
|
|
241157
|
+
commandType: input.commandInput.command.type,
|
|
241158
|
+
sessionId: input.hostIdentity.sessionId,
|
|
241159
|
+
...input.commandInput.browserId ? { browserId: input.commandInput.browserId } : {}
|
|
241160
|
+
});
|
|
241161
|
+
return {
|
|
241162
|
+
ok: true,
|
|
241163
|
+
snapshot: snapshotAfterTimedOutControl(input.preCommandSnapshot, input.commandInput.command),
|
|
241164
|
+
detail: "Agent Browser control command dispatch timed out before post-action observation completed."
|
|
241165
|
+
};
|
|
241166
|
+
}
|
|
241167
|
+
}
|
|
241168
|
+
async function executeHostCommand(input) {
|
|
241169
|
+
const captureTabId = commandCaptureTabId(input.commandInput.command);
|
|
241170
|
+
const hostIdentity = {
|
|
241171
|
+
sessionId: input.hostSessionId,
|
|
241172
|
+
...input.commandInput.browserId ? { browserId: input.commandInput.browserId } : {}
|
|
240969
241173
|
};
|
|
241174
|
+
const preCommandSnapshot = isAgentBrowserBoundedControlCommand(input.commandInput.command) ? await withAgentBrowserBridgeTimeout(`Agent Browser command ${input.commandInput.command.type} preflight session read`, input.bridge.getSession(hostIdentity), agentBrowserCommandTimeoutMs(input.commandInput.command)).catch(() => null) : null;
|
|
241175
|
+
let latestCaptureEvent;
|
|
241176
|
+
const unsubscribeCapture = captureTabId ? await input.bridge.subscribe(hostIdentity, (event) => {
|
|
241177
|
+
if ((event.type === "snapshot.ready" || event.type === "screenshot.ready") && event.sessionId === input.hostSessionId && event.payload.tabId === captureTabId) latestCaptureEvent = event;
|
|
241178
|
+
}) : void 0;
|
|
241179
|
+
let result;
|
|
241180
|
+
try {
|
|
241181
|
+
result = await dispatchHostCommand({
|
|
241182
|
+
...input,
|
|
241183
|
+
hostIdentity,
|
|
241184
|
+
preCommandSnapshot
|
|
241185
|
+
});
|
|
241186
|
+
} finally {
|
|
241187
|
+
unsubscribeCapture?.();
|
|
241188
|
+
}
|
|
241189
|
+
if (input.commandInput.command.type === "closeTab") {
|
|
241190
|
+
await runPromise(input.options?.displayPeers?.closeForSession({
|
|
241191
|
+
sessionId: input.hostSessionId,
|
|
241192
|
+
tabId: input.commandInput.command.tabId
|
|
241193
|
+
}) ?? void_$1);
|
|
241194
|
+
input.state.clearConnectedDisplay({
|
|
241195
|
+
sessionId: input.hostSessionId,
|
|
241196
|
+
tabId: input.commandInput.command.tabId
|
|
241197
|
+
});
|
|
241198
|
+
}
|
|
241199
|
+
const display = input.state.displayForSnapshot(result.snapshot);
|
|
241200
|
+
const controlState = input.state.controlStateFor(input.hostSessionId);
|
|
241201
|
+
controlState.lastAppliedControlSequence = input.controlSequence;
|
|
241202
|
+
return toCommandResult({
|
|
241203
|
+
result,
|
|
241204
|
+
command: input.commandInput,
|
|
241205
|
+
display,
|
|
241206
|
+
controlState,
|
|
241207
|
+
acknowledgedControlSequence: input.controlSequence,
|
|
241208
|
+
latestCapture: latestCaptureEvent ? toCommandLatestCapture({
|
|
241209
|
+
event: latestCaptureEvent,
|
|
241210
|
+
command: input.commandInput,
|
|
241211
|
+
browserId: BrowserId.make(result.snapshot.browserId),
|
|
241212
|
+
display,
|
|
241213
|
+
controlState
|
|
241214
|
+
}) : void 0
|
|
241215
|
+
});
|
|
240970
241216
|
}
|
|
240971
|
-
function
|
|
240972
|
-
|
|
240973
|
-
|
|
240974
|
-
|
|
241217
|
+
async function executeOrderedHostCommand(input) {
|
|
241218
|
+
const hostSessionId = sessionIdForThread(input.commandInput.threadId);
|
|
241219
|
+
return input.state.runSerializedCommand(hostSessionId, async () => {
|
|
241220
|
+
const controlDecision = resolveBrowserControlDecision(input.state.controlStateFor(hostSessionId), input.commandInput);
|
|
241221
|
+
if (controlDecision.type === "reject") throw new BrowserError({ detail: browserControlRejectionDetail(controlDecision) });
|
|
241222
|
+
if (controlDecision.type === "duplicate") return duplicateControlResult({
|
|
241223
|
+
...input,
|
|
241224
|
+
hostSessionId,
|
|
241225
|
+
acknowledgedControlSequence: controlDecision.acknowledgedControlSequence
|
|
241226
|
+
});
|
|
241227
|
+
return executeHostCommand({
|
|
241228
|
+
...input,
|
|
241229
|
+
hostSessionId,
|
|
241230
|
+
controlSequence: controlDecision.controlSequence
|
|
241231
|
+
});
|
|
241232
|
+
});
|
|
240975
241233
|
}
|
|
240976
241234
|
//#endregion
|
|
240977
|
-
//#region
|
|
240978
|
-
|
|
240979
|
-
|
|
240980
|
-
|
|
240981
|
-
|
|
240982
|
-
|
|
240983
|
-
|
|
240984
|
-
|
|
240985
|
-
|
|
240986
|
-
|
|
240987
|
-
|
|
240988
|
-
|
|
240989
|
-
|
|
240990
|
-
|
|
240991
|
-
|
|
240992
|
-
|
|
240993
|
-
|
|
241235
|
+
//#region src/browser/AgentBrowserHostBridgeManagerState.ts
|
|
241236
|
+
function resolveDisplay(snapshot, options) {
|
|
241237
|
+
if (typeof options?.display === "function") return options.display(snapshot);
|
|
241238
|
+
return options?.display ?? DEFAULT_AGENT_BROWSER_DISPLAY;
|
|
241239
|
+
}
|
|
241240
|
+
function subscriptionMatchesEvent(subscription, event) {
|
|
241241
|
+
if (subscription.input.threadId && subscription.input.threadId !== event.threadId) return false;
|
|
241242
|
+
if (subscription.input.browserId && subscription.input.browserId !== event.browserId) return false;
|
|
241243
|
+
if (subscription.input.environmentId && subscription.input.environmentId !== event.environmentId) return false;
|
|
241244
|
+
if (subscription.input.userId && subscription.input.userId !== event.userId) return false;
|
|
241245
|
+
return true;
|
|
241246
|
+
}
|
|
241247
|
+
function createAgentBrowserHostBridgeState(options) {
|
|
241248
|
+
const localSubscriptions = /* @__PURE__ */ new Set();
|
|
241249
|
+
const connectedDisplays = /* @__PURE__ */ new Map();
|
|
241250
|
+
const controlStates = /* @__PURE__ */ new Map();
|
|
241251
|
+
const commandQueues = /* @__PURE__ */ new Map();
|
|
241252
|
+
const controlStateFor = (sessionId) => {
|
|
241253
|
+
const existing = controlStates.get(sessionId);
|
|
241254
|
+
if (existing) return existing;
|
|
241255
|
+
const created = {
|
|
241256
|
+
sessionEpoch: crypto.randomUUID(),
|
|
241257
|
+
lastAppliedControlSequence: 0
|
|
241258
|
+
};
|
|
241259
|
+
controlStates.set(sessionId, created);
|
|
241260
|
+
return created;
|
|
241261
|
+
};
|
|
241262
|
+
const runSerializedCommand = async (sessionId, task) => {
|
|
241263
|
+
const previous = commandQueues.get(sessionId) ?? Promise.resolve();
|
|
241264
|
+
let releaseCurrent;
|
|
241265
|
+
const current = new Promise((resolve) => {
|
|
241266
|
+
releaseCurrent = resolve;
|
|
240994
241267
|
});
|
|
240995
|
-
|
|
240996
|
-
|
|
240997
|
-
|
|
240998
|
-
|
|
240999
|
-
|
|
241000
|
-
|
|
241001
|
-
|
|
241002
|
-
|
|
241003
|
-
* Valid key names are a single, lower or upper-case letter, i.e. "n" and "N".
|
|
241004
|
-
*/
|
|
241005
|
-
createDebug.formatters = {};
|
|
241006
|
-
/**
|
|
241007
|
-
* Selects a color for a debug namespace
|
|
241008
|
-
* @param {String} namespace The namespace string for the debug instance to be colored
|
|
241009
|
-
* @return {Number|String} An ANSI color code for the given namespace
|
|
241010
|
-
* @api private
|
|
241011
|
-
*/
|
|
241012
|
-
function selectColor(namespace) {
|
|
241013
|
-
let hash = 0;
|
|
241014
|
-
for (let i = 0; i < namespace.length; i++) {
|
|
241015
|
-
hash = (hash << 5) - hash + namespace.charCodeAt(i);
|
|
241016
|
-
hash |= 0;
|
|
241017
|
-
}
|
|
241018
|
-
return createDebug.colors[Math.abs(hash) % createDebug.colors.length];
|
|
241019
|
-
}
|
|
241020
|
-
createDebug.selectColor = selectColor;
|
|
241021
|
-
/**
|
|
241022
|
-
* Create a debugger with the given `namespace`.
|
|
241023
|
-
*
|
|
241024
|
-
* @param {String} namespace
|
|
241025
|
-
* @return {Function}
|
|
241026
|
-
* @api public
|
|
241027
|
-
*/
|
|
241028
|
-
function createDebug(namespace) {
|
|
241029
|
-
let prevTime;
|
|
241030
|
-
let enableOverride = null;
|
|
241031
|
-
let namespacesCache;
|
|
241032
|
-
let enabledCache;
|
|
241033
|
-
function debug(...args) {
|
|
241034
|
-
if (!debug.enabled) return;
|
|
241035
|
-
const self = debug;
|
|
241036
|
-
const curr = Number(/* @__PURE__ */ new Date());
|
|
241037
|
-
self.diff = curr - (prevTime || curr);
|
|
241038
|
-
self.prev = prevTime;
|
|
241039
|
-
self.curr = curr;
|
|
241040
|
-
prevTime = curr;
|
|
241041
|
-
args[0] = createDebug.coerce(args[0]);
|
|
241042
|
-
if (typeof args[0] !== "string") args.unshift("%O");
|
|
241043
|
-
let index = 0;
|
|
241044
|
-
args[0] = args[0].replace(/%([a-zA-Z%])/g, (match, format) => {
|
|
241045
|
-
if (match === "%%") return "%";
|
|
241046
|
-
index++;
|
|
241047
|
-
const formatter = createDebug.formatters[format];
|
|
241048
|
-
if (typeof formatter === "function") {
|
|
241049
|
-
const val = args[index];
|
|
241050
|
-
match = formatter.call(self, val);
|
|
241051
|
-
args.splice(index, 1);
|
|
241052
|
-
index--;
|
|
241053
|
-
}
|
|
241054
|
-
return match;
|
|
241055
|
-
});
|
|
241056
|
-
createDebug.formatArgs.call(self, args);
|
|
241057
|
-
(self.log || createDebug.log).apply(self, args);
|
|
241058
|
-
}
|
|
241059
|
-
debug.namespace = namespace;
|
|
241060
|
-
debug.useColors = createDebug.useColors();
|
|
241061
|
-
debug.color = createDebug.selectColor(namespace);
|
|
241062
|
-
debug.extend = extend;
|
|
241063
|
-
debug.destroy = createDebug.destroy;
|
|
241064
|
-
Object.defineProperty(debug, "enabled", {
|
|
241065
|
-
enumerable: true,
|
|
241066
|
-
configurable: false,
|
|
241067
|
-
get: () => {
|
|
241068
|
-
if (enableOverride !== null) return enableOverride;
|
|
241069
|
-
if (namespacesCache !== createDebug.namespaces) {
|
|
241070
|
-
namespacesCache = createDebug.namespaces;
|
|
241071
|
-
enabledCache = createDebug.enabled(namespace);
|
|
241072
|
-
}
|
|
241073
|
-
return enabledCache;
|
|
241074
|
-
},
|
|
241075
|
-
set: (v) => {
|
|
241076
|
-
enableOverride = v;
|
|
241077
|
-
}
|
|
241078
|
-
});
|
|
241079
|
-
if (typeof createDebug.init === "function") createDebug.init(debug);
|
|
241080
|
-
return debug;
|
|
241268
|
+
const queued = previous.catch(() => void 0).then(() => current);
|
|
241269
|
+
commandQueues.set(sessionId, queued);
|
|
241270
|
+
await previous.catch(() => void 0);
|
|
241271
|
+
try {
|
|
241272
|
+
return await task();
|
|
241273
|
+
} finally {
|
|
241274
|
+
releaseCurrent();
|
|
241275
|
+
if (commandQueues.get(sessionId) === queued) commandQueues.delete(sessionId);
|
|
241081
241276
|
}
|
|
241082
|
-
|
|
241083
|
-
|
|
241084
|
-
|
|
241085
|
-
|
|
241277
|
+
};
|
|
241278
|
+
const clearConnectedDisplay = (input) => {
|
|
241279
|
+
const active = connectedDisplays.get(input.sessionId);
|
|
241280
|
+
if (!active) return;
|
|
241281
|
+
if (input.tabId === void 0 || active.tabId === input.tabId) connectedDisplays.delete(input.sessionId);
|
|
241282
|
+
};
|
|
241283
|
+
const displayForSnapshot = (snapshot) => {
|
|
241284
|
+
const fallback = resolveDisplay(snapshot, options);
|
|
241285
|
+
const active = connectedDisplays.get(snapshot.sessionId);
|
|
241286
|
+
if (!active) return fallback;
|
|
241287
|
+
if (!snapshot.tabs.some((tab) => tab.id === active.tabId)) {
|
|
241288
|
+
connectedDisplays.delete(snapshot.sessionId);
|
|
241289
|
+
return fallback;
|
|
241086
241290
|
}
|
|
241087
|
-
|
|
241088
|
-
|
|
241089
|
-
|
|
241090
|
-
|
|
241091
|
-
|
|
241092
|
-
|
|
241093
|
-
|
|
241094
|
-
|
|
241095
|
-
|
|
241096
|
-
|
|
241097
|
-
|
|
241098
|
-
|
|
241099
|
-
|
|
241100
|
-
|
|
241101
|
-
else createDebug.names.push(ns);
|
|
241291
|
+
if (snapshot.activeTabId !== active.tabId) return fallback;
|
|
241292
|
+
const { reason: _reason, detail: _detail, ...fallbackWithoutDegradedReason } = fallback;
|
|
241293
|
+
return {
|
|
241294
|
+
...fallbackWithoutDegradedReason,
|
|
241295
|
+
...active.display,
|
|
241296
|
+
reconnect: active.display.reconnect ?? fallback.reconnect
|
|
241297
|
+
};
|
|
241298
|
+
};
|
|
241299
|
+
const publishLocalEvent = (event) => {
|
|
241300
|
+
for (const subscription of localSubscriptions) {
|
|
241301
|
+
if (!subscriptionMatchesEvent(subscription, event)) continue;
|
|
241302
|
+
runPromise(subscription.listener(event)).catch((error) => {
|
|
241303
|
+
console.warn("[browser] Failed to publish local browser event", { error });
|
|
241304
|
+
});
|
|
241102
241305
|
}
|
|
241103
|
-
|
|
241104
|
-
|
|
241105
|
-
|
|
241106
|
-
|
|
241107
|
-
|
|
241108
|
-
|
|
241109
|
-
|
|
241110
|
-
|
|
241111
|
-
|
|
241112
|
-
|
|
241113
|
-
|
|
241114
|
-
|
|
241306
|
+
};
|
|
241307
|
+
const publishWebRtcIceCandidate = (event) => {
|
|
241308
|
+
publishLocalEvent({
|
|
241309
|
+
eventId: EventId.make(crypto.randomUUID()),
|
|
241310
|
+
browserId: event.input.browserId ?? browserIdFrom(event.input),
|
|
241311
|
+
threadId: event.input.threadId,
|
|
241312
|
+
environmentId: event.input.environmentId ?? DEFAULT_BROWSER_ENVIRONMENT_ID,
|
|
241313
|
+
userId: event.input.userId ?? "local-user",
|
|
241314
|
+
agentId: null,
|
|
241315
|
+
createdAt: nowIso$2(),
|
|
241316
|
+
type: "display.iceCandidate",
|
|
241317
|
+
payload: {
|
|
241318
|
+
sessionId: event.input.sessionId,
|
|
241319
|
+
tabId: event.input.tabId,
|
|
241320
|
+
clientId: event.input.clientId,
|
|
241321
|
+
candidate: event.candidate
|
|
241322
|
+
}
|
|
241323
|
+
});
|
|
241324
|
+
};
|
|
241325
|
+
return {
|
|
241326
|
+
localSubscriptions,
|
|
241327
|
+
connectedDisplays,
|
|
241328
|
+
controlStates,
|
|
241329
|
+
controlStateFor,
|
|
241330
|
+
runSerializedCommand,
|
|
241331
|
+
clearConnectedDisplay,
|
|
241332
|
+
displayForSnapshot,
|
|
241333
|
+
publishWebRtcIceCandidate
|
|
241334
|
+
};
|
|
241335
|
+
}
|
|
241336
|
+
//#endregion
|
|
241337
|
+
//#region src/browser/AgentBrowserHostBridgeManager.ts
|
|
241338
|
+
function openInputForSession(input) {
|
|
241339
|
+
return {
|
|
241340
|
+
sessionId: input.hostSessionId,
|
|
241341
|
+
...input.sessionInput.browserId ? { browserId: input.sessionInput.browserId } : {},
|
|
241342
|
+
...input.initialUrl ? { initialUrl: input.initialUrl } : {},
|
|
241343
|
+
...input.sessionInput.name ? { name: input.sessionInput.name } : {},
|
|
241344
|
+
...input.sessionInput.visibility ? { visibility: input.sessionInput.visibility } : {},
|
|
241345
|
+
...input.sessionInput.viewport ? { viewport: input.sessionInput.viewport } : {}
|
|
241346
|
+
};
|
|
241347
|
+
}
|
|
241348
|
+
function forgetHostSession(input) {
|
|
241349
|
+
input.state.clearConnectedDisplay({ sessionId: input.hostSessionId });
|
|
241350
|
+
input.state.controlStates.delete(input.hostSessionId);
|
|
241351
|
+
input.options?.forgetRuntimeSession?.({ sessionId: input.hostSessionId });
|
|
241352
|
+
}
|
|
241353
|
+
async function quarantineTimedOutOpenSession(input) {
|
|
241354
|
+
console.warn("[browser] Agent Browser openSession timed out; quarantining session", {
|
|
241355
|
+
sessionId: input.hostSessionId,
|
|
241356
|
+
...input.sessionInput.browserId ? { browserId: input.sessionInput.browserId } : {}
|
|
241357
|
+
});
|
|
241358
|
+
await withAgentBrowserBridgeTimeout("Agent Browser openSession quarantine close", input.bridge.closeSession({
|
|
241359
|
+
sessionId: input.hostSessionId,
|
|
241360
|
+
...input.sessionInput.browserId ? { browserId: input.sessionInput.browserId } : {}
|
|
241361
|
+
}), agentBrowserCloseTimeoutMs()).catch(() => void 0);
|
|
241362
|
+
forgetHostSession(input);
|
|
241363
|
+
throw new Error(`${input.error instanceof Error ? input.error.message : String(input.error)} The pending browser session was quarantined and closed.`, { cause: input.error });
|
|
241364
|
+
}
|
|
241365
|
+
async function openHostBridgeSession(input) {
|
|
241366
|
+
const hostSessionId = sessionIdForThread(input.sessionInput.threadId);
|
|
241367
|
+
const initialUrl = input.sessionInput.initialUrl ? normalizeUserVisibleBrowserUrl(input.sessionInput.initialUrl) : void 0;
|
|
241368
|
+
input.options?.prepareRuntimeSession?.({
|
|
241369
|
+
sessionId: hostSessionId,
|
|
241370
|
+
initialUrl: initialUrl ?? "about:blank"
|
|
241371
|
+
});
|
|
241372
|
+
try {
|
|
241373
|
+
const snapshot = await withAgentBrowserBridgeTimeout("Agent Browser openSession", openAgentBrowserSessionReady(input.bridge, openInputForSession({
|
|
241374
|
+
sessionInput: input.sessionInput,
|
|
241375
|
+
hostSessionId,
|
|
241376
|
+
initialUrl
|
|
241377
|
+
})), agentBrowserOpenTimeoutMs());
|
|
241378
|
+
return toForgeSession({
|
|
241379
|
+
snapshot,
|
|
241380
|
+
threadId: input.sessionInput.threadId,
|
|
241381
|
+
environmentId: input.sessionInput.environmentId,
|
|
241382
|
+
userId: input.sessionInput.userId,
|
|
241383
|
+
agentId: input.sessionInput.agentId,
|
|
241384
|
+
ownerRole: input.sessionInput.ownerRole,
|
|
241385
|
+
display: input.state.displayForSnapshot(snapshot),
|
|
241386
|
+
controlState: input.state.controlStateFor(snapshot.sessionId)
|
|
241387
|
+
});
|
|
241388
|
+
} catch (error) {
|
|
241389
|
+
if (!isAgentBrowserBridgeTimeoutError(error)) {
|
|
241390
|
+
input.options?.forgetRuntimeSession?.({ sessionId: hostSessionId });
|
|
241391
|
+
throw error;
|
|
241392
|
+
}
|
|
241393
|
+
return quarantineTimedOutOpenSession({
|
|
241394
|
+
...input,
|
|
241395
|
+
error,
|
|
241396
|
+
hostSessionId
|
|
241397
|
+
});
|
|
241398
|
+
}
|
|
241399
|
+
}
|
|
241400
|
+
async function verifyTimedOutClose(input) {
|
|
241401
|
+
const remaining = await withAgentBrowserBridgeTimeout("Agent Browser closeSession verification", input.bridge.getSession(input.hostIdentity), 2e3).catch(() => void 0);
|
|
241402
|
+
console.warn(remaining === null ? "[browser] Agent Browser closeSession timed out after session removal" : "[browser] Agent Browser closeSession timed out; forgetting session", {
|
|
241403
|
+
sessionId: input.hostIdentity.sessionId,
|
|
241404
|
+
...input.sessionInput.browserId ? { browserId: input.sessionInput.browserId } : {}
|
|
241405
|
+
});
|
|
241406
|
+
}
|
|
241407
|
+
async function closeHostBridgeSession(input) {
|
|
241408
|
+
const hostSessionId = sessionIdForThread(input.sessionInput.threadId);
|
|
241409
|
+
await runPromise(input.options?.displayPeers?.closeForSession({ sessionId: hostSessionId }) ?? void_$1);
|
|
241410
|
+
const hostIdentity = {
|
|
241411
|
+
sessionId: hostSessionId,
|
|
241412
|
+
...input.sessionInput.browserId ? { browserId: input.sessionInput.browserId } : {}
|
|
241413
|
+
};
|
|
241414
|
+
try {
|
|
241415
|
+
await withAgentBrowserBridgeTimeout("Agent Browser closeSession", input.bridge.closeSession(hostIdentity), agentBrowserCloseTimeoutMs());
|
|
241416
|
+
} catch (error) {
|
|
241417
|
+
if (!isAgentBrowserBridgeTimeoutError(error)) throw error;
|
|
241418
|
+
await verifyTimedOutClose({
|
|
241419
|
+
...input,
|
|
241420
|
+
hostIdentity
|
|
241421
|
+
});
|
|
241422
|
+
}
|
|
241423
|
+
forgetHostSession({
|
|
241424
|
+
...input,
|
|
241425
|
+
hostSessionId
|
|
241426
|
+
});
|
|
241427
|
+
}
|
|
241428
|
+
function createAgentBrowserHostBridgeManager(bridge, options) {
|
|
241429
|
+
const state = createAgentBrowserHostBridgeState(options);
|
|
241430
|
+
return {
|
|
241431
|
+
openSession: (input) => tryPromise({
|
|
241432
|
+
try: () => openHostBridgeSession({
|
|
241433
|
+
bridge,
|
|
241434
|
+
options,
|
|
241435
|
+
state,
|
|
241436
|
+
sessionInput: input
|
|
241437
|
+
}),
|
|
241438
|
+
catch: (error) => toBrowserError(error, "Failed to open Agent Browser session.")
|
|
241439
|
+
}),
|
|
241440
|
+
closeSession: (input) => tryPromise({
|
|
241441
|
+
try: () => closeHostBridgeSession({
|
|
241442
|
+
bridge,
|
|
241443
|
+
options,
|
|
241444
|
+
state,
|
|
241445
|
+
sessionInput: input
|
|
241446
|
+
}),
|
|
241447
|
+
catch: (error) => toBrowserError(error, "Failed to close Agent Browser session.")
|
|
241448
|
+
}),
|
|
241449
|
+
clearBrowsingData: () => succeed({
|
|
241450
|
+
closedSessionCount: 0,
|
|
241451
|
+
deletedSessionCount: 0,
|
|
241452
|
+
deletedCaptureCount: 0
|
|
241453
|
+
}),
|
|
241454
|
+
getSession: (input) => tryPromise({
|
|
241455
|
+
try: async () => {
|
|
241456
|
+
const snapshot = await withAgentBrowserBridgeTimeout("Agent Browser getSession", bridge.getSession({
|
|
241457
|
+
sessionId: sessionIdForThread(input.threadId),
|
|
241458
|
+
...input.browserId ? { browserId: input.browserId } : {}
|
|
241459
|
+
}), agentBrowserFastTimeoutMs());
|
|
241460
|
+
return snapshot ? toForgeSession({
|
|
241461
|
+
snapshot,
|
|
241462
|
+
threadId: input.threadId,
|
|
241463
|
+
environmentId: input.environmentId,
|
|
241464
|
+
userId: input.userId,
|
|
241465
|
+
display: state.displayForSnapshot(snapshot),
|
|
241466
|
+
controlState: state.controlStateFor(snapshot.sessionId)
|
|
241467
|
+
}) : null;
|
|
241468
|
+
},
|
|
241469
|
+
catch: (error) => toBrowserError(error, "Failed to read Agent Browser session.")
|
|
241470
|
+
}),
|
|
241471
|
+
listSessions: (input) => tryPromise({
|
|
241472
|
+
try: async () => {
|
|
241473
|
+
return (await withAgentBrowserBridgeTimeout("Agent Browser listSessions", bridge.listSessions(input.browserId ? { browserId: input.browserId } : void 0), agentBrowserFastTimeoutMs())).map((snapshot) => toForgeSession({
|
|
241474
|
+
snapshot,
|
|
241475
|
+
threadId: input.threadId ?? ThreadId.make(snapshot.sessionId),
|
|
241476
|
+
environmentId: input.environmentId,
|
|
241477
|
+
userId: input.userId,
|
|
241478
|
+
display: state.displayForSnapshot(snapshot),
|
|
241479
|
+
controlState: state.controlStateFor(snapshot.sessionId)
|
|
241480
|
+
}));
|
|
241481
|
+
},
|
|
241482
|
+
catch: (error) => toBrowserError(error, "Failed to list Agent Browser sessions.")
|
|
241483
|
+
}),
|
|
241484
|
+
command: (input) => tryPromise({
|
|
241485
|
+
try: () => executeOrderedHostCommand({
|
|
241486
|
+
bridge,
|
|
241487
|
+
options,
|
|
241488
|
+
state,
|
|
241489
|
+
commandInput: input
|
|
241490
|
+
}),
|
|
241491
|
+
catch: (error) => toBrowserError(error, "Failed to run Agent Browser command.")
|
|
241492
|
+
}),
|
|
241493
|
+
attachDisplay: (input) => {
|
|
241494
|
+
const displayPeers = options?.displayPeers;
|
|
241495
|
+
if (!displayPeers) return fail(remoteWebRtcUnavailableError(input));
|
|
241496
|
+
return tryPromise({
|
|
241497
|
+
try: async () => {
|
|
241498
|
+
const iceServers = await resolveDisplayIceServers(options);
|
|
241499
|
+
if (options?.requiresWebRtcRelay === true && !hasTurnRelay(iceServers)) throw remoteWebRtcRelayRequiredError(input);
|
|
241500
|
+
const result = await runPromise(displayPeers.attach(input, {
|
|
241501
|
+
display: {
|
|
241502
|
+
transport: "remote-webrtc",
|
|
241503
|
+
status: "connecting",
|
|
241504
|
+
...input.viewport ? { viewport: input.viewport } : {},
|
|
241505
|
+
devicePixelRatio: 1,
|
|
241506
|
+
...iceServers.length > 0 ? { iceServers } : {},
|
|
241507
|
+
reconnect: {
|
|
241508
|
+
status: "ready",
|
|
241509
|
+
attempt: 0
|
|
241510
|
+
}
|
|
241511
|
+
},
|
|
241512
|
+
onIceCandidate: state.publishWebRtcIceCandidate
|
|
241513
|
+
}));
|
|
241514
|
+
state.connectedDisplays.set(input.sessionId, {
|
|
241515
|
+
tabId: input.tabId,
|
|
241516
|
+
display: result.display
|
|
241517
|
+
});
|
|
241518
|
+
return result;
|
|
241519
|
+
},
|
|
241520
|
+
catch: (error) => {
|
|
241521
|
+
if (isBrowserError(error)) return error;
|
|
241522
|
+
return toBrowserError(error, "Failed to attach WebRTC browser display.");
|
|
241523
|
+
}
|
|
241524
|
+
});
|
|
241525
|
+
},
|
|
241526
|
+
addDisplayIceCandidate: (input) => {
|
|
241527
|
+
const displayPeers = options?.displayPeers;
|
|
241528
|
+
if (!displayPeers) return fail(remoteWebRtcUnavailableError(input));
|
|
241529
|
+
return displayPeers.addIceCandidate(input);
|
|
241530
|
+
},
|
|
241531
|
+
subscribe: (input, listener) => promise(async () => {
|
|
241532
|
+
const localSubscription = {
|
|
241533
|
+
input,
|
|
241534
|
+
listener
|
|
241535
|
+
};
|
|
241536
|
+
state.localSubscriptions.add(localSubscription);
|
|
241537
|
+
const threadId = input.threadId ?? ThreadId.make("unknown-thread");
|
|
241538
|
+
const browserId = browserIdFrom(input);
|
|
241539
|
+
const environmentId = input.environmentId ?? DEFAULT_BROWSER_ENVIRONMENT_ID;
|
|
241540
|
+
const userId = input.userId ?? "local-user";
|
|
241541
|
+
const subscribeInput = {
|
|
241542
|
+
...input.threadId ? { sessionId: sessionIdForThread(input.threadId) } : {},
|
|
241543
|
+
...input.browserId ? { browserId: input.browserId } : {}
|
|
241544
|
+
};
|
|
241545
|
+
const unsubscribeBridge = await bridge.subscribe(subscribeInput, (event) => {
|
|
241546
|
+
if (event.type === "session.closed") state.clearConnectedDisplay({ sessionId: event.payload.sessionId });
|
|
241547
|
+
if (event.type === "tab.closed") state.clearConnectedDisplay({
|
|
241548
|
+
sessionId: event.sessionId,
|
|
241549
|
+
tabId: event.payload.tabId
|
|
241550
|
+
});
|
|
241551
|
+
const mapped = toStreamEvent({
|
|
241552
|
+
event,
|
|
241553
|
+
threadId,
|
|
241554
|
+
browserId,
|
|
241555
|
+
environmentId,
|
|
241556
|
+
userId,
|
|
241557
|
+
agentId: null,
|
|
241558
|
+
ownerRole: "lead",
|
|
241559
|
+
display: event.type === "session.started" || event.type === "session.updated" ? state.displayForSnapshot(event.payload.snapshot) : DEFAULT_AGENT_BROWSER_DISPLAY,
|
|
241560
|
+
controlState: state.controlStateFor(event.type === "session.closed" ? event.payload.sessionId : event.sessionId)
|
|
241561
|
+
});
|
|
241562
|
+
if (mapped) runPromise(listener(mapped));
|
|
241563
|
+
if (event.type === "session.closed") state.controlStates.delete(event.payload.sessionId);
|
|
241564
|
+
});
|
|
241565
|
+
return () => {
|
|
241566
|
+
state.localSubscriptions.delete(localSubscription);
|
|
241567
|
+
unsubscribeBridge();
|
|
241568
|
+
};
|
|
241569
|
+
})
|
|
241570
|
+
};
|
|
241571
|
+
}
|
|
241572
|
+
//#endregion
|
|
241573
|
+
//#region ../../node_modules/.bun/debug@4.4.0+2b91fc17bf64bdfd/node_modules/debug/src/common.js
|
|
241574
|
+
var require_common$2 = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
241575
|
+
/**
|
|
241576
|
+
* This is the common logic for both the Node.js and web browser
|
|
241577
|
+
* implementations of `debug()`.
|
|
241578
|
+
*/
|
|
241579
|
+
function setup(env) {
|
|
241580
|
+
createDebug.debug = createDebug;
|
|
241581
|
+
createDebug.default = createDebug;
|
|
241582
|
+
createDebug.coerce = coerce;
|
|
241583
|
+
createDebug.disable = disable;
|
|
241584
|
+
createDebug.enable = enable;
|
|
241585
|
+
createDebug.enabled = enabled;
|
|
241586
|
+
createDebug.humanize = require_ms();
|
|
241587
|
+
createDebug.destroy = destroy;
|
|
241588
|
+
Object.keys(env).forEach((key) => {
|
|
241589
|
+
createDebug[key] = env[key];
|
|
241590
|
+
});
|
|
241591
|
+
/**
|
|
241592
|
+
* The currently active debug mode names, and names to skip.
|
|
241593
|
+
*/
|
|
241594
|
+
createDebug.names = [];
|
|
241595
|
+
createDebug.skips = [];
|
|
241596
|
+
/**
|
|
241597
|
+
* Map of special "%n" handling functions, for the debug "format" argument.
|
|
241598
|
+
*
|
|
241599
|
+
* Valid key names are a single, lower or upper-case letter, i.e. "n" and "N".
|
|
241600
|
+
*/
|
|
241601
|
+
createDebug.formatters = {};
|
|
241602
|
+
/**
|
|
241603
|
+
* Selects a color for a debug namespace
|
|
241604
|
+
* @param {String} namespace The namespace string for the debug instance to be colored
|
|
241605
|
+
* @return {Number|String} An ANSI color code for the given namespace
|
|
241606
|
+
* @api private
|
|
241607
|
+
*/
|
|
241608
|
+
function selectColor(namespace) {
|
|
241609
|
+
let hash = 0;
|
|
241610
|
+
for (let i = 0; i < namespace.length; i++) {
|
|
241611
|
+
hash = (hash << 5) - hash + namespace.charCodeAt(i);
|
|
241612
|
+
hash |= 0;
|
|
241613
|
+
}
|
|
241614
|
+
return createDebug.colors[Math.abs(hash) % createDebug.colors.length];
|
|
241615
|
+
}
|
|
241616
|
+
createDebug.selectColor = selectColor;
|
|
241617
|
+
/**
|
|
241618
|
+
* Create a debugger with the given `namespace`.
|
|
241619
|
+
*
|
|
241620
|
+
* @param {String} namespace
|
|
241621
|
+
* @return {Function}
|
|
241622
|
+
* @api public
|
|
241623
|
+
*/
|
|
241624
|
+
function createDebug(namespace) {
|
|
241625
|
+
let prevTime;
|
|
241626
|
+
let enableOverride = null;
|
|
241627
|
+
let namespacesCache;
|
|
241628
|
+
let enabledCache;
|
|
241629
|
+
function debug(...args) {
|
|
241630
|
+
if (!debug.enabled) return;
|
|
241631
|
+
const self = debug;
|
|
241632
|
+
const curr = Number(/* @__PURE__ */ new Date());
|
|
241633
|
+
self.diff = curr - (prevTime || curr);
|
|
241634
|
+
self.prev = prevTime;
|
|
241635
|
+
self.curr = curr;
|
|
241636
|
+
prevTime = curr;
|
|
241637
|
+
args[0] = createDebug.coerce(args[0]);
|
|
241638
|
+
if (typeof args[0] !== "string") args.unshift("%O");
|
|
241639
|
+
let index = 0;
|
|
241640
|
+
args[0] = args[0].replace(/%([a-zA-Z%])/g, (match, format) => {
|
|
241641
|
+
if (match === "%%") return "%";
|
|
241642
|
+
index++;
|
|
241643
|
+
const formatter = createDebug.formatters[format];
|
|
241644
|
+
if (typeof formatter === "function") {
|
|
241645
|
+
const val = args[index];
|
|
241646
|
+
match = formatter.call(self, val);
|
|
241647
|
+
args.splice(index, 1);
|
|
241648
|
+
index--;
|
|
241649
|
+
}
|
|
241650
|
+
return match;
|
|
241651
|
+
});
|
|
241652
|
+
createDebug.formatArgs.call(self, args);
|
|
241653
|
+
(self.log || createDebug.log).apply(self, args);
|
|
241654
|
+
}
|
|
241655
|
+
debug.namespace = namespace;
|
|
241656
|
+
debug.useColors = createDebug.useColors();
|
|
241657
|
+
debug.color = createDebug.selectColor(namespace);
|
|
241658
|
+
debug.extend = extend;
|
|
241659
|
+
debug.destroy = createDebug.destroy;
|
|
241660
|
+
Object.defineProperty(debug, "enabled", {
|
|
241661
|
+
enumerable: true,
|
|
241662
|
+
configurable: false,
|
|
241663
|
+
get: () => {
|
|
241664
|
+
if (enableOverride !== null) return enableOverride;
|
|
241665
|
+
if (namespacesCache !== createDebug.namespaces) {
|
|
241666
|
+
namespacesCache = createDebug.namespaces;
|
|
241667
|
+
enabledCache = createDebug.enabled(namespace);
|
|
241668
|
+
}
|
|
241669
|
+
return enabledCache;
|
|
241670
|
+
},
|
|
241671
|
+
set: (v) => {
|
|
241672
|
+
enableOverride = v;
|
|
241673
|
+
}
|
|
241674
|
+
});
|
|
241675
|
+
if (typeof createDebug.init === "function") createDebug.init(debug);
|
|
241676
|
+
return debug;
|
|
241677
|
+
}
|
|
241678
|
+
function extend(namespace, delimiter) {
|
|
241679
|
+
const newDebug = createDebug(this.namespace + (typeof delimiter === "undefined" ? ":" : delimiter) + namespace);
|
|
241680
|
+
newDebug.log = this.log;
|
|
241681
|
+
return newDebug;
|
|
241682
|
+
}
|
|
241683
|
+
/**
|
|
241684
|
+
* Enables a debug mode by namespaces. This can include modes
|
|
241685
|
+
* separated by a colon and wildcards.
|
|
241686
|
+
*
|
|
241687
|
+
* @param {String} namespaces
|
|
241688
|
+
* @api public
|
|
241689
|
+
*/
|
|
241690
|
+
function enable(namespaces) {
|
|
241691
|
+
createDebug.save(namespaces);
|
|
241692
|
+
createDebug.namespaces = namespaces;
|
|
241693
|
+
createDebug.names = [];
|
|
241694
|
+
createDebug.skips = [];
|
|
241695
|
+
const split = (typeof namespaces === "string" ? namespaces : "").trim().replace(" ", ",").split(",").filter(Boolean);
|
|
241696
|
+
for (const ns of split) if (ns[0] === "-") createDebug.skips.push(ns.slice(1));
|
|
241697
|
+
else createDebug.names.push(ns);
|
|
241698
|
+
}
|
|
241699
|
+
/**
|
|
241700
|
+
* Checks if the given string matches a namespace template, honoring
|
|
241701
|
+
* asterisks as wildcards.
|
|
241702
|
+
*
|
|
241703
|
+
* @param {String} search
|
|
241704
|
+
* @param {String} template
|
|
241705
|
+
* @return {Boolean}
|
|
241706
|
+
*/
|
|
241707
|
+
function matchesTemplate(search, template) {
|
|
241708
|
+
let searchIndex = 0;
|
|
241709
|
+
let templateIndex = 0;
|
|
241710
|
+
let starIndex = -1;
|
|
241115
241711
|
let matchIndex = 0;
|
|
241116
241712
|
while (searchIndex < search.length) if (templateIndex < template.length && (template[templateIndex] === search[searchIndex] || template[templateIndex] === "*")) if (template[templateIndex] === "*") {
|
|
241117
241713
|
starIndex = templateIndex;
|
|
@@ -283436,234 +284032,317 @@ async function createBrowserWebRtcDisplayCaptureRuntime(options = {}) {
|
|
|
283436
284032
|
}
|
|
283437
284033
|
}
|
|
283438
284034
|
//#endregion
|
|
283439
|
-
//#region src/
|
|
284035
|
+
//#region src/browser/browserProfilePaths.ts
|
|
283440
284036
|
/**
|
|
283441
|
-
*
|
|
283442
|
-
*
|
|
283443
|
-
* ── why this exists ─────────────────────────────────────────────────────────
|
|
283444
|
-
* A sprite's container cgroup is frozen within ~1 second of the last inbound
|
|
283445
|
-
* request. Processes are paused mid-instruction, not killed, and resume when
|
|
283446
|
-
* traffic returns. Measured on a throwaway sprite with a 1 Hz ticker
|
|
283447
|
-
* (`pocs/sprite-lifecycle-spike/`):
|
|
283448
|
-
*
|
|
283449
|
-
* detached, nothing connected 2 ticks / 121 s = 1.7%
|
|
283450
|
-
* with a task hold 90 ticks / 90 s = 100%
|
|
283451
|
-
*
|
|
283452
|
-
* So an agent turn that nobody is watching effectively stops. fly.io names our
|
|
283453
|
-
* previous approach — relying on a running process after disconnection — as an
|
|
283454
|
-
* anti-pattern, and points at the Tasks API instead. See
|
|
283455
|
-
* https://docs.sprites.dev/keeping-sprites-running/ and
|
|
283456
|
-
* `docs/specs/2026-07-30-sprites-platform-ground-truth.md` §1.
|
|
283457
|
-
*
|
|
283458
|
-
* Note that Services (the platform supervisor) do NOT solve this: "a Sprite with
|
|
283459
|
-
* ten services defined still pauses when it goes idle." Supervision and
|
|
283460
|
-
* wakefulness are separate primitives, and this module is only the second one.
|
|
283461
|
-
*
|
|
283462
|
-
* ── why it lives in the guest ───────────────────────────────────────────────
|
|
283463
|
-
* The Tasks API is guest-only. `GET https://api.sprites.dev/v1/sprites/{n}/tasks`
|
|
283464
|
-
* answers 404; the socket at /.sprite/api.sock is the only surface. CMMD cannot
|
|
283465
|
-
* take this hold on our behalf, which is why the env vars below are exported by
|
|
283466
|
-
* CMMD's provider and consumed here.
|
|
283467
|
-
*
|
|
283468
|
-
* ── the ordering rules ──────────────────────────────────────────────────────
|
|
283469
|
-
* 1. The refresher MUST be stopped before the DELETE is issued. A refresher that
|
|
283470
|
-
* outlives the delete re-acquires the hold on its next tick; that was
|
|
283471
|
-
* previously measured leaking 37 renewals over 18 minutes, pinning a sprite
|
|
283472
|
-
* running — and billing — 33 minutes after its work had ended.
|
|
283473
|
-
* 2. Stopping the interval only cancels FUTURE ticks. A PUT already on the wire
|
|
283474
|
-
* must be JOINED before the DELETE, or it lands afterwards and recreates the
|
|
283475
|
-
* hold for another full expiry window. fly.io's reference worker cancels and
|
|
283476
|
-
* awaits its heartbeat for the same reason.
|
|
283477
|
-
* 3. `release` can arrive while `acquire` is still awaiting its own PUT —
|
|
283478
|
-
* `releaseAll` runs from runtime drain and the shutdown finalizer,
|
|
283479
|
-
* concurrently with the provider event stream. The hold is registered before
|
|
283480
|
-
* that await (so a duplicate turn.started cannot double-acquire), so release
|
|
283481
|
-
* can remove the map entry first; `acquire` must then NOT install a
|
|
283482
|
-
* refresher. One attached to an unmapped hold is unreachable by release,
|
|
283483
|
-
* releaseThread and releaseAll alike, and re-PUTs every 60 s for the life of
|
|
283484
|
-
* the process — an unbounded pin.
|
|
283485
|
-
*
|
|
283486
|
-
* All three are pinned by tests.
|
|
284037
|
+
* Canonical Agent Browser Chromium profile paths.
|
|
283487
284038
|
*
|
|
283488
|
-
*
|
|
283489
|
-
* `
|
|
283490
|
-
*
|
|
283491
|
-
|
|
283492
|
-
|
|
283493
|
-
const SPRITE_TASK_HOLD_SOCKET_ENV = "FORGE_CMMD_SPRITE_TASK_HOLD_SOCKET";
|
|
283494
|
-
const SPRITE_TASK_HOLD_REFRESH_MS = 6e4;
|
|
283495
|
-
/**
|
|
283496
|
-
* Per-request deadline for the guest-socket calls. Must stay strictly under
|
|
283497
|
-
* SPRITE_TASK_HOLD_REFRESH_MS: the manager serializes refreshes, so a PUT that
|
|
283498
|
-
* never settles turns every later tick into a no-op and the hold expires
|
|
283499
|
-
* mid-turn. Bounding the call means a hung socket settles as a rejection, the
|
|
283500
|
-
* next 60 s tick retries, and release is not wedged joining it. From the last
|
|
283501
|
-
* good refresh there are still four attempts (+60/+120/+180/+240 s) before the
|
|
283502
|
-
* 300 s expiry. 10 s matches the `-m 10` cap CMMD already puts on every curl in
|
|
283503
|
-
* its own hold preamble; the guest-side manager was the one path left unbounded.
|
|
284039
|
+
* Both the host bridge adapter (which launches Chromium with a per-session
|
|
284040
|
+
* `userDataDir`) and the orphan reaper (which reconciles live Chromium
|
|
284041
|
+
* processes against registered sessions) must agree byte-for-byte on this
|
|
284042
|
+
* mapping — the reaper matches processes to sessions by profile directory
|
|
284043
|
+
* basename.
|
|
283504
284044
|
*/
|
|
283505
|
-
const
|
|
284045
|
+
const AGENT_BROWSER_PROFILE_ROOT = "/tmp/forge-agent-browser-profiles";
|
|
284046
|
+
/** Sanitized directory basename for a session id. Lossy — never invert it. */
|
|
284047
|
+
function browserProfileSessionKey(sessionId) {
|
|
284048
|
+
return sessionId.replace(/[^a-zA-Z0-9_.-]/g, "_").slice(0, 160) || "default";
|
|
284049
|
+
}
|
|
284050
|
+
function browserProfileDirForSession(sessionId, root = AGENT_BROWSER_PROFILE_ROOT) {
|
|
284051
|
+
return `${root}/${browserProfileSessionKey(sessionId)}`;
|
|
284052
|
+
}
|
|
284053
|
+
//#endregion
|
|
284054
|
+
//#region src/browser/Layers/BrowserOrphanReaper.ts
|
|
283506
284055
|
/**
|
|
283507
|
-
*
|
|
284056
|
+
* E7 production orphan reaper (spec:
|
|
284057
|
+
* docs/specs/2026-06-10-windows-free-dev-and-browser-lifecycle-hardening-plan.md).
|
|
283508
284058
|
*
|
|
283509
|
-
*
|
|
283510
|
-
*
|
|
283511
|
-
*
|
|
284059
|
+
* Periodically reconciles live Chromium browser-main processes (distinct
|
|
284060
|
+
* `--user-data-dir` under the Agent Browser profile root) against the set of
|
|
284061
|
+
* sessions the host bridge knows about, kills orphans, enforces a max-session
|
|
284062
|
+
* TTL through graceful closes, and removes stale profile directories.
|
|
284063
|
+
*
|
|
284064
|
+
* Guard-rails:
|
|
284065
|
+
* - never kills a registered live session;
|
|
284066
|
+
* - the first sweep *attempt* after boot is observe-only (cold-start grace)
|
|
284067
|
+
* -- this is consumed unconditionally by that one attempt, so a transient
|
|
284068
|
+
* read failure on it can never grant a second free pass (which would
|
|
284069
|
+
* otherwise let a restart-orphaned Chromium outlive two sweep intervals
|
|
284070
|
+
* instead of one);
|
|
284071
|
+
* - unregistered Chromium younger than `minOrphanAgeMs` is spared so
|
|
284072
|
+
* in-flight opens are never raced;
|
|
284073
|
+
* - if the session registry cannot be read, the sweep kills nothing --
|
|
284074
|
+
* distinct from a registry that reads back empty (e.g. a freshly booted
|
|
284075
|
+
* server with zero sessions), which is healthy and reapable;
|
|
284076
|
+
* - `protectedSessionKeys` (the shared "default" backend profile) are never
|
|
284077
|
+
* touched.
|
|
283512
284078
|
*/
|
|
283513
|
-
|
|
283514
|
-
|
|
283515
|
-
|
|
284079
|
+
const execFileAsync$2 = promisify$1(execFile);
|
|
284080
|
+
var BrowserReaperStepError = class extends TaggedError("BrowserReaperStepError") {};
|
|
284081
|
+
const DEFAULT_REAPER_INTERVAL_MS = 6e4;
|
|
284082
|
+
const DEFAULT_MAX_SESSION_TTL_MS = 14400 * 1e3;
|
|
284083
|
+
const DEFAULT_MIN_ORPHAN_AGE_MS = 120 * 1e3;
|
|
284084
|
+
const KILL_ESCALATION_TIMEOUT_MS = 5e3;
|
|
284085
|
+
const KILL_POLL_INTERVAL_MS = 100;
|
|
284086
|
+
function positiveIntFromEnv(value) {
|
|
284087
|
+
if (!value) return;
|
|
284088
|
+
const parsed = Number.parseInt(value, 10);
|
|
284089
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : void 0;
|
|
284090
|
+
}
|
|
284091
|
+
function browserOrphanReaperConfigFromEnv(env, profileRoot = "/tmp/forge-agent-browser-profiles") {
|
|
284092
|
+
const enabledRaw = env.FORGE_BROWSER_REAPER_ENABLED?.trim().toLowerCase();
|
|
283516
284093
|
return {
|
|
283517
|
-
|
|
283518
|
-
|
|
284094
|
+
profileRoot,
|
|
284095
|
+
intervalMs: positiveIntFromEnv(env.FORGE_BROWSER_REAPER_INTERVAL_MS) || DEFAULT_REAPER_INTERVAL_MS,
|
|
284096
|
+
maxSessionTtlMs: positiveIntFromEnv(env.FORGE_BROWSER_SESSION_MAX_TTL_MS) ?? DEFAULT_MAX_SESSION_TTL_MS,
|
|
284097
|
+
minOrphanAgeMs: positiveIntFromEnv(env.FORGE_BROWSER_REAPER_MIN_ORPHAN_AGE_MS) ?? DEFAULT_MIN_ORPHAN_AGE_MS,
|
|
284098
|
+
enabled: enabledRaw !== "0" && enabledRaw !== "false",
|
|
284099
|
+
protectedSessionKeys: new Set(["default"])
|
|
283519
284100
|
};
|
|
283520
284101
|
}
|
|
284102
|
+
/** [[dd-]hh:]mm:ss as printed by `ps -o etime=`. Returns null on malformed input. */
|
|
284103
|
+
function parseEtimeToMs(etime) {
|
|
284104
|
+
const match = /^(?:(\d+)-)?(?:(\d+):)?(\d+):(\d+)$/.exec(etime.trim());
|
|
284105
|
+
if (!match) return null;
|
|
284106
|
+
const [, days, hours, minutes, seconds] = match;
|
|
284107
|
+
return (Number(days ?? 0) * 24 * 60 * 60 + Number(hours ?? 0) * 60 * 60 + Number(minutes) * 60 + Number(seconds)) * 1e3;
|
|
284108
|
+
}
|
|
283521
284109
|
/**
|
|
283522
|
-
*
|
|
283523
|
-
*
|
|
284110
|
+
* A Chromium browser-main owns a profile under our root and has no `--type=`
|
|
284111
|
+
* flag (renderer/gpu/utility children all carry one). Killing the main tears
|
|
284112
|
+
* down the whole Chromium process tree.
|
|
283524
284113
|
*/
|
|
283525
|
-
function
|
|
283526
|
-
return
|
|
284114
|
+
function isChromiumMainCommand(command, profileRoot) {
|
|
284115
|
+
return command.includes(`--user-data-dir=${profileRoot}/`) && !/\s--type=/.test(` ${command}`);
|
|
283527
284116
|
}
|
|
283528
|
-
|
|
283529
|
-
|
|
283530
|
-
|
|
283531
|
-
|
|
284117
|
+
function extractUserDataDir(command) {
|
|
284118
|
+
const match = /--user-data-dir=(\S+)/.exec(command);
|
|
284119
|
+
return match ? match[1] ?? null : null;
|
|
284120
|
+
}
|
|
284121
|
+
function profileKeyForDir(userDataDir, profileRoot) {
|
|
284122
|
+
const resolved = path.resolve(userDataDir);
|
|
284123
|
+
const resolvedRoot = path.resolve(profileRoot);
|
|
284124
|
+
if (resolved === resolvedRoot || !resolved.startsWith(resolvedRoot + path.sep)) return null;
|
|
284125
|
+
const relative = resolved.slice(resolvedRoot.length + 1);
|
|
284126
|
+
return relative.includes(path.sep) ? null : relative;
|
|
284127
|
+
}
|
|
284128
|
+
function makeSweepState(deps, config, observeOnly) {
|
|
284129
|
+
return {
|
|
284130
|
+
deps,
|
|
284131
|
+
config,
|
|
284132
|
+
observeOnly,
|
|
284133
|
+
errors: [],
|
|
284134
|
+
wouldReapOrphans: [],
|
|
284135
|
+
wouldCloseTtlSessions: [],
|
|
284136
|
+
wouldRemoveStaleProfiles: [],
|
|
284137
|
+
killedOrphans: [],
|
|
284138
|
+
closedTtlSessions: [],
|
|
284139
|
+
removedStaleProfiles: []
|
|
284140
|
+
};
|
|
284141
|
+
}
|
|
284142
|
+
function sweepReport(state, registryUnavailable) {
|
|
284143
|
+
return {
|
|
284144
|
+
observeOnly: state.observeOnly,
|
|
284145
|
+
registryUnavailable,
|
|
284146
|
+
wouldReapOrphans: state.wouldReapOrphans,
|
|
284147
|
+
wouldCloseTtlSessions: state.wouldCloseTtlSessions,
|
|
284148
|
+
wouldRemoveStaleProfiles: state.wouldRemoveStaleProfiles,
|
|
284149
|
+
killedOrphans: state.killedOrphans,
|
|
284150
|
+
closedTtlSessions: state.closedTtlSessions,
|
|
284151
|
+
removedStaleProfiles: state.removedStaleProfiles,
|
|
284152
|
+
errors: state.errors
|
|
284153
|
+
};
|
|
284154
|
+
}
|
|
284155
|
+
/** Run a dep call; on failure record the error into the state and yield null. */
|
|
284156
|
+
function attemptOrNull(state, label, run) {
|
|
284157
|
+
return tryPromise({
|
|
284158
|
+
try: run,
|
|
284159
|
+
catch: (error) => new BrowserReaperStepError({
|
|
284160
|
+
message: `${label}: ${error instanceof Error ? error.message : String(error)}`,
|
|
284161
|
+
cause: error
|
|
284162
|
+
})
|
|
284163
|
+
}).pipe(map$3((value) => value), catch_((error) => sync$1(() => (state.errors.push(error.message), null))));
|
|
284164
|
+
}
|
|
284165
|
+
/** Like attemptOrNull, but collapse the outcome to a success flag. */
|
|
284166
|
+
function attemptFlag(state, label, run) {
|
|
284167
|
+
return attemptOrNull(state, label, async () => {
|
|
284168
|
+
await run();
|
|
284169
|
+
return true;
|
|
284170
|
+
}).pipe(map$3((value) => value === true));
|
|
284171
|
+
}
|
|
284172
|
+
function mapRegisteredByKey(registered) {
|
|
284173
|
+
const registeredByKey = /* @__PURE__ */ new Map();
|
|
284174
|
+
for (const sessionId of registered) registeredByKey.set(browserProfileSessionKey(sessionId), sessionId);
|
|
284175
|
+
return registeredByKey;
|
|
284176
|
+
}
|
|
284177
|
+
const enforceSessionTtl = (state, sessionId, main) => gen(function* () {
|
|
284178
|
+
if (state.config.maxSessionTtlMs <= 0 || main.ageMs <= state.config.maxSessionTtlMs) return;
|
|
284179
|
+
if (state.observeOnly) {
|
|
284180
|
+
state.wouldCloseTtlSessions.push(sessionId);
|
|
284181
|
+
return;
|
|
283532
284182
|
}
|
|
283533
|
-
}
|
|
283534
|
-
|
|
283535
|
-
*
|
|
283536
|
-
*
|
|
283537
|
-
* Every method is failure-tolerant by design. Acquiring a hold is an
|
|
283538
|
-
* optimisation, not a precondition: if it fails the turn still runs, just at the
|
|
283539
|
-
* degraded speed it has today. Failing the turn instead would convert a billing
|
|
283540
|
-
* inefficiency into an outage.
|
|
283541
|
-
*/
|
|
283542
|
-
var SpriteTaskHoldManager = class {
|
|
283543
|
-
#config;
|
|
283544
|
-
#transport;
|
|
283545
|
-
#timer;
|
|
283546
|
-
#onError;
|
|
283547
|
-
#holds = /* @__PURE__ */ new Map();
|
|
283548
|
-
constructor(options) {
|
|
283549
|
-
this.#config = options.config;
|
|
283550
|
-
this.#transport = options.transport;
|
|
283551
|
-
this.#timer = options.timer ?? DEFAULT_TIMER;
|
|
283552
|
-
this.#onError = options.onError ?? (() => {});
|
|
284183
|
+
if (yield* attemptFlag(state, `closeSession(${sessionId})`, () => state.deps.closeSession(sessionId))) {
|
|
284184
|
+
state.closedTtlSessions.push(sessionId);
|
|
284185
|
+
yield* increment(browserSessionsReapedTotal, { reason: "ttl" });
|
|
283553
284186
|
}
|
|
283554
|
-
|
|
283555
|
-
|
|
283556
|
-
|
|
284187
|
+
});
|
|
284188
|
+
const reapOrphanMain = (state, key, main) => gen(function* () {
|
|
284189
|
+
if (main.ageMs < state.config.minOrphanAgeMs) return;
|
|
284190
|
+
if (state.observeOnly) {
|
|
284191
|
+
state.wouldReapOrphans.push(key);
|
|
284192
|
+
return;
|
|
283557
284193
|
}
|
|
283558
|
-
|
|
283559
|
-
|
|
283560
|
-
|
|
283561
|
-
|
|
283562
|
-
|
|
283563
|
-
|
|
283564
|
-
|
|
283565
|
-
|
|
283566
|
-
|
|
283567
|
-
|
|
283568
|
-
|
|
283569
|
-
|
|
283570
|
-
const
|
|
283571
|
-
|
|
283572
|
-
|
|
283573
|
-
|
|
283574
|
-
|
|
283575
|
-
|
|
283576
|
-
|
|
283577
|
-
|
|
284194
|
+
if (!(yield* attemptFlag(state, `killProcess(${main.pid})`, () => state.deps.killProcess(main.pid)))) return;
|
|
284195
|
+
state.killedOrphans.push(key);
|
|
284196
|
+
yield* increment(browserSessionsReapedTotal, { reason: "orphan" });
|
|
284197
|
+
yield* attemptFlag(state, `removeProfileDir(${main.userDataDir})`, () => state.deps.removeProfileDir(main.userDataDir));
|
|
284198
|
+
});
|
|
284199
|
+
const sweepChromiumMains = (state, registeredByKey, mains) => gen(function* () {
|
|
284200
|
+
const liveKeys = /* @__PURE__ */ new Set();
|
|
284201
|
+
for (const main of mains) {
|
|
284202
|
+
const key = profileKeyForDir(main.userDataDir, state.config.profileRoot);
|
|
284203
|
+
if (key === null) continue;
|
|
284204
|
+
liveKeys.add(key);
|
|
284205
|
+
if (state.config.protectedSessionKeys.has(key)) continue;
|
|
284206
|
+
const registeredSessionId = registeredByKey.get(key);
|
|
284207
|
+
if (registeredSessionId !== void 0) yield* enforceSessionTtl(state, registeredSessionId, main);
|
|
284208
|
+
else yield* reapOrphanMain(state, key, main);
|
|
284209
|
+
}
|
|
284210
|
+
return liveKeys;
|
|
284211
|
+
});
|
|
284212
|
+
function isRetainedProfileDir(state, registeredByKey, liveKeys, entry) {
|
|
284213
|
+
const key = profileKeyForDir(entry.path, state.config.profileRoot);
|
|
284214
|
+
return key === null || state.config.protectedSessionKeys.has(key) || registeredByKey.has(key) || liveKeys.has(key) || entry.ageMs < state.config.minOrphanAgeMs;
|
|
284215
|
+
}
|
|
284216
|
+
const sweepStaleProfileDirs = (state, registeredByKey, liveKeys) => gen(function* () {
|
|
284217
|
+
const profileDirs = yield* attemptOrNull(state, "listProfileDirs", state.deps.listProfileDirs);
|
|
284218
|
+
if (profileDirs === null) return;
|
|
284219
|
+
for (const entry of profileDirs) {
|
|
284220
|
+
if (isRetainedProfileDir(state, registeredByKey, liveKeys, entry)) continue;
|
|
284221
|
+
if (state.observeOnly) {
|
|
284222
|
+
state.wouldRemoveStaleProfiles.push(entry.path);
|
|
284223
|
+
continue;
|
|
284224
|
+
}
|
|
284225
|
+
if (yield* attemptFlag(state, `removeProfileDir(${entry.path})`, () => state.deps.removeProfileDir(entry.path))) {
|
|
284226
|
+
state.removedStaleProfiles.push(entry.path);
|
|
284227
|
+
yield* increment(browserSessionsReapedTotal, { reason: "stale_profile" });
|
|
283578
284228
|
}
|
|
283579
|
-
hold.inFlight.delete(tracked);
|
|
283580
|
-
if (hold.released || this.#holds.get(turnId) !== hold) return;
|
|
283581
|
-
hold.refresher = this.#timer.setInterval(() => {
|
|
283582
|
-
if (hold.inFlight.size > 0) return;
|
|
283583
|
-
const refresh = this.#transport.put(name, 300).catch((cause) => this.#onError(`failed to refresh sprite task hold for turn ${turnId}`, cause));
|
|
283584
|
-
hold.inFlight.add(refresh);
|
|
283585
|
-
refresh.finally(() => hold.inFlight.delete(refresh));
|
|
283586
|
-
}, SPRITE_TASK_HOLD_REFRESH_MS);
|
|
283587
284229
|
}
|
|
283588
|
-
|
|
283589
|
-
|
|
283590
|
-
|
|
283591
|
-
|
|
283592
|
-
|
|
283593
|
-
|
|
283594
|
-
|
|
284230
|
+
});
|
|
284231
|
+
function sweepHasActivity(report) {
|
|
284232
|
+
return report.killedOrphans.length > 0 || report.closedTtlSessions.length > 0 || report.removedStaleProfiles.length > 0 || report.wouldReapOrphans.length > 0 || report.wouldCloseTtlSessions.length > 0 || report.wouldRemoveStaleProfiles.length > 0 || report.errors.length > 0;
|
|
284233
|
+
}
|
|
284234
|
+
const logSweepReportIfActive = (report) => sweepHasActivity(report) ? logInfo("browser orphan reaper sweep", {
|
|
284235
|
+
observeOnly: report.observeOnly,
|
|
284236
|
+
killedOrphans: report.killedOrphans,
|
|
284237
|
+
closedTtlSessions: report.closedTtlSessions,
|
|
284238
|
+
removedStaleProfiles: report.removedStaleProfiles,
|
|
284239
|
+
wouldReapOrphans: report.wouldReapOrphans,
|
|
284240
|
+
wouldCloseTtlSessions: report.wouldCloseTtlSessions,
|
|
284241
|
+
wouldRemoveStaleProfiles: report.wouldRemoveStaleProfiles,
|
|
284242
|
+
errors: report.errors
|
|
284243
|
+
}) : void_$1;
|
|
284244
|
+
function createBrowserOrphanReaper(deps, config) {
|
|
284245
|
+
let coldStartGraceConsumed = false;
|
|
284246
|
+
return { sweep: gen(function* () {
|
|
284247
|
+
const observeOnly = !coldStartGraceConsumed;
|
|
284248
|
+
coldStartGraceConsumed = true;
|
|
284249
|
+
const state = makeSweepState(deps, config, observeOnly);
|
|
284250
|
+
const registered = yield* attemptOrNull(state, "listRegisteredSessionIds", deps.listRegisteredSessionIds);
|
|
284251
|
+
if (registered === null) {
|
|
284252
|
+
yield* logWarning$1("browser orphan reaper: session registry unavailable; skipping", { errors: state.errors });
|
|
284253
|
+
return sweepReport(state, true);
|
|
283595
284254
|
}
|
|
283596
|
-
|
|
283597
|
-
if (
|
|
284255
|
+
const mains = yield* attemptOrNull(state, "listChromiumMains", deps.listChromiumMains);
|
|
284256
|
+
if (mains === null) return sweepReport(state, false);
|
|
284257
|
+
const registeredByKey = mapRegisteredByKey(registered);
|
|
284258
|
+
yield* sweepStaleProfileDirs(state, registeredByKey, yield* sweepChromiumMains(state, registeredByKey, mains));
|
|
284259
|
+
const report = sweepReport(state, false);
|
|
284260
|
+
yield* logSweepReportIfActive(report);
|
|
284261
|
+
return report;
|
|
284262
|
+
}) };
|
|
284263
|
+
}
|
|
284264
|
+
function sleep(ms) {
|
|
284265
|
+
return new Promise((resolve) => {
|
|
284266
|
+
setTimeout(resolve, ms).unref?.();
|
|
284267
|
+
});
|
|
284268
|
+
}
|
|
284269
|
+
function isEsrch(error) {
|
|
284270
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ESRCH";
|
|
284271
|
+
}
|
|
284272
|
+
/** SIGTERM, poll for exit, escalate to SIGKILL. Chromium tears down its own children. */
|
|
284273
|
+
async function killProcessWithEscalation(pid) {
|
|
284274
|
+
try {
|
|
284275
|
+
process.kill(pid, "SIGTERM");
|
|
284276
|
+
} catch (error) {
|
|
284277
|
+
if (isEsrch(error)) return;
|
|
284278
|
+
throw error;
|
|
284279
|
+
}
|
|
284280
|
+
const deadline = Date.now() + KILL_ESCALATION_TIMEOUT_MS;
|
|
284281
|
+
while (Date.now() < deadline) {
|
|
284282
|
+
await sleep(KILL_POLL_INTERVAL_MS);
|
|
283598
284283
|
try {
|
|
283599
|
-
|
|
283600
|
-
} catch
|
|
283601
|
-
|
|
284284
|
+
process.kill(pid, 0);
|
|
284285
|
+
} catch {
|
|
284286
|
+
return;
|
|
283602
284287
|
}
|
|
283603
284288
|
}
|
|
283604
|
-
|
|
283605
|
-
|
|
283606
|
-
|
|
283607
|
-
|
|
283608
|
-
* dead session's turn will never emit `turn.completed`. Releasing by thread —
|
|
283609
|
-
* rather than releasing everything — is what keeps a crash in one worktree
|
|
283610
|
-
* from silently dropping the holds of the other turns running beside it.
|
|
283611
|
-
*/
|
|
283612
|
-
async releaseThread(threadId) {
|
|
283613
|
-
const turnIds = [...this.#holds.entries()].filter(([, hold]) => hold.threadId === threadId).map(([turnId]) => turnId);
|
|
283614
|
-
await Promise.all(turnIds.map((turnId) => this.release(turnId)));
|
|
283615
|
-
}
|
|
283616
|
-
/**
|
|
283617
|
-
* Release everything. Runtime drain and server shutdown must call this, or a
|
|
283618
|
-
* drained sprite keeps billing until each hold expires.
|
|
283619
|
-
*/
|
|
283620
|
-
async releaseAll() {
|
|
283621
|
-
await Promise.all([...this.#holds.keys()].map((turnId) => this.release(turnId)));
|
|
284289
|
+
try {
|
|
284290
|
+
process.kill(pid, "SIGKILL");
|
|
284291
|
+
} catch (error) {
|
|
284292
|
+
if (!isEsrch(error)) throw error;
|
|
283622
284293
|
}
|
|
283623
|
-
}
|
|
283624
|
-
|
|
283625
|
-
|
|
283626
|
-
|
|
283627
|
-
|
|
283628
|
-
|
|
283629
|
-
|
|
283630
|
-
|
|
283631
|
-
|
|
283632
|
-
|
|
283633
|
-
|
|
283634
|
-
|
|
283635
|
-
|
|
283636
|
-
|
|
283637
|
-
|
|
283638
|
-
|
|
283639
|
-
|
|
284294
|
+
}
|
|
284295
|
+
function createSystemBrowserOrphanReaperDeps(input) {
|
|
284296
|
+
const { profileRoot } = input;
|
|
284297
|
+
return {
|
|
284298
|
+
listChromiumMains: async () => {
|
|
284299
|
+
const { stdout } = await execFileAsync$2("ps", ["-axo", "pid=,etime=,args="], { maxBuffer: 16 * 1024 * 1024 });
|
|
284300
|
+
const mains = [];
|
|
284301
|
+
for (const line of stdout.split("\n")) {
|
|
284302
|
+
const match = /^\s*(\d+)\s+(\S+)\s+(.+)$/.exec(line);
|
|
284303
|
+
if (!match) continue;
|
|
284304
|
+
const [, pidRaw, etime, command] = match;
|
|
284305
|
+
if (!isChromiumMainCommand(command, profileRoot)) continue;
|
|
284306
|
+
const userDataDir = extractUserDataDir(command);
|
|
284307
|
+
const ageMs = parseEtimeToMs(etime);
|
|
284308
|
+
if (userDataDir === null || ageMs === null) continue;
|
|
284309
|
+
mains.push({
|
|
284310
|
+
pid: Number(pidRaw),
|
|
284311
|
+
userDataDir,
|
|
284312
|
+
ageMs
|
|
284313
|
+
});
|
|
284314
|
+
}
|
|
284315
|
+
return mains;
|
|
284316
|
+
},
|
|
284317
|
+
listRegisteredSessionIds: async () => (await input.listSessions()).map((session) => session.sessionId),
|
|
284318
|
+
closeSession: input.closeSession,
|
|
284319
|
+
killProcess: killProcessWithEscalation,
|
|
284320
|
+
listProfileDirs: async () => {
|
|
284321
|
+
const entries = await fs$1.readdir(profileRoot, { withFileTypes: true }).catch(() => []);
|
|
284322
|
+
const now = Date.now();
|
|
284323
|
+
const dirs = [];
|
|
284324
|
+
for (const entry of entries) {
|
|
284325
|
+
if (!entry.isDirectory()) continue;
|
|
284326
|
+
const dirPath = path.join(profileRoot, entry.name);
|
|
284327
|
+
const stat = await fs$1.stat(dirPath).catch(() => null);
|
|
284328
|
+
if (stat === null) continue;
|
|
284329
|
+
dirs.push({
|
|
284330
|
+
path: dirPath,
|
|
284331
|
+
ageMs: Math.max(0, now - stat.mtimeMs)
|
|
284332
|
+
});
|
|
284333
|
+
}
|
|
284334
|
+
return dirs;
|
|
284335
|
+
},
|
|
284336
|
+
removeProfileDir: async (dir) => {
|
|
284337
|
+
const resolved = path.resolve(dir);
|
|
284338
|
+
const resolvedRoot = path.resolve(profileRoot);
|
|
284339
|
+
if (resolved === resolvedRoot || !resolved.startsWith(resolvedRoot + path.sep)) throw new Error(`refusing to remove path outside the profile root: ${dir}`);
|
|
284340
|
+
await fs$1.rm(resolved, {
|
|
284341
|
+
recursive: true,
|
|
284342
|
+
force: true
|
|
283640
284343
|
});
|
|
283641
|
-
if (!response.ok) throw new Error(`sprite task API responded ${response.status}`);
|
|
283642
|
-
} finally {
|
|
283643
|
-
clearTimeout(deadline);
|
|
283644
284344
|
}
|
|
283645
284345
|
};
|
|
283646
|
-
return {
|
|
283647
|
-
put: (name, expireSeconds) => call(`/${encodeURIComponent(name)}`, {
|
|
283648
|
-
method: "PUT",
|
|
283649
|
-
headers: { "content-type": "application/json" },
|
|
283650
|
-
body: JSON.stringify({ expire: `${expireSeconds}s` })
|
|
283651
|
-
}),
|
|
283652
|
-
delete: (name) => call(`/${encodeURIComponent(name)}`, { method: "DELETE" })
|
|
283653
|
-
};
|
|
283654
|
-
}
|
|
283655
|
-
/**
|
|
283656
|
-
* Build a manager for this process, or null when the runtime is not a sprite.
|
|
283657
|
-
* Callers treat null as "this feature does not apply here".
|
|
283658
|
-
*/
|
|
283659
|
-
function makeSpriteTaskHoldManager(env = process.env, onError) {
|
|
283660
|
-
const config = resolveSpriteTaskHoldConfig(env);
|
|
283661
|
-
if (config === null) return null;
|
|
283662
|
-
return new SpriteTaskHoldManager({
|
|
283663
|
-
config,
|
|
283664
|
-
transport: makeSpriteTaskHoldTransport(config),
|
|
283665
|
-
...onError ? { onError } : {}
|
|
283666
|
-
});
|
|
283667
284346
|
}
|
|
283668
284347
|
//#endregion
|
|
283669
284348
|
//#region src/browser/Layers/displaySessionTaskHold.ts
|
|
@@ -284095,25 +284774,6 @@ function createWebRtcDisplaySessionManager(input = {}) {
|
|
|
284095
284774
|
})
|
|
284096
284775
|
};
|
|
284097
284776
|
}
|
|
284098
|
-
//#endregion
|
|
284099
|
-
//#region src/browser/browserProfilePaths.ts
|
|
284100
|
-
/**
|
|
284101
|
-
* Canonical Agent Browser Chromium profile paths.
|
|
284102
|
-
*
|
|
284103
|
-
* Both the host bridge adapter (which launches Chromium with a per-session
|
|
284104
|
-
* `userDataDir`) and the orphan reaper (which reconciles live Chromium
|
|
284105
|
-
* processes against registered sessions) must agree byte-for-byte on this
|
|
284106
|
-
* mapping — the reaper matches processes to sessions by profile directory
|
|
284107
|
-
* basename.
|
|
284108
|
-
*/
|
|
284109
|
-
const AGENT_BROWSER_PROFILE_ROOT = "/tmp/forge-agent-browser-profiles";
|
|
284110
|
-
/** Sanitized directory basename for a session id. Lossy — never invert it. */
|
|
284111
|
-
function browserProfileSessionKey(sessionId) {
|
|
284112
|
-
return sessionId.replace(/[^a-zA-Z0-9_.-]/g, "_").slice(0, 160) || "default";
|
|
284113
|
-
}
|
|
284114
|
-
function browserProfileDirForSession(sessionId, root = AGENT_BROWSER_PROFILE_ROOT) {
|
|
284115
|
-
return `${root}/${browserProfileSessionKey(sessionId)}`;
|
|
284116
|
-
}
|
|
284117
284777
|
const HOST_BRIDGE_RECONNECT_MAX_MS = 1e3;
|
|
284118
284778
|
/**
|
|
284119
284779
|
* Exponential backoff, capped.
|
|
@@ -284193,620 +284853,9 @@ function drainHostBridgeNdjsonBuffer(buffer, callback, parseLine = (line) => JSO
|
|
|
284193
284853
|
return nextBuffer;
|
|
284194
284854
|
}
|
|
284195
284855
|
//#endregion
|
|
284196
|
-
//#region src/browser/
|
|
284197
|
-
/**
|
|
284198
|
-
* E7 production orphan reaper (spec:
|
|
284199
|
-
* docs/specs/2026-06-10-windows-free-dev-and-browser-lifecycle-hardening-plan.md).
|
|
284200
|
-
*
|
|
284201
|
-
* Periodically reconciles live Chromium browser-main processes (distinct
|
|
284202
|
-
* `--user-data-dir` under the Agent Browser profile root) against the set of
|
|
284203
|
-
* sessions the host bridge knows about, kills orphans, enforces a max-session
|
|
284204
|
-
* TTL through graceful closes, and removes stale profile directories.
|
|
284205
|
-
*
|
|
284206
|
-
* Guard-rails:
|
|
284207
|
-
* - never kills a registered live session;
|
|
284208
|
-
* - the first sweep *attempt* after boot is observe-only (cold-start grace)
|
|
284209
|
-
* -- this is consumed unconditionally by that one attempt, so a transient
|
|
284210
|
-
* read failure on it can never grant a second free pass (which would
|
|
284211
|
-
* otherwise let a restart-orphaned Chromium outlive two sweep intervals
|
|
284212
|
-
* instead of one);
|
|
284213
|
-
* - unregistered Chromium younger than `minOrphanAgeMs` is spared so
|
|
284214
|
-
* in-flight opens are never raced;
|
|
284215
|
-
* - if the session registry cannot be read, the sweep kills nothing --
|
|
284216
|
-
* distinct from a registry that reads back empty (e.g. a freshly booted
|
|
284217
|
-
* server with zero sessions), which is healthy and reapable;
|
|
284218
|
-
* - `protectedSessionKeys` (the shared "default" backend profile) are never
|
|
284219
|
-
* touched.
|
|
284220
|
-
*/
|
|
284221
|
-
const execFileAsync$2 = promisify$1(execFile);
|
|
284222
|
-
var BrowserReaperStepError = class extends TaggedError("BrowserReaperStepError") {};
|
|
284223
|
-
const DEFAULT_REAPER_INTERVAL_MS = 6e4;
|
|
284224
|
-
const DEFAULT_MAX_SESSION_TTL_MS = 14400 * 1e3;
|
|
284225
|
-
const DEFAULT_MIN_ORPHAN_AGE_MS = 120 * 1e3;
|
|
284226
|
-
const KILL_ESCALATION_TIMEOUT_MS = 5e3;
|
|
284227
|
-
const KILL_POLL_INTERVAL_MS = 100;
|
|
284228
|
-
function positiveIntFromEnv(value) {
|
|
284229
|
-
if (!value) return;
|
|
284230
|
-
const parsed = Number.parseInt(value, 10);
|
|
284231
|
-
return Number.isFinite(parsed) && parsed >= 0 ? parsed : void 0;
|
|
284232
|
-
}
|
|
284233
|
-
function browserOrphanReaperConfigFromEnv(env, profileRoot = "/tmp/forge-agent-browser-profiles") {
|
|
284234
|
-
const enabledRaw = env.FORGE_BROWSER_REAPER_ENABLED?.trim().toLowerCase();
|
|
284235
|
-
return {
|
|
284236
|
-
profileRoot,
|
|
284237
|
-
intervalMs: positiveIntFromEnv(env.FORGE_BROWSER_REAPER_INTERVAL_MS) || DEFAULT_REAPER_INTERVAL_MS,
|
|
284238
|
-
maxSessionTtlMs: positiveIntFromEnv(env.FORGE_BROWSER_SESSION_MAX_TTL_MS) ?? DEFAULT_MAX_SESSION_TTL_MS,
|
|
284239
|
-
minOrphanAgeMs: positiveIntFromEnv(env.FORGE_BROWSER_REAPER_MIN_ORPHAN_AGE_MS) ?? DEFAULT_MIN_ORPHAN_AGE_MS,
|
|
284240
|
-
enabled: enabledRaw !== "0" && enabledRaw !== "false",
|
|
284241
|
-
protectedSessionKeys: new Set(["default"])
|
|
284242
|
-
};
|
|
284243
|
-
}
|
|
284244
|
-
/** [[dd-]hh:]mm:ss as printed by `ps -o etime=`. Returns null on malformed input. */
|
|
284245
|
-
function parseEtimeToMs(etime) {
|
|
284246
|
-
const match = /^(?:(\d+)-)?(?:(\d+):)?(\d+):(\d+)$/.exec(etime.trim());
|
|
284247
|
-
if (!match) return null;
|
|
284248
|
-
const [, days, hours, minutes, seconds] = match;
|
|
284249
|
-
return (Number(days ?? 0) * 24 * 60 * 60 + Number(hours ?? 0) * 60 * 60 + Number(minutes) * 60 + Number(seconds)) * 1e3;
|
|
284250
|
-
}
|
|
284251
|
-
/**
|
|
284252
|
-
* A Chromium browser-main owns a profile under our root and has no `--type=`
|
|
284253
|
-
* flag (renderer/gpu/utility children all carry one). Killing the main tears
|
|
284254
|
-
* down the whole Chromium process tree.
|
|
284255
|
-
*/
|
|
284256
|
-
function isChromiumMainCommand(command, profileRoot) {
|
|
284257
|
-
return command.includes(`--user-data-dir=${profileRoot}/`) && !/\s--type=/.test(` ${command}`);
|
|
284258
|
-
}
|
|
284259
|
-
function extractUserDataDir(command) {
|
|
284260
|
-
const match = /--user-data-dir=(\S+)/.exec(command);
|
|
284261
|
-
return match ? match[1] ?? null : null;
|
|
284262
|
-
}
|
|
284263
|
-
function profileKeyForDir(userDataDir, profileRoot) {
|
|
284264
|
-
const resolved = path.resolve(userDataDir);
|
|
284265
|
-
const resolvedRoot = path.resolve(profileRoot);
|
|
284266
|
-
if (resolved === resolvedRoot || !resolved.startsWith(resolvedRoot + path.sep)) return null;
|
|
284267
|
-
const relative = resolved.slice(resolvedRoot.length + 1);
|
|
284268
|
-
return relative.includes(path.sep) ? null : relative;
|
|
284269
|
-
}
|
|
284270
|
-
function makeSweepState(deps, config, observeOnly) {
|
|
284271
|
-
return {
|
|
284272
|
-
deps,
|
|
284273
|
-
config,
|
|
284274
|
-
observeOnly,
|
|
284275
|
-
errors: [],
|
|
284276
|
-
wouldReapOrphans: [],
|
|
284277
|
-
wouldCloseTtlSessions: [],
|
|
284278
|
-
wouldRemoveStaleProfiles: [],
|
|
284279
|
-
killedOrphans: [],
|
|
284280
|
-
closedTtlSessions: [],
|
|
284281
|
-
removedStaleProfiles: []
|
|
284282
|
-
};
|
|
284283
|
-
}
|
|
284284
|
-
function sweepReport(state, registryUnavailable) {
|
|
284285
|
-
return {
|
|
284286
|
-
observeOnly: state.observeOnly,
|
|
284287
|
-
registryUnavailable,
|
|
284288
|
-
wouldReapOrphans: state.wouldReapOrphans,
|
|
284289
|
-
wouldCloseTtlSessions: state.wouldCloseTtlSessions,
|
|
284290
|
-
wouldRemoveStaleProfiles: state.wouldRemoveStaleProfiles,
|
|
284291
|
-
killedOrphans: state.killedOrphans,
|
|
284292
|
-
closedTtlSessions: state.closedTtlSessions,
|
|
284293
|
-
removedStaleProfiles: state.removedStaleProfiles,
|
|
284294
|
-
errors: state.errors
|
|
284295
|
-
};
|
|
284296
|
-
}
|
|
284297
|
-
/** Run a dep call; on failure record the error into the state and yield null. */
|
|
284298
|
-
function attemptOrNull(state, label, run) {
|
|
284299
|
-
return tryPromise({
|
|
284300
|
-
try: run,
|
|
284301
|
-
catch: (error) => new BrowserReaperStepError({
|
|
284302
|
-
message: `${label}: ${error instanceof Error ? error.message : String(error)}`,
|
|
284303
|
-
cause: error
|
|
284304
|
-
})
|
|
284305
|
-
}).pipe(map$3((value) => value), catch_((error) => sync$1(() => (state.errors.push(error.message), null))));
|
|
284306
|
-
}
|
|
284307
|
-
/** Like attemptOrNull, but collapse the outcome to a success flag. */
|
|
284308
|
-
function attemptFlag(state, label, run) {
|
|
284309
|
-
return attemptOrNull(state, label, async () => {
|
|
284310
|
-
await run();
|
|
284311
|
-
return true;
|
|
284312
|
-
}).pipe(map$3((value) => value === true));
|
|
284313
|
-
}
|
|
284314
|
-
function mapRegisteredByKey(registered) {
|
|
284315
|
-
const registeredByKey = /* @__PURE__ */ new Map();
|
|
284316
|
-
for (const sessionId of registered) registeredByKey.set(browserProfileSessionKey(sessionId), sessionId);
|
|
284317
|
-
return registeredByKey;
|
|
284318
|
-
}
|
|
284319
|
-
const enforceSessionTtl = (state, sessionId, main) => gen(function* () {
|
|
284320
|
-
if (state.config.maxSessionTtlMs <= 0 || main.ageMs <= state.config.maxSessionTtlMs) return;
|
|
284321
|
-
if (state.observeOnly) {
|
|
284322
|
-
state.wouldCloseTtlSessions.push(sessionId);
|
|
284323
|
-
return;
|
|
284324
|
-
}
|
|
284325
|
-
if (yield* attemptFlag(state, `closeSession(${sessionId})`, () => state.deps.closeSession(sessionId))) {
|
|
284326
|
-
state.closedTtlSessions.push(sessionId);
|
|
284327
|
-
yield* increment(browserSessionsReapedTotal, { reason: "ttl" });
|
|
284328
|
-
}
|
|
284329
|
-
});
|
|
284330
|
-
const reapOrphanMain = (state, key, main) => gen(function* () {
|
|
284331
|
-
if (main.ageMs < state.config.minOrphanAgeMs) return;
|
|
284332
|
-
if (state.observeOnly) {
|
|
284333
|
-
state.wouldReapOrphans.push(key);
|
|
284334
|
-
return;
|
|
284335
|
-
}
|
|
284336
|
-
if (!(yield* attemptFlag(state, `killProcess(${main.pid})`, () => state.deps.killProcess(main.pid)))) return;
|
|
284337
|
-
state.killedOrphans.push(key);
|
|
284338
|
-
yield* increment(browserSessionsReapedTotal, { reason: "orphan" });
|
|
284339
|
-
yield* attemptFlag(state, `removeProfileDir(${main.userDataDir})`, () => state.deps.removeProfileDir(main.userDataDir));
|
|
284340
|
-
});
|
|
284341
|
-
const sweepChromiumMains = (state, registeredByKey, mains) => gen(function* () {
|
|
284342
|
-
const liveKeys = /* @__PURE__ */ new Set();
|
|
284343
|
-
for (const main of mains) {
|
|
284344
|
-
const key = profileKeyForDir(main.userDataDir, state.config.profileRoot);
|
|
284345
|
-
if (key === null) continue;
|
|
284346
|
-
liveKeys.add(key);
|
|
284347
|
-
if (state.config.protectedSessionKeys.has(key)) continue;
|
|
284348
|
-
const registeredSessionId = registeredByKey.get(key);
|
|
284349
|
-
if (registeredSessionId !== void 0) yield* enforceSessionTtl(state, registeredSessionId, main);
|
|
284350
|
-
else yield* reapOrphanMain(state, key, main);
|
|
284351
|
-
}
|
|
284352
|
-
return liveKeys;
|
|
284353
|
-
});
|
|
284354
|
-
function isRetainedProfileDir(state, registeredByKey, liveKeys, entry) {
|
|
284355
|
-
const key = profileKeyForDir(entry.path, state.config.profileRoot);
|
|
284356
|
-
return key === null || state.config.protectedSessionKeys.has(key) || registeredByKey.has(key) || liveKeys.has(key) || entry.ageMs < state.config.minOrphanAgeMs;
|
|
284357
|
-
}
|
|
284358
|
-
const sweepStaleProfileDirs = (state, registeredByKey, liveKeys) => gen(function* () {
|
|
284359
|
-
const profileDirs = yield* attemptOrNull(state, "listProfileDirs", state.deps.listProfileDirs);
|
|
284360
|
-
if (profileDirs === null) return;
|
|
284361
|
-
for (const entry of profileDirs) {
|
|
284362
|
-
if (isRetainedProfileDir(state, registeredByKey, liveKeys, entry)) continue;
|
|
284363
|
-
if (state.observeOnly) {
|
|
284364
|
-
state.wouldRemoveStaleProfiles.push(entry.path);
|
|
284365
|
-
continue;
|
|
284366
|
-
}
|
|
284367
|
-
if (yield* attemptFlag(state, `removeProfileDir(${entry.path})`, () => state.deps.removeProfileDir(entry.path))) {
|
|
284368
|
-
state.removedStaleProfiles.push(entry.path);
|
|
284369
|
-
yield* increment(browserSessionsReapedTotal, { reason: "stale_profile" });
|
|
284370
|
-
}
|
|
284371
|
-
}
|
|
284372
|
-
});
|
|
284373
|
-
function sweepHasActivity(report) {
|
|
284374
|
-
return report.killedOrphans.length > 0 || report.closedTtlSessions.length > 0 || report.removedStaleProfiles.length > 0 || report.wouldReapOrphans.length > 0 || report.wouldCloseTtlSessions.length > 0 || report.wouldRemoveStaleProfiles.length > 0 || report.errors.length > 0;
|
|
284375
|
-
}
|
|
284376
|
-
const logSweepReportIfActive = (report) => sweepHasActivity(report) ? logInfo("browser orphan reaper sweep", {
|
|
284377
|
-
observeOnly: report.observeOnly,
|
|
284378
|
-
killedOrphans: report.killedOrphans,
|
|
284379
|
-
closedTtlSessions: report.closedTtlSessions,
|
|
284380
|
-
removedStaleProfiles: report.removedStaleProfiles,
|
|
284381
|
-
wouldReapOrphans: report.wouldReapOrphans,
|
|
284382
|
-
wouldCloseTtlSessions: report.wouldCloseTtlSessions,
|
|
284383
|
-
wouldRemoveStaleProfiles: report.wouldRemoveStaleProfiles,
|
|
284384
|
-
errors: report.errors
|
|
284385
|
-
}) : void_$1;
|
|
284386
|
-
function createBrowserOrphanReaper(deps, config) {
|
|
284387
|
-
let coldStartGraceConsumed = false;
|
|
284388
|
-
return { sweep: gen(function* () {
|
|
284389
|
-
const observeOnly = !coldStartGraceConsumed;
|
|
284390
|
-
coldStartGraceConsumed = true;
|
|
284391
|
-
const state = makeSweepState(deps, config, observeOnly);
|
|
284392
|
-
const registered = yield* attemptOrNull(state, "listRegisteredSessionIds", deps.listRegisteredSessionIds);
|
|
284393
|
-
if (registered === null) {
|
|
284394
|
-
yield* logWarning$1("browser orphan reaper: session registry unavailable; skipping", { errors: state.errors });
|
|
284395
|
-
return sweepReport(state, true);
|
|
284396
|
-
}
|
|
284397
|
-
const mains = yield* attemptOrNull(state, "listChromiumMains", deps.listChromiumMains);
|
|
284398
|
-
if (mains === null) return sweepReport(state, false);
|
|
284399
|
-
const registeredByKey = mapRegisteredByKey(registered);
|
|
284400
|
-
yield* sweepStaleProfileDirs(state, registeredByKey, yield* sweepChromiumMains(state, registeredByKey, mains));
|
|
284401
|
-
const report = sweepReport(state, false);
|
|
284402
|
-
yield* logSweepReportIfActive(report);
|
|
284403
|
-
return report;
|
|
284404
|
-
}) };
|
|
284405
|
-
}
|
|
284406
|
-
function sleep(ms) {
|
|
284407
|
-
return new Promise((resolve) => {
|
|
284408
|
-
setTimeout(resolve, ms).unref?.();
|
|
284409
|
-
});
|
|
284410
|
-
}
|
|
284411
|
-
function isEsrch(error) {
|
|
284412
|
-
return typeof error === "object" && error !== null && "code" in error && error.code === "ESRCH";
|
|
284413
|
-
}
|
|
284414
|
-
/** SIGTERM, poll for exit, escalate to SIGKILL. Chromium tears down its own children. */
|
|
284415
|
-
async function killProcessWithEscalation(pid) {
|
|
284416
|
-
try {
|
|
284417
|
-
process.kill(pid, "SIGTERM");
|
|
284418
|
-
} catch (error) {
|
|
284419
|
-
if (isEsrch(error)) return;
|
|
284420
|
-
throw error;
|
|
284421
|
-
}
|
|
284422
|
-
const deadline = Date.now() + KILL_ESCALATION_TIMEOUT_MS;
|
|
284423
|
-
while (Date.now() < deadline) {
|
|
284424
|
-
await sleep(KILL_POLL_INTERVAL_MS);
|
|
284425
|
-
try {
|
|
284426
|
-
process.kill(pid, 0);
|
|
284427
|
-
} catch {
|
|
284428
|
-
return;
|
|
284429
|
-
}
|
|
284430
|
-
}
|
|
284431
|
-
try {
|
|
284432
|
-
process.kill(pid, "SIGKILL");
|
|
284433
|
-
} catch (error) {
|
|
284434
|
-
if (!isEsrch(error)) throw error;
|
|
284435
|
-
}
|
|
284436
|
-
}
|
|
284437
|
-
function createSystemBrowserOrphanReaperDeps(input) {
|
|
284438
|
-
const { profileRoot } = input;
|
|
284439
|
-
return {
|
|
284440
|
-
listChromiumMains: async () => {
|
|
284441
|
-
const { stdout } = await execFileAsync$2("ps", ["-axo", "pid=,etime=,args="], { maxBuffer: 16 * 1024 * 1024 });
|
|
284442
|
-
const mains = [];
|
|
284443
|
-
for (const line of stdout.split("\n")) {
|
|
284444
|
-
const match = /^\s*(\d+)\s+(\S+)\s+(.+)$/.exec(line);
|
|
284445
|
-
if (!match) continue;
|
|
284446
|
-
const [, pidRaw, etime, command] = match;
|
|
284447
|
-
if (!isChromiumMainCommand(command, profileRoot)) continue;
|
|
284448
|
-
const userDataDir = extractUserDataDir(command);
|
|
284449
|
-
const ageMs = parseEtimeToMs(etime);
|
|
284450
|
-
if (userDataDir === null || ageMs === null) continue;
|
|
284451
|
-
mains.push({
|
|
284452
|
-
pid: Number(pidRaw),
|
|
284453
|
-
userDataDir,
|
|
284454
|
-
ageMs
|
|
284455
|
-
});
|
|
284456
|
-
}
|
|
284457
|
-
return mains;
|
|
284458
|
-
},
|
|
284459
|
-
listRegisteredSessionIds: async () => (await input.listSessions()).map((session) => session.sessionId),
|
|
284460
|
-
closeSession: input.closeSession,
|
|
284461
|
-
killProcess: killProcessWithEscalation,
|
|
284462
|
-
listProfileDirs: async () => {
|
|
284463
|
-
const entries = await fs$1.readdir(profileRoot, { withFileTypes: true }).catch(() => []);
|
|
284464
|
-
const now = Date.now();
|
|
284465
|
-
const dirs = [];
|
|
284466
|
-
for (const entry of entries) {
|
|
284467
|
-
if (!entry.isDirectory()) continue;
|
|
284468
|
-
const dirPath = path.join(profileRoot, entry.name);
|
|
284469
|
-
const stat = await fs$1.stat(dirPath).catch(() => null);
|
|
284470
|
-
if (stat === null) continue;
|
|
284471
|
-
dirs.push({
|
|
284472
|
-
path: dirPath,
|
|
284473
|
-
ageMs: Math.max(0, now - stat.mtimeMs)
|
|
284474
|
-
});
|
|
284475
|
-
}
|
|
284476
|
-
return dirs;
|
|
284477
|
-
},
|
|
284478
|
-
removeProfileDir: async (dir) => {
|
|
284479
|
-
const resolved = path.resolve(dir);
|
|
284480
|
-
const resolvedRoot = path.resolve(profileRoot);
|
|
284481
|
-
if (resolved === resolvedRoot || !resolved.startsWith(resolvedRoot + path.sep)) throw new Error(`refusing to remove path outside the profile root: ${dir}`);
|
|
284482
|
-
await fs$1.rm(resolved, {
|
|
284483
|
-
recursive: true,
|
|
284484
|
-
force: true
|
|
284485
|
-
});
|
|
284486
|
-
}
|
|
284487
|
-
};
|
|
284488
|
-
}
|
|
284489
|
-
//#endregion
|
|
284490
|
-
//#region src/browser/browserControlProtocol.ts
|
|
284491
|
-
function resolveBrowserControlDecision(state, metadata) {
|
|
284492
|
-
const hasSessionEpoch = metadata.sessionEpoch !== void 0;
|
|
284493
|
-
const hasControlSequence = metadata.controlSequence !== void 0;
|
|
284494
|
-
const expectedControlSequence = state.lastAppliedControlSequence + 1;
|
|
284495
|
-
if (!hasSessionEpoch && !hasControlSequence) return {
|
|
284496
|
-
type: "apply",
|
|
284497
|
-
controlSequence: expectedControlSequence
|
|
284498
|
-
};
|
|
284499
|
-
if (!hasSessionEpoch || !hasControlSequence) return {
|
|
284500
|
-
type: "reject",
|
|
284501
|
-
reason: "incomplete_metadata",
|
|
284502
|
-
expectedSessionEpoch: state.sessionEpoch,
|
|
284503
|
-
expectedControlSequence,
|
|
284504
|
-
acknowledgedControlSequence: state.lastAppliedControlSequence
|
|
284505
|
-
};
|
|
284506
|
-
if (metadata.sessionEpoch !== state.sessionEpoch) return {
|
|
284507
|
-
type: "reject",
|
|
284508
|
-
reason: "stale_epoch",
|
|
284509
|
-
expectedSessionEpoch: state.sessionEpoch,
|
|
284510
|
-
acknowledgedControlSequence: state.lastAppliedControlSequence
|
|
284511
|
-
};
|
|
284512
|
-
if (metadata.controlSequence <= state.lastAppliedControlSequence) return {
|
|
284513
|
-
type: "duplicate",
|
|
284514
|
-
acknowledgedControlSequence: state.lastAppliedControlSequence
|
|
284515
|
-
};
|
|
284516
|
-
if (metadata.controlSequence !== expectedControlSequence) return {
|
|
284517
|
-
type: "reject",
|
|
284518
|
-
reason: "sequence_gap",
|
|
284519
|
-
expectedControlSequence,
|
|
284520
|
-
acknowledgedControlSequence: state.lastAppliedControlSequence
|
|
284521
|
-
};
|
|
284522
|
-
return {
|
|
284523
|
-
type: "apply",
|
|
284524
|
-
controlSequence: metadata.controlSequence
|
|
284525
|
-
};
|
|
284526
|
-
}
|
|
284527
|
-
function browserControlRejectionDetail(decision) {
|
|
284528
|
-
switch (decision.reason) {
|
|
284529
|
-
case "stale_epoch": return `Browser session is stale. Expected epoch ${decision.expectedSessionEpoch}.`;
|
|
284530
|
-
case "sequence_gap": return `Browser control sequence is out of order. Expected ${decision.expectedControlSequence}.`;
|
|
284531
|
-
case "incomplete_metadata": return "Browser control ordering requires both sessionEpoch and controlSequence.";
|
|
284532
|
-
}
|
|
284533
|
-
}
|
|
284534
|
-
//#endregion
|
|
284535
|
-
//#region src/browser/AgentBrowserHostBridgeAdapter.ts
|
|
284536
|
-
const DEFAULT_URL = "about:blank";
|
|
284537
|
-
const DEFAULT_RENDER_MODE = "interactive";
|
|
284538
|
-
const DEFAULT_AGENT_BROWSER_ID = "embedded";
|
|
284539
|
-
const DEFAULT_AGENT_BROWSER_VIEWPORT = {
|
|
284540
|
-
width: 1280,
|
|
284541
|
-
height: 720
|
|
284542
|
-
};
|
|
284856
|
+
//#region src/browser/NativeHostBrowserBridgeClient.ts
|
|
284543
284857
|
const NATIVE_HOST_EVENT_RECONNECT_BASE_MS = 25;
|
|
284544
284858
|
const NATIVE_HOST_EVENT_RECONNECT_MAX_MS = HOST_BRIDGE_RECONNECT_MAX_MS;
|
|
284545
|
-
const ABSOLUTE_WEB_URL_PATTERN = /^[a-z][a-z\d+\-.]*:\/\//i;
|
|
284546
|
-
const SPECIAL_URL_PATTERN = /^(about|blob|chrome|data|devtools|file):/i;
|
|
284547
|
-
const LOCAL_URL_LIKE_PATTERN = /^(localhost|127\.0\.0\.1|\[::1\]|::1|[a-z0-9.-]+\.localhost)(?::\d+)?(?:[/?#].*)?$/i;
|
|
284548
|
-
const DEFAULT_AGENT_BROWSER_DISPLAY = {
|
|
284549
|
-
transport: "remote-webrtc",
|
|
284550
|
-
status: "connected",
|
|
284551
|
-
reconnect: {
|
|
284552
|
-
status: "ready",
|
|
284553
|
-
attempt: 0
|
|
284554
|
-
}
|
|
284555
|
-
};
|
|
284556
|
-
const DEFAULT_AGENT_BROWSER_CONTROL_COMMAND_TIMEOUT_MS = 1500;
|
|
284557
|
-
const DEFAULT_AGENT_BROWSER_FAST_COMMAND_TIMEOUT_MS = 1e4;
|
|
284558
|
-
const DEFAULT_AGENT_BROWSER_NAVIGATION_COMMAND_TIMEOUT_MS = 9e4;
|
|
284559
|
-
const DEFAULT_AGENT_BROWSER_CAPTURE_COMMAND_TIMEOUT_MS = 9e4;
|
|
284560
|
-
const DEFAULT_AGENT_BROWSER_BRIDGE_OPEN_TIMEOUT_MS = 9e4;
|
|
284561
|
-
const DEFAULT_AGENT_BROWSER_BRIDGE_CLOSE_TIMEOUT_MS = 3e4;
|
|
284562
|
-
const DEFAULT_AGENT_BROWSER_PLAYWRIGHT_BACKEND_CLOSE_TIMEOUT_MS = 1e4;
|
|
284563
|
-
const REMOTE_WEBRTC_UNAVAILABLE_DETAIL = "Remote WebRTC browser display is not available: the current Agent Browser runtime exposes tab control but does not provide a media transport.";
|
|
284564
|
-
const REMOTE_WEBRTC_RELAY_REQUIRED_DETAIL = "Remote WebRTC browser display requires a TURN relay in cloud mode. Configure FORGE_BROWSER_WEBRTC_ICE_SERVERS with authenticated turn: or turns: URLs.";
|
|
284565
|
-
function nowIso$2() {
|
|
284566
|
-
return (/* @__PURE__ */ new Date()).toISOString();
|
|
284567
|
-
}
|
|
284568
|
-
function positiveTimeoutMs(value) {
|
|
284569
|
-
if (!value) return;
|
|
284570
|
-
const parsed = Number.parseInt(value, 10);
|
|
284571
|
-
return Number.isFinite(parsed) && parsed > 0 ? parsed : void 0;
|
|
284572
|
-
}
|
|
284573
|
-
function agentBrowserCommandTimeoutMs(command, env = process.env) {
|
|
284574
|
-
const explicitCommandTimeout = positiveTimeoutMs(env.FORGE_AGENT_BROWSER_BRIDGE_COMMAND_TIMEOUT_MS);
|
|
284575
|
-
if (explicitCommandTimeout !== void 0) return explicitCommandTimeout;
|
|
284576
|
-
if (isAgentBrowserBoundedControlCommand(command)) return positiveTimeoutMs(env.FORGE_AGENT_BROWSER_BRIDGE_CONTROL_TIMEOUT_MS) ?? DEFAULT_AGENT_BROWSER_CONTROL_COMMAND_TIMEOUT_MS;
|
|
284577
|
-
switch (command.type) {
|
|
284578
|
-
case "screenshot":
|
|
284579
|
-
case "snapshot": return positiveTimeoutMs(env.FORGE_AGENT_BROWSER_BRIDGE_CAPTURE_TIMEOUT_MS) ?? DEFAULT_AGENT_BROWSER_CAPTURE_COMMAND_TIMEOUT_MS;
|
|
284580
|
-
case "newTab":
|
|
284581
|
-
case "navigate":
|
|
284582
|
-
case "goBack":
|
|
284583
|
-
case "goForward":
|
|
284584
|
-
case "reload": return DEFAULT_AGENT_BROWSER_NAVIGATION_COMMAND_TIMEOUT_MS;
|
|
284585
|
-
default: return DEFAULT_AGENT_BROWSER_FAST_COMMAND_TIMEOUT_MS;
|
|
284586
|
-
}
|
|
284587
|
-
}
|
|
284588
|
-
function agentBrowserCloseTimeoutMs(env = process.env) {
|
|
284589
|
-
return positiveTimeoutMs(env.FORGE_AGENT_BROWSER_BRIDGE_CLOSE_TIMEOUT_MS) ?? DEFAULT_AGENT_BROWSER_BRIDGE_CLOSE_TIMEOUT_MS;
|
|
284590
|
-
}
|
|
284591
|
-
function agentBrowserOpenTimeoutMs(env = process.env) {
|
|
284592
|
-
return positiveTimeoutMs(env.FORGE_AGENT_BROWSER_BRIDGE_OPEN_TIMEOUT_MS) ?? DEFAULT_AGENT_BROWSER_BRIDGE_OPEN_TIMEOUT_MS;
|
|
284593
|
-
}
|
|
284594
|
-
function agentBrowserFastTimeoutMs(env = process.env) {
|
|
284595
|
-
return positiveTimeoutMs(env.FORGE_AGENT_BROWSER_BRIDGE_COMMAND_TIMEOUT_MS) ?? DEFAULT_AGENT_BROWSER_FAST_COMMAND_TIMEOUT_MS;
|
|
284596
|
-
}
|
|
284597
|
-
function agentBrowserPlaywrightBackendCloseTimeoutMs(env = process.env) {
|
|
284598
|
-
return positiveTimeoutMs(env.FORGE_AGENT_BROWSER_PLAYWRIGHT_BACKEND_CLOSE_TIMEOUT_MS) ?? DEFAULT_AGENT_BROWSER_PLAYWRIGHT_BACKEND_CLOSE_TIMEOUT_MS;
|
|
284599
|
-
}
|
|
284600
|
-
async function withAgentBrowserBridgeTimeout(label, promise, timeoutMs) {
|
|
284601
|
-
let timeout;
|
|
284602
|
-
try {
|
|
284603
|
-
return await Promise.race([promise, new Promise((_, reject) => {
|
|
284604
|
-
timeout = setTimeout(() => {
|
|
284605
|
-
reject(/* @__PURE__ */ new Error(`${label} timed out after ${Math.ceil(timeoutMs / 1e3)}s.`));
|
|
284606
|
-
}, timeoutMs);
|
|
284607
|
-
})]);
|
|
284608
|
-
} finally {
|
|
284609
|
-
if (timeout !== void 0) clearTimeout(timeout);
|
|
284610
|
-
}
|
|
284611
|
-
}
|
|
284612
|
-
async function openAgentBrowserSessionReady(bridge, input) {
|
|
284613
|
-
let resolveStarted;
|
|
284614
|
-
const started = new Promise((resolve) => {
|
|
284615
|
-
resolveStarted = resolve;
|
|
284616
|
-
});
|
|
284617
|
-
const unsubscribe = await bridge.subscribe({
|
|
284618
|
-
sessionId: input.sessionId,
|
|
284619
|
-
...input.browserId ? { browserId: input.browserId } : {}
|
|
284620
|
-
}, (event) => {
|
|
284621
|
-
if (event.type === "session.started" && event.sessionId === input.sessionId) resolveStarted(event.payload.snapshot);
|
|
284622
|
-
});
|
|
284623
|
-
try {
|
|
284624
|
-
return await Promise.race([bridge.openSession(input), started]);
|
|
284625
|
-
} finally {
|
|
284626
|
-
unsubscribe();
|
|
284627
|
-
}
|
|
284628
|
-
}
|
|
284629
|
-
function isAgentBrowserBridgeTimeoutError(error) {
|
|
284630
|
-
const message = [
|
|
284631
|
-
is$1(BrowserError)(error) ? error.detail : void 0,
|
|
284632
|
-
error instanceof Error ? error.message : void 0,
|
|
284633
|
-
error instanceof Error ? error.stack : void 0,
|
|
284634
|
-
typeof error === "string" ? error : void 0,
|
|
284635
|
-
(() => {
|
|
284636
|
-
try {
|
|
284637
|
-
return JSON.stringify(error);
|
|
284638
|
-
} catch {
|
|
284639
|
-
return;
|
|
284640
|
-
}
|
|
284641
|
-
})(),
|
|
284642
|
-
String(error)
|
|
284643
|
-
].filter((part) => typeof part === "string" && part.length > 0).join("\n");
|
|
284644
|
-
return /timed out after \d+s\./.test(message);
|
|
284645
|
-
}
|
|
284646
|
-
function isAgentBrowserBoundedControlCommand(command) {
|
|
284647
|
-
switch (command.type) {
|
|
284648
|
-
case "click":
|
|
284649
|
-
case "clickPoint":
|
|
284650
|
-
case "pressKey":
|
|
284651
|
-
case "resetViewport":
|
|
284652
|
-
case "scroll":
|
|
284653
|
-
case "setViewport":
|
|
284654
|
-
case "type":
|
|
284655
|
-
case "typeText":
|
|
284656
|
-
case "wheel": return true;
|
|
284657
|
-
default: return false;
|
|
284658
|
-
}
|
|
284659
|
-
}
|
|
284660
|
-
function snapshotAfterTimedOutControl(snapshot, command) {
|
|
284661
|
-
switch (command.type) {
|
|
284662
|
-
case "setViewport": return {
|
|
284663
|
-
...snapshot,
|
|
284664
|
-
viewport: command.viewport,
|
|
284665
|
-
updatedAt: nowIso$2()
|
|
284666
|
-
};
|
|
284667
|
-
case "resetViewport": return {
|
|
284668
|
-
...snapshot,
|
|
284669
|
-
viewport: DEFAULT_AGENT_BROWSER_VIEWPORT,
|
|
284670
|
-
updatedAt: nowIso$2()
|
|
284671
|
-
};
|
|
284672
|
-
default: return snapshot;
|
|
284673
|
-
}
|
|
284674
|
-
}
|
|
284675
|
-
async function closeAgentBrowserBackendWithTimeout(closeBackend, env = process.env) {
|
|
284676
|
-
try {
|
|
284677
|
-
await withAgentBrowserBridgeTimeout("Agent Browser Playwright backend close", closeBackend(), agentBrowserPlaywrightBackendCloseTimeoutMs(env));
|
|
284678
|
-
} catch (error) {
|
|
284679
|
-
if (!isAgentBrowserBridgeTimeoutError(error)) throw error;
|
|
284680
|
-
console.warn("[browser] Agent Browser Playwright backend close timed out; continuing", { timeoutMs: agentBrowserPlaywrightBackendCloseTimeoutMs(env) });
|
|
284681
|
-
}
|
|
284682
|
-
}
|
|
284683
|
-
var ForgeAgentBrowserPlaywrightBackend = class extends PlaywrightBackend {
|
|
284684
|
-
async close() {
|
|
284685
|
-
await closeAgentBrowserBackendWithTimeout(() => super.close());
|
|
284686
|
-
}
|
|
284687
|
-
};
|
|
284688
|
-
function toBrowserError(error, fallback) {
|
|
284689
|
-
console.warn("[browser] Agent Browser host bridge operation failed", {
|
|
284690
|
-
operation: fallback,
|
|
284691
|
-
error
|
|
284692
|
-
});
|
|
284693
|
-
if (is$1(BrowserError)(error)) return error;
|
|
284694
|
-
if (error instanceof Error && error.message.trim().length > 0) return new BrowserError({ detail: error.message.trim() });
|
|
284695
|
-
return new BrowserError({ detail: fallback });
|
|
284696
|
-
}
|
|
284697
|
-
function remoteWebRtcUnavailableError(_input) {
|
|
284698
|
-
return new BrowserError({ detail: REMOTE_WEBRTC_UNAVAILABLE_DETAIL });
|
|
284699
|
-
}
|
|
284700
|
-
function remoteWebRtcRelayRequiredError(_input) {
|
|
284701
|
-
return new BrowserError({ detail: REMOTE_WEBRTC_RELAY_REQUIRED_DETAIL });
|
|
284702
|
-
}
|
|
284703
|
-
function subscriptionMatchesEvent(subscription, event) {
|
|
284704
|
-
if (subscription.input.threadId && subscription.input.threadId !== event.threadId) return false;
|
|
284705
|
-
if (subscription.input.browserId && subscription.input.browserId !== event.browserId) return false;
|
|
284706
|
-
if (subscription.input.environmentId && subscription.input.environmentId !== event.environmentId) return false;
|
|
284707
|
-
if (subscription.input.userId && subscription.input.userId !== event.userId) return false;
|
|
284708
|
-
return true;
|
|
284709
|
-
}
|
|
284710
|
-
function sessionIdForThread(threadId) {
|
|
284711
|
-
return BrowserSessionId.make(threadId);
|
|
284712
|
-
}
|
|
284713
|
-
function browserBackendOptionsForSession(input) {
|
|
284714
|
-
return {
|
|
284715
|
-
...input.baseOptions,
|
|
284716
|
-
userDataDir: browserProfileDirForSession(input.sessionId)
|
|
284717
|
-
};
|
|
284718
|
-
}
|
|
284719
|
-
function browserIdFrom(input) {
|
|
284720
|
-
return BrowserId.make(input.browserId ?? DEFAULT_AGENT_BROWSER_ID);
|
|
284721
|
-
}
|
|
284722
|
-
function normalizeUserVisibleBrowserUrl(url) {
|
|
284723
|
-
const trimmed = url.trim();
|
|
284724
|
-
if (trimmed.length === 0) return trimmed;
|
|
284725
|
-
if (ABSOLUTE_WEB_URL_PATTERN.test(trimmed) || SPECIAL_URL_PATTERN.test(trimmed) || trimmed.startsWith("//")) return trimmed;
|
|
284726
|
-
if (LOCAL_URL_LIKE_PATTERN.test(trimmed)) return `http://${trimmed}`;
|
|
284727
|
-
return `https://${trimmed}`;
|
|
284728
|
-
}
|
|
284729
|
-
function agentBrowserDisplayForRuntimeMode(input) {
|
|
284730
|
-
if (input.mode !== "desktop") {
|
|
284731
|
-
const hasRelay = hasTurnRelay(input.iceServers ?? []);
|
|
284732
|
-
return {
|
|
284733
|
-
transport: "remote-webrtc",
|
|
284734
|
-
status: "degraded",
|
|
284735
|
-
reason: input.requiresRelay === true && !hasRelay ? "network_unreachable" : "transport_unavailable",
|
|
284736
|
-
detail: input.requiresRelay === true && !hasRelay ? REMOTE_WEBRTC_RELAY_REQUIRED_DETAIL : "Remote WebRTC browser display is not attached yet.",
|
|
284737
|
-
...input.viewport ? { viewport: input.viewport } : {},
|
|
284738
|
-
...input.iceServers && input.iceServers.length > 0 ? { iceServers: input.iceServers } : {},
|
|
284739
|
-
reconnect: {
|
|
284740
|
-
status: "ready",
|
|
284741
|
-
attempt: 0
|
|
284742
|
-
}
|
|
284743
|
-
};
|
|
284744
|
-
}
|
|
284745
|
-
return {
|
|
284746
|
-
transport: "native-embedded",
|
|
284747
|
-
status: "connected",
|
|
284748
|
-
...input.viewport ? { viewport: input.viewport } : {},
|
|
284749
|
-
reconnect: {
|
|
284750
|
-
status: "ready",
|
|
284751
|
-
attempt: 0
|
|
284752
|
-
}
|
|
284753
|
-
};
|
|
284754
|
-
}
|
|
284755
|
-
function hasTurnRelay(iceServers) {
|
|
284756
|
-
return browserWebRtcIceServersIncludeRelay(iceServers);
|
|
284757
|
-
}
|
|
284758
|
-
async function resolveDisplayIceServers(options) {
|
|
284759
|
-
return options?.resolveDisplayIceServers ? await options.resolveDisplayIceServers() : options?.displayIceServers ?? [];
|
|
284760
|
-
}
|
|
284761
|
-
function webRtcDisplayRequiresRelay(serverConfig) {
|
|
284762
|
-
return serverConfig.mode === "web" && serverConfig.devUrl === void 0;
|
|
284763
|
-
}
|
|
284764
|
-
function toForgeTab(input) {
|
|
284765
|
-
return {
|
|
284766
|
-
tabId: BrowserTabId.make(input.tab.id),
|
|
284767
|
-
restoreKey: input.tab.id,
|
|
284768
|
-
...input.tab.title ? { title: input.tab.title } : {},
|
|
284769
|
-
url: input.tab.url ?? DEFAULT_URL,
|
|
284770
|
-
isActive: input.tab.isActive,
|
|
284771
|
-
canGoBack: input.tab.canGoBack ?? false,
|
|
284772
|
-
canGoForward: input.tab.canGoForward ?? false,
|
|
284773
|
-
renderMode: DEFAULT_RENDER_MODE,
|
|
284774
|
-
status: "live",
|
|
284775
|
-
...input.tab.navigationStatus !== void 0 ? { navigationStatus: input.tab.navigationStatus } : {},
|
|
284776
|
-
...input.tab.requestedUrl !== void 0 ? { requestedUrl: input.tab.requestedUrl } : {},
|
|
284777
|
-
...input.tab.navigationError !== void 0 ? { navigationError: input.tab.navigationError } : {},
|
|
284778
|
-
updatedAt: input.updatedAt
|
|
284779
|
-
};
|
|
284780
|
-
}
|
|
284781
|
-
function toForgeSession(input) {
|
|
284782
|
-
return {
|
|
284783
|
-
browserId: BrowserId.make(input.snapshot.browserId),
|
|
284784
|
-
threadId: input.threadId,
|
|
284785
|
-
environmentId: EnvironmentId.make(input.environmentId ?? DEFAULT_BROWSER_ENVIRONMENT_ID),
|
|
284786
|
-
userId: BrowserUserId.make(input.userId ?? "local-user"),
|
|
284787
|
-
agentId: input.agentId ?? null,
|
|
284788
|
-
ownerRole: input.ownerRole ?? "lead",
|
|
284789
|
-
name: input.snapshot.name,
|
|
284790
|
-
visibility: input.snapshot.visibility,
|
|
284791
|
-
viewport: input.snapshot.viewport,
|
|
284792
|
-
display: input.display ?? DEFAULT_AGENT_BROWSER_DISPLAY,
|
|
284793
|
-
sessionId: BrowserSessionId.make(input.snapshot.sessionId),
|
|
284794
|
-
...input.controlState ? {
|
|
284795
|
-
sessionEpoch: input.controlState.sessionEpoch,
|
|
284796
|
-
lastAppliedControlSequence: input.controlState.lastAppliedControlSequence
|
|
284797
|
-
} : {},
|
|
284798
|
-
activeTabId: input.snapshot.activeTabId ? BrowserTabId.make(input.snapshot.activeTabId) : null,
|
|
284799
|
-
tabs: input.snapshot.tabs.map((tab) => toForgeTab({
|
|
284800
|
-
tab,
|
|
284801
|
-
updatedAt: input.snapshot.updatedAt
|
|
284802
|
-
})),
|
|
284803
|
-
updatedAt: input.snapshot.updatedAt
|
|
284804
|
-
};
|
|
284805
|
-
}
|
|
284806
|
-
function resolveDisplay(snapshot, options) {
|
|
284807
|
-
if (typeof options?.display === "function") return options.display(snapshot);
|
|
284808
|
-
return options?.display ?? DEFAULT_AGENT_BROWSER_DISPLAY;
|
|
284809
|
-
}
|
|
284810
284859
|
function nativeHostBridgeUrl(baseUrl) {
|
|
284811
284860
|
return hostBridgePathUrl(baseUrl, "/agent-browser/host-bridge");
|
|
284812
284861
|
}
|
|
@@ -284898,6 +284947,63 @@ function subscribeToNativeHostBridgeEventsViaNodeHttp(input) {
|
|
|
284898
284947
|
request.end();
|
|
284899
284948
|
});
|
|
284900
284949
|
}
|
|
284950
|
+
async function requestNativeHostBridge(input) {
|
|
284951
|
+
const abortController = new AbortController();
|
|
284952
|
+
const timeout = setTimeout(() => abortController.abort(), input.method === "closeSession" ? input.deps.closeSessionTimeoutMs() : 3e4);
|
|
284953
|
+
const response = await input.fetchImpl(nativeHostBridgeUrl(input.options.baseUrl), {
|
|
284954
|
+
method: "POST",
|
|
284955
|
+
headers: {
|
|
284956
|
+
authorization: `Bearer ${input.options.token}`,
|
|
284957
|
+
"content-type": "application/json"
|
|
284958
|
+
},
|
|
284959
|
+
body: JSON.stringify({
|
|
284960
|
+
method: input.method,
|
|
284961
|
+
params: input.params
|
|
284962
|
+
}),
|
|
284963
|
+
signal: abortController.signal
|
|
284964
|
+
}).finally(() => {
|
|
284965
|
+
clearTimeout(timeout);
|
|
284966
|
+
});
|
|
284967
|
+
if (!response.ok) {
|
|
284968
|
+
const message = await readNativeHostBridgeError(response);
|
|
284969
|
+
throw new Error(`Native browser host bridge request failed: ${response.status} ${message}`);
|
|
284970
|
+
}
|
|
284971
|
+
return (await response.json()).result;
|
|
284972
|
+
}
|
|
284973
|
+
async function readNativeHostBridgeEventsViaFetch(input) {
|
|
284974
|
+
const response = await input.fetchImpl(nativeHostBridgeEventsUrl(input.options.baseUrl, input.identity), {
|
|
284975
|
+
headers: { authorization: `Bearer ${input.options.token}` },
|
|
284976
|
+
signal: input.abortSignal
|
|
284977
|
+
});
|
|
284978
|
+
if (!response.ok) {
|
|
284979
|
+
const message = await readNativeHostBridgeError(response);
|
|
284980
|
+
throw new Error(`Native browser host bridge event stream failed: ${response.status} ${message}`);
|
|
284981
|
+
}
|
|
284982
|
+
const reader = response.body?.getReader();
|
|
284983
|
+
if (!reader) throw new Error("Native browser host bridge event stream is unavailable.");
|
|
284984
|
+
await readNativeHostBridgeEventReader(reader, input);
|
|
284985
|
+
}
|
|
284986
|
+
async function readNativeHostBridgeEventReader(reader, input) {
|
|
284987
|
+
const decoder = new TextDecoder();
|
|
284988
|
+
let buffer = "";
|
|
284989
|
+
while (!input.abortSignal.aborted) {
|
|
284990
|
+
const { done, value } = await reader.read();
|
|
284991
|
+
if (done) return;
|
|
284992
|
+
buffer = drainNativeHostBridgeNdjsonBuffer(buffer + decoder.decode(value, { stream: true }), input.callback);
|
|
284993
|
+
}
|
|
284994
|
+
}
|
|
284995
|
+
async function readNativeHostBridgeEvents(input) {
|
|
284996
|
+
if (!input.options.fetch) {
|
|
284997
|
+
await subscribeToNativeHostBridgeEventsViaNodeHttp({
|
|
284998
|
+
abortSignal: input.abortSignal,
|
|
284999
|
+
callback: input.callback,
|
|
285000
|
+
input: input.identity,
|
|
285001
|
+
options: input.options
|
|
285002
|
+
});
|
|
285003
|
+
return;
|
|
285004
|
+
}
|
|
285005
|
+
await readNativeHostBridgeEventsViaFetch(input);
|
|
285006
|
+
}
|
|
284901
285007
|
function abortableDelay(ms, signal) {
|
|
284902
285008
|
if (signal.aborted) return Promise.resolve();
|
|
284903
285009
|
return new Promise((resolve) => {
|
|
@@ -284908,82 +285014,51 @@ function abortableDelay(ms, signal) {
|
|
|
284908
285014
|
}, { once: true });
|
|
284909
285015
|
});
|
|
284910
285016
|
}
|
|
284911
|
-
function
|
|
284912
|
-
|
|
284913
|
-
|
|
284914
|
-
|
|
284915
|
-
|
|
284916
|
-
|
|
284917
|
-
|
|
284918
|
-
|
|
284919
|
-
|
|
284920
|
-
"content-type": "application/json"
|
|
284921
|
-
},
|
|
284922
|
-
body: JSON.stringify({
|
|
284923
|
-
method,
|
|
284924
|
-
params
|
|
284925
|
-
}),
|
|
284926
|
-
signal: abortController.signal
|
|
284927
|
-
}).finally(() => {
|
|
284928
|
-
clearTimeout(timeout);
|
|
284929
|
-
});
|
|
284930
|
-
if (!response.ok) {
|
|
284931
|
-
const message = await readNativeHostBridgeError(response);
|
|
284932
|
-
throw new Error(`Native browser host bridge request failed: ${response.status} ${message}`);
|
|
285017
|
+
async function maintainNativeHostBridgeEventSubscription(input) {
|
|
285018
|
+
let attempt = 0;
|
|
285019
|
+
while (!input.abortSignal.aborted) {
|
|
285020
|
+
try {
|
|
285021
|
+
await readNativeHostBridgeEvents(input);
|
|
285022
|
+
attempt = 0;
|
|
285023
|
+
} catch (error) {
|
|
285024
|
+
if (input.abortSignal.aborted) return;
|
|
285025
|
+
console.warn("[browser] native host bridge event stream failed", error);
|
|
284933
285026
|
}
|
|
284934
|
-
|
|
284935
|
-
|
|
284936
|
-
|
|
284937
|
-
|
|
284938
|
-
|
|
284939
|
-
|
|
284940
|
-
|
|
284941
|
-
|
|
284942
|
-
|
|
284943
|
-
|
|
284944
|
-
|
|
284945
|
-
|
|
284946
|
-
|
|
284947
|
-
|
|
284948
|
-
|
|
284949
|
-
|
|
284950
|
-
|
|
284951
|
-
|
|
284952
|
-
if (!response.ok) {
|
|
284953
|
-
const message = await readNativeHostBridgeError(response);
|
|
284954
|
-
throw new Error(`Native browser host bridge event stream failed: ${response.status} ${message}`);
|
|
284955
|
-
}
|
|
284956
|
-
const reader = response.body?.getReader();
|
|
284957
|
-
if (!reader) throw new Error("Native browser host bridge event stream is unavailable.");
|
|
284958
|
-
const decoder = new TextDecoder();
|
|
284959
|
-
let buffer = "";
|
|
284960
|
-
while (!abortController.signal.aborted) {
|
|
284961
|
-
const { done, value } = await reader.read();
|
|
284962
|
-
if (done) return;
|
|
284963
|
-
buffer = drainNativeHostBridgeNdjsonBuffer(buffer + decoder.decode(value, { stream: true }), callback);
|
|
284964
|
-
}
|
|
284965
|
-
};
|
|
284966
|
-
(async () => {
|
|
284967
|
-
let attempt = 0;
|
|
284968
|
-
while (!abortController.signal.aborted) {
|
|
284969
|
-
try {
|
|
284970
|
-
await readStream();
|
|
284971
|
-
attempt = 0;
|
|
284972
|
-
} catch (error) {
|
|
284973
|
-
if (abortController.signal.aborted) return;
|
|
284974
|
-
console.warn("[browser] native host bridge event stream failed", error);
|
|
284975
|
-
}
|
|
284976
|
-
if (!abortController.signal.aborted) {
|
|
284977
|
-
const delayMs = hostBridgeReconnectDelayMs(attempt, NATIVE_HOST_EVENT_RECONNECT_BASE_MS, NATIVE_HOST_EVENT_RECONNECT_MAX_MS);
|
|
284978
|
-
attempt += 1;
|
|
284979
|
-
await abortableDelay(delayMs, abortController.signal);
|
|
284980
|
-
}
|
|
284981
|
-
}
|
|
284982
|
-
})();
|
|
284983
|
-
return () => {
|
|
284984
|
-
abortController.abort();
|
|
284985
|
-
};
|
|
285027
|
+
if (!input.abortSignal.aborted) {
|
|
285028
|
+
const delayMs = hostBridgeReconnectDelayMs(attempt, NATIVE_HOST_EVENT_RECONNECT_BASE_MS, NATIVE_HOST_EVENT_RECONNECT_MAX_MS);
|
|
285029
|
+
attempt += 1;
|
|
285030
|
+
await abortableDelay(delayMs, input.abortSignal);
|
|
285031
|
+
}
|
|
285032
|
+
}
|
|
285033
|
+
}
|
|
285034
|
+
function subscribeToNativeHostBridgeEvents(input) {
|
|
285035
|
+
const abortController = new AbortController();
|
|
285036
|
+
maintainNativeHostBridgeEventSubscription({
|
|
285037
|
+
abortSignal: abortController.signal,
|
|
285038
|
+
callback: input.callback,
|
|
285039
|
+
fetchImpl: input.fetchImpl,
|
|
285040
|
+
identity: input.identity,
|
|
285041
|
+
options: input.options
|
|
285042
|
+
});
|
|
285043
|
+
return () => {
|
|
285044
|
+
abortController.abort();
|
|
284986
285045
|
};
|
|
285046
|
+
}
|
|
285047
|
+
function createNativeHostBrowserBridgeClientImplementation(options, deps) {
|
|
285048
|
+
const fetchImpl = options.fetch ?? fetch;
|
|
285049
|
+
const request = (method, params) => requestNativeHostBridge({
|
|
285050
|
+
deps,
|
|
285051
|
+
fetchImpl,
|
|
285052
|
+
method,
|
|
285053
|
+
options,
|
|
285054
|
+
params
|
|
285055
|
+
});
|
|
285056
|
+
const subscribe = (identity, callback) => subscribeToNativeHostBridgeEvents({
|
|
285057
|
+
callback,
|
|
285058
|
+
fetchImpl,
|
|
285059
|
+
identity,
|
|
285060
|
+
options
|
|
285061
|
+
});
|
|
284987
285062
|
return {
|
|
284988
285063
|
closeSession: (input = {}) => request("closeSession", input),
|
|
284989
285064
|
command: (input) => request("command", input),
|
|
@@ -284994,634 +285069,31 @@ function createNativeHostBrowserBridgeClient(options) {
|
|
|
284994
285069
|
subscribe: async (input, callback) => subscribe(input, callback)
|
|
284995
285070
|
};
|
|
284996
285071
|
}
|
|
284997
|
-
|
|
284998
|
-
|
|
284999
|
-
|
|
285000
|
-
|
|
285001
|
-
tabId: command.tabId
|
|
285002
|
-
};
|
|
285003
|
-
case "click": return {
|
|
285004
|
-
type: "click",
|
|
285005
|
-
tabId: command.tabId,
|
|
285006
|
-
selector: command.selector
|
|
285007
|
-
};
|
|
285008
|
-
case "clickPoint": return {
|
|
285009
|
-
type: "clickPoint",
|
|
285010
|
-
tabId: command.tabId,
|
|
285011
|
-
x: command.x,
|
|
285012
|
-
y: command.y,
|
|
285013
|
-
...command.button !== void 0 ? { button: command.button } : {},
|
|
285014
|
-
...command.clickCount !== void 0 ? { clickCount: command.clickCount } : {}
|
|
285015
|
-
};
|
|
285016
|
-
case "closeTab": return {
|
|
285017
|
-
type: "closeTab",
|
|
285018
|
-
tabId: command.tabId
|
|
285019
|
-
};
|
|
285020
|
-
case "consoleLogs": return {
|
|
285021
|
-
type: "consoleLogs",
|
|
285022
|
-
...command.tabId !== void 0 ? { tabId: command.tabId } : {},
|
|
285023
|
-
...command.limit !== void 0 ? { limit: command.limit } : {},
|
|
285024
|
-
...command.levels !== void 0 ? { levels: command.levels } : {}
|
|
285025
|
-
};
|
|
285026
|
-
case "goBack": return {
|
|
285027
|
-
type: "goBack",
|
|
285028
|
-
tabId: command.tabId
|
|
285029
|
-
};
|
|
285030
|
-
case "goForward": return {
|
|
285031
|
-
type: "goForward",
|
|
285032
|
-
tabId: command.tabId
|
|
285033
|
-
};
|
|
285034
|
-
case "nameSession": return {
|
|
285035
|
-
type: "nameSession",
|
|
285036
|
-
name: command.name
|
|
285037
|
-
};
|
|
285038
|
-
case "navigate": return {
|
|
285039
|
-
type: "navigate",
|
|
285040
|
-
url: normalizeUserVisibleBrowserUrl(command.url),
|
|
285041
|
-
...command.tabId !== void 0 ? { tabId: command.tabId } : {}
|
|
285042
|
-
};
|
|
285043
|
-
case "newTab": return {
|
|
285044
|
-
type: "newTab",
|
|
285045
|
-
...command.url !== void 0 ? { url: normalizeUserVisibleBrowserUrl(command.url) } : {},
|
|
285046
|
-
...command.makeActive !== void 0 ? { makeActive: command.makeActive } : {}
|
|
285047
|
-
};
|
|
285048
|
-
case "pressKey": return {
|
|
285049
|
-
type: "pressKey",
|
|
285050
|
-
tabId: command.tabId,
|
|
285051
|
-
key: command.key
|
|
285052
|
-
};
|
|
285053
|
-
case "reload": return {
|
|
285054
|
-
type: "reload",
|
|
285055
|
-
tabId: command.tabId
|
|
285056
|
-
};
|
|
285057
|
-
case "resetViewport": return { type: "resetViewport" };
|
|
285058
|
-
case "screenshot": return {
|
|
285059
|
-
type: "screenshot",
|
|
285060
|
-
tabId: command.tabId,
|
|
285061
|
-
...command.fullPage !== void 0 ? { fullPage: command.fullPage } : {}
|
|
285062
|
-
};
|
|
285063
|
-
case "scroll": return {
|
|
285064
|
-
type: "scroll",
|
|
285065
|
-
tabId: command.tabId,
|
|
285066
|
-
scrollX: 0,
|
|
285067
|
-
scrollY: command.toTop === true ? -900 : 900,
|
|
285068
|
-
...command.x !== void 0 ? { x: command.x } : {},
|
|
285069
|
-
...command.y !== void 0 ? { y: command.y } : {}
|
|
285070
|
-
};
|
|
285071
|
-
case "setViewport": return {
|
|
285072
|
-
type: "setViewport",
|
|
285073
|
-
viewport: command.viewport
|
|
285074
|
-
};
|
|
285075
|
-
case "setVisibility": return {
|
|
285076
|
-
type: "setVisibility",
|
|
285077
|
-
visibility: command.visibility
|
|
285078
|
-
};
|
|
285079
|
-
case "snapshot": return {
|
|
285080
|
-
type: "snapshot",
|
|
285081
|
-
tabId: command.tabId
|
|
285082
|
-
};
|
|
285083
|
-
case "type": return {
|
|
285084
|
-
type: "type",
|
|
285085
|
-
tabId: command.tabId,
|
|
285086
|
-
selector: command.selector,
|
|
285087
|
-
text: command.text,
|
|
285088
|
-
...command.append !== void 0 ? { append: command.append } : {}
|
|
285089
|
-
};
|
|
285090
|
-
case "typeText": return {
|
|
285091
|
-
type: "typeText",
|
|
285092
|
-
tabId: command.tabId,
|
|
285093
|
-
text: command.text,
|
|
285094
|
-
...command.pressEnter !== void 0 ? { pressEnter: command.pressEnter } : {}
|
|
285095
|
-
};
|
|
285096
|
-
case "wheel": return {
|
|
285097
|
-
type: "wheel",
|
|
285098
|
-
tabId: command.tabId,
|
|
285099
|
-
deltaX: command.deltaX,
|
|
285100
|
-
deltaY: command.deltaY,
|
|
285101
|
-
...command.x !== void 0 ? { x: command.x } : {},
|
|
285102
|
-
...command.y !== void 0 ? { y: command.y } : {}
|
|
285103
|
-
};
|
|
285104
|
-
default: throw new Error(`Unsupported browser command: ${command.type}`);
|
|
285105
|
-
}
|
|
285106
|
-
}
|
|
285107
|
-
function toStreamEvent(input) {
|
|
285108
|
-
const base = {
|
|
285109
|
-
eventId: EventId.make(crypto.randomUUID()),
|
|
285110
|
-
browserId: input.browserId,
|
|
285111
|
-
threadId: input.threadId,
|
|
285112
|
-
environmentId: EnvironmentId.make(input.environmentId),
|
|
285113
|
-
userId: BrowserUserId.make(input.userId),
|
|
285114
|
-
agentId: input.agentId,
|
|
285115
|
-
...input.controlState ? { sessionEpoch: input.controlState.sessionEpoch } : {},
|
|
285116
|
-
createdAt: nowIso$2()
|
|
285117
|
-
};
|
|
285118
|
-
switch (input.event.type) {
|
|
285119
|
-
case "session.started": return {
|
|
285120
|
-
...base,
|
|
285121
|
-
type: "session.started",
|
|
285122
|
-
payload: { snapshot: toForgeSession({
|
|
285123
|
-
snapshot: input.event.payload.snapshot,
|
|
285124
|
-
threadId: input.threadId,
|
|
285125
|
-
environmentId: input.environmentId,
|
|
285126
|
-
userId: input.userId,
|
|
285127
|
-
agentId: input.agentId,
|
|
285128
|
-
ownerRole: input.ownerRole,
|
|
285129
|
-
display: input.display,
|
|
285130
|
-
controlState: input.controlState
|
|
285131
|
-
}) }
|
|
285132
|
-
};
|
|
285133
|
-
case "session.updated": return {
|
|
285134
|
-
...base,
|
|
285135
|
-
type: input.event.type,
|
|
285136
|
-
payload: { snapshot: toForgeSession({
|
|
285137
|
-
snapshot: input.event.payload.snapshot,
|
|
285138
|
-
threadId: input.threadId,
|
|
285139
|
-
environmentId: input.environmentId,
|
|
285140
|
-
userId: input.userId,
|
|
285141
|
-
agentId: input.agentId,
|
|
285142
|
-
ownerRole: input.ownerRole,
|
|
285143
|
-
display: input.display,
|
|
285144
|
-
controlState: input.controlState
|
|
285145
|
-
}) }
|
|
285146
|
-
};
|
|
285147
|
-
case "session.closed": return {
|
|
285148
|
-
...base,
|
|
285149
|
-
type: "session.closed",
|
|
285150
|
-
payload: { sessionId: BrowserSessionId.make(input.event.payload.sessionId) }
|
|
285151
|
-
};
|
|
285152
|
-
case "tab.closed": return {
|
|
285153
|
-
...base,
|
|
285154
|
-
type: "tab.closed",
|
|
285155
|
-
payload: {
|
|
285156
|
-
sessionId: BrowserSessionId.make(input.event.sessionId),
|
|
285157
|
-
tabId: BrowserTabId.make(input.event.payload.tabId)
|
|
285158
|
-
}
|
|
285159
|
-
};
|
|
285160
|
-
case "snapshot.ready": return {
|
|
285161
|
-
...base,
|
|
285162
|
-
type: "snapshot.ready",
|
|
285163
|
-
payload: {
|
|
285164
|
-
sessionId: BrowserSessionId.make(input.event.sessionId),
|
|
285165
|
-
tabId: BrowserTabId.make(input.event.payload.tabId),
|
|
285166
|
-
captureId: BrowserCaptureId.make(input.event.payload.captureId ?? crypto.randomUUID()),
|
|
285167
|
-
capturedAt: input.event.payload.capturedAt,
|
|
285168
|
-
url: input.event.payload.url ?? DEFAULT_URL,
|
|
285169
|
-
viewport: input.event.payload.viewport,
|
|
285170
|
-
content: input.event.payload.content
|
|
285171
|
-
}
|
|
285172
|
-
};
|
|
285173
|
-
case "screenshot.ready": return {
|
|
285174
|
-
...base,
|
|
285175
|
-
type: "screenshot.ready",
|
|
285176
|
-
payload: {
|
|
285177
|
-
sessionId: BrowserSessionId.make(input.event.sessionId),
|
|
285178
|
-
tabId: BrowserTabId.make(input.event.payload.tabId),
|
|
285179
|
-
captureId: BrowserCaptureId.make(input.event.payload.captureId ?? crypto.randomUUID()),
|
|
285180
|
-
capturedAt: input.event.payload.capturedAt,
|
|
285181
|
-
url: input.event.payload.url ?? DEFAULT_URL,
|
|
285182
|
-
viewport: input.event.payload.viewport,
|
|
285183
|
-
imageDataUrl: input.event.payload.imageDataUrl
|
|
285184
|
-
}
|
|
285185
|
-
};
|
|
285186
|
-
case "console.logs.ready": return {
|
|
285187
|
-
...base,
|
|
285188
|
-
type: "console.logs.ready",
|
|
285189
|
-
payload: {
|
|
285190
|
-
sessionId: BrowserSessionId.make(input.event.sessionId),
|
|
285191
|
-
tabId: BrowserTabId.make(input.event.payload.tabId),
|
|
285192
|
-
logs: input.event.payload.logs.map((log) => ({
|
|
285193
|
-
level: log.level === "warn" ? "warning" : log.level === "log" ? "info" : log.level,
|
|
285194
|
-
message: log.message,
|
|
285195
|
-
createdAt: log.timestamp
|
|
285196
|
-
}))
|
|
285197
|
-
}
|
|
285198
|
-
};
|
|
285199
|
-
case "action.failed": return {
|
|
285200
|
-
...base,
|
|
285201
|
-
type: "action.failed",
|
|
285202
|
-
payload: {
|
|
285203
|
-
commandType: input.event.payload.commandType,
|
|
285204
|
-
message: input.event.payload.message,
|
|
285205
|
-
...input.event.payload.tabId ? { tabId: BrowserTabId.make(input.event.payload.tabId) } : {}
|
|
285206
|
-
}
|
|
285207
|
-
};
|
|
285208
|
-
case "dom.visible.ready":
|
|
285209
|
-
case "element.info.ready":
|
|
285210
|
-
case "element.screenshot.ready":
|
|
285211
|
-
case "clipboard.text.ready": return null;
|
|
285212
|
-
}
|
|
285072
|
+
//#endregion
|
|
285073
|
+
//#region src/browser/AgentBrowserHostBridgeAdapter.ts
|
|
285074
|
+
function createNativeHostBrowserBridgeClient(options) {
|
|
285075
|
+
return createNativeHostBrowserBridgeClientImplementation(options, { closeSessionTimeoutMs: agentBrowserCloseTimeoutMs });
|
|
285213
285076
|
}
|
|
285214
|
-
function
|
|
285077
|
+
function browserBackendOptionsForSession(input) {
|
|
285215
285078
|
return {
|
|
285216
|
-
|
|
285217
|
-
|
|
285218
|
-
snapshot: input.result.snapshot,
|
|
285219
|
-
threadId: input.command.threadId,
|
|
285220
|
-
environmentId: input.command.environmentId,
|
|
285221
|
-
userId: input.command.userId,
|
|
285222
|
-
display: input.display,
|
|
285223
|
-
controlState: input.controlState
|
|
285224
|
-
}),
|
|
285225
|
-
sessionEpoch: input.controlState.sessionEpoch,
|
|
285226
|
-
acknowledgedControlSequence: input.acknowledgedControlSequence,
|
|
285227
|
-
...input.result.detail ? { detail: input.result.detail } : {},
|
|
285228
|
-
...input.latestCapture ? { latestCapture: input.latestCapture } : {}
|
|
285079
|
+
...input.baseOptions,
|
|
285080
|
+
userDataDir: browserProfileDirForSession(input.sessionId)
|
|
285229
285081
|
};
|
|
285230
285082
|
}
|
|
285231
|
-
|
|
285232
|
-
|
|
285233
|
-
|
|
285234
|
-
|
|
285235
|
-
|
|
285236
|
-
|
|
285237
|
-
|
|
285238
|
-
function
|
|
285239
|
-
|
|
285240
|
-
|
|
285241
|
-
|
|
285242
|
-
event: input.event,
|
|
285243
|
-
threadId: input.command.threadId,
|
|
285244
|
-
browserId: input.browserId,
|
|
285245
|
-
environmentId: input.command.environmentId ?? DEFAULT_BROWSER_ENVIRONMENT_ID,
|
|
285246
|
-
userId: input.command.userId ?? "local-user",
|
|
285247
|
-
agentId: null,
|
|
285248
|
-
ownerRole: "lead",
|
|
285249
|
-
display: input.display,
|
|
285250
|
-
controlState: input.controlState
|
|
285251
|
-
});
|
|
285252
|
-
return mapped?.type === "snapshot.ready" || mapped?.type === "screenshot.ready" ? mapped : void 0;
|
|
285253
|
-
}
|
|
285254
|
-
function createAgentBrowserHostBridgeManager(bridge, options) {
|
|
285255
|
-
const localSubscriptions = /* @__PURE__ */ new Set();
|
|
285256
|
-
const connectedDisplays = /* @__PURE__ */ new Map();
|
|
285257
|
-
const controlStates = /* @__PURE__ */ new Map();
|
|
285258
|
-
const commandQueues = /* @__PURE__ */ new Map();
|
|
285259
|
-
const controlStateFor = (sessionId) => {
|
|
285260
|
-
const existing = controlStates.get(sessionId);
|
|
285261
|
-
if (existing) return existing;
|
|
285262
|
-
const created = {
|
|
285263
|
-
sessionEpoch: crypto.randomUUID(),
|
|
285264
|
-
lastAppliedControlSequence: 0
|
|
285265
|
-
};
|
|
285266
|
-
controlStates.set(sessionId, created);
|
|
285267
|
-
return created;
|
|
285268
|
-
};
|
|
285269
|
-
const runSerializedCommand = async (sessionId, task) => {
|
|
285270
|
-
const previous = commandQueues.get(sessionId) ?? Promise.resolve();
|
|
285271
|
-
let releaseCurrent;
|
|
285272
|
-
const current = new Promise((resolve) => {
|
|
285273
|
-
releaseCurrent = resolve;
|
|
285274
|
-
});
|
|
285275
|
-
const queued = previous.catch(() => void 0).then(() => current);
|
|
285276
|
-
commandQueues.set(sessionId, queued);
|
|
285277
|
-
await previous.catch(() => void 0);
|
|
285278
|
-
try {
|
|
285279
|
-
return await task();
|
|
285280
|
-
} finally {
|
|
285281
|
-
releaseCurrent();
|
|
285282
|
-
if (commandQueues.get(sessionId) === queued) commandQueues.delete(sessionId);
|
|
285283
|
-
}
|
|
285284
|
-
};
|
|
285285
|
-
const clearConnectedDisplay = (input) => {
|
|
285286
|
-
const active = connectedDisplays.get(input.sessionId);
|
|
285287
|
-
if (!active) return;
|
|
285288
|
-
if (input.tabId === void 0 || active.tabId === input.tabId) connectedDisplays.delete(input.sessionId);
|
|
285289
|
-
};
|
|
285290
|
-
const displayForSnapshot = (snapshot) => {
|
|
285291
|
-
const fallback = resolveDisplay(snapshot, options);
|
|
285292
|
-
const active = connectedDisplays.get(snapshot.sessionId);
|
|
285293
|
-
if (!active) return fallback;
|
|
285294
|
-
if (!snapshot.tabs.some((tab) => tab.id === active.tabId)) {
|
|
285295
|
-
connectedDisplays.delete(snapshot.sessionId);
|
|
285296
|
-
return fallback;
|
|
285297
|
-
}
|
|
285298
|
-
if (snapshot.activeTabId !== active.tabId) return fallback;
|
|
285299
|
-
const { reason: _reason, detail: _detail, ...fallbackWithoutDegradedReason } = fallback;
|
|
285300
|
-
return {
|
|
285301
|
-
...fallbackWithoutDegradedReason,
|
|
285302
|
-
...active.display,
|
|
285303
|
-
reconnect: active.display.reconnect ?? fallback.reconnect
|
|
285304
|
-
};
|
|
285305
|
-
};
|
|
285306
|
-
const duplicateControlResult = async (input, hostSessionId, controlState, acknowledgedControlSequence) => {
|
|
285307
|
-
const snapshot = await withAgentBrowserBridgeTimeout("Agent Browser duplicate command session read", bridge.getSession({
|
|
285308
|
-
sessionId: hostSessionId,
|
|
285309
|
-
...input.browserId ? { browserId: input.browserId } : {}
|
|
285310
|
-
}), agentBrowserFastTimeoutMs());
|
|
285311
|
-
if (!snapshot) throw new BrowserError({ detail: "Browser session not found." });
|
|
285312
|
-
return {
|
|
285313
|
-
ok: true,
|
|
285314
|
-
snapshot: toForgeSession({
|
|
285315
|
-
snapshot,
|
|
285316
|
-
threadId: input.threadId,
|
|
285317
|
-
environmentId: input.environmentId,
|
|
285318
|
-
userId: input.userId,
|
|
285319
|
-
display: displayForSnapshot(snapshot),
|
|
285320
|
-
controlState
|
|
285321
|
-
}),
|
|
285322
|
-
sessionEpoch: controlState.sessionEpoch,
|
|
285323
|
-
acknowledgedControlSequence,
|
|
285324
|
-
detail: "Browser control was already applied"
|
|
285325
|
-
};
|
|
285326
|
-
};
|
|
285327
|
-
const dispatchHostCommand = async (input, hostIdentity, preCommandSnapshot) => {
|
|
285328
|
-
try {
|
|
285329
|
-
return await withAgentBrowserBridgeTimeout(`Agent Browser command ${input.command.type}`, bridge.command({
|
|
285330
|
-
...hostIdentity,
|
|
285331
|
-
command: toHostCommand(input.command)
|
|
285332
|
-
}), agentBrowserCommandTimeoutMs(input.command));
|
|
285333
|
-
} catch (error) {
|
|
285334
|
-
if (!(isAgentBrowserBridgeTimeoutError(error) && isAgentBrowserBoundedControlCommand(input.command) && preCommandSnapshot !== null)) throw error;
|
|
285335
|
-
console.warn("[browser] Agent Browser control command timed out after dispatch", {
|
|
285336
|
-
commandType: input.command.type,
|
|
285337
|
-
sessionId: hostIdentity.sessionId,
|
|
285338
|
-
...input.browserId ? { browserId: input.browserId } : {}
|
|
285339
|
-
});
|
|
285340
|
-
return {
|
|
285341
|
-
ok: true,
|
|
285342
|
-
snapshot: snapshotAfterTimedOutControl(preCommandSnapshot, input.command),
|
|
285343
|
-
detail: "Agent Browser control command dispatch timed out before post-action observation completed."
|
|
285344
|
-
};
|
|
285345
|
-
}
|
|
285346
|
-
};
|
|
285347
|
-
const executeHostCommand = async (input, hostSessionId, controlState, controlSequence) => {
|
|
285348
|
-
const captureTabId = commandCaptureTabId(input.command);
|
|
285349
|
-
const hostIdentity = {
|
|
285350
|
-
sessionId: hostSessionId,
|
|
285351
|
-
...input.browserId ? { browserId: input.browserId } : {}
|
|
285352
|
-
};
|
|
285353
|
-
const preCommandSnapshot = isAgentBrowserBoundedControlCommand(input.command) ? await withAgentBrowserBridgeTimeout(`Agent Browser command ${input.command.type} preflight session read`, bridge.getSession(hostIdentity), agentBrowserCommandTimeoutMs(input.command)).catch(() => null) : null;
|
|
285354
|
-
let latestCaptureEvent;
|
|
285355
|
-
const unsubscribeCapture = captureTabId ? await bridge.subscribe(hostIdentity, (event) => {
|
|
285356
|
-
if ((event.type === "snapshot.ready" || event.type === "screenshot.ready") && event.sessionId === hostSessionId && event.payload.tabId === captureTabId) latestCaptureEvent = event;
|
|
285357
|
-
}) : void 0;
|
|
285358
|
-
let result;
|
|
285359
|
-
try {
|
|
285360
|
-
result = await dispatchHostCommand(input, hostIdentity, preCommandSnapshot);
|
|
285361
|
-
} finally {
|
|
285362
|
-
unsubscribeCapture?.();
|
|
285363
|
-
}
|
|
285364
|
-
if (input.command.type === "closeTab") {
|
|
285365
|
-
await runPromise(options?.displayPeers?.closeForSession({
|
|
285366
|
-
sessionId: hostSessionId,
|
|
285367
|
-
tabId: input.command.tabId
|
|
285368
|
-
}) ?? void_$1);
|
|
285369
|
-
clearConnectedDisplay({
|
|
285370
|
-
sessionId: hostSessionId,
|
|
285371
|
-
tabId: input.command.tabId
|
|
285372
|
-
});
|
|
285373
|
-
}
|
|
285374
|
-
const display = displayForSnapshot(result.snapshot);
|
|
285375
|
-
controlState.lastAppliedControlSequence = controlSequence;
|
|
285376
|
-
return toCommandResult({
|
|
285377
|
-
result,
|
|
285378
|
-
command: input,
|
|
285379
|
-
display,
|
|
285380
|
-
controlState,
|
|
285381
|
-
acknowledgedControlSequence: controlSequence,
|
|
285382
|
-
latestCapture: latestCaptureEvent ? toCommandLatestCapture({
|
|
285383
|
-
event: latestCaptureEvent,
|
|
285384
|
-
command: input,
|
|
285385
|
-
browserId: BrowserId.make(result.snapshot.browserId),
|
|
285386
|
-
display,
|
|
285387
|
-
controlState
|
|
285388
|
-
}) : void 0
|
|
285389
|
-
});
|
|
285390
|
-
};
|
|
285391
|
-
const executeOrderedHostCommand = async (input) => {
|
|
285392
|
-
const hostSessionId = sessionIdForThread(input.threadId);
|
|
285393
|
-
return runSerializedCommand(hostSessionId, async () => {
|
|
285394
|
-
const controlState = controlStateFor(hostSessionId);
|
|
285395
|
-
const controlDecision = resolveBrowserControlDecision(controlState, input);
|
|
285396
|
-
if (controlDecision.type === "reject") throw new BrowserError({ detail: browserControlRejectionDetail(controlDecision) });
|
|
285397
|
-
if (controlDecision.type === "duplicate") return duplicateControlResult(input, hostSessionId, controlState, controlDecision.acknowledgedControlSequence);
|
|
285398
|
-
return executeHostCommand(input, hostSessionId, controlState, controlDecision.controlSequence);
|
|
285399
|
-
});
|
|
285400
|
-
};
|
|
285401
|
-
const publishLocalEvent = (event) => {
|
|
285402
|
-
for (const subscription of localSubscriptions) {
|
|
285403
|
-
if (!subscriptionMatchesEvent(subscription, event)) continue;
|
|
285404
|
-
runPromise(subscription.listener(event)).catch((error) => {
|
|
285405
|
-
console.warn("[browser] Failed to publish local browser event", { error });
|
|
285406
|
-
});
|
|
285083
|
+
/**
|
|
285084
|
+
* Builds the Playwright-backed backend from the lazily loaded module. The
|
|
285085
|
+
* `playwright` package and everything that statically imports it must stay out
|
|
285086
|
+
* of the standalone bundle's eager import graph. Deferring the import to
|
|
285087
|
+
* backend construction keeps boot playwright-free and lets a later provision
|
|
285088
|
+
* satisfy first use.
|
|
285089
|
+
*/
|
|
285090
|
+
function createForgeAgentBrowserPlaywrightBackend(PlaywrightBackend, options) {
|
|
285091
|
+
class ForgeAgentBrowserPlaywrightBackend extends PlaywrightBackend {
|
|
285092
|
+
async close() {
|
|
285093
|
+
await closeAgentBrowserBackendWithTimeout(() => super.close());
|
|
285407
285094
|
}
|
|
285408
|
-
}
|
|
285409
|
-
|
|
285410
|
-
publishLocalEvent({
|
|
285411
|
-
eventId: EventId.make(crypto.randomUUID()),
|
|
285412
|
-
browserId: event.input.browserId ?? browserIdFrom(event.input),
|
|
285413
|
-
threadId: event.input.threadId,
|
|
285414
|
-
environmentId: event.input.environmentId ?? DEFAULT_BROWSER_ENVIRONMENT_ID,
|
|
285415
|
-
userId: event.input.userId ?? "local-user",
|
|
285416
|
-
agentId: null,
|
|
285417
|
-
createdAt: nowIso$2(),
|
|
285418
|
-
type: "display.iceCandidate",
|
|
285419
|
-
payload: {
|
|
285420
|
-
sessionId: event.input.sessionId,
|
|
285421
|
-
tabId: event.input.tabId,
|
|
285422
|
-
clientId: event.input.clientId,
|
|
285423
|
-
candidate: event.candidate
|
|
285424
|
-
}
|
|
285425
|
-
});
|
|
285426
|
-
};
|
|
285427
|
-
return {
|
|
285428
|
-
openSession: (input) => tryPromise({
|
|
285429
|
-
try: async () => {
|
|
285430
|
-
const hostSessionId = sessionIdForThread(input.threadId);
|
|
285431
|
-
const initialUrl = input.initialUrl ? normalizeUserVisibleBrowserUrl(input.initialUrl) : void 0;
|
|
285432
|
-
options?.prepareRuntimeSession?.({
|
|
285433
|
-
sessionId: hostSessionId,
|
|
285434
|
-
initialUrl: initialUrl ?? DEFAULT_URL
|
|
285435
|
-
});
|
|
285436
|
-
let snapshot;
|
|
285437
|
-
try {
|
|
285438
|
-
snapshot = await withAgentBrowserBridgeTimeout("Agent Browser openSession", openAgentBrowserSessionReady(bridge, {
|
|
285439
|
-
sessionId: hostSessionId,
|
|
285440
|
-
...input.browserId ? { browserId: input.browserId } : {},
|
|
285441
|
-
...initialUrl ? { initialUrl } : {},
|
|
285442
|
-
...input.name ? { name: input.name } : {},
|
|
285443
|
-
...input.visibility ? { visibility: input.visibility } : {},
|
|
285444
|
-
...input.viewport ? { viewport: input.viewport } : {}
|
|
285445
|
-
}), agentBrowserOpenTimeoutMs());
|
|
285446
|
-
} catch (error) {
|
|
285447
|
-
if (!isAgentBrowserBridgeTimeoutError(error)) {
|
|
285448
|
-
options?.forgetRuntimeSession?.({ sessionId: hostSessionId });
|
|
285449
|
-
throw error;
|
|
285450
|
-
}
|
|
285451
|
-
console.warn("[browser] Agent Browser openSession timed out; quarantining session", {
|
|
285452
|
-
sessionId: hostSessionId,
|
|
285453
|
-
...input.browserId ? { browserId: input.browserId } : {}
|
|
285454
|
-
});
|
|
285455
|
-
await withAgentBrowserBridgeTimeout("Agent Browser openSession quarantine close", bridge.closeSession({
|
|
285456
|
-
sessionId: hostSessionId,
|
|
285457
|
-
...input.browserId ? { browserId: input.browserId } : {}
|
|
285458
|
-
}), agentBrowserCloseTimeoutMs()).catch(() => void 0);
|
|
285459
|
-
clearConnectedDisplay({ sessionId: hostSessionId });
|
|
285460
|
-
controlStates.delete(hostSessionId);
|
|
285461
|
-
options?.forgetRuntimeSession?.({ sessionId: hostSessionId });
|
|
285462
|
-
throw new Error(`${error instanceof Error ? error.message : String(error)} The pending browser session was quarantined and closed.`, { cause: error });
|
|
285463
|
-
}
|
|
285464
|
-
return toForgeSession({
|
|
285465
|
-
snapshot,
|
|
285466
|
-
threadId: input.threadId,
|
|
285467
|
-
environmentId: input.environmentId,
|
|
285468
|
-
userId: input.userId,
|
|
285469
|
-
agentId: input.agentId,
|
|
285470
|
-
ownerRole: input.ownerRole,
|
|
285471
|
-
display: displayForSnapshot(snapshot),
|
|
285472
|
-
controlState: controlStateFor(snapshot.sessionId)
|
|
285473
|
-
});
|
|
285474
|
-
},
|
|
285475
|
-
catch: (error) => toBrowserError(error, "Failed to open Agent Browser session.")
|
|
285476
|
-
}),
|
|
285477
|
-
closeSession: (input) => tryPromise({
|
|
285478
|
-
try: async () => {
|
|
285479
|
-
await runPromise(options?.displayPeers?.closeForSession({ sessionId: sessionIdForThread(input.threadId) }) ?? void_$1);
|
|
285480
|
-
const hostSessionId = sessionIdForThread(input.threadId);
|
|
285481
|
-
const hostIdentity = {
|
|
285482
|
-
sessionId: hostSessionId,
|
|
285483
|
-
...input.browserId ? { browserId: input.browserId } : {}
|
|
285484
|
-
};
|
|
285485
|
-
try {
|
|
285486
|
-
await withAgentBrowserBridgeTimeout("Agent Browser closeSession", bridge.closeSession(hostIdentity), agentBrowserCloseTimeoutMs());
|
|
285487
|
-
} catch (error) {
|
|
285488
|
-
if (!isAgentBrowserBridgeTimeoutError(error)) throw error;
|
|
285489
|
-
if (await withAgentBrowserBridgeTimeout("Agent Browser closeSession verification", bridge.getSession(hostIdentity), 2e3).catch(() => void 0) === null) console.warn("[browser] Agent Browser closeSession timed out after session removal", {
|
|
285490
|
-
sessionId: hostSessionId,
|
|
285491
|
-
...input.browserId ? { browserId: input.browserId } : {}
|
|
285492
|
-
});
|
|
285493
|
-
else console.warn("[browser] Agent Browser closeSession timed out; forgetting session", {
|
|
285494
|
-
sessionId: hostSessionId,
|
|
285495
|
-
...input.browserId ? { browserId: input.browserId } : {}
|
|
285496
|
-
});
|
|
285497
|
-
clearConnectedDisplay({ sessionId: hostSessionId });
|
|
285498
|
-
controlStates.delete(hostSessionId);
|
|
285499
|
-
options?.forgetRuntimeSession?.({ sessionId: hostSessionId });
|
|
285500
|
-
return;
|
|
285501
|
-
}
|
|
285502
|
-
clearConnectedDisplay({ sessionId: hostSessionId });
|
|
285503
|
-
controlStates.delete(hostSessionId);
|
|
285504
|
-
options?.forgetRuntimeSession?.({ sessionId: hostSessionId });
|
|
285505
|
-
},
|
|
285506
|
-
catch: (error) => toBrowserError(error, "Failed to close Agent Browser session.")
|
|
285507
|
-
}),
|
|
285508
|
-
clearBrowsingData: () => succeed({
|
|
285509
|
-
closedSessionCount: 0,
|
|
285510
|
-
deletedSessionCount: 0,
|
|
285511
|
-
deletedCaptureCount: 0
|
|
285512
|
-
}),
|
|
285513
|
-
getSession: (input) => tryPromise({
|
|
285514
|
-
try: async () => {
|
|
285515
|
-
const snapshot = await withAgentBrowserBridgeTimeout("Agent Browser getSession", bridge.getSession({
|
|
285516
|
-
sessionId: sessionIdForThread(input.threadId),
|
|
285517
|
-
...input.browserId ? { browserId: input.browserId } : {}
|
|
285518
|
-
}), agentBrowserFastTimeoutMs());
|
|
285519
|
-
return snapshot ? toForgeSession({
|
|
285520
|
-
snapshot,
|
|
285521
|
-
threadId: input.threadId,
|
|
285522
|
-
environmentId: input.environmentId,
|
|
285523
|
-
userId: input.userId,
|
|
285524
|
-
display: displayForSnapshot(snapshot),
|
|
285525
|
-
controlState: controlStateFor(snapshot.sessionId)
|
|
285526
|
-
}) : null;
|
|
285527
|
-
},
|
|
285528
|
-
catch: (error) => toBrowserError(error, "Failed to read Agent Browser session.")
|
|
285529
|
-
}),
|
|
285530
|
-
listSessions: (input) => tryPromise({
|
|
285531
|
-
try: async () => {
|
|
285532
|
-
return (await withAgentBrowserBridgeTimeout("Agent Browser listSessions", bridge.listSessions(input.browserId ? { browserId: input.browserId } : void 0), agentBrowserFastTimeoutMs())).map((snapshot) => toForgeSession({
|
|
285533
|
-
snapshot,
|
|
285534
|
-
threadId: input.threadId ?? ThreadId.make(snapshot.sessionId),
|
|
285535
|
-
environmentId: input.environmentId,
|
|
285536
|
-
userId: input.userId,
|
|
285537
|
-
display: displayForSnapshot(snapshot),
|
|
285538
|
-
controlState: controlStateFor(snapshot.sessionId)
|
|
285539
|
-
}));
|
|
285540
|
-
},
|
|
285541
|
-
catch: (error) => toBrowserError(error, "Failed to list Agent Browser sessions.")
|
|
285542
|
-
}),
|
|
285543
|
-
command: (input) => tryPromise({
|
|
285544
|
-
try: () => executeOrderedHostCommand(input),
|
|
285545
|
-
catch: (error) => toBrowserError(error, "Failed to run Agent Browser command.")
|
|
285546
|
-
}),
|
|
285547
|
-
attachDisplay: (input) => {
|
|
285548
|
-
const displayPeers = options?.displayPeers;
|
|
285549
|
-
if (!displayPeers) return fail(remoteWebRtcUnavailableError(input));
|
|
285550
|
-
return tryPromise({
|
|
285551
|
-
try: async () => {
|
|
285552
|
-
const iceServers = await resolveDisplayIceServers(options);
|
|
285553
|
-
if (options?.requiresWebRtcRelay === true && !hasTurnRelay(iceServers)) throw remoteWebRtcRelayRequiredError(input);
|
|
285554
|
-
const result = await runPromise(displayPeers.attach(input, {
|
|
285555
|
-
display: {
|
|
285556
|
-
transport: "remote-webrtc",
|
|
285557
|
-
status: "connecting",
|
|
285558
|
-
...input.viewport ? { viewport: input.viewport } : {},
|
|
285559
|
-
devicePixelRatio: 1,
|
|
285560
|
-
...iceServers.length > 0 ? { iceServers } : {},
|
|
285561
|
-
reconnect: {
|
|
285562
|
-
status: "ready",
|
|
285563
|
-
attempt: 0
|
|
285564
|
-
}
|
|
285565
|
-
},
|
|
285566
|
-
onIceCandidate: publishWebRtcIceCandidate
|
|
285567
|
-
}));
|
|
285568
|
-
connectedDisplays.set(input.sessionId, {
|
|
285569
|
-
tabId: input.tabId,
|
|
285570
|
-
display: result.display
|
|
285571
|
-
});
|
|
285572
|
-
return result;
|
|
285573
|
-
},
|
|
285574
|
-
catch: (error) => {
|
|
285575
|
-
if (is$1(BrowserError)(error)) return error;
|
|
285576
|
-
return toBrowserError(error, "Failed to attach WebRTC browser display.");
|
|
285577
|
-
}
|
|
285578
|
-
});
|
|
285579
|
-
},
|
|
285580
|
-
addDisplayIceCandidate: (input) => {
|
|
285581
|
-
const displayPeers = options?.displayPeers;
|
|
285582
|
-
if (!displayPeers) return fail(remoteWebRtcUnavailableError(input));
|
|
285583
|
-
return displayPeers.addIceCandidate(input);
|
|
285584
|
-
},
|
|
285585
|
-
subscribe: (input, listener) => promise(async () => {
|
|
285586
|
-
const localSubscription = {
|
|
285587
|
-
input,
|
|
285588
|
-
listener
|
|
285589
|
-
};
|
|
285590
|
-
localSubscriptions.add(localSubscription);
|
|
285591
|
-
const threadId = input.threadId ?? ThreadId.make("unknown-thread");
|
|
285592
|
-
const browserId = browserIdFrom(input);
|
|
285593
|
-
const environmentId = input.environmentId ?? DEFAULT_BROWSER_ENVIRONMENT_ID;
|
|
285594
|
-
const userId = input.userId ?? "local-user";
|
|
285595
|
-
const subscribeInput = {
|
|
285596
|
-
...input.threadId ? { sessionId: sessionIdForThread(input.threadId) } : {},
|
|
285597
|
-
...input.browserId ? { browserId: input.browserId } : {}
|
|
285598
|
-
};
|
|
285599
|
-
const unsubscribeBridge = await bridge.subscribe(subscribeInput, (event) => {
|
|
285600
|
-
if (event.type === "session.closed") clearConnectedDisplay({ sessionId: event.payload.sessionId });
|
|
285601
|
-
if (event.type === "tab.closed") clearConnectedDisplay({
|
|
285602
|
-
sessionId: event.sessionId,
|
|
285603
|
-
tabId: event.payload.tabId
|
|
285604
|
-
});
|
|
285605
|
-
const mapped = toStreamEvent({
|
|
285606
|
-
event,
|
|
285607
|
-
threadId,
|
|
285608
|
-
browserId,
|
|
285609
|
-
environmentId,
|
|
285610
|
-
userId,
|
|
285611
|
-
agentId: null,
|
|
285612
|
-
ownerRole: "lead",
|
|
285613
|
-
display: event.type === "session.started" || event.type === "session.updated" ? displayForSnapshot(event.payload.snapshot) : DEFAULT_AGENT_BROWSER_DISPLAY,
|
|
285614
|
-
controlState: controlStateFor(event.type === "session.closed" ? event.payload.sessionId : event.sessionId)
|
|
285615
|
-
});
|
|
285616
|
-
if (mapped) runPromise(listener(mapped));
|
|
285617
|
-
if (event.type === "session.closed") controlStates.delete(event.payload.sessionId);
|
|
285618
|
-
});
|
|
285619
|
-
return () => {
|
|
285620
|
-
localSubscriptions.delete(localSubscription);
|
|
285621
|
-
unsubscribeBridge();
|
|
285622
|
-
};
|
|
285623
|
-
})
|
|
285624
|
-
};
|
|
285095
|
+
}
|
|
285096
|
+
return new ForgeAgentBrowserPlaywrightBackend(options);
|
|
285625
285097
|
}
|
|
285626
285098
|
function shouldUseNativeHostBridge(serverConfig) {
|
|
285627
285099
|
return serverConfig.mode === "desktop" && typeof serverConfig.nativeBrowserHostUrl === "string" && serverConfig.nativeBrowserHostUrl.trim().length > 0 && typeof serverConfig.nativeBrowserHostToken === "string" && serverConfig.nativeBrowserHostToken.trim().length > 0;
|
|
@@ -285646,8 +285118,9 @@ const AgentBrowserManagerLive = effect(BrowserManager, gen(function* () {
|
|
|
285646
285118
|
const displayCaptureRuntime = yield* promise(() => createBrowserWebRtcDisplayCaptureRuntime({ chromiumExecutablePath: process.env.FORGE_PLAYWRIGHT_EXECUTABLE_PATH }));
|
|
285647
285119
|
yield* addFinalizer$2(() => promise(() => displayCaptureRuntime.close()).pipe(orDie));
|
|
285648
285120
|
if (!displayCaptureRuntime.available) yield* (!requiresWebRtcRelay && displayCaptureRuntime.unavailableDetail?.includes("root-owned /tmp/.X11-unix") === true ? logInfo : logWarning$1)("browser WebRTC display capture runtime unavailable", { detail: displayCaptureRuntime.unavailableDetail });
|
|
285121
|
+
const runLayerFork = runForkWith(yield* context());
|
|
285649
285122
|
const displayTaskHoldManager = makeSpriteTaskHoldManager(process.env, (message, cause) => {
|
|
285650
|
-
|
|
285123
|
+
runLayerFork(logWarning$1(message, { cause: cause instanceof Error ? cause.message : cause }));
|
|
285651
285124
|
});
|
|
285652
285125
|
const displayPeers = createWebRtcDisplaySessionManager({
|
|
285653
285126
|
mediaSourceFactory: displayCaptureRuntime.mediaSourceFactory,
|
|
@@ -285659,70 +285132,98 @@ const AgentBrowserManagerLive = effect(BrowserManager, gen(function* () {
|
|
|
285659
285132
|
yield* addFinalizer$2(() => promise(() => displayTaskHoldManager?.releaseAll() ?? Promise.resolve()));
|
|
285660
285133
|
const baseBackendOptions = displayCaptureRuntime.backendOptions;
|
|
285661
285134
|
const sessionInitialUrls = /* @__PURE__ */ new Map();
|
|
285662
|
-
|
|
285663
|
-
|
|
285664
|
-
|
|
285665
|
-
|
|
285666
|
-
|
|
285667
|
-
|
|
285668
|
-
|
|
285669
|
-
|
|
285670
|
-
|
|
285671
|
-
|
|
285672
|
-
|
|
285673
|
-
|
|
285674
|
-
|
|
285135
|
+
let runtimeBuild = null;
|
|
285136
|
+
const lazyDisposers = [];
|
|
285137
|
+
const buildPlaywrightRuntime = async () => {
|
|
285138
|
+
const { PlaywrightBackend } = await import("./playwright-backend-DVQOhqRO.mjs");
|
|
285139
|
+
const runtime = createAgentBrowserRuntime({
|
|
285140
|
+
backends: { [DEFAULT_AGENT_BROWSER_ID]: createForgeAgentBrowserPlaywrightBackend(PlaywrightBackend, {
|
|
285141
|
+
...browserBackendOptionsForSession({
|
|
285142
|
+
baseOptions: baseBackendOptions,
|
|
285143
|
+
sessionId: "default"
|
|
285144
|
+
}),
|
|
285145
|
+
name: "Forge Agent Browser"
|
|
285146
|
+
}) },
|
|
285147
|
+
backendFactory: (input) => createForgeAgentBrowserPlaywrightBackend(PlaywrightBackend, {
|
|
285148
|
+
...browserBackendOptionsForSession({
|
|
285149
|
+
baseOptions: baseBackendOptions,
|
|
285150
|
+
initialUrl: sessionInitialUrls.get(input.sessionId),
|
|
285151
|
+
sessionId: input.sessionId
|
|
285152
|
+
}),
|
|
285153
|
+
name: `Forge Agent Browser ${input.sessionId}`
|
|
285154
|
+
})
|
|
285155
|
+
});
|
|
285156
|
+
lazyDisposers.push(() => runtime.close());
|
|
285157
|
+
const hostBridge = createHostBrowserBridge({
|
|
285158
|
+
browserId: DEFAULT_AGENT_BROWSER_ID,
|
|
285159
|
+
closeTabsOnSessionClose: false,
|
|
285160
|
+
defaultViewport: DEFAULT_AGENT_BROWSER_VIEWPORT,
|
|
285161
|
+
runtime,
|
|
285162
|
+
tabLifecycle: {
|
|
285163
|
+
activateNextTabOnClose: true,
|
|
285164
|
+
createFallbackTabOnLastClose: true,
|
|
285165
|
+
fallbackUrl: DEFAULT_URL
|
|
285166
|
+
}
|
|
285167
|
+
});
|
|
285168
|
+
const reaperConfig = browserOrphanReaperConfigFromEnv(process.env, AGENT_BROWSER_PROFILE_ROOT);
|
|
285169
|
+
if (reaperConfig.enabled && process.platform !== "win32") {
|
|
285170
|
+
const reaper = createBrowserOrphanReaper(createSystemBrowserOrphanReaperDeps({
|
|
285171
|
+
profileRoot: AGENT_BROWSER_PROFILE_ROOT,
|
|
285172
|
+
listSessions: () => hostBridge.listSessions(),
|
|
285173
|
+
closeSession: async (sessionId) => {
|
|
285174
|
+
await hostBridge.closeSession({ sessionId: BrowserSessionId.make(sessionId) });
|
|
285175
|
+
}
|
|
285176
|
+
}), reaperConfig);
|
|
285177
|
+
const reaperInterval = setInterval(() => {
|
|
285178
|
+
runLayerFork(reaper.sweep);
|
|
285179
|
+
}, reaperConfig.intervalMs);
|
|
285180
|
+
reaperInterval.unref?.();
|
|
285181
|
+
lazyDisposers.push(() => clearInterval(reaperInterval));
|
|
285182
|
+
runLayerFork(reaper.sweep);
|
|
285183
|
+
}
|
|
285184
|
+
return createAgentBrowserHostBridgeManager(hostBridge, {
|
|
285185
|
+
display: (snapshot) => agentBrowserDisplayForRuntimeMode({
|
|
285186
|
+
mode: serverConfig.mode,
|
|
285187
|
+
iceServers: serverConfig.browserWebRtcIceServerResolver.getCachedIceServers().length > 0 ? serverConfig.browserWebRtcIceServerResolver.getCachedIceServers() : serverConfig.browserWebRtcIceServers,
|
|
285188
|
+
requiresRelay: requiresWebRtcRelay,
|
|
285189
|
+
viewport: snapshot.viewport
|
|
285675
285190
|
}),
|
|
285676
|
-
|
|
285677
|
-
|
|
285678
|
-
|
|
285679
|
-
|
|
285680
|
-
|
|
285681
|
-
|
|
285682
|
-
|
|
285683
|
-
|
|
285684
|
-
|
|
285685
|
-
|
|
285686
|
-
|
|
285687
|
-
|
|
285688
|
-
|
|
285689
|
-
|
|
285690
|
-
});
|
|
285691
|
-
const
|
|
285692
|
-
|
|
285693
|
-
|
|
285694
|
-
|
|
285695
|
-
listSessions: () => hostBridge.listSessions(),
|
|
285696
|
-
closeSession: async (sessionId) => {
|
|
285697
|
-
await hostBridge.closeSession({ sessionId: BrowserSessionId.make(sessionId) });
|
|
285698
|
-
}
|
|
285699
|
-
}), reaperConfig);
|
|
285700
|
-
const runReaperFork = runForkWith(yield* context());
|
|
285701
|
-
const reaperInterval = setInterval(() => {
|
|
285702
|
-
runReaperFork(reaper.sweep);
|
|
285703
|
-
}, reaperConfig.intervalMs);
|
|
285704
|
-
reaperInterval.unref?.();
|
|
285705
|
-
yield* addFinalizer$2(() => sync$1(() => clearInterval(reaperInterval)));
|
|
285706
|
-
runReaperFork(reaper.sweep);
|
|
285707
|
-
}
|
|
285708
|
-
return createAgentBrowserHostBridgeManager(hostBridge, {
|
|
285709
|
-
display: (snapshot) => agentBrowserDisplayForRuntimeMode({
|
|
285710
|
-
mode: serverConfig.mode,
|
|
285711
|
-
iceServers: serverConfig.browserWebRtcIceServerResolver.getCachedIceServers().length > 0 ? serverConfig.browserWebRtcIceServerResolver.getCachedIceServers() : serverConfig.browserWebRtcIceServers,
|
|
285712
|
-
requiresRelay: requiresWebRtcRelay,
|
|
285713
|
-
viewport: snapshot.viewport
|
|
285714
|
-
}),
|
|
285715
|
-
displayIceServers: serverConfig.browserWebRtcIceServers,
|
|
285716
|
-
resolveDisplayIceServers: serverConfig.browserWebRtcIceServerResolver.resolveIceServers,
|
|
285717
|
-
displayPeers,
|
|
285718
|
-
prepareRuntimeSession: (input) => {
|
|
285719
|
-
sessionInitialUrls.set(input.sessionId, input.initialUrl);
|
|
285720
|
-
},
|
|
285721
|
-
forgetRuntimeSession: (input) => {
|
|
285722
|
-
sessionInitialUrls.delete(input.sessionId);
|
|
285191
|
+
displayIceServers: serverConfig.browserWebRtcIceServers,
|
|
285192
|
+
resolveDisplayIceServers: serverConfig.browserWebRtcIceServerResolver.resolveIceServers,
|
|
285193
|
+
displayPeers,
|
|
285194
|
+
prepareRuntimeSession: (input) => {
|
|
285195
|
+
sessionInitialUrls.set(input.sessionId, input.initialUrl);
|
|
285196
|
+
},
|
|
285197
|
+
forgetRuntimeSession: (input) => {
|
|
285198
|
+
sessionInitialUrls.delete(input.sessionId);
|
|
285199
|
+
},
|
|
285200
|
+
requiresWebRtcRelay
|
|
285201
|
+
});
|
|
285202
|
+
};
|
|
285203
|
+
yield* addFinalizer$2(() => promise(async () => {
|
|
285204
|
+
for (const dispose of lazyDisposers.toReversed()) await dispose();
|
|
285205
|
+
}).pipe(orDie));
|
|
285206
|
+
const ensurePlaywrightRuntime = () => tryPromise({
|
|
285207
|
+
try: () => {
|
|
285208
|
+
runtimeBuild ??= buildPlaywrightRuntime();
|
|
285209
|
+
return runtimeBuild;
|
|
285723
285210
|
},
|
|
285724
|
-
|
|
285211
|
+
catch: (cause) => {
|
|
285212
|
+
runtimeBuild = null;
|
|
285213
|
+
return toBrowserError(cause, "Browser backend could not be loaded. Browser packages load on first browser use and must be provisioned on this runtime.");
|
|
285214
|
+
}
|
|
285725
285215
|
});
|
|
285216
|
+
return {
|
|
285217
|
+
openSession: (input) => flatMap(ensurePlaywrightRuntime(), (m) => m.openSession(input)),
|
|
285218
|
+
closeSession: (input) => flatMap(ensurePlaywrightRuntime(), (m) => m.closeSession(input)),
|
|
285219
|
+
clearBrowsingData: (input) => flatMap(ensurePlaywrightRuntime(), (m) => m.clearBrowsingData(input)),
|
|
285220
|
+
getSession: (input) => flatMap(ensurePlaywrightRuntime(), (m) => m.getSession(input)),
|
|
285221
|
+
listSessions: (input) => flatMap(ensurePlaywrightRuntime(), (m) => m.listSessions(input)),
|
|
285222
|
+
command: (input) => flatMap(ensurePlaywrightRuntime(), (m) => m.command(input)),
|
|
285223
|
+
attachDisplay: (input) => flatMap(ensurePlaywrightRuntime(), (m) => m.attachDisplay(input)),
|
|
285224
|
+
addDisplayIceCandidate: (input) => flatMap(ensurePlaywrightRuntime(), (m) => m.addDisplayIceCandidate(input)),
|
|
285225
|
+
subscribe: (input, listener) => flatMap(orDie(ensurePlaywrightRuntime()), (m) => m.subscribe(input, listener))
|
|
285226
|
+
};
|
|
285726
285227
|
}));
|
|
285727
285228
|
//#endregion
|
|
285728
285229
|
//#region src/devTunnel/http.ts
|
|
@@ -297678,7 +297179,7 @@ function resolveBuildCommitFromEnv(env) {
|
|
|
297678
297179
|
* environment descriptor down instead of reporting an honest "unknown".
|
|
297679
297180
|
*/
|
|
297680
297181
|
function readBakedBuildCommit() {
|
|
297681
|
-
return "
|
|
297182
|
+
return "22e4740c3b851434e13fd47fc22833c48d658450";
|
|
297682
297183
|
}
|
|
297683
297184
|
async function resolveServerBuildCommit(input) {
|
|
297684
297185
|
if (isFullCommitSha(input.baked)) return input.baked;
|
|
@@ -321180,13 +320681,13 @@ const gitUrlCloneRouteLayer = add$1("POST", "/api/cloud/git/clone-url", gen(func
|
|
|
321180
320681
|
//#endregion
|
|
321181
320682
|
//#region src/server.ts
|
|
321182
320683
|
const PtyAdapterLive = unwrap(gen(function* () {
|
|
321183
|
-
if (typeof Bun !== "undefined") return (yield* promise(() => import("./BunPTY-
|
|
321184
|
-
else return (yield* promise(() => import("./NodePTY-
|
|
320684
|
+
if (typeof Bun !== "undefined") return (yield* promise(() => import("./BunPTY-Csgwhbi5.mjs"))).layer;
|
|
320685
|
+
else return (yield* promise(() => import("./NodePTY-DgXWLtWp.mjs"))).layer;
|
|
321185
320686
|
}));
|
|
321186
320687
|
const HttpServerLive = unwrap(gen(function* () {
|
|
321187
320688
|
const config = yield* ServerConfig;
|
|
321188
|
-
if (typeof Bun !== "undefined") return (yield* promise(() => import("./BunHttpServer-
|
|
321189
|
-
const [NodeHttpServer, NodeHttp] = yield* all([promise(() => import("./NodeHttpServer-
|
|
320689
|
+
if (typeof Bun !== "undefined") return (yield* promise(() => import("./BunHttpServer-DjilmGOM.mjs"))).layer(bunHttpServerOptions(config));
|
|
320690
|
+
const [NodeHttpServer, NodeHttp] = yield* all([promise(() => import("./NodeHttpServer-Ds3Gp1Gp.mjs")), promise(() => import("node:http"))]);
|
|
321190
320691
|
return NodeHttpServer.layer(NodeHttp.createServer, {
|
|
321191
320692
|
host: config.host,
|
|
321192
320693
|
port: config.port
|
|
@@ -321194,7 +320695,7 @@ const HttpServerLive = unwrap(gen(function* () {
|
|
|
321194
320695
|
}));
|
|
321195
320696
|
const PlatformServicesLive = unwrap(gen(function* () {
|
|
321196
320697
|
if (typeof Bun !== "undefined") {
|
|
321197
|
-
const { layer } = yield* promise(() => import("./BunServices-
|
|
320698
|
+
const { layer } = yield* promise(() => import("./BunServices-BZKzHdrG.mjs"));
|
|
321198
320699
|
return layer;
|
|
321199
320700
|
} else {
|
|
321200
320701
|
const { layer } = yield* promise(() => import("./NodeServices-D3IxOzHl.mjs").then((n) => n.t));
|
|
@@ -322192,7 +321693,7 @@ const connectLoginCommand = make$34("login", {
|
|
|
322192
321693
|
})).json();
|
|
322193
321694
|
},
|
|
322194
321695
|
openBrowser: async (url) => {
|
|
322195
|
-
const open = (await import("./open-
|
|
321696
|
+
const open = (await import("./open-2-reBrpv.mjs")).default;
|
|
322196
321697
|
await open(url);
|
|
322197
321698
|
return true;
|
|
322198
321699
|
},
|