@cmmd-center/forge 0.13.30 → 0.13.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.mjs CHANGED
@@ -65937,7 +65937,7 @@ function normalizeNumberish(value) {
65937
65937
  }
65938
65938
  //#endregion
65939
65939
  //#region package.json
65940
- var version$1 = "0.13.30";
65940
+ var version$1 = "0.13.32";
65941
65941
  //#endregion
65942
65942
  //#region src/sentry.ts
65943
65943
  const SERVER_APP_NAME = "forge-server";
@@ -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);
@@ -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() {
@@ -239184,7 +239184,7 @@ var AgentBrowser = class {
239184
239184
  await this.options.events?.record({
239185
239185
  type: "browser.safety.confirmation.requested",
239186
239186
  ...request,
239187
- createdAt: nowIso$3()
239187
+ createdAt: nowIso$5()
239188
239188
  });
239189
239189
  const result = await this.resolveConfirmation(request);
239190
239190
  const approved = typeof result === "boolean" ? result : result.approved;
@@ -239192,7 +239192,7 @@ var AgentBrowser = class {
239192
239192
  type: "browser.safety.confirmation.resolved",
239193
239193
  approved,
239194
239194
  requestId: request.requestId,
239195
- resolvedAt: nowIso$3(),
239195
+ resolvedAt: nowIso$5(),
239196
239196
  ...this.events.sessionFields()
239197
239197
  });
239198
239198
  if (!approved) throw new BrowserSafetyError(decision.reason ?? "Browser action was not confirmed.", action, {
@@ -240322,6 +240322,1254 @@ function isPromiseLike(value) {
240322
240322
  return !!value && typeof value.then === "function";
240323
240323
  }
240324
240324
  //#endregion
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;
240402
+ return {
240403
+ socketPath,
240404
+ namePrefix: env["FORGE_CMMD_SPRITE_TASK_HOLD_PREFIX"]?.trim() ?? ""
240405
+ };
240406
+ }
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);
240418
+ }
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 ?? (() => {});
240439
+ }
240440
+ /** Turn ids that currently hold the sprite awake. Test/diagnostic surface. */
240441
+ activeTurnIds() {
240442
+ return [...this.#holds.keys()];
240443
+ }
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);
240458
+ try {
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;
240464
+ }
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);
240473
+ }
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;
240481
+ }
240482
+ this.#holds.delete(turnId);
240483
+ if (hold.inFlight.size > 0) await Promise.all(hold.inFlight);
240484
+ try {
240485
+ await this.#transport.delete(hold.name);
240486
+ } catch (cause) {
240487
+ this.#onError(`failed to release sprite task hold for turn ${turnId}`, cause);
240488
+ }
240489
+ }
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)));
240501
+ }
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)));
240508
+ }
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);
240521
+ try {
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}`);
240528
+ } finally {
240529
+ clearTimeout(deadline);
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
+ }
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;
240600
+ }
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;
240717
+ }
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
+ };
240744
+ }
240745
+ return {
240746
+ transport: "native-embedded",
240747
+ status: "connected",
240748
+ ...input.viewport ? { viewport: input.viewport } : {},
240749
+ reconnect: {
240750
+ status: "ready",
240751
+ attempt: 0
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
240774
+ }
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)
240872
+ }
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
240885
+ }
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
240898
+ }
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
+ }))
240911
+ }
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;
240926
+ }
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;
241011
+ }
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);
241035
+ }
241036
+ }
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
+ }
241053
+ }
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);
241070
+ }
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
+ }
241078
+ }
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
+ };
241112
+ return {
241113
+ type: "apply",
241114
+ controlSequence: metadata.controlSequence
241115
+ };
241116
+ }
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
+ }
241123
+ }
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);
241133
+ return {
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 } : {}
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
+ });
241216
+ }
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
+ });
241233
+ }
241234
+ //#endregion
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;
241267
+ });
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);
241276
+ }
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;
241290
+ }
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
+ });
241305
+ }
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
240325
241573
  //#region ../../node_modules/.bun/debug@4.4.0+2b91fc17bf64bdfd/node_modules/debug/src/common.js
240326
241574
  var require_common$2 = /* @__PURE__ */ __commonJSMin(((exports, module) => {
240327
241575
  /**
@@ -282784,234 +284032,317 @@ async function createBrowserWebRtcDisplayCaptureRuntime(options = {}) {
282784
284032
  }
282785
284033
  }
282786
284034
  //#endregion
282787
- //#region src/sprite/spriteTaskHold.ts
284035
+ //#region src/browser/browserProfilePaths.ts
282788
284036
  /**
282789
- * Keeps a Fly.io Sprite awake for the duration of an agent turn.
282790
- *
282791
- * ── why this exists ─────────────────────────────────────────────────────────
282792
- * A sprite's container cgroup is frozen within ~1 second of the last inbound
282793
- * request. Processes are paused mid-instruction, not killed, and resume when
282794
- * traffic returns. Measured on a throwaway sprite with a 1 Hz ticker
282795
- * (`pocs/sprite-lifecycle-spike/`):
282796
- *
282797
- * detached, nothing connected 2 ticks / 121 s = 1.7%
282798
- * with a task hold 90 ticks / 90 s = 100%
282799
- *
282800
- * So an agent turn that nobody is watching effectively stops. fly.io names our
282801
- * previous approach — relying on a running process after disconnection — as an
282802
- * anti-pattern, and points at the Tasks API instead. See
282803
- * https://docs.sprites.dev/keeping-sprites-running/ and
282804
- * `docs/specs/2026-07-30-sprites-platform-ground-truth.md` §1.
282805
- *
282806
- * Note that Services (the platform supervisor) do NOT solve this: "a Sprite with
282807
- * ten services defined still pauses when it goes idle." Supervision and
282808
- * wakefulness are separate primitives, and this module is only the second one.
282809
- *
282810
- * ── why it lives in the guest ───────────────────────────────────────────────
282811
- * The Tasks API is guest-only. `GET https://api.sprites.dev/v1/sprites/{n}/tasks`
282812
- * answers 404; the socket at /.sprite/api.sock is the only surface. CMMD cannot
282813
- * take this hold on our behalf, which is why the env vars below are exported by
282814
- * CMMD's provider and consumed here.
282815
- *
282816
- * ── the ordering rules ──────────────────────────────────────────────────────
282817
- * 1. The refresher MUST be stopped before the DELETE is issued. A refresher that
282818
- * outlives the delete re-acquires the hold on its next tick; that was
282819
- * previously measured leaking 37 renewals over 18 minutes, pinning a sprite
282820
- * running — and billing — 33 minutes after its work had ended.
282821
- * 2. Stopping the interval only cancels FUTURE ticks. A PUT already on the wire
282822
- * must be JOINED before the DELETE, or it lands afterwards and recreates the
282823
- * hold for another full expiry window. fly.io's reference worker cancels and
282824
- * awaits its heartbeat for the same reason.
282825
- * 3. `release` can arrive while `acquire` is still awaiting its own PUT —
282826
- * `releaseAll` runs from runtime drain and the shutdown finalizer,
282827
- * concurrently with the provider event stream. The hold is registered before
282828
- * that await (so a duplicate turn.started cannot double-acquire), so release
282829
- * can remove the map entry first; `acquire` must then NOT install a
282830
- * refresher. One attached to an unmapped hold is unreachable by release,
282831
- * releaseThread and releaseAll alike, and re-PUTs every 60 s for the life of
282832
- * the process — an unbounded pin.
282833
- *
282834
- * All three are pinned by tests.
284037
+ * Canonical Agent Browser Chromium profile paths.
282835
284038
  *
282836
- * Pure and dependency-free on purpose, for the same reason as
282837
- * `provider/claudeCredentialContainment.ts`: this rule is cost-critical and must
282838
- * stay directly unit-testable without booting the Effect runtime.
282839
- */
282840
- /** Set by CMMD's Sprites provider on every project sprite. Absent elsewhere. */
282841
- const SPRITE_TASK_HOLD_SOCKET_ENV = "FORGE_CMMD_SPRITE_TASK_HOLD_SOCKET";
282842
- const SPRITE_TASK_HOLD_REFRESH_MS = 6e4;
282843
- /**
282844
- * Per-request deadline for the guest-socket calls. Must stay strictly under
282845
- * SPRITE_TASK_HOLD_REFRESH_MS: the manager serializes refreshes, so a PUT that
282846
- * never settles turns every later tick into a no-op and the hold expires
282847
- * mid-turn. Bounding the call means a hung socket settles as a rejection, the
282848
- * next 60 s tick retries, and release is not wedged joining it. From the last
282849
- * good refresh there are still four attempts (+60/+120/+180/+240 s) before the
282850
- * 300 s expiry. 10 s matches the `-m 10` cap CMMD already puts on every curl in
282851
- * 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.
282852
284044
  */
282853
- 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
282854
284055
  /**
282855
- * 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).
282856
284058
  *
282857
- * Returns null on every runtime that is not a project sprite — desktop-local and
282858
- * browser runtimes have no `/.sprite/api.sock`, so the whole feature no-ops
282859
- * 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.
282860
284078
  */
282861
- function resolveSpriteTaskHoldConfig(env = process.env) {
282862
- const socketPath = env[SPRITE_TASK_HOLD_SOCKET_ENV]?.trim();
282863
- 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();
282864
284093
  return {
282865
- socketPath,
282866
- 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"])
282867
284100
  };
282868
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
+ }
282869
284109
  /**
282870
- * Hold names must be unique per turn: several git worktrees inside one sprite
282871
- * 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.
282872
284113
  */
