@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.
Files changed (44) hide show
  1. package/dist/{BunHttpServer-C0oPP_3A.cjs → BunHttpServer-CXndtc1j.cjs} +2 -2
  2. package/dist/{BunHttpServer-FbbQmsl_.mjs → BunHttpServer-DjilmGOM.mjs} +2 -2
  3. package/dist/{BunPTY-uyreNRGp.cjs → BunPTY-BiTlUj63.cjs} +1 -1
  4. package/dist/{BunPTY-DkAIzLIx.mjs → BunPTY-Csgwhbi5.mjs} +1 -1
  5. package/dist/{BunServices-DC7nhA3w.mjs → BunServices-BZKzHdrG.mjs} +1 -1
  6. package/dist/{BunServices-DkenwD7S.cjs → BunServices-DsWG7VM1.cjs} +1 -1
  7. package/dist/{NodeHttpServer-Cjd3bFrD.cjs → NodeHttpServer-B4dCKqYY.cjs} +1 -1
  8. package/dist/{NodeHttpServer-BJ4l15f-.mjs → NodeHttpServer-Ds3Gp1Gp.mjs} +1 -1
  9. package/dist/{NodePTY-BehazUMi.cjs → NodePTY-BJ7874MO.cjs} +1 -1
  10. package/dist/{NodePTY-BhezYy9H.mjs → NodePTY-DgXWLtWp.mjs} +1 -1
  11. package/dist/{NodeSqliteClient-CfsdC3AB.cjs → NodeSqliteClient-CPsJxxdD.cjs} +1 -1
  12. package/dist/{NodeSqliteClient-md8xd9hY.mjs → NodeSqliteClient-D29w60zZ.mjs} +1 -1
  13. package/dist/{SqliteClient-BKB695RF.mjs → SqliteClient-Bgo_s5sA.mjs} +1 -1
  14. package/dist/{SqliteClient-O1ABIngw.cjs → SqliteClient-k0pbfeIC.cjs} +1 -1
  15. package/dist/bin.cjs +1879 -2378
  16. package/dist/bin.mjs +1876 -2375
  17. package/dist/client/assets/{DiffPanel-CsYeFcEd.js → DiffPanel-rxdARnAn.js} +2 -2
  18. package/dist/client/assets/{DiffPanel.logic-BhrQueD8.js → DiffPanel.logic-pioWZtAT.js} +2 -2
  19. package/dist/client/assets/{DiffPanelShell-CssAtWUC.js → DiffPanelShell-CJre8MyV.js} +3 -3
  20. package/dist/client/assets/{DiffWorkerPoolProvider-C0iUCn9m.js → DiffWorkerPoolProvider-BoinSMK1.js} +2 -2
  21. package/dist/client/assets/{PullRequestCodePanel-O-XZJVnK.js → PullRequestCodePanel-BR0xA-je.js} +2 -2
  22. package/dist/client/assets/{Virtualizer-Z2t67-w7.js → Virtualizer-H0mi-vRc.js} +2 -2
  23. package/dist/client/assets/{index-AMVPSS6N.js → index-DQtjHxYt.js} +6 -6
  24. package/dist/client/index.html +4 -4
  25. package/dist/{execAsync-BUcaFQ1D.cjs → execAsync--H9ZZOiC.cjs} +1 -1
  26. package/dist/{execAsync-BNNdR88o.mjs → execAsync-BJ4g1UuR.mjs} +1 -1
  27. package/dist/forge-build.json +1 -1
  28. package/dist/{getMachineId-bsd-3FzKrfVS.cjs → getMachineId-bsd-CJJWgsvW.cjs} +2 -2
  29. package/dist/{getMachineId-bsd-BuYwT36r.mjs → getMachineId-bsd-CvzjzhCJ.mjs} +2 -2
  30. package/dist/{getMachineId-darwin-RZKhGQ6C.mjs → getMachineId-darwin-BkjZ6NK7.mjs} +2 -2
  31. package/dist/{getMachineId-darwin-J2wNbebX.cjs → getMachineId-darwin-hZeIAq-t.cjs} +2 -2
  32. package/dist/{getMachineId-linux-BDLTrARB.cjs → getMachineId-linux-B4Pn5gp9.cjs} +1 -1
  33. package/dist/{getMachineId-linux-D5HSP0rH.mjs → getMachineId-linux-BaBxzaf3.mjs} +1 -1
  34. package/dist/{getMachineId-unsupported-DRh5MFwr.cjs → getMachineId-unsupported-B3jtoOL0.cjs} +1 -1
  35. package/dist/{getMachineId-unsupported-IaplPZnC.mjs → getMachineId-unsupported-EhCzrOph.mjs} +1 -1
  36. package/dist/{getMachineId-win-D220ursp.mjs → getMachineId-win-BFwtIsaZ.mjs} +2 -2
  37. package/dist/{getMachineId-win-BA8dHcjg.cjs → getMachineId-win-Bb4lJE8e.cjs} +2 -2
  38. package/dist/{open-D8mGyWmt.mjs → open-2-reBrpv.mjs} +1 -1
  39. package/dist/{open-BAsPVCWX.cjs → open-CghsF25U.cjs} +1 -1
  40. package/dist/playwright-backend-BONxTCSA.cjs +643 -0
  41. package/dist/playwright-backend-DVQOhqRO.mjs +642 -0
  42. package/dist/settle-B6Rn65UV.cjs +28 -0
  43. package/dist/settle-BJcmpbvq.mjs +23 -0
  44. 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-RZKhGQ6C.mjs").then((m) => /* @__PURE__ */ __toESM(m.default))).getMachineId;
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-D5HSP0rH.mjs").then((m) => /* @__PURE__ */ __toESM(m.default))).getMachineId;
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-BuYwT36r.mjs").then((m) => /* @__PURE__ */ __toESM(m.default))).getMachineId;
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-D220ursp.mjs").then((m) => /* @__PURE__ */ __toESM(m.default))).getMachineId;
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-IaplPZnC.mjs").then((m) => /* @__PURE__ */ __toESM(m.default))).getMachineId;
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.29";
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-D8mGyWmt.mjs"),
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$5() {
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$5()
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$5()
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$5()
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-BKB695RF.mjs"),
118057
- node: () => import("./NodeSqliteClient-md8xd9hY.mjs")
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$4 = map$3(now, formatIso);
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$4);
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$4;
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$4,
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$4;
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$3() {
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$3()
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$3()
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$3()
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$1(action)
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$1(action)
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$1(action),
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$1(action),
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$1(action),
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$1(action),
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$1(action),
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$3()
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$3(),
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$1(action) {
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 ../../node_modules/.bun/@cmmd-center+agent-browser@0.6.2+69e331b0475d65dc/node_modules/@cmmd-center/agent-browser/dist/backends/playwright-locators.js
240344
- function locatorForAction(page, action) {
240345
- return action.query ? locatorFromQuery(page, action.query) : page.locator(action.selector ?? "");
240346
- }
240347
- function locatorFromQuery(page, query) {
240348
- switch (query.kind) {
240349
- case "and": return locatorFromQuery(page, query.left).and(locatorFromQuery(page, query.right));
240350
- case "css": return page.locator(query.selector);
240351
- case "filter": return locatorFromQuery(page, query.base).filter(filterOptions(page, query.options));
240352
- case "first": return locatorFromQuery(page, query.base).first();
240353
- case "frame": return locatorInFrame(page, page.frameLocator(query.selector), query.child);
240354
- case "label": return page.getByLabel(query.text, query.options);
240355
- case "last": return locatorFromQuery(page, query.base).last();
240356
- case "locator": return locatorFromQuery(page, query.base).locator(query.selector, filterOptions(page, query.options));
240357
- case "nth": return locatorFromQuery(page, query.base).nth(query.index);
240358
- case "or": return locatorFromQuery(page, query.left).or(locatorFromQuery(page, query.right));
240359
- case "placeholder": return page.getByPlaceholder(query.text, query.options);
240360
- case "role": return page.getByRole(query.role, query.options);
240361
- case "testId": return page.getByTestId(query.testId);
240362
- case "text": return page.getByText(query.text, query.options);
240363
- }
240364
- }
240365
- function locatorInFrame(page, frame, query) {
240366
- switch (query.kind) {
240367
- case "css": return frame.locator(query.selector);
240368
- case "filter": return locatorInFrame(page, frame, query.base).filter(filterOptions(page, query.options));
240369
- case "first": return locatorInFrame(page, frame, query.base).first();
240370
- case "label": return frame.getByLabel(query.text, query.options);
240371
- case "last": return locatorInFrame(page, frame, query.base).last();
240372
- case "locator": return locatorInFrame(page, frame, query.base).locator(query.selector, filterOptions(page, query.options));
240373
- case "nth": return locatorInFrame(page, frame, query.base).nth(query.index);
240374
- case "placeholder": return frame.getByPlaceholder(query.text, query.options);
240375
- case "role": return frame.getByRole(query.role, query.options);
240376
- case "testId": return frame.getByTestId(query.testId);
240377
- case "text": return frame.getByText(query.text, query.options);
240378
- case "and":
240379
- case "frame":
240380
- case "or": throw new Error(`Unsupported frame locator query kind: ${query.kind}`);
240381
- }
240382
- }
240383
- function filterOptions(page, options) {
240384
- if (!options) return void 0;
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
- ...options.has === void 0 ? {} : { has: locatorFromQuery(page, options.has) },
240387
- ...options.hasNot === void 0 ? {} : { hasNot: locatorFromQuery(page, options.hasNot) },
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
- //#endregion
240393
- //#region ../../node_modules/.bun/@cmmd-center+agent-browser@0.6.2+69e331b0475d65dc/node_modules/@cmmd-center/agent-browser/dist/backends/playwright-backend.js
240394
- const TAB_METADATA_TIMEOUT_MS = 250;
240395
- const TAB_CONTROL_TIMEOUT_MS = 500;
240396
- var PlaywrightBackend = class {
240397
- options;
240398
- browser;
240399
- context;
240400
- pageIds = /* @__PURE__ */ new WeakMap();
240401
- logs = /* @__PURE__ */ new WeakMap();
240402
- registeredPages = /* @__PURE__ */ new WeakSet();
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
- async listTabs() {
240421
- const context = await this.getContext();
240422
- return Promise.all(context.pages().map((page) => this.toTab(page).info()));
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
- async selectedTab() {
240425
- const page = (await this.getContext()).pages().at(-1);
240426
- if (page) await settleValueWithin(page.bringToFront(), TAB_CONTROL_TIMEOUT_MS);
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 newTab() {
240430
- const page = await (await this.getContext()).newPage();
240431
- await this.preparePage(page);
240432
- await settleValueWithin(page.bringToFront(), TAB_CONTROL_TIMEOUT_MS);
240433
- return this.toTab(page);
240434
- }
240435
- async getTab(id) {
240436
- const page = (await this.getContext()).pages().find((candidate) => this.idFor(candidate) === id);
240437
- if (!page) throw new Error(`Tab not found: ${id}`);
240438
- await settleValueWithin(page.bringToFront(), TAB_CONTROL_TIMEOUT_MS);
240439
- return this.toTab(page);
240440
- }
240441
- async getContext() {
240442
- /* v8 ignore next -- persistent headed/default launch combinations are covered by smoke, not unit coverage. */
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
- const windowResult = await settleValueWithin(session.send("Browser.getWindowForTarget"), TAB_CONTROL_TIMEOUT_MS);
240485
- /* v8 ignore next -- Chromium may decline window control in unsupported headless/CDP environments. */
240486
- if (windowResult.status !== "fulfilled" || typeof windowResult.value.windowId !== "number") return;
240487
- await settleValueWithin(session.send("Browser.setWindowBounds", {
240488
- bounds: {
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
- return id;
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
- logsFor(page) {
240519
- let entries = this.logs.get(page);
240520
- if (!entries) {
240521
- entries = [];
240522
- this.logs.set(page, entries);
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
- return entries;
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
- const historyResult = await settleValueWithin(session.send("Page.getNavigationHistory"), TAB_METADATA_TIMEOUT_MS);
240559
- /* v8 ignore next -- Chromium navigation-history failures are bounded external CDP behavior. */
240560
- if (historyResult.status !== "fulfilled") return {};
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
- async goto(url) {
240571
- await this.page.goto(url, { waitUntil: "domcontentloaded" });
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
- async reload() {
240574
- await this.page.reload({ waitUntil: "domcontentloaded" });
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
- async forceReload() {
240577
- const client = await this.page.context().newCDPSession(this.page);
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 client.send("Page.reload", { ignoreCache: true });
240580
- /* v8 ignore next -- defensive fallback for navigations that do not settle after CDP reload. */
240581
- await this.page.waitForLoadState("domcontentloaded", { timeout: 3e4 }).catch(() => void 0);
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
- await client.detach();
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
- async back() {
240587
- await this.page.goBack({ waitUntil: "domcontentloaded" });
240588
- }
240589
- async forward() {
240590
- await this.page.goForward({ waitUntil: "domcontentloaded" });
240591
- }
240592
- async close() {
240593
- await this.page.close();
240594
- }
240595
- async screenshot(options = {}) {
240596
- const bytes = await this.page.screenshot({
240597
- ...options.fullPage === void 0 ? {} : { fullPage: options.fullPage },
240598
- ...options.clip === void 0 ? {} : { clip: options.clip },
240599
- ...options.mimeType === void 0 ? {} : { type: options.mimeType === "image/jpeg" ? "jpeg" : "png" },
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
- async setZoom(zoom) {
240612
- /* v8 ignore next 3 -- browser-executed callback runs outside Node coverage */
240613
- await this.page.evaluate((level) => {
240614
- document.documentElement.style.zoom = String(level);
240615
- }, zoom.level);
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
- async clearCookies() {
240618
- await this.page.context().clearCookies();
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
- async clearCache() {
240621
- const client = await this.page.context().newCDPSession(this.page);
240622
- try {
240623
- await client.send("Network.clearBrowserCache");
240624
- } finally {
240625
- await client.detach();
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
- async domSnapshot() {
240629
- return this.page.locator("body").ariaSnapshot({ timeout: 5e3 });
240630
- }
240631
- async evaluate(expression, arg) {
240632
- /* v8 ignore next 4 -- browser-executed callback runs outside Node coverage */
240633
- return this.page.evaluate((input) => {
240634
- return new Function("arg", `return (${input.expression})(arg);`)(input.arg);
240635
- }, {
240636
- arg,
240637
- expression
240638
- });
240639
- }
240640
- async waitForLoadState(state = "load", timeoutMs) {
240641
- await this.page.waitForLoadState(state, timeoutOptions(timeoutMs));
240642
- }
240643
- async waitForURL(url, timeoutMs) {
240644
- await this.page.waitForURL(url, timeoutOptions(timeoutMs));
240645
- }
240646
- async waitForDownload(timeoutMs) {
240647
- return toBrowserDownload(await this.page.waitForEvent("download", timeoutOptions(timeoutMs)));
240648
- }
240649
- async elementInfo(options) {
240650
- /* v8 ignore start -- browser-executed DOM inspection runs outside Node coverage */
240651
- return this.page.evaluate(({ x, y, includeNonInteractable }) => {
240652
- const element = document.elementFromPoint(x, y);
240653
- if (!element || element === document.documentElement || element === document.body) return;
240654
- const rect = element.getBoundingClientRect();
240655
- if (rect.width <= 0 || rect.height <= 0) return;
240656
- const style = window.getComputedStyle(element);
240657
- if (!includeNonInteractable && (style.visibility === "hidden" || style.display === "none" || style.pointerEvents === "none")) return;
240658
- const tagName = element.tagName.toLowerCase();
240659
- const visibleText = visibleElementText(element);
240660
- const ariaName = element.getAttribute("aria-label") ?? element.getAttribute("alt") ?? void 0;
240661
- const role = element.getAttribute("role") ?? inferRole(element);
240662
- const candidates = selectorCandidates(element);
240663
- const testId = element.getAttribute("data-testid") ?? element.getAttribute("data-test-id") ?? void 0;
240664
- const nodeId = ensureNodeId(element);
240665
- const preview = compactText(`${tagName}${role ? ` role=${role}` : ""}${ariaName ? ` aria="${ariaName}"` : ""}${visibleText ? ` text="${visibleText}"` : ""}`);
240666
- return {
240667
- ...ariaName ? { ariaName } : {},
240668
- boundingBox: {
240669
- x: rect.x,
240670
- y: rect.y,
240671
- width: rect.width,
240672
- height: rect.height
240673
- },
240674
- nodeId,
240675
- preview,
240676
- ...role ? { role } : {},
240677
- selector: {
240678
- candidates,
240679
- primary: candidates[0] ?? null
240680
- },
240681
- tagName,
240682
- ...testId ? { testId } : {},
240683
- ...visibleText ? { visibleText } : {}
240684
- };
240685
- function compactText(value) {
240686
- return value.replace(/\s+/g, " ").trim().slice(0, 180);
240687
- }
240688
- function visibleElementText(node) {
240689
- if (node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement) return compactText(node.value || node.placeholder || "");
240690
- if (node instanceof HTMLSelectElement) return compactText(node.selectedOptions[0]?.textContent ?? node.value);
240691
- return compactText(node.textContent ?? "") || void 0;
240692
- }
240693
- function inferRole(node) {
240694
- if (node instanceof HTMLAnchorElement && node.href) return "link";
240695
- if (node instanceof HTMLButtonElement) return "button";
240696
- if (node instanceof HTMLInputElement) return node.type || "textbox";
240697
- if (node instanceof HTMLSelectElement) return "combobox";
240698
- if (node instanceof HTMLTextAreaElement) return "textbox";
240699
- if (/^h[1-6]$/i.test(node.tagName)) return "heading";
240700
- }
240701
- function ensureNodeId(node) {
240702
- const existing = node.getAttribute("data-agent-browser-node-id");
240703
- if (existing) return Number(existing);
240704
- const next = Number(document.documentElement.getAttribute("data-agent-browser-next-node-id") ?? "1");
240705
- document.documentElement.setAttribute("data-agent-browser-next-node-id", String(next + 1));
240706
- node.setAttribute("data-agent-browser-node-id", String(next));
240707
- return next;
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
- function selectorCandidates(node) {
240710
- const candidates = [];
240711
- if (node.id) candidates.push(`#${cssEscape(node.id)}`);
240712
- const testId = node.getAttribute("data-testid") ?? node.getAttribute("data-test-id");
240713
- if (testId) candidates.push(`[data-testid="${cssString(testId)}"]`);
240714
- const href = node.getAttribute("href");
240715
- if (href) candidates.push(`${node.tagName.toLowerCase()}[href="${cssString(href)}"]`);
240716
- const aria = node.getAttribute("aria-label");
240717
- if (aria) candidates.push(`${node.tagName.toLowerCase()}[aria-label="${cssString(aria)}"]`);
240718
- const name = node.getAttribute("name");
240719
- if (name) candidates.push(`${node.tagName.toLowerCase()}[name="${cssString(name)}"]`);
240720
- const parts = [];
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
- function cssEscape(value) {
240748
- return CSS.escape(value);
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
- function cssString(value) {
240751
- return value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
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
- }, options);
240754
- /* v8 ignore stop */
240755
- }
240756
- async elementScreenshot(options) {
240757
- const element = await this.elementInfo(options);
240758
- if (!element?.boundingBox) return void 0;
240759
- return this.screenshot({ clip: element.boundingBox });
240760
- }
240761
- async click(action) {
240762
- const locator = this.locatorFor(action);
240763
- await assertSingle(locator.count(), locatorActionLabel(action));
240764
- await locator.click(timeoutOptions(action.timeoutMs));
240765
- }
240766
- async doubleClick(action) {
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
- byNodeId(nodeId) {
240929
- return this.page.locator(`[data-agent-browser-node-id="${cssStringForSelector(nodeId)}"]`);
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
- locatorFor(action) {
240932
- return locatorForAction(this.page, action);
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 mouseButton(button) {
240940
- if (button === 2) return "middle";
240941
- if (button === 3) return "right";
240942
- return "left";
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 cssStringForSelector(value) {
240945
- return value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
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 timeoutOptions(timeoutMs) {
240948
- return timeoutMs === void 0 ? {} : { timeout: timeoutMs };
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
- function toBrowserDownload(download) {
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
- failure: () => download.failure(),
240953
- path: () => download.path().catch(() => null),
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
- async function assertSingle(countPromise, selector) {
240960
- const count = await countPromise;
240961
- if (count !== 1) throw new Error(`Selector must resolve to exactly one element. selector=${selector} count=${count}`);
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
- function toConsoleLog(message, url) {
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
- level: normalizeLevel(message.type()),
240966
- message: message.text(),
240967
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
240968
- url
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 normalizeLevel(level) {
240972
- if (level === "debug" || level === "info" || level === "log" || level === "warn" || level === "error") return level;
240973
- /* v8 ignore next -- Playwright currently normalizes known console message types */
240974
- return "log";
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 ../../node_modules/.bun/debug@4.4.0+2b91fc17bf64bdfd/node_modules/debug/src/common.js
240978
- var require_common$2 = /* @__PURE__ */ __commonJSMin(((exports, module) => {
240979
- /**
240980
- * This is the common logic for both the Node.js and web browser
240981
- * implementations of `debug()`.
240982
- */
240983
- function setup(env) {
240984
- createDebug.debug = createDebug;
240985
- createDebug.default = createDebug;
240986
- createDebug.coerce = coerce;
240987
- createDebug.disable = disable;
240988
- createDebug.enable = enable;
240989
- createDebug.enabled = enabled;
240990
- createDebug.humanize = require_ms();
240991
- createDebug.destroy = destroy;
240992
- Object.keys(env).forEach((key) => {
240993
- createDebug[key] = env[key];
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
- * The currently active debug mode names, and names to skip.
240997
- */
240998
- createDebug.names = [];
240999
- createDebug.skips = [];
241000
- /**
241001
- * Map of special "%n" handling functions, for the debug "format" argument.
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
- function extend(namespace, delimiter) {
241083
- const newDebug = createDebug(this.namespace + (typeof delimiter === "undefined" ? ":" : delimiter) + namespace);
241084
- newDebug.log = this.log;
241085
- return newDebug;
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
- * Enables a debug mode by namespaces. This can include modes
241089
- * separated by a colon and wildcards.
241090
- *
241091
- * @param {String} namespaces
241092
- * @api public
241093
- */
241094
- function enable(namespaces) {
241095
- createDebug.save(namespaces);
241096
- createDebug.namespaces = namespaces;
241097
- createDebug.names = [];
241098
- createDebug.skips = [];
241099
- const split = (typeof namespaces === "string" ? namespaces : "").trim().replace(" ", ",").split(",").filter(Boolean);
241100
- for (const ns of split) if (ns[0] === "-") createDebug.skips.push(ns.slice(1));
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
- * Checks if the given string matches a namespace template, honoring
241105
- * asterisks as wildcards.
241106
- *
241107
- * @param {String} search
241108
- * @param {String} template
241109
- * @return {Boolean}
241110
- */
241111
- function matchesTemplate(search, template) {
241112
- let searchIndex = 0;
241113
- let templateIndex = 0;
241114
- let starIndex = -1;
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/sprite/spriteTaskHold.ts
284035
+ //#region src/browser/browserProfilePaths.ts
283440
284036
  /**
283441
- * Keeps a Fly.io Sprite awake for the duration of an agent turn.
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
- * Pure and dependency-free on purpose, for the same reason as
283489
- * `provider/claudeCredentialContainment.ts`: this rule is cost-critical and must
283490
- * stay directly unit-testable without booting the Effect runtime.
283491
- */
283492
- /** Set by CMMD's Sprites provider on every project sprite. Absent elsewhere. */
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 SPRITE_TASK_HOLD_REQUEST_TIMEOUT_MS = 1e4;
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
- * Resolve the hold configuration from the process environment.
284056
+ * E7 production orphan reaper (spec:
284057
+ * docs/specs/2026-06-10-windows-free-dev-and-browser-lifecycle-hardening-plan.md).
283508
284058
  *
283509
- * Returns null on every runtime that is not a project sprite — desktop-local and
283510
- * browser runtimes have no `/.sprite/api.sock`, so the whole feature no-ops
283511
- * there rather than erroring.
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
- function resolveSpriteTaskHoldConfig(env = process.env) {
283514
- const socketPath = env[SPRITE_TASK_HOLD_SOCKET_ENV]?.trim();
283515
- if (socketPath === void 0 || socketPath.length === 0) return null;
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
- socketPath,
283518
- namePrefix: env["FORGE_CMMD_SPRITE_TASK_HOLD_PREFIX"]?.trim() ?? ""
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
- * Hold names must be unique per turn: several git worktrees inside one sprite
283523
- * can run turns concurrently, and each has to be released independently.
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 spriteTaskHoldName(namePrefix, turnId) {
283526
- return `${namePrefix}${turnId}`;
284114
+ function isChromiumMainCommand(command, profileRoot) {
284115
+ return command.includes(`--user-data-dir=${profileRoot}/`) && !/\s--type=/.test(` ${command}`);
283527
284116
  }
283528
- const DEFAULT_TIMER = {
283529
- setInterval: (handler, ms) => setInterval(handler, ms),
283530
- clearInterval: (handle) => {
283531
- clearInterval(handle);
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
- * Tracks one hold per active turn.
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
- /** Turn ids that currently hold the sprite awake. Test/diagnostic surface. */
283555
- activeTurnIds() {
283556
- return [...this.#holds.keys()];
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
- async acquire(turnId, threadId = null) {
283559
- if (this.#holds.has(turnId)) return;
283560
- const name = spriteTaskHoldName(this.#config.namePrefix, turnId);
283561
- const hold = {
283562
- name,
283563
- threadId,
283564
- refresher: null,
283565
- inFlight: /* @__PURE__ */ new Set(),
283566
- released: false
283567
- };
283568
- this.#holds.set(turnId, hold);
283569
- const put = this.#transport.put(name, 300);
283570
- const tracked = put.then(() => {}, () => {});
283571
- hold.inFlight.add(tracked);
283572
- try {
283573
- await put;
283574
- } catch (cause) {
283575
- hold.inFlight.delete(tracked);
283576
- this.#onError(`failed to acquire sprite task hold for turn ${turnId}`, cause);
283577
- if (hold.released || this.#holds.get(turnId) !== hold) return;
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
- async release(turnId) {
283589
- const hold = this.#holds.get(turnId);
283590
- if (!hold) return;
283591
- hold.released = true;
283592
- if (hold.refresher !== null) {
283593
- this.#timer.clearInterval(hold.refresher);
283594
- hold.refresher = null;
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
- this.#holds.delete(turnId);
283597
- if (hold.inFlight.size > 0) await Promise.all(hold.inFlight);
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
- await this.#transport.delete(hold.name);
283600
- } catch (cause) {
283601
- this.#onError(`failed to release sprite task hold for turn ${turnId}`, cause);
284284
+ process.kill(pid, 0);
284285
+ } catch {
284286
+ return;
283602
284287
  }
283603
284288
  }
283604
- /**
283605
- * Release every hold belonging to one thread.
283606
- *
283607
- * `session.exited` names only the thread whose provider process died, and a
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
- * HTTP-over-unix-socket transport for the guest API.
283626
- *
283627
- * Bun's fetch takes a `unix` option; the URL host is ignored by the socket but
283628
- * still has to parse, which is why it is the literal `http://sprite` the
283629
- * platform docs use.
283630
- */
283631
- function makeSpriteTaskHoldTransport(config, fetchImpl = fetch) {
283632
- const call = async (path, init) => {
283633
- const controller = new AbortController();
283634
- const deadline = setTimeout(() => controller.abort(), SPRITE_TASK_HOLD_REQUEST_TIMEOUT_MS);
283635
- try {
283636
- const response = await fetchImpl(`http://sprite/v1/tasks${path}`, {
283637
- ...init,
283638
- signal: controller.signal,
283639
- unix: config.socketPath
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/Layers/BrowserOrphanReaper.ts
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 createNativeHostBrowserBridgeClient(options) {
284912
- const fetchImpl = options.fetch ?? fetch;
284913
- const request = async (method, params) => {
284914
- const abortController = new AbortController();
284915
- const timeout = setTimeout(() => abortController.abort(), method === "closeSession" ? agentBrowserCloseTimeoutMs() : 3e4);
284916
- const response = await fetchImpl(nativeHostBridgeUrl(options.baseUrl), {
284917
- method: "POST",
284918
- headers: {
284919
- authorization: `Bearer ${options.token}`,
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
- return (await response.json()).result;
284935
- };
284936
- const subscribe = (input, callback) => {
284937
- const abortController = new AbortController();
284938
- const readStream = async () => {
284939
- if (!options.fetch) {
284940
- await subscribeToNativeHostBridgeEventsViaNodeHttp({
284941
- abortSignal: abortController.signal,
284942
- callback,
284943
- input,
284944
- options
284945
- });
284946
- return;
284947
- }
284948
- const response = await fetchImpl(nativeHostBridgeEventsUrl(options.baseUrl, input), {
284949
- headers: { authorization: `Bearer ${options.token}` },
284950
- signal: abortController.signal
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
- function toHostCommand(command) {
284998
- switch (command.type) {
284999
- case "activateTab": return {
285000
- type: "activateTab",
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 toCommandResult(input) {
285077
+ function browserBackendOptionsForSession(input) {
285215
285078
  return {
285216
- ok: input.result.ok,
285217
- snapshot: toForgeSession({
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
- function commandCaptureTabId(command) {
285232
- switch (command.type) {
285233
- case "snapshot":
285234
- case "screenshot": return command.tabId;
285235
- default: return;
285236
- }
285237
- }
285238
- function toCommandLatestCapture(input) {
285239
- const tabId = commandCaptureTabId(input.command.command);
285240
- if (tabId === void 0 || input.event.type !== "snapshot.ready" && input.event.type !== "screenshot.ready" || input.event.sessionId !== sessionIdForThread(input.command.threadId) || input.event.payload.tabId !== tabId) return;
285241
- const mapped = toStreamEvent({
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
- const publishWebRtcIceCandidate = (event) => {
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
- runFork(logWarning$1(message, { cause: cause instanceof Error ? cause.message : cause }));
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
- const runtime = createAgentBrowserRuntime({
285663
- backends: { [DEFAULT_AGENT_BROWSER_ID]: new ForgeAgentBrowserPlaywrightBackend({
285664
- ...browserBackendOptionsForSession({
285665
- baseOptions: baseBackendOptions,
285666
- sessionId: "default"
285667
- }),
285668
- name: "Forge Agent Browser"
285669
- }) },
285670
- backendFactory: (input) => new ForgeAgentBrowserPlaywrightBackend({
285671
- ...browserBackendOptionsForSession({
285672
- baseOptions: baseBackendOptions,
285673
- initialUrl: sessionInitialUrls.get(input.sessionId),
285674
- sessionId: input.sessionId
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
- name: `Forge Agent Browser ${input.sessionId}`
285677
- })
285678
- });
285679
- yield* addFinalizer$2(() => promise(() => runtime.close()).pipe(orDie));
285680
- const hostBridge = createHostBrowserBridge({
285681
- browserId: DEFAULT_AGENT_BROWSER_ID,
285682
- closeTabsOnSessionClose: false,
285683
- defaultViewport: DEFAULT_AGENT_BROWSER_VIEWPORT,
285684
- runtime,
285685
- tabLifecycle: {
285686
- activateNextTabOnClose: true,
285687
- createFallbackTabOnLastClose: true,
285688
- fallbackUrl: DEFAULT_URL
285689
- }
285690
- });
285691
- const reaperConfig = browserOrphanReaperConfigFromEnv(process.env, AGENT_BROWSER_PROFILE_ROOT);
285692
- if (reaperConfig.enabled && process.platform !== "win32") {
285693
- const reaper = createBrowserOrphanReaper(createSystemBrowserOrphanReaperDeps({
285694
- profileRoot: AGENT_BROWSER_PROFILE_ROOT,
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
- requiresWebRtcRelay
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 "3aa3aed2accf8eb5e70a159243d778df7c98a4be";
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-DkAIzLIx.mjs"))).layer;
321184
- else return (yield* promise(() => import("./NodePTY-BhezYy9H.mjs"))).layer;
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-FbbQmsl_.mjs"))).layer(bunHttpServerOptions(config));
321189
- const [NodeHttpServer, NodeHttp] = yield* all([promise(() => import("./NodeHttpServer-BJ4l15f-.mjs")), promise(() => import("node:http"))]);
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-DC7nhA3w.mjs"));
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-D8mGyWmt.mjs")).default;
321696
+ const open = (await import("./open-2-reBrpv.mjs")).default;
322196
321697
  await open(url);
322197
321698
  return true;
322198
321699
  },