282873
- function spriteTaskHoldName(namePrefix, turnId) {
282874
- return `${namePrefix}${turnId}`;
284114
+ function isChromiumMainCommand(command, profileRoot) {
284115
+ return command.includes(`--user-data-dir=${profileRoot}/`) && !/\s--type=/.test(` ${command}`);
282875
284116
  }
282876
- const DEFAULT_TIMER = {
282877
- setInterval: (handler, ms) => setInterval(handler, ms),
282878
- clearInterval: (handle) => {
282879
- 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;
282880
284182
  }
282881
- };
282882
- /**
282883
- * Tracks one hold per active turn.
282884
- *
282885
- * Every method is failure-tolerant by design. Acquiring a hold is an
282886
- * optimisation, not a precondition: if it fails the turn still runs, just at the
282887
- * degraded speed it has today. Failing the turn instead would convert a billing
282888
- * inefficiency into an outage.
282889
- */
282890
- var SpriteTaskHoldManager = class {
282891
- #config;
282892
- #transport;
282893
- #timer;
282894
- #onError;
282895
- #holds = /* @__PURE__ */ new Map();
282896
- constructor(options) {
282897
- this.#config = options.config;
282898
- this.#transport = options.transport;
282899
- this.#timer = options.timer ?? DEFAULT_TIMER;
282900
- 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" });
282901
284186
  }
282902
- /** Turn ids that currently hold the sprite awake. Test/diagnostic surface. */
282903
- activeTurnIds() {
282904
- 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;
282905
284193
  }
282906
- async acquire(turnId, threadId = null) {
282907
- if (this.#holds.has(turnId)) return;
282908
- const name = spriteTaskHoldName(this.#config.namePrefix, turnId);
282909
- const hold = {
282910
- name,
282911
- threadId,
282912
- refresher: null,
282913
- inFlight: /* @__PURE__ */ new Set(),
282914
- released: false
282915
- };
282916
- this.#holds.set(turnId, hold);
282917
- const put = this.#transport.put(name, 300);
282918
- const tracked = put.then(() => {}, () => {});
282919
- hold.inFlight.add(tracked);
282920
- try {
282921
- await put;
282922
- } catch (cause) {
282923
- hold.inFlight.delete(tracked);
282924
- this.#onError(`failed to acquire sprite task hold for turn ${turnId}`, cause);
282925
- 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" });
282926
284228
  }
282927
- hold.inFlight.delete(tracked);
282928
- if (hold.released || this.#holds.get(turnId) !== hold) return;
282929
- hold.refresher = this.#timer.setInterval(() => {
282930
- if (hold.inFlight.size > 0) return;
282931
- const refresh = this.#transport.put(name, 300).catch((cause) => this.#onError(`failed to refresh sprite task hold for turn ${turnId}`, cause));
282932
- hold.inFlight.add(refresh);
282933
- refresh.finally(() => hold.inFlight.delete(refresh));
282934
- }, SPRITE_TASK_HOLD_REFRESH_MS);
282935
284229
  }
282936
- async release(turnId) {
282937
- const hold = this.#holds.get(turnId);
282938
- if (!hold) return;
282939
- hold.released = true;
282940
- if (hold.refresher !== null) {
282941
- this.#timer.clearInterval(hold.refresher);
282942
- 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);
282943
284254
  }
282944
- this.#holds.delete(turnId);
282945
- 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);
282946
284283
  try {
282947
- await this.#transport.delete(hold.name);
282948
- } catch (cause) {
282949
- this.#onError(`failed to release sprite task hold for turn ${turnId}`, cause);
284284
+ process.kill(pid, 0);
284285
+ } catch {
284286
+ return;
282950
284287
  }
282951
284288
  }
282952
- /**
282953
- * Release every hold belonging to one thread.
282954
- *
282955
- * `session.exited` names only the thread whose provider process died, and a
282956
- * dead session's turn will never emit `turn.completed`. Releasing by thread —
282957
- * rather than releasing everything — is what keeps a crash in one worktree
282958
- * from silently dropping the holds of the other turns running beside it.
282959
- */
282960
- async releaseThread(threadId) {
282961
- const turnIds = [...this.#holds.entries()].filter(([, hold]) => hold.threadId === threadId).map(([turnId]) => turnId);
282962
- await Promise.all(turnIds.map((turnId) => this.release(turnId)));
282963
- }
282964
- /**
282965
- * Release everything. Runtime drain and server shutdown must call this, or a
282966
- * drained sprite keeps billing until each hold expires.
282967
- */
282968
- async releaseAll() {
282969
- 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;
282970
284293
  }
282971
- };
282972
- /**
282973
- * HTTP-over-unix-socket transport for the guest API.
282974
- *
282975
- * Bun's fetch takes a `unix` option; the URL host is ignored by the socket but
282976
- * still has to parse, which is why it is the literal `http://sprite` the
282977
- * platform docs use.
282978
- */
282979
- function makeSpriteTaskHoldTransport(config, fetchImpl = fetch) {
282980
- const call = async (path, init) => {
282981
- const controller = new AbortController();
282982
- const deadline = setTimeout(() => controller.abort(), SPRITE_TASK_HOLD_REQUEST_TIMEOUT_MS);
282983
- try {
282984
- const response = await fetchImpl(`http://sprite/v1/tasks${path}`, {
282985
- ...init,
282986
- signal: controller.signal,
282987
- 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
282988
284343
  });
282989
- if (!response.ok) throw new Error(`sprite task API responded ${response.status}`);
282990
- } finally {
282991
- clearTimeout(deadline);
282992
284344
  }
282993
284345
  };
282994
- return {
282995
- put: (name, expireSeconds) => call(`/${encodeURIComponent(name)}`, {
282996
- method: "PUT",
282997
- headers: { "content-type": "application/json" },
282998
- body: JSON.stringify({ expire: `${expireSeconds}s` })
282999
- }),
283000
- delete: (name) => call(`/${encodeURIComponent(name)}`, { method: "DELETE" })
283001
- };
283002
- }
283003
- /**
283004
- * Build a manager for this process, or null when the runtime is not a sprite.
283005
- * Callers treat null as "this feature does not apply here".
283006
- */
283007
- function makeSpriteTaskHoldManager(env = process.env, onError) {
283008
- const config = resolveSpriteTaskHoldConfig(env);
283009
- if (config === null) return null;
283010
- return new SpriteTaskHoldManager({
283011
- config,
283012
- transport: makeSpriteTaskHoldTransport(config),
283013
- ...onError ? { onError } : {}
283014
- });
283015
284346
  }
283016
284347
  //#endregion
283017
284348
  //#region src/browser/Layers/displaySessionTaskHold.ts
@@ -283443,364 +284774,6 @@ function createWebRtcDisplaySessionManager(input = {}) {
283443
284774
  })
283444
284775
  };
283445
284776
  }
283446
- //#endregion
283447
- //#region src/browser/browserProfilePaths.ts
283448
- /**
283449
- * Canonical Agent Browser Chromium profile paths.
283450
- *
283451
- * Both the host bridge adapter (which launches Chromium with a per-session
283452
- * `userDataDir`) and the orphan reaper (which reconciles live Chromium
283453
- * processes against registered sessions) must agree byte-for-byte on this
283454
- * mapping — the reaper matches processes to sessions by profile directory
283455
- * basename.
283456
- */
283457
- const AGENT_BROWSER_PROFILE_ROOT = "/tmp/forge-agent-browser-profiles";
283458
- /** Sanitized directory basename for a session id. Lossy — never invert it. */
283459
- function browserProfileSessionKey(sessionId) {
283460
- return sessionId.replace(/[^a-zA-Z0-9_.-]/g, "_").slice(0, 160) || "default";
283461
- }
283462
- function browserProfileDirForSession(sessionId, root = AGENT_BROWSER_PROFILE_ROOT) {
283463
- return `${root}/${browserProfileSessionKey(sessionId)}`;
283464
- }
283465
- //#endregion
283466
- //#region src/browser/Layers/BrowserOrphanReaper.ts
283467
- /**
283468
- * E7 production orphan reaper (spec:
283469
- * docs/specs/2026-06-10-windows-free-dev-and-browser-lifecycle-hardening-plan.md).
283470
- *
283471
- * Periodically reconciles live Chromium browser-main processes (distinct
283472
- * `--user-data-dir` under the Agent Browser profile root) against the set of
283473
- * sessions the host bridge knows about, kills orphans, enforces a max-session
283474
- * TTL through graceful closes, and removes stale profile directories.
283475
- *
283476
- * Guard-rails:
283477
- * - never kills a registered live session;
283478
- * - the first sweep *attempt* after boot is observe-only (cold-start grace)
283479
- * -- this is consumed unconditionally by that one attempt, so a transient
283480
- * read failure on it can never grant a second free pass (which would
283481
- * otherwise let a restart-orphaned Chromium outlive two sweep intervals
283482
- * instead of one);
283483
- * - unregistered Chromium younger than `minOrphanAgeMs` is spared so
283484
- * in-flight opens are never raced;
283485
- * - if the session registry cannot be read, the sweep kills nothing --
283486
- * distinct from a registry that reads back empty (e.g. a freshly booted
283487
- * server with zero sessions), which is healthy and reapable;
283488
- * - `protectedSessionKeys` (the shared "default" backend profile) are never
283489
- * touched.
283490
- */
283491
- const execFileAsync$2 = promisify$1(execFile);
283492
- var BrowserReaperStepError = class extends TaggedError("BrowserReaperStepError") {};
283493
- const DEFAULT_REAPER_INTERVAL_MS = 6e4;
283494
- const DEFAULT_MAX_SESSION_TTL_MS = 14400 * 1e3;
283495
- const DEFAULT_MIN_ORPHAN_AGE_MS = 120 * 1e3;
283496
- const KILL_ESCALATION_TIMEOUT_MS = 5e3;
283497
- const KILL_POLL_INTERVAL_MS = 100;
283498
- function positiveIntFromEnv(value) {
283499
- if (!value) return;
283500
- const parsed = Number.parseInt(value, 10);
283501
- return Number.isFinite(parsed) && parsed >= 0 ? parsed : void 0;
283502
- }
283503
- function browserOrphanReaperConfigFromEnv(env, profileRoot = "/tmp/forge-agent-browser-profiles") {
283504
- const enabledRaw = env.FORGE_BROWSER_REAPER_ENABLED?.trim().toLowerCase();
283505
- return {
283506
- profileRoot,
283507
- intervalMs: positiveIntFromEnv(env.FORGE_BROWSER_REAPER_INTERVAL_MS) || DEFAULT_REAPER_INTERVAL_MS,
283508
- maxSessionTtlMs: positiveIntFromEnv(env.FORGE_BROWSER_SESSION_MAX_TTL_MS) ?? DEFAULT_MAX_SESSION_TTL_MS,
283509
- minOrphanAgeMs: positiveIntFromEnv(env.FORGE_BROWSER_REAPER_MIN_ORPHAN_AGE_MS) ?? DEFAULT_MIN_ORPHAN_AGE_MS,
283510
- enabled: enabledRaw !== "0" && enabledRaw !== "false",
283511
- protectedSessionKeys: new Set(["default"])
283512
- };
283513
- }
283514
- /** [[dd-]hh:]mm:ss as printed by `ps -o etime=`. Returns null on malformed input. */
283515
- function parseEtimeToMs(etime) {
283516
- const match = /^(?:(\d+)-)?(?:(\d+):)?(\d+):(\d+)$/.exec(etime.trim());
283517
- if (!match) return null;
283518
- const [, days, hours, minutes, seconds] = match;
283519
- return (Number(days ?? 0) * 24 * 60 * 60 + Number(hours ?? 0) * 60 * 60 + Number(minutes) * 60 + Number(seconds)) * 1e3;
283520
- }
283521
- /**
283522
- * A Chromium browser-main owns a profile under our root and has no `--type=`
283523
- * flag (renderer/gpu/utility children all carry one). Killing the main tears
283524
- * down the whole Chromium process tree.
283525
- */
283526
- function isChromiumMainCommand(command, profileRoot) {
283527
- return command.includes(`--user-data-dir=${profileRoot}/`) && !/\s--type=/.test(` ${command}`);
283528
- }
283529
- function extractUserDataDir(command) {
283530
- const match = /--user-data-dir=(\S+)/.exec(command);
283531
- return match ? match[1] ?? null : null;
283532
- }
283533
- function profileKeyForDir(userDataDir, profileRoot) {
283534
- const resolved = path.resolve(userDataDir);
283535
- const resolvedRoot = path.resolve(profileRoot);
283536
- if (resolved === resolvedRoot || !resolved.startsWith(resolvedRoot + path.sep)) return null;
283537
- const relative = resolved.slice(resolvedRoot.length + 1);
283538
- return relative.includes(path.sep) ? null : relative;
283539
- }
283540
- function makeSweepState(deps, config, observeOnly) {
283541
- return {
283542
- deps,
283543
- config,
283544
- observeOnly,
283545
- errors: [],
283546
- wouldReapOrphans: [],
283547
- wouldCloseTtlSessions: [],
283548
- wouldRemoveStaleProfiles: [],
283549
- killedOrphans: [],
283550
- closedTtlSessions: [],
283551
- removedStaleProfiles: []
283552
- };
283553
- }
283554
- function sweepReport(state, registryUnavailable) {
283555
- return {
283556
- observeOnly: state.observeOnly,
283557
- registryUnavailable,
283558
- wouldReapOrphans: state.wouldReapOrphans,
283559
- wouldCloseTtlSessions: state.wouldCloseTtlSessions,
283560
- wouldRemoveStaleProfiles: state.wouldRemoveStaleProfiles,
283561
- killedOrphans: state.killedOrphans,
283562
- closedTtlSessions: state.closedTtlSessions,
283563
- removedStaleProfiles: state.removedStaleProfiles,
283564
- errors: state.errors
283565
- };
283566
- }
283567
- /** Run a dep call; on failure record the error into the state and yield null. */
283568
- function attemptOrNull(state, label, run) {
283569
- return tryPromise({
283570
- try: run,
283571
- catch: (error) => new BrowserReaperStepError({
283572
- message: `${label}: ${error instanceof Error ? error.message : String(error)}`,
283573
- cause: error
283574
- })
283575
- }).pipe(map$3((value) => value), catch_((error) => sync$1(() => (state.errors.push(error.message), null))));
283576
- }
283577
- /** Like attemptOrNull, but collapse the outcome to a success flag. */
283578
- function attemptFlag(state, label, run) {
283579
- return attemptOrNull(state, label, async () => {
283580
- await run();
283581
- return true;
283582
- }).pipe(map$3((value) => value === true));
283583
- }
283584
- function mapRegisteredByKey(registered) {
283585
- const registeredByKey = /* @__PURE__ */ new Map();
283586
- for (const sessionId of registered) registeredByKey.set(browserProfileSessionKey(sessionId), sessionId);
283587
- return registeredByKey;
283588
- }
283589
- const enforceSessionTtl = (state, sessionId, main) => gen(function* () {
283590
- if (state.config.maxSessionTtlMs <= 0 || main.ageMs <= state.config.maxSessionTtlMs) return;
283591
- if (state.observeOnly) {
283592
- state.wouldCloseTtlSessions.push(sessionId);
283593
- return;
283594
- }
283595
- if (yield* attemptFlag(state, `closeSession(${sessionId})`, () => state.deps.closeSession(sessionId))) {
283596
- state.closedTtlSessions.push(sessionId);
283597
- yield* increment(browserSessionsReapedTotal, { reason: "ttl" });
283598
- }
283599
- });
283600
- const reapOrphanMain = (state, key, main) => gen(function* () {
283601
- if (main.ageMs < state.config.minOrphanAgeMs) return;
283602
- if (state.observeOnly) {
283603
- state.wouldReapOrphans.push(key);
283604
- return;
283605
- }
283606
- if (!(yield* attemptFlag(state, `killProcess(${main.pid})`, () => state.deps.killProcess(main.pid)))) return;
283607
- state.killedOrphans.push(key);
283608
- yield* increment(browserSessionsReapedTotal, { reason: "orphan" });
283609
- yield* attemptFlag(state, `removeProfileDir(${main.userDataDir})`, () => state.deps.removeProfileDir(main.userDataDir));
283610
- });
283611
- const sweepChromiumMains = (state, registeredByKey, mains) => gen(function* () {
283612
- const liveKeys = /* @__PURE__ */ new Set();
283613
- for (const main of mains) {
283614
- const key = profileKeyForDir(main.userDataDir, state.config.profileRoot);
283615
- if (key === null) continue;
283616
- liveKeys.add(key);
283617
- if (state.config.protectedSessionKeys.has(key)) continue;
283618
- const registeredSessionId = registeredByKey.get(key);
283619
- if (registeredSessionId !== void 0) yield* enforceSessionTtl(state, registeredSessionId, main);
283620
- else yield* reapOrphanMain(state, key, main);
283621
- }
283622
- return liveKeys;
283623
- });
283624
- function isRetainedProfileDir(state, registeredByKey, liveKeys, entry) {
283625
- const key = profileKeyForDir(entry.path, state.config.profileRoot);
283626
- return key === null || state.config.protectedSessionKeys.has(key) || registeredByKey.has(key) || liveKeys.has(key) || entry.ageMs < state.config.minOrphanAgeMs;
283627
- }
283628
- const sweepStaleProfileDirs = (state, registeredByKey, liveKeys) => gen(function* () {
283629
- const profileDirs = yield* attemptOrNull(state, "listProfileDirs", state.deps.listProfileDirs);
283630
- if (profileDirs === null) return;
283631
- for (const entry of profileDirs) {
283632
- if (isRetainedProfileDir(state, registeredByKey, liveKeys, entry)) continue;
283633
- if (state.observeOnly) {
283634
- state.wouldRemoveStaleProfiles.push(entry.path);
283635
- continue;
283636
- }
283637
- if (yield* attemptFlag(state, `removeProfileDir(${entry.path})`, () => state.deps.removeProfileDir(entry.path))) {
283638
- state.removedStaleProfiles.push(entry.path);
283639
- yield* increment(browserSessionsReapedTotal, { reason: "stale_profile" });
283640
- }
283641
- }
283642
- });
283643
- function sweepHasActivity(report) {
283644
- 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;
283645
- }
283646
- const logSweepReportIfActive = (report) => sweepHasActivity(report) ? logInfo("browser orphan reaper sweep", {
283647
- observeOnly: report.observeOnly,
283648
- killedOrphans: report.killedOrphans,
283649
- closedTtlSessions: report.closedTtlSessions,
283650
- removedStaleProfiles: report.removedStaleProfiles,
283651
- wouldReapOrphans: report.wouldReapOrphans,
283652
- wouldCloseTtlSessions: report.wouldCloseTtlSessions,
283653
- wouldRemoveStaleProfiles: report.wouldRemoveStaleProfiles,
283654
- errors: report.errors
283655
- }) : void_$1;
283656
- function createBrowserOrphanReaper(deps, config) {
283657
- let coldStartGraceConsumed = false;
283658
- return { sweep: gen(function* () {
283659
- const observeOnly = !coldStartGraceConsumed;
283660
- coldStartGraceConsumed = true;
283661
- const state = makeSweepState(deps, config, observeOnly);
283662
- const registered = yield* attemptOrNull(state, "listRegisteredSessionIds", deps.listRegisteredSessionIds);
283663
- if (registered === null) {
283664
- yield* logWarning$1("browser orphan reaper: session registry unavailable; skipping", { errors: state.errors });
283665
- return sweepReport(state, true);
283666
- }
283667
- const mains = yield* attemptOrNull(state, "listChromiumMains", deps.listChromiumMains);
283668
- if (mains === null) return sweepReport(state, false);
283669
- const registeredByKey = mapRegisteredByKey(registered);
283670
- yield* sweepStaleProfileDirs(state, registeredByKey, yield* sweepChromiumMains(state, registeredByKey, mains));
283671
- const report = sweepReport(state, false);
283672
- yield* logSweepReportIfActive(report);
283673
- return report;
283674
- }) };
283675
- }
283676
- function sleep(ms) {
283677
- return new Promise((resolve) => {
283678
- setTimeout(resolve, ms).unref?.();
283679
- });
283680
- }
283681
- function isEsrch(error) {
283682
- return typeof error === "object" && error !== null && "code" in error && error.code === "ESRCH";
283683
- }
283684
- /** SIGTERM, poll for exit, escalate to SIGKILL. Chromium tears down its own children. */
283685
- async function killProcessWithEscalation(pid) {
283686
- try {
283687
- process.kill(pid, "SIGTERM");
283688
- } catch (error) {
283689
- if (isEsrch(error)) return;
283690
- throw error;
283691
- }
283692
- const deadline = Date.now() + KILL_ESCALATION_TIMEOUT_MS;
283693
- while (Date.now() < deadline) {
283694
- await sleep(KILL_POLL_INTERVAL_MS);
283695
- try {
283696
- process.kill(pid, 0);
283697
- } catch {
283698
- return;
283699
- }
283700
- }
283701
- try {
283702
- process.kill(pid, "SIGKILL");
283703
- } catch (error) {
283704
- if (!isEsrch(error)) throw error;
283705
- }
283706
- }
283707
- function createSystemBrowserOrphanReaperDeps(input) {
283708
- const { profileRoot } = input;
283709
- return {
283710
- listChromiumMains: async () => {
283711
- const { stdout } = await execFileAsync$2("ps", ["-axo", "pid=,etime=,args="], { maxBuffer: 16 * 1024 * 1024 });
283712
- const mains = [];
283713
- for (const line of stdout.split("\n")) {
283714
- const match = /^\s*(\d+)\s+(\S+)\s+(.+)$/.exec(line);
283715
- if (!match) continue;
283716
- const [, pidRaw, etime, command] = match;
283717
- if (!isChromiumMainCommand(command, profileRoot)) continue;
283718
- const userDataDir = extractUserDataDir(command);
283719
- const ageMs = parseEtimeToMs(etime);
283720
- if (userDataDir === null || ageMs === null) continue;
283721
- mains.push({
283722
- pid: Number(pidRaw),
283723
- userDataDir,
283724
- ageMs
283725
- });
283726
- }
283727
- return mains;
283728
- },
283729
- listRegisteredSessionIds: async () => (await input.listSessions()).map((session) => session.sessionId),
283730
- closeSession: input.closeSession,
283731
- killProcess: killProcessWithEscalation,
283732
- listProfileDirs: async () => {
283733
- const entries = await fs$1.readdir(profileRoot, { withFileTypes: true }).catch(() => []);
283734
- const now = Date.now();
283735
- const dirs = [];
283736
- for (const entry of entries) {
283737
- if (!entry.isDirectory()) continue;
283738
- const dirPath = path.join(profileRoot, entry.name);
283739
- const stat = await fs$1.stat(dirPath).catch(() => null);
283740
- if (stat === null) continue;
283741
- dirs.push({
283742
- path: dirPath,
283743
- ageMs: Math.max(0, now - stat.mtimeMs)
283744
- });
283745
- }
283746
- return dirs;
283747
- },
283748
- removeProfileDir: async (dir) => {
283749
- const resolved = path.resolve(dir);
283750
- const resolvedRoot = path.resolve(profileRoot);
283751
- if (resolved === resolvedRoot || !resolved.startsWith(resolvedRoot + path.sep)) throw new Error(`refusing to remove path outside the profile root: ${dir}`);
283752
- await fs$1.rm(resolved, {
283753
- recursive: true,
283754
- force: true
283755
- });
283756
- }
283757
- };
283758
- }
283759
- //#endregion
283760
- //#region src/browser/browserControlProtocol.ts
283761
- function resolveBrowserControlDecision(state, metadata) {
283762
- const hasSessionEpoch = metadata.sessionEpoch !== void 0;
283763
- const hasControlSequence = metadata.controlSequence !== void 0;
283764
- const expectedControlSequence = state.lastAppliedControlSequence + 1;
283765
- if (!hasSessionEpoch && !hasControlSequence) return {
283766
- type: "apply",
283767
- controlSequence: expectedControlSequence
283768
- };
283769
- if (!hasSessionEpoch || !hasControlSequence) return {
283770
- type: "reject",
283771
- reason: "incomplete_metadata",
283772
- expectedSessionEpoch: state.sessionEpoch,
283773
- expectedControlSequence,
283774
- acknowledgedControlSequence: state.lastAppliedControlSequence
283775
- };
283776
- if (metadata.sessionEpoch !== state.sessionEpoch) return {
283777
- type: "reject",
283778
- reason: "stale_epoch",
283779
- expectedSessionEpoch: state.sessionEpoch,
283780
- acknowledgedControlSequence: state.lastAppliedControlSequence
283781
- };
283782
- if (metadata.controlSequence <= state.lastAppliedControlSequence) return {
283783
- type: "duplicate",
283784
- acknowledgedControlSequence: state.lastAppliedControlSequence
283785
- };
283786
- if (metadata.controlSequence !== expectedControlSequence) return {
283787
- type: "reject",
283788
- reason: "sequence_gap",
283789
- expectedControlSequence,
283790
- acknowledgedControlSequence: state.lastAppliedControlSequence
283791
- };
283792
- return {
283793
- type: "apply",
283794
- controlSequence: metadata.controlSequence
283795
- };
283796
- }
283797
- function browserControlRejectionDetail(decision) {
283798
- switch (decision.reason) {
283799
- case "stale_epoch": return `Browser session is stale. Expected epoch ${decision.expectedSessionEpoch}.`;
283800
- case "sequence_gap": return `Browser control sequence is out of order. Expected ${decision.expectedControlSequence}.`;
283801
- case "incomplete_metadata": return "Browser control ordering requires both sessionEpoch and controlSequence.";
283802
- }
283803
- }
283804
284777
  const HOST_BRIDGE_RECONNECT_MAX_MS = 1e3;
283805
284778
  /**
283806
284779
  * Exponential backoff, capped.
@@ -284098,162 +285071,21 @@ function createNativeHostBrowserBridgeClientImplementation(options, deps) {
284098
285071
  }
284099
285072
  //#endregion
284100
285073
  //#region src/browser/AgentBrowserHostBridgeAdapter.ts
284101
- const DEFAULT_URL = "about:blank";
284102
- const DEFAULT_RENDER_MODE = "interactive";
284103
- const DEFAULT_AGENT_BROWSER_ID = "embedded";
284104
- const DEFAULT_AGENT_BROWSER_VIEWPORT = {
284105
- width: 1280,
284106
- height: 720
284107
- };
284108
- const ABSOLUTE_WEB_URL_PATTERN = /^[a-z][a-z\d+\-.]*:\/\//i;
284109
- const SPECIAL_URL_PATTERN = /^(about|blob|chrome|data|devtools|file):/i;
284110
- const LOCAL_URL_LIKE_PATTERN = /^(localhost|127\.0\.0\.1|\[::1\]|::1|[a-z0-9.-]+\.localhost)(?::\d+)?(?:[/?#].*)?$/i;
284111
- const DEFAULT_AGENT_BROWSER_DISPLAY = {
284112
- transport: "remote-webrtc",
284113
- status: "connected",
284114
- reconnect: {
284115
- status: "ready",
284116
- attempt: 0
284117
- }
284118
- };
284119
- const DEFAULT_AGENT_BROWSER_CONTROL_COMMAND_TIMEOUT_MS = 1500;
284120
- const DEFAULT_AGENT_BROWSER_FAST_COMMAND_TIMEOUT_MS = 1e4;
284121
- const DEFAULT_AGENT_BROWSER_NAVIGATION_COMMAND_TIMEOUT_MS = 9e4;
284122
- const DEFAULT_AGENT_BROWSER_CAPTURE_COMMAND_TIMEOUT_MS = 9e4;
284123
- const DEFAULT_AGENT_BROWSER_BRIDGE_OPEN_TIMEOUT_MS = 9e4;
284124
- const DEFAULT_AGENT_BROWSER_BRIDGE_CLOSE_TIMEOUT_MS = 3e4;
284125
- const DEFAULT_AGENT_BROWSER_PLAYWRIGHT_BACKEND_CLOSE_TIMEOUT_MS = 1e4;
284126
- 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.";
284127
- 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.";
284128
- function nowIso$2() {
284129
- return (/* @__PURE__ */ new Date()).toISOString();
284130
- }
284131
- function positiveTimeoutMs(value) {
284132
- if (!value) return;
284133
- const parsed = Number.parseInt(value, 10);
284134
- return Number.isFinite(parsed) && parsed > 0 ? parsed : void 0;
284135
- }
284136
- function agentBrowserCommandTimeoutMs(command, env = process.env) {
284137
- const explicitCommandTimeout = positiveTimeoutMs(env.FORGE_AGENT_BROWSER_BRIDGE_COMMAND_TIMEOUT_MS);
284138
- if (explicitCommandTimeout !== void 0) return explicitCommandTimeout;
284139
- if (isAgentBrowserBoundedControlCommand(command)) return positiveTimeoutMs(env.FORGE_AGENT_BROWSER_BRIDGE_CONTROL_TIMEOUT_MS) ?? DEFAULT_AGENT_BROWSER_CONTROL_COMMAND_TIMEOUT_MS;
284140
- switch (command.type) {
284141
- case "screenshot":
284142
- case "snapshot": return positiveTimeoutMs(env.FORGE_AGENT_BROWSER_BRIDGE_CAPTURE_TIMEOUT_MS) ?? DEFAULT_AGENT_BROWSER_CAPTURE_COMMAND_TIMEOUT_MS;
284143
- case "newTab":
284144
- case "navigate":
284145
- case "goBack":
284146
- case "goForward":
284147
- case "reload": return DEFAULT_AGENT_BROWSER_NAVIGATION_COMMAND_TIMEOUT_MS;
284148
- default: return DEFAULT_AGENT_BROWSER_FAST_COMMAND_TIMEOUT_MS;
284149
- }
284150
- }
284151
- function agentBrowserCloseTimeoutMs(env = process.env) {
284152
- return positiveTimeoutMs(env.FORGE_AGENT_BROWSER_BRIDGE_CLOSE_TIMEOUT_MS) ?? DEFAULT_AGENT_BROWSER_BRIDGE_CLOSE_TIMEOUT_MS;
284153
- }
284154
- function agentBrowserOpenTimeoutMs(env = process.env) {
284155
- return positiveTimeoutMs(env.FORGE_AGENT_BROWSER_BRIDGE_OPEN_TIMEOUT_MS) ?? DEFAULT_AGENT_BROWSER_BRIDGE_OPEN_TIMEOUT_MS;
284156
- }
284157
- function agentBrowserFastTimeoutMs(env = process.env) {
284158
- return positiveTimeoutMs(env.FORGE_AGENT_BROWSER_BRIDGE_COMMAND_TIMEOUT_MS) ?? DEFAULT_AGENT_BROWSER_FAST_COMMAND_TIMEOUT_MS;
284159
- }
284160
- function agentBrowserPlaywrightBackendCloseTimeoutMs(env = process.env) {
284161
- return positiveTimeoutMs(env.FORGE_AGENT_BROWSER_PLAYWRIGHT_BACKEND_CLOSE_TIMEOUT_MS) ?? DEFAULT_AGENT_BROWSER_PLAYWRIGHT_BACKEND_CLOSE_TIMEOUT_MS;
284162
- }
284163
- async function withAgentBrowserBridgeTimeout(label, promise, timeoutMs) {
284164
- let timeout;
284165
- try {
284166
- return await Promise.race([promise, new Promise((_, reject) => {
284167
- timeout = setTimeout(() => {
284168
- reject(/* @__PURE__ */ new Error(`${label} timed out after ${Math.ceil(timeoutMs / 1e3)}s.`));
284169
- }, timeoutMs);
284170
- })]);
284171
- } finally {
284172
- if (timeout !== void 0) clearTimeout(timeout);
284173
- }
284174
- }
284175
- async function openAgentBrowserSessionReady(bridge, input) {
284176
- let resolveStarted;
284177
- const started = new Promise((resolve) => {
284178
- resolveStarted = resolve;
284179
- });
284180
- const unsubscribe = await bridge.subscribe({
284181
- sessionId: input.sessionId,
284182
- ...input.browserId ? { browserId: input.browserId } : {}
284183
- }, (event) => {
284184
- if (event.type === "session.started" && event.sessionId === input.sessionId) resolveStarted(event.payload.snapshot);
284185
- });
284186
- try {
284187
- return await Promise.race([bridge.openSession(input), started]);
284188
- } finally {
284189
- unsubscribe();
284190
- }
284191
- }
284192
- function isAgentBrowserBridgeTimeoutError(error) {
284193
- const message = [
284194
- is$1(BrowserError)(error) ? error.detail : void 0,
284195
- error instanceof Error ? error.message : void 0,
284196
- error instanceof Error ? error.stack : void 0,
284197
- typeof error === "string" ? error : void 0,
284198
- (() => {
284199
- try {
284200
- return JSON.stringify(error);
284201
- } catch {
284202
- return;
284203
- }
284204
- })(),
284205
- String(error)
284206
- ].filter((part) => typeof part === "string" && part.length > 0).join("\n");
284207
- return /timed out after \d+s\./.test(message);
284208
- }
284209
- function isAgentBrowserBoundedControlCommand(command) {
284210
- switch (command.type) {
284211
- case "click":
284212
- case "clickPoint":
284213
- case "pressKey":
284214
- case "resetViewport":
284215
- case "scroll":
284216
- case "setViewport":
284217
- case "type":
284218
- case "typeText":
284219
- case "wheel": return true;
284220
- default: return false;
284221
- }
284222
- }
284223
- function snapshotAfterTimedOutControl(snapshot, command) {
284224
- switch (command.type) {
284225
- case "setViewport": return {
284226
- ...snapshot,
284227
- viewport: command.viewport,
284228
- updatedAt: nowIso$2()
284229
- };
284230
- case "resetViewport": return {
284231
- ...snapshot,
284232
- viewport: DEFAULT_AGENT_BROWSER_VIEWPORT,
284233
- updatedAt: nowIso$2()
284234
- };
284235
- default: return snapshot;
284236
- }
284237
- }
284238
- async function closeAgentBrowserBackendWithTimeout(closeBackend, env = process.env) {
284239
- try {
284240
- await withAgentBrowserBridgeTimeout("Agent Browser Playwright backend close", closeBackend(), agentBrowserPlaywrightBackendCloseTimeoutMs(env));
284241
- } catch (error) {
284242
- if (!isAgentBrowserBridgeTimeoutError(error)) throw error;
284243
- console.warn("[browser] Agent Browser Playwright backend close timed out; continuing", { timeoutMs: agentBrowserPlaywrightBackendCloseTimeoutMs(env) });
284244
- }
284245
- }
284246
285074
  function createNativeHostBrowserBridgeClient(options) {
284247
285075
  return createNativeHostBrowserBridgeClientImplementation(options, { closeSessionTimeoutMs: agentBrowserCloseTimeoutMs });
284248
285076
  }
285077
+ function browserBackendOptionsForSession(input) {
285078
+ return {
285079
+ ...input.baseOptions,
285080
+ userDataDir: browserProfileDirForSession(input.sessionId)
285081
+ };
285082
+ }
284249
285083
  /**
284250
285084
  * Builds the Playwright-backed backend from the lazily loaded module. The
284251
- * `playwright` package (and everything that statically imports it) must stay
284252
- * out of the standalone bundle's eager import graph: the bundle keeps
284253
- * `playwright` external, so a static import would hoist it to the top of
284254
- * `dist/bin.mjs` and the server would fail to boot on any machine that has
284255
- * not provisioned playwright yet. Deferring the import to backend construction
284256
- * keeps boot playwright-free and lets a later provision satisfy first use.
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.
284257
285089
  */
284258
285090
  function createForgeAgentBrowserPlaywrightBackend(PlaywrightBackend, options) {
284259
285091
  class ForgeAgentBrowserPlaywrightBackend extends PlaywrightBackend {
@@ -284263,757 +285095,6 @@ function createForgeAgentBrowserPlaywrightBackend(PlaywrightBackend, options) {
284263
285095
  }
284264
285096
  return new ForgeAgentBrowserPlaywrightBackend(options);
284265
285097
  }
284266
- function toBrowserError(error, fallback) {
284267
- console.warn("[browser] Agent Browser host bridge operation failed", {
284268
- operation: fallback,
284269
- error
284270
- });
284271
- if (is$1(BrowserError)(error)) return error;
284272
- if (error instanceof Error && error.message.trim().length > 0) return new BrowserError({ detail: error.message.trim() });
284273
- return new BrowserError({ detail: fallback });
284274
- }
284275
- function remoteWebRtcUnavailableError(_input) {
284276
- return new BrowserError({ detail: REMOTE_WEBRTC_UNAVAILABLE_DETAIL });
284277
- }
284278
- function remoteWebRtcRelayRequiredError(_input) {
284279
- return new BrowserError({ detail: REMOTE_WEBRTC_RELAY_REQUIRED_DETAIL });
284280
- }
284281
- function subscriptionMatchesEvent(subscription, event) {
284282
- if (subscription.input.threadId && subscription.input.threadId !== event.threadId) return false;
284283
- if (subscription.input.browserId && subscription.input.browserId !== event.browserId) return false;
284284
- if (subscription.input.environmentId && subscription.input.environmentId !== event.environmentId) return false;
284285
- if (subscription.input.userId && subscription.input.userId !== event.userId) return false;
284286
- return true;
284287
- }
284288
- function sessionIdForThread(threadId) {
284289
- return BrowserSessionId.make(threadId);
284290
- }
284291
- function browserBackendOptionsForSession(input) {
284292
- return {
284293
- ...input.baseOptions,
284294
- userDataDir: browserProfileDirForSession(input.sessionId)
284295
- };
284296
- }
284297
- function browserIdFrom(input) {
284298
- return BrowserId.make(input.browserId ?? DEFAULT_AGENT_BROWSER_ID);
284299
- }
284300
- function normalizeUserVisibleBrowserUrl(url) {
284301
- const trimmed = url.trim();
284302
- if (trimmed.length === 0) return trimmed;
284303
- if (ABSOLUTE_WEB_URL_PATTERN.test(trimmed) || SPECIAL_URL_PATTERN.test(trimmed) || trimmed.startsWith("//")) return trimmed;
284304
- if (LOCAL_URL_LIKE_PATTERN.test(trimmed)) return `http://${trimmed}`;
284305
- return `https://${trimmed}`;
284306
- }
284307
- function agentBrowserDisplayForRuntimeMode(input) {
284308
- if (input.mode !== "desktop") {
284309
- const hasRelay = hasTurnRelay(input.iceServers ?? []);
284310
- return {
284311
- transport: "remote-webrtc",
284312
- status: "degraded",
284313
- reason: input.requiresRelay === true && !hasRelay ? "network_unreachable" : "transport_unavailable",
284314
- detail: input.requiresRelay === true && !hasRelay ? REMOTE_WEBRTC_RELAY_REQUIRED_DETAIL : "Remote WebRTC browser display is not attached yet.",
284315
- ...input.viewport ? { viewport: input.viewport } : {},
284316
- ...input.iceServers && input.iceServers.length > 0 ? { iceServers: input.iceServers } : {},
284317
- reconnect: {
284318
- status: "ready",
284319
- attempt: 0
284320
- }
284321
- };
284322
- }
284323
- return {
284324
- transport: "native-embedded",
284325
- status: "connected",
284326
- ...input.viewport ? { viewport: input.viewport } : {},
284327
- reconnect: {
284328
- status: "ready",
284329
- attempt: 0
284330
- }
284331
- };
284332
- }
284333
- function hasTurnRelay(iceServers) {
284334
- return browserWebRtcIceServersIncludeRelay(iceServers);
284335
- }
284336
- async function resolveDisplayIceServers(options) {
284337
- return options?.resolveDisplayIceServers ? await options.resolveDisplayIceServers() : options?.displayIceServers ?? [];
284338
- }
284339
- function webRtcDisplayRequiresRelay(serverConfig) {
284340
- return serverConfig.mode === "web" && serverConfig.devUrl === void 0;
284341
- }
284342
- function toForgeTab(input) {
284343
- return {
284344
- tabId: BrowserTabId.make(input.tab.id),
284345
- restoreKey: input.tab.id,
284346
- ...input.tab.title ? { title: input.tab.title } : {},
284347
- url: input.tab.url ?? DEFAULT_URL,
284348
- isActive: input.tab.isActive,
284349
- canGoBack: input.tab.canGoBack ?? false,
284350
- canGoForward: input.tab.canGoForward ?? false,
284351
- renderMode: DEFAULT_RENDER_MODE,
284352
- status: "live",
284353
- ...input.tab.navigationStatus !== void 0 ? { navigationStatus: input.tab.navigationStatus } : {},
284354
- ...input.tab.requestedUrl !== void 0 ? { requestedUrl: input.tab.requestedUrl } : {},
284355
- ...input.tab.navigationError !== void 0 ? { navigationError: input.tab.navigationError } : {},
284356
- updatedAt: input.updatedAt
284357
- };
284358
- }
284359
- function toForgeSession(input) {
284360
- return {
284361
- browserId: BrowserId.make(input.snapshot.browserId),
284362
- threadId: input.threadId,
284363
- environmentId: EnvironmentId.make(input.environmentId ?? DEFAULT_BROWSER_ENVIRONMENT_ID),
284364
- userId: BrowserUserId.make(input.userId ?? "local-user"),
284365
- agentId: input.agentId ?? null,
284366
- ownerRole: input.ownerRole ?? "lead",
284367
- name: input.snapshot.name,
284368
- visibility: input.snapshot.visibility,
284369
- viewport: input.snapshot.viewport,
284370
- display: input.display ?? DEFAULT_AGENT_BROWSER_DISPLAY,
284371
- sessionId: BrowserSessionId.make(input.snapshot.sessionId),
284372
- ...input.controlState ? {
284373
- sessionEpoch: input.controlState.sessionEpoch,
284374
- lastAppliedControlSequence: input.controlState.lastAppliedControlSequence
284375
- } : {},
284376
- activeTabId: input.snapshot.activeTabId ? BrowserTabId.make(input.snapshot.activeTabId) : null,
284377
- tabs: input.snapshot.tabs.map((tab) => toForgeTab({
284378
- tab,
284379
- updatedAt: input.snapshot.updatedAt
284380
- })),
284381
- updatedAt: input.snapshot.updatedAt
284382
- };
284383
- }
284384
- function resolveDisplay(snapshot, options) {
284385
- if (typeof options?.display === "function") return options.display(snapshot);
284386
- return options?.display ?? DEFAULT_AGENT_BROWSER_DISPLAY;
284387
- }
284388
- function toHostCommand(command) {
284389
- switch (command.type) {
284390
- case "activateTab": return {
284391
- type: "activateTab",
284392
- tabId: command.tabId
284393
- };
284394
- case "click": return {
284395
- type: "click",
284396
- tabId: command.tabId,
284397
- selector: command.selector
284398
- };
284399
- case "clickPoint": return {
284400
- type: "clickPoint",
284401
- tabId: command.tabId,
284402
- x: command.x,
284403
- y: command.y,
284404
- ...command.button !== void 0 ? { button: command.button } : {},
284405
- ...command.clickCount !== void 0 ? { clickCount: command.clickCount } : {}
284406
- };
284407
- case "closeTab": return {
284408
- type: "closeTab",
284409
- tabId: command.tabId
284410
- };
284411
- case "consoleLogs": return {
284412
- type: "consoleLogs",
284413
- ...command.tabId !== void 0 ? { tabId: command.tabId } : {},
284414
- ...command.limit !== void 0 ? { limit: command.limit } : {},
284415
- ...command.levels !== void 0 ? { levels: command.levels } : {}
284416
- };
284417
- case "goBack": return {
284418
- type: "goBack",
284419
- tabId: command.tabId
284420
- };
284421
- case "goForward": return {
284422
- type: "goForward",
284423
- tabId: command.tabId
284424
- };
284425
- case "nameSession": return {
284426
- type: "nameSession",
284427
- name: command.name
284428
- };
284429
- case "navigate": return {
284430
- type: "navigate",
284431
- url: normalizeUserVisibleBrowserUrl(command.url),
284432
- ...command.tabId !== void 0 ? { tabId: command.tabId } : {}
284433
- };
284434
- case "newTab": return {
284435
- type: "newTab",
284436
- ...command.url !== void 0 ? { url: normalizeUserVisibleBrowserUrl(command.url) } : {},
284437
- ...command.makeActive !== void 0 ? { makeActive: command.makeActive } : {}
284438
- };
284439
- case "pressKey": return {
284440
- type: "pressKey",
284441
- tabId: command.tabId,
284442
- key: command.key
284443
- };
284444
- case "reload": return {
284445
- type: "reload",
284446
- tabId: command.tabId
284447
- };
284448
- case "resetViewport": return { type: "resetViewport" };
284449
- case "screenshot": return {
284450
- type: "screenshot",
284451
- tabId: command.tabId,
284452
- ...command.fullPage !== void 0 ? { fullPage: command.fullPage } : {}
284453
- };
284454
- case "scroll": return {
284455
- type: "scroll",
284456
- tabId: command.tabId,
284457
- scrollX: 0,
284458
- scrollY: command.toTop === true ? -900 : 900,
284459
- ...command.x !== void 0 ? { x: command.x } : {},
284460
- ...command.y !== void 0 ? { y: command.y } : {}
284461
- };
284462
- case "setViewport": return {
284463
- type: "setViewport",
284464
- viewport: command.viewport
284465
- };
284466
- case "setVisibility": return {
284467
- type: "setVisibility",
284468
- visibility: command.visibility
284469
- };
284470
- case "snapshot": return {
284471
- type: "snapshot",
284472
- tabId: command.tabId
284473
- };
284474
- case "type": return {
284475
- type: "type",
284476
- tabId: command.tabId,
284477
- selector: command.selector,
284478
- text: command.text,
284479
- ...command.append !== void 0 ? { append: command.append } : {}
284480
- };
284481
- case "typeText": return {
284482
- type: "typeText",
284483
- tabId: command.tabId,
284484
- text: command.text,
284485
- ...command.pressEnter !== void 0 ? { pressEnter: command.pressEnter } : {}
284486
- };
284487
- case "wheel": return {
284488
- type: "wheel",
284489
- tabId: command.tabId,
284490
- deltaX: command.deltaX,
284491
- deltaY: command.deltaY,
284492
- ...command.x !== void 0 ? { x: command.x } : {},
284493
- ...command.y !== void 0 ? { y: command.y } : {}
284494
- };
284495
- default: throw new Error(`Unsupported browser command: ${command.type}`);
284496
- }
284497
- }
284498
- function toStreamEvent(input) {
284499
- const base = {
284500
- eventId: EventId.make(crypto.randomUUID()),
284501
- browserId: input.browserId,
284502
- threadId: input.threadId,
284503
- environmentId: EnvironmentId.make(input.environmentId),
284504
- userId: BrowserUserId.make(input.userId),
284505
- agentId: input.agentId,
284506
- ...input.controlState ? { sessionEpoch: input.controlState.sessionEpoch } : {},
284507
- createdAt: nowIso$2()
284508
- };
284509
- switch (input.event.type) {
284510
- case "session.started": return {
284511
- ...base,
284512
- type: "session.started",
284513
- payload: { snapshot: toForgeSession({
284514
- snapshot: input.event.payload.snapshot,
284515
- threadId: input.threadId,
284516
- environmentId: input.environmentId,
284517
- userId: input.userId,
284518
- agentId: input.agentId,
284519
- ownerRole: input.ownerRole,
284520
- display: input.display,
284521
- controlState: input.controlState
284522
- }) }
284523
- };
284524
- case "session.updated": return {
284525
- ...base,
284526
- type: input.event.type,
284527
- payload: { snapshot: toForgeSession({
284528
- snapshot: input.event.payload.snapshot,
284529
- threadId: input.threadId,
284530
- environmentId: input.environmentId,
284531
- userId: input.userId,
284532
- agentId: input.agentId,
284533
- ownerRole: input.ownerRole,
284534
- display: input.display,
284535
- controlState: input.controlState
284536
- }) }
284537
- };
284538
- case "session.closed": return {
284539
- ...base,
284540
- type: "session.closed",
284541
- payload: { sessionId: BrowserSessionId.make(input.event.payload.sessionId) }
284542
- };
284543
- case "tab.closed": return {
284544
- ...base,
284545
- type: "tab.closed",
284546
- payload: {
284547
- sessionId: BrowserSessionId.make(input.event.sessionId),
284548
- tabId: BrowserTabId.make(input.event.payload.tabId)
284549
- }
284550
- };
284551
- case "snapshot.ready": return {
284552
- ...base,
284553
- type: "snapshot.ready",
284554
- payload: {
284555
- sessionId: BrowserSessionId.make(input.event.sessionId),
284556
- tabId: BrowserTabId.make(input.event.payload.tabId),
284557
- captureId: BrowserCaptureId.make(input.event.payload.captureId ?? crypto.randomUUID()),
284558
- capturedAt: input.event.payload.capturedAt,
284559
- url: input.event.payload.url ?? DEFAULT_URL,
284560
- viewport: input.event.payload.viewport,
284561
- content: input.event.payload.content
284562
- }
284563
- };
284564
- case "screenshot.ready": return {
284565
- ...base,
284566
- type: "screenshot.ready",
284567
- payload: {
284568
- sessionId: BrowserSessionId.make(input.event.sessionId),
284569
- tabId: BrowserTabId.make(input.event.payload.tabId),
284570
- captureId: BrowserCaptureId.make(input.event.payload.captureId ?? crypto.randomUUID()),
284571
- capturedAt: input.event.payload.capturedAt,
284572
- url: input.event.payload.url ?? DEFAULT_URL,
284573
- viewport: input.event.payload.viewport,
284574
- imageDataUrl: input.event.payload.imageDataUrl
284575
- }
284576
- };
284577
- case "console.logs.ready": return {
284578
- ...base,
284579
- type: "console.logs.ready",
284580
- payload: {
284581
- sessionId: BrowserSessionId.make(input.event.sessionId),
284582
- tabId: BrowserTabId.make(input.event.payload.tabId),
284583
- logs: input.event.payload.logs.map((log) => ({
284584
- level: log.level === "warn" ? "warning" : log.level === "log" ? "info" : log.level,
284585
- message: log.message,
284586
- createdAt: log.timestamp
284587
- }))
284588
- }
284589
- };
284590
- case "action.failed": return {
284591
- ...base,
284592
- type: "action.failed",
284593
- payload: {
284594
- commandType: input.event.payload.commandType,
284595
- message: input.event.payload.message,
284596
- ...input.event.payload.tabId ? { tabId: BrowserTabId.make(input.event.payload.tabId) } : {}
284597
- }
284598
- };
284599
- case "dom.visible.ready":
284600
- case "element.info.ready":
284601
- case "element.screenshot.ready":
284602
- case "clipboard.text.ready": return null;
284603
- }
284604
- }
284605
- function toCommandResult(input) {
284606
- return {
284607
- ok: input.result.ok,
284608
- snapshot: toForgeSession({
284609
- snapshot: input.result.snapshot,
284610
- threadId: input.command.threadId,
284611
- environmentId: input.command.environmentId,
284612
- userId: input.command.userId,
284613
- display: input.display,
284614
- controlState: input.controlState
284615
- }),
284616
- sessionEpoch: input.controlState.sessionEpoch,
284617
- acknowledgedControlSequence: input.acknowledgedControlSequence,
284618
- ...input.result.detail ? { detail: input.result.detail } : {},
284619
- ...input.latestCapture ? { latestCapture: input.latestCapture } : {}
284620
- };
284621
- }
284622
- function commandCaptureTabId(command) {
284623
- switch (command.type) {
284624
- case "snapshot":
284625
- case "screenshot": return command.tabId;
284626
- default: return;
284627
- }
284628
- }
284629
- function toCommandLatestCapture(input) {
284630
- const tabId = commandCaptureTabId(input.command.command);
284631
- 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;
284632
- const mapped = toStreamEvent({
284633
- event: input.event,
284634
- threadId: input.command.threadId,
284635
- browserId: input.browserId,
284636
- environmentId: input.command.environmentId ?? DEFAULT_BROWSER_ENVIRONMENT_ID,
284637
- userId: input.command.userId ?? "local-user",
284638
- agentId: null,
284639
- ownerRole: "lead",
284640
- display: input.display,
284641
- controlState: input.controlState
284642
- });
284643
- return mapped?.type === "snapshot.ready" || mapped?.type === "screenshot.ready" ? mapped : void 0;
284644
- }
284645
- function createAgentBrowserHostBridgeManager(bridge, options) {
284646
- const localSubscriptions = /* @__PURE__ */ new Set();
284647
- const connectedDisplays = /* @__PURE__ */ new Map();
284648
- const controlStates = /* @__PURE__ */ new Map();
284649
- const commandQueues = /* @__PURE__ */ new Map();
284650
- const controlStateFor = (sessionId) => {
284651
- const existing = controlStates.get(sessionId);
284652
- if (existing) return existing;
284653
- const created = {
284654
- sessionEpoch: crypto.randomUUID(),
284655
- lastAppliedControlSequence: 0
284656
- };
284657
- controlStates.set(sessionId, created);
284658
- return created;
284659
- };
284660
- const runSerializedCommand = async (sessionId, task) => {
284661
- const previous = commandQueues.get(sessionId) ?? Promise.resolve();
284662
- let releaseCurrent;
284663
- const current = new Promise((resolve) => {
284664
- releaseCurrent = resolve;
284665
- });
284666
- const queued = previous.catch(() => void 0).then(() => current);
284667
- commandQueues.set(sessionId, queued);
284668
- await previous.catch(() => void 0);
284669
- try {
284670
- return await task();
284671
- } finally {
284672
- releaseCurrent();
284673
- if (commandQueues.get(sessionId) === queued) commandQueues.delete(sessionId);
284674
- }
284675
- };
284676
- const clearConnectedDisplay = (input) => {
284677
- const active = connectedDisplays.get(input.sessionId);
284678
- if (!active) return;
284679
- if (input.tabId === void 0 || active.tabId === input.tabId) connectedDisplays.delete(input.sessionId);
284680
- };
284681
- const displayForSnapshot = (snapshot) => {
284682
- const fallback = resolveDisplay(snapshot, options);
284683
- const active = connectedDisplays.get(snapshot.sessionId);
284684
- if (!active) return fallback;
284685
- if (!snapshot.tabs.some((tab) => tab.id === active.tabId)) {
284686
- connectedDisplays.delete(snapshot.sessionId);
284687
- return fallback;
284688
- }
284689
- if (snapshot.activeTabId !== active.tabId) return fallback;
284690
- const { reason: _reason, detail: _detail, ...fallbackWithoutDegradedReason } = fallback;
284691
- return {
284692
- ...fallbackWithoutDegradedReason,
284693
- ...active.display,
284694
- reconnect: active.display.reconnect ?? fallback.reconnect
284695
- };
284696
- };
284697
- const duplicateControlResult = async (input, hostSessionId, controlState, acknowledgedControlSequence) => {
284698
- const snapshot = await withAgentBrowserBridgeTimeout("Agent Browser duplicate command session read", bridge.getSession({
284699
- sessionId: hostSessionId,
284700
- ...input.browserId ? { browserId: input.browserId } : {}
284701
- }), agentBrowserFastTimeoutMs());
284702
- if (!snapshot) throw new BrowserError({ detail: "Browser session not found." });
284703
- return {
284704
- ok: true,
284705
- snapshot: toForgeSession({
284706
- snapshot,
284707
- threadId: input.threadId,
284708
- environmentId: input.environmentId,
284709
- userId: input.userId,
284710
- display: displayForSnapshot(snapshot),
284711
- controlState
284712
- }),
284713
- sessionEpoch: controlState.sessionEpoch,
284714
- acknowledgedControlSequence,
284715
- detail: "Browser control was already applied"
284716
- };
284717
- };
284718
- const dispatchHostCommand = async (input, hostIdentity, preCommandSnapshot) => {
284719
- try {
284720
- return await withAgentBrowserBridgeTimeout(`Agent Browser command ${input.command.type}`, bridge.command({
284721
- ...hostIdentity,
284722
- command: toHostCommand(input.command)
284723
- }), agentBrowserCommandTimeoutMs(input.command));
284724
- } catch (error) {
284725
- if (!(isAgentBrowserBridgeTimeoutError(error) && isAgentBrowserBoundedControlCommand(input.command) && preCommandSnapshot !== null)) throw error;
284726
- console.warn("[browser] Agent Browser control command timed out after dispatch", {
284727
- commandType: input.command.type,
284728
- sessionId: hostIdentity.sessionId,
284729
- ...input.browserId ? { browserId: input.browserId } : {}
284730
- });
284731
- return {
284732
- ok: true,
284733
- snapshot: snapshotAfterTimedOutControl(preCommandSnapshot, input.command),
284734
- detail: "Agent Browser control command dispatch timed out before post-action observation completed."
284735
- };
284736
- }
284737
- };
284738
- const executeHostCommand = async (input, hostSessionId, controlState, controlSequence) => {
284739
- const captureTabId = commandCaptureTabId(input.command);
284740
- const hostIdentity = {
284741
- sessionId: hostSessionId,
284742
- ...input.browserId ? { browserId: input.browserId } : {}
284743
- };
284744
- 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;
284745
- let latestCaptureEvent;
284746
- const unsubscribeCapture = captureTabId ? await bridge.subscribe(hostIdentity, (event) => {
284747
- if ((event.type === "snapshot.ready" || event.type === "screenshot.ready") && event.sessionId === hostSessionId && event.payload.tabId === captureTabId) latestCaptureEvent = event;
284748
- }) : void 0;
284749
- let result;
284750
- try {
284751
- result = await dispatchHostCommand(input, hostIdentity, preCommandSnapshot);
284752
- } finally {
284753
- unsubscribeCapture?.();
284754
- }
284755
- if (input.command.type === "closeTab") {
284756
- await runPromise(options?.displayPeers?.closeForSession({
284757
- sessionId: hostSessionId,
284758
- tabId: input.command.tabId
284759
- }) ?? void_$1);
284760
- clearConnectedDisplay({
284761
- sessionId: hostSessionId,
284762
- tabId: input.command.tabId
284763
- });
284764
- }
284765
- const display = displayForSnapshot(result.snapshot);
284766
- controlState.lastAppliedControlSequence = controlSequence;
284767
- return toCommandResult({
284768
- result,
284769
- command: input,
284770
- display,
284771
- controlState,
284772
- acknowledgedControlSequence: controlSequence,
284773
- latestCapture: latestCaptureEvent ? toCommandLatestCapture({
284774
- event: latestCaptureEvent,
284775
- command: input,
284776
- browserId: BrowserId.make(result.snapshot.browserId),
284777
- display,
284778
- controlState
284779
- }) : void 0
284780
- });
284781
- };
284782
- const executeOrderedHostCommand = async (input) => {
284783
- const hostSessionId = sessionIdForThread(input.threadId);
284784
- return runSerializedCommand(hostSessionId, async () => {
284785
- const controlState = controlStateFor(hostSessionId);
284786
- const controlDecision = resolveBrowserControlDecision(controlState, input);
284787
- if (controlDecision.type === "reject") throw new BrowserError({ detail: browserControlRejectionDetail(controlDecision) });
284788
- if (controlDecision.type === "duplicate") return duplicateControlResult(input, hostSessionId, controlState, controlDecision.acknowledgedControlSequence);
284789
- return executeHostCommand(input, hostSessionId, controlState, controlDecision.controlSequence);
284790
- });
284791
- };
284792
- const publishLocalEvent = (event) => {
284793
- for (const subscription of localSubscriptions) {
284794
- if (!subscriptionMatchesEvent(subscription, event)) continue;
284795
- runPromise(subscription.listener(event)).catch((error) => {
284796
- console.warn("[browser] Failed to publish local browser event", { error });
284797
- });
284798
- }
284799
- };
284800
- const publishWebRtcIceCandidate = (event) => {
284801
- publishLocalEvent({
284802
- eventId: EventId.make(crypto.randomUUID()),
284803
- browserId: event.input.browserId ?? browserIdFrom(event.input),
284804
- threadId: event.input.threadId,
284805
- environmentId: event.input.environmentId ?? DEFAULT_BROWSER_ENVIRONMENT_ID,
284806
- userId: event.input.userId ?? "local-user",
284807
- agentId: null,
284808
- createdAt: nowIso$2(),
284809
- type: "display.iceCandidate",
284810
- payload: {
284811
- sessionId: event.input.sessionId,
284812
- tabId: event.input.tabId,
284813
- clientId: event.input.clientId,
284814
- candidate: event.candidate
284815
- }
284816
- });
284817
- };
284818
- return {
284819
- openSession: (input) => tryPromise({
284820
- try: async () => {
284821
- const hostSessionId = sessionIdForThread(input.threadId);
284822
- const initialUrl = input.initialUrl ? normalizeUserVisibleBrowserUrl(input.initialUrl) : void 0;
284823
- options?.prepareRuntimeSession?.({
284824
- sessionId: hostSessionId,
284825
- initialUrl: initialUrl ?? DEFAULT_URL
284826
- });
284827
- let snapshot;
284828
- try {
284829
- snapshot = await withAgentBrowserBridgeTimeout("Agent Browser openSession", openAgentBrowserSessionReady(bridge, {
284830
- sessionId: hostSessionId,
284831
- ...input.browserId ? { browserId: input.browserId } : {},
284832
- ...initialUrl ? { initialUrl } : {},
284833
- ...input.name ? { name: input.name } : {},
284834
- ...input.visibility ? { visibility: input.visibility } : {},
284835
- ...input.viewport ? { viewport: input.viewport } : {}
284836
- }), agentBrowserOpenTimeoutMs());
284837
- } catch (error) {
284838
- if (!isAgentBrowserBridgeTimeoutError(error)) {
284839
- options?.forgetRuntimeSession?.({ sessionId: hostSessionId });
284840
- throw error;
284841
- }
284842
- console.warn("[browser] Agent Browser openSession timed out; quarantining session", {
284843
- sessionId: hostSessionId,
284844
- ...input.browserId ? { browserId: input.browserId } : {}
284845
- });
284846
- await withAgentBrowserBridgeTimeout("Agent Browser openSession quarantine close", bridge.closeSession({
284847
- sessionId: hostSessionId,
284848
- ...input.browserId ? { browserId: input.browserId } : {}
284849
- }), agentBrowserCloseTimeoutMs()).catch(() => void 0);
284850
- clearConnectedDisplay({ sessionId: hostSessionId });
284851
- controlStates.delete(hostSessionId);
284852
- options?.forgetRuntimeSession?.({ sessionId: hostSessionId });
284853
- throw new Error(`${error instanceof Error ? error.message : String(error)} The pending browser session was quarantined and closed.`, { cause: error });
284854
- }
284855
- return toForgeSession({
284856
- snapshot,
284857
- threadId: input.threadId,
284858
- environmentId: input.environmentId,
284859
- userId: input.userId,
284860
- agentId: input.agentId,
284861
- ownerRole: input.ownerRole,
284862
- display: displayForSnapshot(snapshot),
284863
- controlState: controlStateFor(snapshot.sessionId)
284864
- });
284865
- },
284866
- catch: (error) => toBrowserError(error, "Failed to open Agent Browser session.")
284867
- }),
284868
- closeSession: (input) => tryPromise({
284869
- try: async () => {
284870
- await runPromise(options?.displayPeers?.closeForSession({ sessionId: sessionIdForThread(input.threadId) }) ?? void_$1);
284871
- const hostSessionId = sessionIdForThread(input.threadId);
284872
- const hostIdentity = {
284873
- sessionId: hostSessionId,
284874
- ...input.browserId ? { browserId: input.browserId } : {}
284875
- };
284876
- try {
284877
- await withAgentBrowserBridgeTimeout("Agent Browser closeSession", bridge.closeSession(hostIdentity), agentBrowserCloseTimeoutMs());
284878
- } catch (error) {
284879
- if (!isAgentBrowserBridgeTimeoutError(error)) throw error;
284880
- 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", {
284881
- sessionId: hostSessionId,
284882
- ...input.browserId ? { browserId: input.browserId } : {}
284883
- });
284884
- else console.warn("[browser] Agent Browser closeSession timed out; forgetting session", {
284885
- sessionId: hostSessionId,
284886
- ...input.browserId ? { browserId: input.browserId } : {}
284887
- });
284888
- clearConnectedDisplay({ sessionId: hostSessionId });
284889
- controlStates.delete(hostSessionId);
284890
- options?.forgetRuntimeSession?.({ sessionId: hostSessionId });
284891
- return;
284892
- }
284893
- clearConnectedDisplay({ sessionId: hostSessionId });
284894
- controlStates.delete(hostSessionId);
284895
- options?.forgetRuntimeSession?.({ sessionId: hostSessionId });
284896
- },
284897
- catch: (error) => toBrowserError(error, "Failed to close Agent Browser session.")
284898
- }),
284899
- clearBrowsingData: () => succeed({
284900
- closedSessionCount: 0,
284901
- deletedSessionCount: 0,
284902
- deletedCaptureCount: 0
284903
- }),
284904
- getSession: (input) => tryPromise({
284905
- try: async () => {
284906
- const snapshot = await withAgentBrowserBridgeTimeout("Agent Browser getSession", bridge.getSession({
284907
- sessionId: sessionIdForThread(input.threadId),
284908
- ...input.browserId ? { browserId: input.browserId } : {}
284909
- }), agentBrowserFastTimeoutMs());
284910
- return snapshot ? toForgeSession({
284911
- snapshot,
284912
- threadId: input.threadId,
284913
- environmentId: input.environmentId,
284914
- userId: input.userId,
284915
- display: displayForSnapshot(snapshot),
284916
- controlState: controlStateFor(snapshot.sessionId)
284917
- }) : null;
284918
- },
284919
- catch: (error) => toBrowserError(error, "Failed to read Agent Browser session.")
284920
- }),
284921
- listSessions: (input) => tryPromise({
284922
- try: async () => {
284923
- return (await withAgentBrowserBridgeTimeout("Agent Browser listSessions", bridge.listSessions(input.browserId ? { browserId: input.browserId } : void 0), agentBrowserFastTimeoutMs())).map((snapshot) => toForgeSession({
284924
- snapshot,
284925
- threadId: input.threadId ?? ThreadId.make(snapshot.sessionId),
284926
- environmentId: input.environmentId,
284927
- userId: input.userId,
284928
- display: displayForSnapshot(snapshot),
284929
- controlState: controlStateFor(snapshot.sessionId)
284930
- }));
284931
- },
284932
- catch: (error) => toBrowserError(error, "Failed to list Agent Browser sessions.")
284933
- }),
284934
- command: (input) => tryPromise({
284935
- try: () => executeOrderedHostCommand(input),
284936
- catch: (error) => toBrowserError(error, "Failed to run Agent Browser command.")
284937
- }),
284938
- attachDisplay: (input) => {
284939
- const displayPeers = options?.displayPeers;
284940
- if (!displayPeers) return fail(remoteWebRtcUnavailableError(input));
284941
- return tryPromise({
284942
- try: async () => {
284943
- const iceServers = await resolveDisplayIceServers(options);
284944
- if (options?.requiresWebRtcRelay === true && !hasTurnRelay(iceServers)) throw remoteWebRtcRelayRequiredError(input);
284945
- const result = await runPromise(displayPeers.attach(input, {
284946
- display: {
284947
- transport: "remote-webrtc",
284948
- status: "connecting",
284949
- ...input.viewport ? { viewport: input.viewport } : {},
284950
- devicePixelRatio: 1,
284951
- ...iceServers.length > 0 ? { iceServers } : {},
284952
- reconnect: {
284953
- status: "ready",
284954
- attempt: 0
284955
- }
284956
- },
284957
- onIceCandidate: publishWebRtcIceCandidate
284958
- }));
284959
- connectedDisplays.set(input.sessionId, {
284960
- tabId: input.tabId,
284961
- display: result.display
284962
- });
284963
- return result;
284964
- },
284965
- catch: (error) => {
284966
- if (is$1(BrowserError)(error)) return error;
284967
- return toBrowserError(error, "Failed to attach WebRTC browser display.");
284968
- }
284969
- });
284970
- },
284971
- addDisplayIceCandidate: (input) => {
284972
- const displayPeers = options?.displayPeers;
284973
- if (!displayPeers) return fail(remoteWebRtcUnavailableError(input));
284974
- return displayPeers.addIceCandidate(input);
284975
- },
284976
- subscribe: (input, listener) => promise(async () => {
284977
- const localSubscription = {
284978
- input,
284979
- listener
284980
- };
284981
- localSubscriptions.add(localSubscription);
284982
- const threadId = input.threadId ?? ThreadId.make("unknown-thread");
284983
- const browserId = browserIdFrom(input);
284984
- const environmentId = input.environmentId ?? DEFAULT_BROWSER_ENVIRONMENT_ID;
284985
- const userId = input.userId ?? "local-user";
284986
- const subscribeInput = {
284987
- ...input.threadId ? { sessionId: sessionIdForThread(input.threadId) } : {},
284988
- ...input.browserId ? { browserId: input.browserId } : {}
284989
- };
284990
- const unsubscribeBridge = await bridge.subscribe(subscribeInput, (event) => {
284991
- if (event.type === "session.closed") clearConnectedDisplay({ sessionId: event.payload.sessionId });
284992
- if (event.type === "tab.closed") clearConnectedDisplay({
284993
- sessionId: event.sessionId,
284994
- tabId: event.payload.tabId
284995
- });
284996
- const mapped = toStreamEvent({
284997
- event,
284998
- threadId,
284999
- browserId,
285000
- environmentId,
285001
- userId,
285002
- agentId: null,
285003
- ownerRole: "lead",
285004
- display: event.type === "session.started" || event.type === "session.updated" ? displayForSnapshot(event.payload.snapshot) : DEFAULT_AGENT_BROWSER_DISPLAY,
285005
- controlState: controlStateFor(event.type === "session.closed" ? event.payload.sessionId : event.sessionId)
285006
- });
285007
- if (mapped) runPromise(listener(mapped));
285008
- if (event.type === "session.closed") controlStates.delete(event.payload.sessionId);
285009
- });
285010
- return () => {
285011
- localSubscriptions.delete(localSubscription);
285012
- unsubscribeBridge();
285013
- };
285014
- })
285015
- };
285016
- }
285017
285098
  function shouldUseNativeHostBridge(serverConfig) {
285018
285099
  return serverConfig.mode === "desktop" && typeof serverConfig.nativeBrowserHostUrl === "string" && serverConfig.nativeBrowserHostUrl.trim().length > 0 && typeof serverConfig.nativeBrowserHostToken === "string" && serverConfig.nativeBrowserHostToken.trim().length > 0;
285019
285100
  }
@@ -285037,8 +285118,9 @@ const AgentBrowserManagerLive = effect(BrowserManager, gen(function* () {
285037
285118
  const displayCaptureRuntime = yield* promise(() => createBrowserWebRtcDisplayCaptureRuntime({ chromiumExecutablePath: process.env.FORGE_PLAYWRIGHT_EXECUTABLE_PATH }));
285038
285119
  yield* addFinalizer$2(() => promise(() => displayCaptureRuntime.close()).pipe(orDie));
285039
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());
285040
285122
  const displayTaskHoldManager = makeSpriteTaskHoldManager(process.env, (message, cause) => {
285041
- runFork(logWarning$1(message, { cause: cause instanceof Error ? cause.message : cause }));
285123
+ runLayerFork(logWarning$1(message, { cause: cause instanceof Error ? cause.message : cause }));
285042
285124
  });
285043
285125
  const displayPeers = createWebRtcDisplaySessionManager({
285044
285126
  mediaSourceFactory: displayCaptureRuntime.mediaSourceFactory,
@@ -285050,7 +285132,6 @@ const AgentBrowserManagerLive = effect(BrowserManager, gen(function* () {
285050
285132
  yield* addFinalizer$2(() => promise(() => displayTaskHoldManager?.releaseAll() ?? Promise.resolve()));
285051
285133
  const baseBackendOptions = displayCaptureRuntime.backendOptions;
285052
285134
  const sessionInitialUrls = /* @__PURE__ */ new Map();
285053
- const reaperContext = yield* context();
285054
285135
  let runtimeBuild = null;
285055
285136
  const lazyDisposers = [];
285056
285137
  const buildPlaywrightRuntime = async () => {
@@ -285093,13 +285174,12 @@ const AgentBrowserManagerLive = effect(BrowserManager, gen(function* () {
285093
285174
  await hostBridge.closeSession({ sessionId: BrowserSessionId.make(sessionId) });
285094
285175
  }
285095
285176
  }), reaperConfig);
285096
- const runReaperFork = runForkWith(reaperContext);
285097
285177
  const reaperInterval = setInterval(() => {
285098
- runReaperFork(reaper.sweep);
285178
+ runLayerFork(reaper.sweep);
285099
285179
  }, reaperConfig.intervalMs);
285100
285180
  reaperInterval.unref?.();
285101
285181
  lazyDisposers.push(() => clearInterval(reaperInterval));
285102
- runReaperFork(reaper.sweep);
285182
+ runLayerFork(reaper.sweep);
285103
285183
  }
285104
285184
  return createAgentBrowserHostBridgeManager(hostBridge, {
285105
285185
  display: (snapshot) => agentBrowserDisplayForRuntimeMode({
@@ -297099,7 +297179,7 @@ function resolveBuildCommitFromEnv(env) {
297099
297179
  * environment descriptor down instead of reporting an honest "unknown".
297100
297180
  */
297101
297181
  function readBakedBuildCommit() {
297102
- return "e0ef2b5174f2f50934814b1865cabaa9c7de0ce7";
297182
+ return "c3949b7e95339eacff389726dd77379ace4989b7";
297103
297183
  }
297104
297184
  async function resolveServerBuildCommit(input) {
297105
297185
  if (isFullCommitSha(input.baked)) return input.baked;