@cmmd-center/forge 0.13.30 → 0.13.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.cjs CHANGED
@@ -66232,7 +66232,7 @@ function normalizeNumberish(value) {
66232
66232
  }
66233
66233
  //#endregion
66234
66234
  //#region package.json
66235
- var version$1 = "0.13.30";
66235
+ var version$1 = "0.13.31";
66236
66236
  //#endregion
66237
66237
  //#region src/sentry.ts
66238
66238
  const SERVER_APP_NAME = "forge-server";
@@ -101200,7 +101200,7 @@ var BrowserManager = class extends require_Schema$1.Service()("forge/browser/Ser
101200
101200
  const DEFAULT_PORT_MIN = 5200;
101201
101201
  const DEFAULT_PORT_MAX = 5999;
101202
101202
  var ThreadDevRuntime = class extends require_Schema$1.Service()("forge/threadDevRuntime/ThreadDevRuntime") {};
101203
- function nowIso$5() {
101203
+ function nowIso$7() {
101204
101204
  return (/* @__PURE__ */ new Date()).toISOString();
101205
101205
  }
101206
101206
  function previewUrlForPort(port) {
@@ -101227,7 +101227,7 @@ const ThreadDevRuntimeLive = require_Schema$1.effect(ThreadDevRuntime, require_S
101227
101227
  allocatedPort: port,
101228
101228
  status: "allocated",
101229
101229
  previewUrl: previewUrlForPort(port),
101230
- updatedAt: nowIso$5()
101230
+ updatedAt: nowIso$7()
101231
101231
  };
101232
101232
  yield* require_HttpServer.update(byThreadId, (current) => {
101233
101233
  const next = new Map(current);
@@ -101249,7 +101249,7 @@ const ThreadDevRuntimeLive = require_Schema$1.effect(ThreadDevRuntime, require_S
101249
101249
  next.set(input.threadId, {
101250
101250
  ...existing,
101251
101251
  status: "released",
101252
- updatedAt: nowIso$5()
101252
+ updatedAt: nowIso$7()
101253
101253
  });
101254
101254
  return next;
101255
101255
  });
@@ -101275,7 +101275,7 @@ const ThreadDevRuntimeLive = require_Schema$1.effect(ThreadDevRuntime, require_S
101275
101275
  ...patch.terminalId !== void 0 ? { terminalId: patch.terminalId } : {},
101276
101276
  ...patch.previewUrl !== void 0 ? { previewUrl: patch.previewUrl } : {},
101277
101277
  ...patch.tunnelId !== void 0 ? { tunnelId: patch.tunnelId } : {},
101278
- updatedAt: nowIso$5()
101278
+ updatedAt: nowIso$7()
101279
101279
  };
101280
101280
  yield* require_HttpServer.update(byThreadId, (current) => {
101281
101281
  const next = new Map(current);
@@ -231299,7 +231299,7 @@ const OpenCodeRuntimeLive = require_Schema$1.effect(OpenCodeRuntime, makeOpenCod
231299
231299
  //#endregion
231300
231300
  //#region src/provider/Layers/OpenCodeAdapter.ts
231301
231301
  const PROVIDER$5 = "opencode";
231302
- const nowIso$4 = require_Schema$1.map(require_Schema$1.now, require_Schema$1.formatIso);
231302
+ const nowIso$6 = require_Schema$1.map(require_Schema$1.now, require_Schema$1.formatIso);
231303
231303
  /**
231304
231304
  * Map a tagged OpenCodeRuntimeError produced by {@link runOpenCodeSdk} into
231305
231305
  * the adapter-boundary `ProviderAdapterRequestError`. SDK-method-level call
@@ -231326,7 +231326,7 @@ const toProcessError = (threadId, cause) => new ProviderAdapterProcessError({
231326
231326
  });
231327
231327
  const buildEventBase = (input) => require_Schema$1.gen(function* () {
231328
231328
  const uuid = yield* nextUUIDv4;
231329
- const createdAt = input.createdAt ?? (yield* nowIso$4);
231329
+ const createdAt = input.createdAt ?? (yield* nowIso$6);
231330
231330
  return {
231331
231331
  eventId: EventId.make(uuid),
231332
231332
  provider: PROVIDER$5,
@@ -231485,7 +231485,7 @@ function sessionErrorMessage(error) {
231485
231485
  }
231486
231486
  function updateProviderSession(context, patch, options) {
231487
231487
  return require_Schema$1.gen(function* () {
231488
- const updatedAt = yield* nowIso$4;
231488
+ const updatedAt = yield* nowIso$6;
231489
231489
  const nextSession = {
231490
231490
  ...context.session,
231491
231491
  ...patch,
@@ -231607,7 +231607,7 @@ function makeOpenCodeAdapter(openCodeSettings, options) {
231607
231607
  if (("properties" in event ? event.properties.sessionID : void 0) !== context.openCodeSessionId) return;
231608
231608
  const turnId = context.activeTurnId;
231609
231609
  yield* writeNativeEventBestEffort(context.session.threadId, {
231610
- observedAt: yield* nowIso$4,
231610
+ observedAt: yield* nowIso$6,
231611
231611
  event: {
231612
231612
  provider: PROVIDER$5,
231613
231613
  threadId: context.session.threadId,
@@ -231923,7 +231923,7 @@ function makeOpenCodeAdapter(openCodeSettings, options) {
231923
231923
  yield* require_Schema$1.close(started.sessionScope, require_Schema$1.void_$1).pipe(require_Schema$1.ignore);
231924
231924
  return raceWinner.session;
231925
231925
  }
231926
- const createdAt = yield* nowIso$4;
231926
+ const createdAt = yield* nowIso$6;
231927
231927
  const session = {
231928
231928
  provider: PROVIDER$5,
231929
231929
  status: "ready",
@@ -239130,7 +239130,7 @@ function filterEvents(events, options) {
239130
239130
  });
239131
239131
  return options.limit === void 0 ? filtered : filtered.slice(-options.limit);
239132
239132
  }
239133
- function nowIso$3() {
239133
+ function nowIso$5() {
239134
239134
  return (/* @__PURE__ */ new Date()).toISOString();
239135
239135
  }
239136
239136
  //#endregion
@@ -239145,7 +239145,7 @@ var BrowserActionEventRecorder = class {
239145
239145
  type: "browser.action",
239146
239146
  action,
239147
239147
  ...this.sessionFields(),
239148
- timestamp: nowIso$3()
239148
+ timestamp: nowIso$5()
239149
239149
  });
239150
239150
  }
239151
239151
  async result(action) {
@@ -239154,7 +239154,7 @@ var BrowserActionEventRecorder = class {
239154
239154
  action,
239155
239155
  ok: true,
239156
239156
  ...this.sessionFields(),
239157
- timestamp: nowIso$3()
239157
+ timestamp: nowIso$5()
239158
239158
  });
239159
239159
  }
239160
239160
  async error(action, error) {
@@ -239163,7 +239163,7 @@ var BrowserActionEventRecorder = class {
239163
239163
  action,
239164
239164
  error: error instanceof Error ? error.message : String(error),
239165
239165
  ...this.sessionFields(),
239166
- timestamp: nowIso$3()
239166
+ timestamp: nowIso$5()
239167
239167
  });
239168
239168
  }
239169
239169
  sessionFields() {
@@ -239666,7 +239666,7 @@ var AgentBrowser = class {
239666
239666
  await this.options.events?.record({
239667
239667
  type: "browser.safety.confirmation.requested",
239668
239668
  ...request,
239669
- createdAt: nowIso$3()
239669
+ createdAt: nowIso$5()
239670
239670
  });
239671
239671
  const result = await this.resolveConfirmation(request);
239672
239672
  const approved = typeof result === "boolean" ? result : result.approved;
@@ -239674,7 +239674,7 @@ var AgentBrowser = class {
239674
239674
  type: "browser.safety.confirmation.resolved",
239675
239675
  approved,
239676
239676
  requestId: request.requestId,
239677
- resolvedAt: nowIso$3(),
239677
+ resolvedAt: nowIso$5(),
239678
239678
  ...this.events.sessionFields()
239679
239679
  });
239680
239680
  if (!approved) throw new BrowserSafetyError(decision.reason ?? "Browser action was not confirmed.", action, {
@@ -240804,6 +240804,1254 @@ function isPromiseLike(value) {
240804
240804
  return !!value && typeof value.then === "function";
240805
240805
  }
240806
240806
  //#endregion
240807
+ //#region src/sprite/spriteTaskHold.ts
240808
+ /**
240809
+ * Keeps a Fly.io Sprite awake for the duration of an agent turn.
240810
+ *
240811
+ * ── why this exists ─────────────────────────────────────────────────────────
240812
+ * A sprite's container cgroup is frozen within ~1 second of the last inbound
240813
+ * request. Processes are paused mid-instruction, not killed, and resume when
240814
+ * traffic returns. Measured on a throwaway sprite with a 1 Hz ticker
240815
+ * (`pocs/sprite-lifecycle-spike/`):
240816
+ *
240817
+ * detached, nothing connected 2 ticks / 121 s = 1.7%
240818
+ * with a task hold 90 ticks / 90 s = 100%
240819
+ *
240820
+ * So an agent turn that nobody is watching effectively stops. fly.io names our
240821
+ * previous approach — relying on a running process after disconnection — as an
240822
+ * anti-pattern, and points at the Tasks API instead. See
240823
+ * https://docs.sprites.dev/keeping-sprites-running/ and
240824
+ * `docs/specs/2026-07-30-sprites-platform-ground-truth.md` §1.
240825
+ *
240826
+ * Note that Services (the platform supervisor) do NOT solve this: "a Sprite with
240827
+ * ten services defined still pauses when it goes idle." Supervision and
240828
+ * wakefulness are separate primitives, and this module is only the second one.
240829
+ *
240830
+ * ── why it lives in the guest ───────────────────────────────────────────────
240831
+ * The Tasks API is guest-only. `GET https://api.sprites.dev/v1/sprites/{n}/tasks`
240832
+ * answers 404; the socket at /.sprite/api.sock is the only surface. CMMD cannot
240833
+ * take this hold on our behalf, which is why the env vars below are exported by
240834
+ * CMMD's provider and consumed here.
240835
+ *
240836
+ * ── the ordering rules ──────────────────────────────────────────────────────
240837
+ * 1. The refresher MUST be stopped before the DELETE is issued. A refresher that
240838
+ * outlives the delete re-acquires the hold on its next tick; that was
240839
+ * previously measured leaking 37 renewals over 18 minutes, pinning a sprite
240840
+ * running — and billing — 33 minutes after its work had ended.
240841
+ * 2. Stopping the interval only cancels FUTURE ticks. A PUT already on the wire
240842
+ * must be JOINED before the DELETE, or it lands afterwards and recreates the
240843
+ * hold for another full expiry window. fly.io's reference worker cancels and
240844
+ * awaits its heartbeat for the same reason.
240845
+ * 3. `release` can arrive while `acquire` is still awaiting its own PUT —
240846
+ * `releaseAll` runs from runtime drain and the shutdown finalizer,
240847
+ * concurrently with the provider event stream. The hold is registered before
240848
+ * that await (so a duplicate turn.started cannot double-acquire), so release
240849
+ * can remove the map entry first; `acquire` must then NOT install a
240850
+ * refresher. One attached to an unmapped hold is unreachable by release,
240851
+ * releaseThread and releaseAll alike, and re-PUTs every 60 s for the life of
240852
+ * the process — an unbounded pin.
240853
+ *
240854
+ * All three are pinned by tests.
240855
+ *
240856
+ * Pure and dependency-free on purpose, for the same reason as
240857
+ * `provider/claudeCredentialContainment.ts`: this rule is cost-critical and must
240858
+ * stay directly unit-testable without booting the Effect runtime.
240859
+ */
240860
+ /** Set by CMMD's Sprites provider on every project sprite. Absent elsewhere. */
240861
+ const SPRITE_TASK_HOLD_SOCKET_ENV = "FORGE_CMMD_SPRITE_TASK_HOLD_SOCKET";
240862
+ const SPRITE_TASK_HOLD_REFRESH_MS = 6e4;
240863
+ /**
240864
+ * Per-request deadline for the guest-socket calls. Must stay strictly under
240865
+ * SPRITE_TASK_HOLD_REFRESH_MS: the manager serializes refreshes, so a PUT that
240866
+ * never settles turns every later tick into a no-op and the hold expires
240867
+ * mid-turn. Bounding the call means a hung socket settles as a rejection, the
240868
+ * next 60 s tick retries, and release is not wedged joining it. From the last
240869
+ * good refresh there are still four attempts (+60/+120/+180/+240 s) before the
240870
+ * 300 s expiry. 10 s matches the `-m 10` cap CMMD already puts on every curl in
240871
+ * its own hold preamble; the guest-side manager was the one path left unbounded.
240872
+ */
240873
+ const SPRITE_TASK_HOLD_REQUEST_TIMEOUT_MS = 1e4;
240874
+ /**
240875
+ * Resolve the hold configuration from the process environment.
240876
+ *
240877
+ * Returns null on every runtime that is not a project sprite — desktop-local and
240878
+ * browser runtimes have no `/.sprite/api.sock`, so the whole feature no-ops
240879
+ * there rather than erroring.
240880
+ */
240881
+ function resolveSpriteTaskHoldConfig(env = process.env) {
240882
+ const socketPath = env[SPRITE_TASK_HOLD_SOCKET_ENV]?.trim();
240883
+ if (socketPath === void 0 || socketPath.length === 0) return null;
240884
+ return {
240885
+ socketPath,
240886
+ namePrefix: env["FORGE_CMMD_SPRITE_TASK_HOLD_PREFIX"]?.trim() ?? ""
240887
+ };
240888
+ }
240889
+ /**
240890
+ * Hold names must be unique per turn: several git worktrees inside one sprite
240891
+ * can run turns concurrently, and each has to be released independently.
240892
+ */
240893
+ function spriteTaskHoldName(namePrefix, turnId) {
240894
+ return `${namePrefix}${turnId}`;
240895
+ }
240896
+ const DEFAULT_TIMER = {
240897
+ setInterval: (handler, ms) => setInterval(handler, ms),
240898
+ clearInterval: (handle) => {
240899
+ clearInterval(handle);
240900
+ }
240901
+ };
240902
+ /**
240903
+ * Tracks one hold per active turn.
240904
+ *
240905
+ * Every method is failure-tolerant by design. Acquiring a hold is an
240906
+ * optimisation, not a precondition: if it fails the turn still runs, just at the
240907
+ * degraded speed it has today. Failing the turn instead would convert a billing
240908
+ * inefficiency into an outage.
240909
+ */
240910
+ var SpriteTaskHoldManager = class {
240911
+ #config;
240912
+ #transport;
240913
+ #timer;
240914
+ #onError;
240915
+ #holds = /* @__PURE__ */ new Map();
240916
+ constructor(options) {
240917
+ this.#config = options.config;
240918
+ this.#transport = options.transport;
240919
+ this.#timer = options.timer ?? DEFAULT_TIMER;
240920
+ this.#onError = options.onError ?? (() => {});
240921
+ }
240922
+ /** Turn ids that currently hold the sprite awake. Test/diagnostic surface. */
240923
+ activeTurnIds() {
240924
+ return [...this.#holds.keys()];
240925
+ }
240926
+ async acquire(turnId, threadId = null) {
240927
+ if (this.#holds.has(turnId)) return;
240928
+ const name = spriteTaskHoldName(this.#config.namePrefix, turnId);
240929
+ const hold = {
240930
+ name,
240931
+ threadId,
240932
+ refresher: null,
240933
+ inFlight: /* @__PURE__ */ new Set(),
240934
+ released: false
240935
+ };
240936
+ this.#holds.set(turnId, hold);
240937
+ const put = this.#transport.put(name, 300);
240938
+ const tracked = put.then(() => {}, () => {});
240939
+ hold.inFlight.add(tracked);
240940
+ try {
240941
+ await put;
240942
+ } catch (cause) {
240943
+ hold.inFlight.delete(tracked);
240944
+ this.#onError(`failed to acquire sprite task hold for turn ${turnId}`, cause);
240945
+ if (hold.released || this.#holds.get(turnId) !== hold) return;
240946
+ }
240947
+ hold.inFlight.delete(tracked);
240948
+ if (hold.released || this.#holds.get(turnId) !== hold) return;
240949
+ hold.refresher = this.#timer.setInterval(() => {
240950
+ if (hold.inFlight.size > 0) return;
240951
+ const refresh = this.#transport.put(name, 300).catch((cause) => this.#onError(`failed to refresh sprite task hold for turn ${turnId}`, cause));
240952
+ hold.inFlight.add(refresh);
240953
+ refresh.finally(() => hold.inFlight.delete(refresh));
240954
+ }, SPRITE_TASK_HOLD_REFRESH_MS);
240955
+ }
240956
+ async release(turnId) {
240957
+ const hold = this.#holds.get(turnId);
240958
+ if (!hold) return;
240959
+ hold.released = true;
240960
+ if (hold.refresher !== null) {
240961
+ this.#timer.clearInterval(hold.refresher);
240962
+ hold.refresher = null;
240963
+ }
240964
+ this.#holds.delete(turnId);
240965
+ if (hold.inFlight.size > 0) await Promise.all(hold.inFlight);
240966
+ try {
240967
+ await this.#transport.delete(hold.name);
240968
+ } catch (cause) {
240969
+ this.#onError(`failed to release sprite task hold for turn ${turnId}`, cause);
240970
+ }
240971
+ }
240972
+ /**
240973
+ * Release every hold belonging to one thread.
240974
+ *
240975
+ * `session.exited` names only the thread whose provider process died, and a
240976
+ * dead session's turn will never emit `turn.completed`. Releasing by thread —
240977
+ * rather than releasing everything — is what keeps a crash in one worktree
240978
+ * from silently dropping the holds of the other turns running beside it.
240979
+ */
240980
+ async releaseThread(threadId) {
240981
+ const turnIds = [...this.#holds.entries()].filter(([, hold]) => hold.threadId === threadId).map(([turnId]) => turnId);
240982
+ await Promise.all(turnIds.map((turnId) => this.release(turnId)));
240983
+ }
240984
+ /**
240985
+ * Release everything. Runtime drain and server shutdown must call this, or a
240986
+ * drained sprite keeps billing until each hold expires.
240987
+ */
240988
+ async releaseAll() {
240989
+ await Promise.all([...this.#holds.keys()].map((turnId) => this.release(turnId)));
240990
+ }
240991
+ };
240992
+ /**
240993
+ * HTTP-over-unix-socket transport for the guest API.
240994
+ *
240995
+ * Bun's fetch takes a `unix` option; the URL host is ignored by the socket but
240996
+ * still has to parse, which is why it is the literal `http://sprite` the
240997
+ * platform docs use.
240998
+ */
240999
+ function makeSpriteTaskHoldTransport(config, fetchImpl = fetch) {
241000
+ const call = async (path, init) => {
241001
+ const controller = new AbortController();
241002
+ const deadline = setTimeout(() => controller.abort(), SPRITE_TASK_HOLD_REQUEST_TIMEOUT_MS);
241003
+ try {
241004
+ const response = await fetchImpl(`http://sprite/v1/tasks${path}`, {
241005
+ ...init,
241006
+ signal: controller.signal,
241007
+ unix: config.socketPath
241008
+ });
241009
+ if (!response.ok) throw new Error(`sprite task API responded ${response.status}`);
241010
+ } finally {
241011
+ clearTimeout(deadline);
241012
+ }
241013
+ };
241014
+ return {
241015
+ put: (name, expireSeconds) => call(`/${encodeURIComponent(name)}`, {
241016
+ method: "PUT",
241017
+ headers: { "content-type": "application/json" },
241018
+ body: JSON.stringify({ expire: `${expireSeconds}s` })
241019
+ }),
241020
+ delete: (name) => call(`/${encodeURIComponent(name)}`, { method: "DELETE" })
241021
+ };
241022
+ }
241023
+ /**
241024
+ * Build a manager for this process, or null when the runtime is not a sprite.
241025
+ * Callers treat null as "this feature does not apply here".
241026
+ */
241027
+ function makeSpriteTaskHoldManager(env = process.env, onError) {
241028
+ const config = resolveSpriteTaskHoldConfig(env);
241029
+ if (config === null) return null;
241030
+ return new SpriteTaskHoldManager({
241031
+ config,
241032
+ transport: makeSpriteTaskHoldTransport(config),
241033
+ ...onError ? { onError } : {}
241034
+ });
241035
+ }
241036
+ //#endregion
241037
+ //#region src/browser/AgentBrowserHostBridgeCommands.ts
241038
+ const DEFAULT_AGENT_BROWSER_VIEWPORT = {
241039
+ width: 1280,
241040
+ height: 720
241041
+ };
241042
+ const ABSOLUTE_WEB_URL_PATTERN = /^[a-z][a-z\d+\-.]*:\/\//i;
241043
+ const SPECIAL_URL_PATTERN = /^(about|blob|chrome|data|devtools|file):/i;
241044
+ const LOCAL_URL_LIKE_PATTERN = /^(localhost|127\.0\.0\.1|\[::1\]|::1|[a-z0-9.-]+\.localhost)(?::\d+)?(?:[/?#].*)?$/i;
241045
+ function nowIso$4() {
241046
+ return (/* @__PURE__ */ new Date()).toISOString();
241047
+ }
241048
+ function normalizeUserVisibleBrowserUrl(url) {
241049
+ const trimmed = url.trim();
241050
+ if (trimmed.length === 0) return trimmed;
241051
+ if (ABSOLUTE_WEB_URL_PATTERN.test(trimmed) || SPECIAL_URL_PATTERN.test(trimmed) || trimmed.startsWith("//")) return trimmed;
241052
+ if (LOCAL_URL_LIKE_PATTERN.test(trimmed)) return `http://${trimmed}`;
241053
+ return `https://${trimmed}`;
241054
+ }
241055
+ function isAgentBrowserBoundedControlCommand(command) {
241056
+ switch (command.type) {
241057
+ case "click":
241058
+ case "clickPoint":
241059
+ case "pressKey":
241060
+ case "resetViewport":
241061
+ case "scroll":
241062
+ case "setViewport":
241063
+ case "type":
241064
+ case "typeText":
241065
+ case "wheel": return true;
241066
+ default: return false;
241067
+ }
241068
+ }
241069
+ function snapshotAfterTimedOutControl(snapshot, command) {
241070
+ switch (command.type) {
241071
+ case "setViewport": return {
241072
+ ...snapshot,
241073
+ viewport: command.viewport,
241074
+ updatedAt: nowIso$4()
241075
+ };
241076
+ case "resetViewport": return {
241077
+ ...snapshot,
241078
+ viewport: DEFAULT_AGENT_BROWSER_VIEWPORT,
241079
+ updatedAt: nowIso$4()
241080
+ };
241081
+ default: return snapshot;
241082
+ }
241083
+ }
241084
+ const HOST_COMMAND_MAPPERS = {
241085
+ activateTab: (command) => ({
241086
+ type: "activateTab",
241087
+ tabId: command.tabId
241088
+ }),
241089
+ click: (command) => ({
241090
+ type: "click",
241091
+ tabId: command.tabId,
241092
+ selector: command.selector
241093
+ }),
241094
+ clickPoint: (command) => ({
241095
+ type: "clickPoint",
241096
+ tabId: command.tabId,
241097
+ x: command.x,
241098
+ y: command.y,
241099
+ ...command.button !== void 0 ? { button: command.button } : {},
241100
+ ...command.clickCount !== void 0 ? { clickCount: command.clickCount } : {}
241101
+ }),
241102
+ closeTab: (command) => ({
241103
+ type: "closeTab",
241104
+ tabId: command.tabId
241105
+ }),
241106
+ consoleLogs: (command) => ({
241107
+ type: "consoleLogs",
241108
+ ...command.tabId !== void 0 ? { tabId: command.tabId } : {},
241109
+ ...command.limit !== void 0 ? { limit: command.limit } : {},
241110
+ ...command.levels !== void 0 ? { levels: command.levels } : {}
241111
+ }),
241112
+ goBack: (command) => ({
241113
+ type: "goBack",
241114
+ tabId: command.tabId
241115
+ }),
241116
+ goForward: (command) => ({
241117
+ type: "goForward",
241118
+ tabId: command.tabId
241119
+ }),
241120
+ nameSession: (command) => ({
241121
+ type: "nameSession",
241122
+ name: command.name
241123
+ }),
241124
+ navigate: (command) => ({
241125
+ type: "navigate",
241126
+ url: normalizeUserVisibleBrowserUrl(command.url),
241127
+ ...command.tabId !== void 0 ? { tabId: command.tabId } : {}
241128
+ }),
241129
+ newTab: (command) => ({
241130
+ type: "newTab",
241131
+ ...command.url !== void 0 ? { url: normalizeUserVisibleBrowserUrl(command.url) } : {},
241132
+ ...command.makeActive !== void 0 ? { makeActive: command.makeActive } : {}
241133
+ }),
241134
+ pressKey: (command) => ({
241135
+ type: "pressKey",
241136
+ tabId: command.tabId,
241137
+ key: command.key
241138
+ }),
241139
+ reload: (command) => ({
241140
+ type: "reload",
241141
+ tabId: command.tabId
241142
+ }),
241143
+ resetViewport: () => ({ type: "resetViewport" }),
241144
+ screenshot: (command) => ({
241145
+ type: "screenshot",
241146
+ tabId: command.tabId,
241147
+ ...command.fullPage !== void 0 ? { fullPage: command.fullPage } : {}
241148
+ }),
241149
+ scroll: (command) => ({
241150
+ type: "scroll",
241151
+ tabId: command.tabId,
241152
+ scrollX: 0,
241153
+ scrollY: command.toTop === true ? -900 : 900,
241154
+ ...command.x !== void 0 ? { x: command.x } : {},
241155
+ ...command.y !== void 0 ? { y: command.y } : {}
241156
+ }),
241157
+ setViewport: (command) => ({
241158
+ type: "setViewport",
241159
+ viewport: command.viewport
241160
+ }),
241161
+ setVisibility: (command) => ({
241162
+ type: "setVisibility",
241163
+ visibility: command.visibility
241164
+ }),
241165
+ snapshot: (command) => ({
241166
+ type: "snapshot",
241167
+ tabId: command.tabId
241168
+ }),
241169
+ type: (command) => ({
241170
+ type: "type",
241171
+ tabId: command.tabId,
241172
+ selector: command.selector,
241173
+ text: command.text,
241174
+ ...command.append !== void 0 ? { append: command.append } : {}
241175
+ }),
241176
+ typeText: (command) => ({
241177
+ type: "typeText",
241178
+ tabId: command.tabId,
241179
+ text: command.text,
241180
+ ...command.pressEnter !== void 0 ? { pressEnter: command.pressEnter } : {}
241181
+ }),
241182
+ wheel: (command) => ({
241183
+ type: "wheel",
241184
+ tabId: command.tabId,
241185
+ deltaX: command.deltaX,
241186
+ deltaY: command.deltaY,
241187
+ ...command.x !== void 0 ? { x: command.x } : {},
241188
+ ...command.y !== void 0 ? { y: command.y } : {}
241189
+ })
241190
+ };
241191
+ function toHostCommand(command) {
241192
+ return HOST_COMMAND_MAPPERS[command.type](command);
241193
+ }
241194
+ function commandCaptureTabId(command) {
241195
+ switch (command.type) {
241196
+ case "snapshot":
241197
+ case "screenshot": return command.tabId;
241198
+ default: return;
241199
+ }
241200
+ }
241201
+ //#endregion
241202
+ //#region src/browser/AgentBrowserHostBridgeDisplay.ts
241203
+ 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.";
241204
+ 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.";
241205
+ function remoteWebRtcUnavailableError(_input) {
241206
+ return new BrowserError({ detail: REMOTE_WEBRTC_UNAVAILABLE_DETAIL });
241207
+ }
241208
+ function remoteWebRtcRelayRequiredError(_input) {
241209
+ return new BrowserError({ detail: REMOTE_WEBRTC_RELAY_REQUIRED_DETAIL });
241210
+ }
241211
+ function agentBrowserDisplayForRuntimeMode(input) {
241212
+ if (input.mode !== "desktop") {
241213
+ const hasRelay = hasTurnRelay(input.iceServers ?? []);
241214
+ return {
241215
+ transport: "remote-webrtc",
241216
+ status: "degraded",
241217
+ reason: input.requiresRelay === true && !hasRelay ? "network_unreachable" : "transport_unavailable",
241218
+ detail: input.requiresRelay === true && !hasRelay ? REMOTE_WEBRTC_RELAY_REQUIRED_DETAIL : "Remote WebRTC browser display is not attached yet.",
241219
+ ...input.viewport ? { viewport: input.viewport } : {},
241220
+ ...input.iceServers && input.iceServers.length > 0 ? { iceServers: input.iceServers } : {},
241221
+ reconnect: {
241222
+ status: "ready",
241223
+ attempt: 0
241224
+ }
241225
+ };
241226
+ }
241227
+ return {
241228
+ transport: "native-embedded",
241229
+ status: "connected",
241230
+ ...input.viewport ? { viewport: input.viewport } : {},
241231
+ reconnect: {
241232
+ status: "ready",
241233
+ attempt: 0
241234
+ }
241235
+ };
241236
+ }
241237
+ function hasTurnRelay(iceServers) {
241238
+ return browserWebRtcIceServersIncludeRelay(iceServers);
241239
+ }
241240
+ async function resolveDisplayIceServers(options) {
241241
+ return options?.resolveDisplayIceServers ? await options.resolveDisplayIceServers() : options?.displayIceServers ?? [];
241242
+ }
241243
+ function webRtcDisplayRequiresRelay(serverConfig) {
241244
+ return serverConfig.mode === "web" && serverConfig.devUrl === void 0;
241245
+ }
241246
+ //#endregion
241247
+ //#region src/browser/AgentBrowserHostBridgeProjection.ts
241248
+ const DEFAULT_URL$1 = "about:blank";
241249
+ const DEFAULT_RENDER_MODE = "interactive";
241250
+ const DEFAULT_AGENT_BROWSER_DISPLAY = {
241251
+ transport: "remote-webrtc",
241252
+ status: "connected",
241253
+ reconnect: {
241254
+ status: "ready",
241255
+ attempt: 0
241256
+ }
241257
+ };
241258
+ function nowIso$3() {
241259
+ return (/* @__PURE__ */ new Date()).toISOString();
241260
+ }
241261
+ function toForgeTab(input) {
241262
+ return {
241263
+ tabId: BrowserTabId.make(input.tab.id),
241264
+ restoreKey: input.tab.id,
241265
+ ...input.tab.title ? { title: input.tab.title } : {},
241266
+ url: input.tab.url ?? DEFAULT_URL$1,
241267
+ isActive: input.tab.isActive,
241268
+ canGoBack: input.tab.canGoBack ?? false,
241269
+ canGoForward: input.tab.canGoForward ?? false,
241270
+ renderMode: DEFAULT_RENDER_MODE,
241271
+ status: "live",
241272
+ ...input.tab.navigationStatus !== void 0 ? { navigationStatus: input.tab.navigationStatus } : {},
241273
+ ...input.tab.requestedUrl !== void 0 ? { requestedUrl: input.tab.requestedUrl } : {},
241274
+ ...input.tab.navigationError !== void 0 ? { navigationError: input.tab.navigationError } : {},
241275
+ updatedAt: input.updatedAt
241276
+ };
241277
+ }
241278
+ function toForgeSession(input) {
241279
+ return {
241280
+ browserId: BrowserId.make(input.snapshot.browserId),
241281
+ threadId: input.threadId,
241282
+ environmentId: EnvironmentId.make(input.environmentId ?? DEFAULT_BROWSER_ENVIRONMENT_ID),
241283
+ userId: BrowserUserId.make(input.userId ?? "local-user"),
241284
+ agentId: input.agentId ?? null,
241285
+ ownerRole: input.ownerRole ?? "lead",
241286
+ name: input.snapshot.name,
241287
+ visibility: input.snapshot.visibility,
241288
+ viewport: input.snapshot.viewport,
241289
+ display: input.display ?? DEFAULT_AGENT_BROWSER_DISPLAY,
241290
+ sessionId: BrowserSessionId.make(input.snapshot.sessionId),
241291
+ ...input.controlState ? {
241292
+ sessionEpoch: input.controlState.sessionEpoch,
241293
+ lastAppliedControlSequence: input.controlState.lastAppliedControlSequence
241294
+ } : {},
241295
+ activeTabId: input.snapshot.activeTabId ? BrowserTabId.make(input.snapshot.activeTabId) : null,
241296
+ tabs: input.snapshot.tabs.map((tab) => toForgeTab({
241297
+ tab,
241298
+ updatedAt: input.snapshot.updatedAt
241299
+ })),
241300
+ updatedAt: input.snapshot.updatedAt
241301
+ };
241302
+ }
241303
+ function toStreamEvent(input) {
241304
+ const base = {
241305
+ eventId: EventId.make(crypto.randomUUID()),
241306
+ browserId: input.browserId,
241307
+ threadId: input.threadId,
241308
+ environmentId: EnvironmentId.make(input.environmentId),
241309
+ userId: BrowserUserId.make(input.userId),
241310
+ agentId: input.agentId,
241311
+ ...input.controlState ? { sessionEpoch: input.controlState.sessionEpoch } : {},
241312
+ createdAt: nowIso$3()
241313
+ };
241314
+ switch (input.event.type) {
241315
+ case "session.started": return {
241316
+ ...base,
241317
+ type: "session.started",
241318
+ payload: { snapshot: toForgeSession({
241319
+ snapshot: input.event.payload.snapshot,
241320
+ threadId: input.threadId,
241321
+ environmentId: input.environmentId,
241322
+ userId: input.userId,
241323
+ agentId: input.agentId,
241324
+ ownerRole: input.ownerRole,
241325
+ display: input.display,
241326
+ controlState: input.controlState
241327
+ }) }
241328
+ };
241329
+ case "session.updated": return {
241330
+ ...base,
241331
+ type: input.event.type,
241332
+ payload: { snapshot: toForgeSession({
241333
+ snapshot: input.event.payload.snapshot,
241334
+ threadId: input.threadId,
241335
+ environmentId: input.environmentId,
241336
+ userId: input.userId,
241337
+ agentId: input.agentId,
241338
+ ownerRole: input.ownerRole,
241339
+ display: input.display,
241340
+ controlState: input.controlState
241341
+ }) }
241342
+ };
241343
+ case "session.closed": return {
241344
+ ...base,
241345
+ type: "session.closed",
241346
+ payload: { sessionId: BrowserSessionId.make(input.event.payload.sessionId) }
241347
+ };
241348
+ case "tab.closed": return {
241349
+ ...base,
241350
+ type: "tab.closed",
241351
+ payload: {
241352
+ sessionId: BrowserSessionId.make(input.event.sessionId),
241353
+ tabId: BrowserTabId.make(input.event.payload.tabId)
241354
+ }
241355
+ };
241356
+ case "snapshot.ready": return {
241357
+ ...base,
241358
+ type: "snapshot.ready",
241359
+ payload: {
241360
+ sessionId: BrowserSessionId.make(input.event.sessionId),
241361
+ tabId: BrowserTabId.make(input.event.payload.tabId),
241362
+ captureId: BrowserCaptureId.make(input.event.payload.captureId ?? crypto.randomUUID()),
241363
+ capturedAt: input.event.payload.capturedAt,
241364
+ url: input.event.payload.url ?? DEFAULT_URL$1,
241365
+ viewport: input.event.payload.viewport,
241366
+ content: input.event.payload.content
241367
+ }
241368
+ };
241369
+ case "screenshot.ready": return {
241370
+ ...base,
241371
+ type: "screenshot.ready",
241372
+ payload: {
241373
+ sessionId: BrowserSessionId.make(input.event.sessionId),
241374
+ tabId: BrowserTabId.make(input.event.payload.tabId),
241375
+ captureId: BrowserCaptureId.make(input.event.payload.captureId ?? crypto.randomUUID()),
241376
+ capturedAt: input.event.payload.capturedAt,
241377
+ url: input.event.payload.url ?? DEFAULT_URL$1,
241378
+ viewport: input.event.payload.viewport,
241379
+ imageDataUrl: input.event.payload.imageDataUrl
241380
+ }
241381
+ };
241382
+ case "console.logs.ready": return {
241383
+ ...base,
241384
+ type: "console.logs.ready",
241385
+ payload: {
241386
+ sessionId: BrowserSessionId.make(input.event.sessionId),
241387
+ tabId: BrowserTabId.make(input.event.payload.tabId),
241388
+ logs: input.event.payload.logs.map((log) => ({
241389
+ level: log.level === "warn" ? "warning" : log.level === "log" ? "info" : log.level,
241390
+ message: log.message,
241391
+ createdAt: log.timestamp
241392
+ }))
241393
+ }
241394
+ };
241395
+ case "action.failed": return {
241396
+ ...base,
241397
+ type: "action.failed",
241398
+ payload: {
241399
+ commandType: input.event.payload.commandType,
241400
+ message: input.event.payload.message,
241401
+ ...input.event.payload.tabId ? { tabId: BrowserTabId.make(input.event.payload.tabId) } : {}
241402
+ }
241403
+ };
241404
+ case "dom.visible.ready":
241405
+ case "element.info.ready":
241406
+ case "element.screenshot.ready":
241407
+ case "clipboard.text.ready": return null;
241408
+ }
241409
+ }
241410
+ function toCommandResult(input) {
241411
+ return {
241412
+ ok: input.result.ok,
241413
+ snapshot: toForgeSession({
241414
+ snapshot: input.result.snapshot,
241415
+ threadId: input.command.threadId,
241416
+ environmentId: input.command.environmentId,
241417
+ userId: input.command.userId,
241418
+ display: input.display,
241419
+ controlState: input.controlState
241420
+ }),
241421
+ sessionEpoch: input.controlState.sessionEpoch,
241422
+ acknowledgedControlSequence: input.acknowledgedControlSequence,
241423
+ ...input.result.detail ? { detail: input.result.detail } : {},
241424
+ ...input.latestCapture ? { latestCapture: input.latestCapture } : {}
241425
+ };
241426
+ }
241427
+ function toCommandLatestCapture(input) {
241428
+ const tabId = commandCaptureTabId(input.command.command);
241429
+ 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;
241430
+ const mapped = toStreamEvent({
241431
+ event: input.event,
241432
+ threadId: input.command.threadId,
241433
+ browserId: input.browserId,
241434
+ environmentId: input.command.environmentId ?? DEFAULT_BROWSER_ENVIRONMENT_ID,
241435
+ userId: input.command.userId ?? "local-user",
241436
+ agentId: null,
241437
+ ownerRole: "lead",
241438
+ display: input.display,
241439
+ controlState: input.controlState
241440
+ });
241441
+ return mapped?.type === "snapshot.ready" || mapped?.type === "screenshot.ready" ? mapped : void 0;
241442
+ }
241443
+ //#endregion
241444
+ //#region src/browser/AgentBrowserHostBridgeShared.ts
241445
+ const DEFAULT_AGENT_BROWSER_ID = "embedded";
241446
+ const DEFAULT_URL = "about:blank";
241447
+ const isBrowserError = require_Schema$1.is(BrowserError);
241448
+ function nowIso$2() {
241449
+ return (/* @__PURE__ */ new Date()).toISOString();
241450
+ }
241451
+ function sessionIdForThread(threadId) {
241452
+ return BrowserSessionId.make(threadId);
241453
+ }
241454
+ function browserIdFrom(input) {
241455
+ return BrowserId.make(input.browserId ?? "embedded");
241456
+ }
241457
+ function toBrowserError(error, fallback) {
241458
+ console.warn("[browser] Agent Browser host bridge operation failed", {
241459
+ operation: fallback,
241460
+ error
241461
+ });
241462
+ if (isBrowserError(error)) return error;
241463
+ if (error instanceof Error && error.message.trim().length > 0) return new BrowserError({ detail: error.message.trim() });
241464
+ return new BrowserError({ detail: fallback });
241465
+ }
241466
+ //#endregion
241467
+ //#region src/browser/AgentBrowserHostBridgeTimeouts.ts
241468
+ const DEFAULT_AGENT_BROWSER_CONTROL_COMMAND_TIMEOUT_MS = 1500;
241469
+ const DEFAULT_AGENT_BROWSER_FAST_COMMAND_TIMEOUT_MS = 1e4;
241470
+ const DEFAULT_AGENT_BROWSER_NAVIGATION_COMMAND_TIMEOUT_MS = 9e4;
241471
+ const DEFAULT_AGENT_BROWSER_CAPTURE_COMMAND_TIMEOUT_MS = 9e4;
241472
+ const DEFAULT_AGENT_BROWSER_BRIDGE_OPEN_TIMEOUT_MS = 9e4;
241473
+ const DEFAULT_AGENT_BROWSER_BRIDGE_CLOSE_TIMEOUT_MS = 3e4;
241474
+ const DEFAULT_AGENT_BROWSER_PLAYWRIGHT_BACKEND_CLOSE_TIMEOUT_MS = 1e4;
241475
+ function positiveTimeoutMs(value) {
241476
+ if (!value) return;
241477
+ const parsed = Number.parseInt(value, 10);
241478
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : void 0;
241479
+ }
241480
+ function agentBrowserCommandTimeoutMs(command, env = process.env) {
241481
+ const explicitCommandTimeout = positiveTimeoutMs(env.FORGE_AGENT_BROWSER_BRIDGE_COMMAND_TIMEOUT_MS);
241482
+ if (explicitCommandTimeout !== void 0) return explicitCommandTimeout;
241483
+ if (isAgentBrowserBoundedControlCommand(command)) return positiveTimeoutMs(env.FORGE_AGENT_BROWSER_BRIDGE_CONTROL_TIMEOUT_MS) ?? DEFAULT_AGENT_BROWSER_CONTROL_COMMAND_TIMEOUT_MS;
241484
+ switch (command.type) {
241485
+ case "screenshot":
241486
+ case "snapshot": return positiveTimeoutMs(env.FORGE_AGENT_BROWSER_BRIDGE_CAPTURE_TIMEOUT_MS) ?? DEFAULT_AGENT_BROWSER_CAPTURE_COMMAND_TIMEOUT_MS;
241487
+ case "newTab":
241488
+ case "navigate":
241489
+ case "goBack":
241490
+ case "goForward":
241491
+ case "reload": return DEFAULT_AGENT_BROWSER_NAVIGATION_COMMAND_TIMEOUT_MS;
241492
+ default: return DEFAULT_AGENT_BROWSER_FAST_COMMAND_TIMEOUT_MS;
241493
+ }
241494
+ }
241495
+ function agentBrowserCloseTimeoutMs(env = process.env) {
241496
+ return positiveTimeoutMs(env.FORGE_AGENT_BROWSER_BRIDGE_CLOSE_TIMEOUT_MS) ?? DEFAULT_AGENT_BROWSER_BRIDGE_CLOSE_TIMEOUT_MS;
241497
+ }
241498
+ function agentBrowserOpenTimeoutMs(env = process.env) {
241499
+ return positiveTimeoutMs(env.FORGE_AGENT_BROWSER_BRIDGE_OPEN_TIMEOUT_MS) ?? DEFAULT_AGENT_BROWSER_BRIDGE_OPEN_TIMEOUT_MS;
241500
+ }
241501
+ function agentBrowserFastTimeoutMs(env = process.env) {
241502
+ return positiveTimeoutMs(env.FORGE_AGENT_BROWSER_BRIDGE_COMMAND_TIMEOUT_MS) ?? DEFAULT_AGENT_BROWSER_FAST_COMMAND_TIMEOUT_MS;
241503
+ }
241504
+ function agentBrowserPlaywrightBackendCloseTimeoutMs(env = process.env) {
241505
+ return positiveTimeoutMs(env.FORGE_AGENT_BROWSER_PLAYWRIGHT_BACKEND_CLOSE_TIMEOUT_MS) ?? DEFAULT_AGENT_BROWSER_PLAYWRIGHT_BACKEND_CLOSE_TIMEOUT_MS;
241506
+ }
241507
+ async function withAgentBrowserBridgeTimeout(label, promise, timeoutMs) {
241508
+ let timeout;
241509
+ try {
241510
+ return await Promise.race([promise, new Promise((_, reject) => {
241511
+ timeout = setTimeout(() => {
241512
+ reject(/* @__PURE__ */ new Error(`${label} timed out after ${Math.ceil(timeoutMs / 1e3)}s.`));
241513
+ }, timeoutMs);
241514
+ })]);
241515
+ } finally {
241516
+ if (timeout !== void 0) clearTimeout(timeout);
241517
+ }
241518
+ }
241519
+ async function openAgentBrowserSessionReady(bridge, input) {
241520
+ let resolveStarted;
241521
+ const started = new Promise((resolve) => {
241522
+ resolveStarted = resolve;
241523
+ });
241524
+ const unsubscribe = await bridge.subscribe({
241525
+ sessionId: input.sessionId,
241526
+ ...input.browserId ? { browserId: input.browserId } : {}
241527
+ }, (event) => {
241528
+ if (event.type === "session.started" && event.sessionId === input.sessionId) resolveStarted(event.payload.snapshot);
241529
+ });
241530
+ try {
241531
+ return await Promise.race([bridge.openSession(input), started]);
241532
+ } finally {
241533
+ unsubscribe();
241534
+ }
241535
+ }
241536
+ function isAgentBrowserBridgeTimeoutError(error) {
241537
+ const message = [
241538
+ isBrowserError(error) ? error.detail : void 0,
241539
+ error instanceof Error ? error.message : void 0,
241540
+ error instanceof Error ? error.stack : void 0,
241541
+ typeof error === "string" ? error : void 0,
241542
+ (() => {
241543
+ try {
241544
+ return JSON.stringify(error);
241545
+ } catch {
241546
+ return;
241547
+ }
241548
+ })(),
241549
+ String(error)
241550
+ ].filter((part) => typeof part === "string" && part.length > 0).join("\n");
241551
+ return /timed out after \d+s\./.test(message);
241552
+ }
241553
+ async function closeAgentBrowserBackendWithTimeout(closeBackend, env = process.env) {
241554
+ try {
241555
+ await withAgentBrowserBridgeTimeout("Agent Browser Playwright backend close", closeBackend(), agentBrowserPlaywrightBackendCloseTimeoutMs(env));
241556
+ } catch (error) {
241557
+ if (!isAgentBrowserBridgeTimeoutError(error)) throw error;
241558
+ console.warn("[browser] Agent Browser Playwright backend close timed out; continuing", { timeoutMs: agentBrowserPlaywrightBackendCloseTimeoutMs(env) });
241559
+ }
241560
+ }
241561
+ //#endregion
241562
+ //#region src/browser/browserControlProtocol.ts
241563
+ function resolveBrowserControlDecision(state, metadata) {
241564
+ const hasSessionEpoch = metadata.sessionEpoch !== void 0;
241565
+ const hasControlSequence = metadata.controlSequence !== void 0;
241566
+ const expectedControlSequence = state.lastAppliedControlSequence + 1;
241567
+ if (!hasSessionEpoch && !hasControlSequence) return {
241568
+ type: "apply",
241569
+ controlSequence: expectedControlSequence
241570
+ };
241571
+ if (!hasSessionEpoch || !hasControlSequence) return {
241572
+ type: "reject",
241573
+ reason: "incomplete_metadata",
241574
+ expectedSessionEpoch: state.sessionEpoch,
241575
+ expectedControlSequence,
241576
+ acknowledgedControlSequence: state.lastAppliedControlSequence
241577
+ };
241578
+ if (metadata.sessionEpoch !== state.sessionEpoch) return {
241579
+ type: "reject",
241580
+ reason: "stale_epoch",
241581
+ expectedSessionEpoch: state.sessionEpoch,
241582
+ acknowledgedControlSequence: state.lastAppliedControlSequence
241583
+ };
241584
+ if (metadata.controlSequence <= state.lastAppliedControlSequence) return {
241585
+ type: "duplicate",
241586
+ acknowledgedControlSequence: state.lastAppliedControlSequence
241587
+ };
241588
+ if (metadata.controlSequence !== expectedControlSequence) return {
241589
+ type: "reject",
241590
+ reason: "sequence_gap",
241591
+ expectedControlSequence,
241592
+ acknowledgedControlSequence: state.lastAppliedControlSequence
241593
+ };
241594
+ return {
241595
+ type: "apply",
241596
+ controlSequence: metadata.controlSequence
241597
+ };
241598
+ }
241599
+ function browserControlRejectionDetail(decision) {
241600
+ switch (decision.reason) {
241601
+ case "stale_epoch": return `Browser session is stale. Expected epoch ${decision.expectedSessionEpoch}.`;
241602
+ case "sequence_gap": return `Browser control sequence is out of order. Expected ${decision.expectedControlSequence}.`;
241603
+ case "incomplete_metadata": return "Browser control ordering requires both sessionEpoch and controlSequence.";
241604
+ }
241605
+ }
241606
+ //#endregion
241607
+ //#region src/browser/AgentBrowserHostBridgeManagerCommands.ts
241608
+ async function duplicateControlResult(input) {
241609
+ const snapshot = await withAgentBrowserBridgeTimeout("Agent Browser duplicate command session read", input.bridge.getSession({
241610
+ sessionId: input.hostSessionId,
241611
+ ...input.commandInput.browserId ? { browserId: input.commandInput.browserId } : {}
241612
+ }), agentBrowserFastTimeoutMs());
241613
+ if (!snapshot) throw new BrowserError({ detail: "Browser session not found." });
241614
+ const controlState = input.state.controlStateFor(input.hostSessionId);
241615
+ return {
241616
+ ok: true,
241617
+ snapshot: toForgeSession({
241618
+ snapshot,
241619
+ threadId: input.commandInput.threadId,
241620
+ environmentId: input.commandInput.environmentId,
241621
+ userId: input.commandInput.userId,
241622
+ display: input.state.displayForSnapshot(snapshot),
241623
+ controlState
241624
+ }),
241625
+ sessionEpoch: controlState.sessionEpoch,
241626
+ acknowledgedControlSequence: input.acknowledgedControlSequence,
241627
+ detail: "Browser control was already applied"
241628
+ };
241629
+ }
241630
+ async function dispatchHostCommand(input) {
241631
+ try {
241632
+ return await withAgentBrowserBridgeTimeout(`Agent Browser command ${input.commandInput.command.type}`, input.bridge.command({
241633
+ ...input.hostIdentity,
241634
+ command: toHostCommand(input.commandInput.command)
241635
+ }), agentBrowserCommandTimeoutMs(input.commandInput.command));
241636
+ } catch (error) {
241637
+ if (!(isAgentBrowserBridgeTimeoutError(error) && isAgentBrowserBoundedControlCommand(input.commandInput.command) && input.preCommandSnapshot !== null)) throw error;
241638
+ console.warn("[browser] Agent Browser control command timed out after dispatch", {
241639
+ commandType: input.commandInput.command.type,
241640
+ sessionId: input.hostIdentity.sessionId,
241641
+ ...input.commandInput.browserId ? { browserId: input.commandInput.browserId } : {}
241642
+ });
241643
+ return {
241644
+ ok: true,
241645
+ snapshot: snapshotAfterTimedOutControl(input.preCommandSnapshot, input.commandInput.command),
241646
+ detail: "Agent Browser control command dispatch timed out before post-action observation completed."
241647
+ };
241648
+ }
241649
+ }
241650
+ async function executeHostCommand(input) {
241651
+ const captureTabId = commandCaptureTabId(input.commandInput.command);
241652
+ const hostIdentity = {
241653
+ sessionId: input.hostSessionId,
241654
+ ...input.commandInput.browserId ? { browserId: input.commandInput.browserId } : {}
241655
+ };
241656
+ 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;
241657
+ let latestCaptureEvent;
241658
+ const unsubscribeCapture = captureTabId ? await input.bridge.subscribe(hostIdentity, (event) => {
241659
+ if ((event.type === "snapshot.ready" || event.type === "screenshot.ready") && event.sessionId === input.hostSessionId && event.payload.tabId === captureTabId) latestCaptureEvent = event;
241660
+ }) : void 0;
241661
+ let result;
241662
+ try {
241663
+ result = await dispatchHostCommand({
241664
+ ...input,
241665
+ hostIdentity,
241666
+ preCommandSnapshot
241667
+ });
241668
+ } finally {
241669
+ unsubscribeCapture?.();
241670
+ }
241671
+ if (input.commandInput.command.type === "closeTab") {
241672
+ await require_Schema$1.runPromise(input.options?.displayPeers?.closeForSession({
241673
+ sessionId: input.hostSessionId,
241674
+ tabId: input.commandInput.command.tabId
241675
+ }) ?? require_Schema$1.void_);
241676
+ input.state.clearConnectedDisplay({
241677
+ sessionId: input.hostSessionId,
241678
+ tabId: input.commandInput.command.tabId
241679
+ });
241680
+ }
241681
+ const display = input.state.displayForSnapshot(result.snapshot);
241682
+ const controlState = input.state.controlStateFor(input.hostSessionId);
241683
+ controlState.lastAppliedControlSequence = input.controlSequence;
241684
+ return toCommandResult({
241685
+ result,
241686
+ command: input.commandInput,
241687
+ display,
241688
+ controlState,
241689
+ acknowledgedControlSequence: input.controlSequence,
241690
+ latestCapture: latestCaptureEvent ? toCommandLatestCapture({
241691
+ event: latestCaptureEvent,
241692
+ command: input.commandInput,
241693
+ browserId: BrowserId.make(result.snapshot.browserId),
241694
+ display,
241695
+ controlState
241696
+ }) : void 0
241697
+ });
241698
+ }
241699
+ async function executeOrderedHostCommand(input) {
241700
+ const hostSessionId = sessionIdForThread(input.commandInput.threadId);
241701
+ return input.state.runSerializedCommand(hostSessionId, async () => {
241702
+ const controlDecision = resolveBrowserControlDecision(input.state.controlStateFor(hostSessionId), input.commandInput);
241703
+ if (controlDecision.type === "reject") throw new BrowserError({ detail: browserControlRejectionDetail(controlDecision) });
241704
+ if (controlDecision.type === "duplicate") return duplicateControlResult({
241705
+ ...input,
241706
+ hostSessionId,
241707
+ acknowledgedControlSequence: controlDecision.acknowledgedControlSequence
241708
+ });
241709
+ return executeHostCommand({
241710
+ ...input,
241711
+ hostSessionId,
241712
+ controlSequence: controlDecision.controlSequence
241713
+ });
241714
+ });
241715
+ }
241716
+ //#endregion
241717
+ //#region src/browser/AgentBrowserHostBridgeManagerState.ts
241718
+ function resolveDisplay(snapshot, options) {
241719
+ if (typeof options?.display === "function") return options.display(snapshot);
241720
+ return options?.display ?? DEFAULT_AGENT_BROWSER_DISPLAY;
241721
+ }
241722
+ function subscriptionMatchesEvent(subscription, event) {
241723
+ if (subscription.input.threadId && subscription.input.threadId !== event.threadId) return false;
241724
+ if (subscription.input.browserId && subscription.input.browserId !== event.browserId) return false;
241725
+ if (subscription.input.environmentId && subscription.input.environmentId !== event.environmentId) return false;
241726
+ if (subscription.input.userId && subscription.input.userId !== event.userId) return false;
241727
+ return true;
241728
+ }
241729
+ function createAgentBrowserHostBridgeState(options) {
241730
+ const localSubscriptions = /* @__PURE__ */ new Set();
241731
+ const connectedDisplays = /* @__PURE__ */ new Map();
241732
+ const controlStates = /* @__PURE__ */ new Map();
241733
+ const commandQueues = /* @__PURE__ */ new Map();
241734
+ const controlStateFor = (sessionId) => {
241735
+ const existing = controlStates.get(sessionId);
241736
+ if (existing) return existing;
241737
+ const created = {
241738
+ sessionEpoch: crypto.randomUUID(),
241739
+ lastAppliedControlSequence: 0
241740
+ };
241741
+ controlStates.set(sessionId, created);
241742
+ return created;
241743
+ };
241744
+ const runSerializedCommand = async (sessionId, task) => {
241745
+ const previous = commandQueues.get(sessionId) ?? Promise.resolve();
241746
+ let releaseCurrent;
241747
+ const current = new Promise((resolve) => {
241748
+ releaseCurrent = resolve;
241749
+ });
241750
+ const queued = previous.catch(() => void 0).then(() => current);
241751
+ commandQueues.set(sessionId, queued);
241752
+ await previous.catch(() => void 0);
241753
+ try {
241754
+ return await task();
241755
+ } finally {
241756
+ releaseCurrent();
241757
+ if (commandQueues.get(sessionId) === queued) commandQueues.delete(sessionId);
241758
+ }
241759
+ };
241760
+ const clearConnectedDisplay = (input) => {
241761
+ const active = connectedDisplays.get(input.sessionId);
241762
+ if (!active) return;
241763
+ if (input.tabId === void 0 || active.tabId === input.tabId) connectedDisplays.delete(input.sessionId);
241764
+ };
241765
+ const displayForSnapshot = (snapshot) => {
241766
+ const fallback = resolveDisplay(snapshot, options);
241767
+ const active = connectedDisplays.get(snapshot.sessionId);
241768
+ if (!active) return fallback;
241769
+ if (!snapshot.tabs.some((tab) => tab.id === active.tabId)) {
241770
+ connectedDisplays.delete(snapshot.sessionId);
241771
+ return fallback;
241772
+ }
241773
+ if (snapshot.activeTabId !== active.tabId) return fallback;
241774
+ const { reason: _reason, detail: _detail, ...fallbackWithoutDegradedReason } = fallback;
241775
+ return {
241776
+ ...fallbackWithoutDegradedReason,
241777
+ ...active.display,
241778
+ reconnect: active.display.reconnect ?? fallback.reconnect
241779
+ };
241780
+ };
241781
+ const publishLocalEvent = (event) => {
241782
+ for (const subscription of localSubscriptions) {
241783
+ if (!subscriptionMatchesEvent(subscription, event)) continue;
241784
+ require_Schema$1.runPromise(subscription.listener(event)).catch((error) => {
241785
+ console.warn("[browser] Failed to publish local browser event", { error });
241786
+ });
241787
+ }
241788
+ };
241789
+ const publishWebRtcIceCandidate = (event) => {
241790
+ publishLocalEvent({
241791
+ eventId: EventId.make(crypto.randomUUID()),
241792
+ browserId: event.input.browserId ?? browserIdFrom(event.input),
241793
+ threadId: event.input.threadId,
241794
+ environmentId: event.input.environmentId ?? DEFAULT_BROWSER_ENVIRONMENT_ID,
241795
+ userId: event.input.userId ?? "local-user",
241796
+ agentId: null,
241797
+ createdAt: nowIso$2(),
241798
+ type: "display.iceCandidate",
241799
+ payload: {
241800
+ sessionId: event.input.sessionId,
241801
+ tabId: event.input.tabId,
241802
+ clientId: event.input.clientId,
241803
+ candidate: event.candidate
241804
+ }
241805
+ });
241806
+ };
241807
+ return {
241808
+ localSubscriptions,
241809
+ connectedDisplays,
241810
+ controlStates,
241811
+ controlStateFor,
241812
+ runSerializedCommand,
241813
+ clearConnectedDisplay,
241814
+ displayForSnapshot,
241815
+ publishWebRtcIceCandidate
241816
+ };
241817
+ }
241818
+ //#endregion
241819
+ //#region src/browser/AgentBrowserHostBridgeManager.ts
241820
+ function openInputForSession(input) {
241821
+ return {
241822
+ sessionId: input.hostSessionId,
241823
+ ...input.sessionInput.browserId ? { browserId: input.sessionInput.browserId } : {},
241824
+ ...input.initialUrl ? { initialUrl: input.initialUrl } : {},
241825
+ ...input.sessionInput.name ? { name: input.sessionInput.name } : {},
241826
+ ...input.sessionInput.visibility ? { visibility: input.sessionInput.visibility } : {},
241827
+ ...input.sessionInput.viewport ? { viewport: input.sessionInput.viewport } : {}
241828
+ };
241829
+ }
241830
+ function forgetHostSession(input) {
241831
+ input.state.clearConnectedDisplay({ sessionId: input.hostSessionId });
241832
+ input.state.controlStates.delete(input.hostSessionId);
241833
+ input.options?.forgetRuntimeSession?.({ sessionId: input.hostSessionId });
241834
+ }
241835
+ async function quarantineTimedOutOpenSession(input) {
241836
+ console.warn("[browser] Agent Browser openSession timed out; quarantining session", {
241837
+ sessionId: input.hostSessionId,
241838
+ ...input.sessionInput.browserId ? { browserId: input.sessionInput.browserId } : {}
241839
+ });
241840
+ await withAgentBrowserBridgeTimeout("Agent Browser openSession quarantine close", input.bridge.closeSession({
241841
+ sessionId: input.hostSessionId,
241842
+ ...input.sessionInput.browserId ? { browserId: input.sessionInput.browserId } : {}
241843
+ }), agentBrowserCloseTimeoutMs()).catch(() => void 0);
241844
+ forgetHostSession(input);
241845
+ throw new Error(`${input.error instanceof Error ? input.error.message : String(input.error)} The pending browser session was quarantined and closed.`, { cause: input.error });
241846
+ }
241847
+ async function openHostBridgeSession(input) {
241848
+ const hostSessionId = sessionIdForThread(input.sessionInput.threadId);
241849
+ const initialUrl = input.sessionInput.initialUrl ? normalizeUserVisibleBrowserUrl(input.sessionInput.initialUrl) : void 0;
241850
+ input.options?.prepareRuntimeSession?.({
241851
+ sessionId: hostSessionId,
241852
+ initialUrl: initialUrl ?? "about:blank"
241853
+ });
241854
+ try {
241855
+ const snapshot = await withAgentBrowserBridgeTimeout("Agent Browser openSession", openAgentBrowserSessionReady(input.bridge, openInputForSession({
241856
+ sessionInput: input.sessionInput,
241857
+ hostSessionId,
241858
+ initialUrl
241859
+ })), agentBrowserOpenTimeoutMs());
241860
+ return toForgeSession({
241861
+ snapshot,
241862
+ threadId: input.sessionInput.threadId,
241863
+ environmentId: input.sessionInput.environmentId,
241864
+ userId: input.sessionInput.userId,
241865
+ agentId: input.sessionInput.agentId,
241866
+ ownerRole: input.sessionInput.ownerRole,
241867
+ display: input.state.displayForSnapshot(snapshot),
241868
+ controlState: input.state.controlStateFor(snapshot.sessionId)
241869
+ });
241870
+ } catch (error) {
241871
+ if (!isAgentBrowserBridgeTimeoutError(error)) {
241872
+ input.options?.forgetRuntimeSession?.({ sessionId: hostSessionId });
241873
+ throw error;
241874
+ }
241875
+ return quarantineTimedOutOpenSession({
241876
+ ...input,
241877
+ error,
241878
+ hostSessionId
241879
+ });
241880
+ }
241881
+ }
241882
+ async function verifyTimedOutClose(input) {
241883
+ const remaining = await withAgentBrowserBridgeTimeout("Agent Browser closeSession verification", input.bridge.getSession(input.hostIdentity), 2e3).catch(() => void 0);
241884
+ console.warn(remaining === null ? "[browser] Agent Browser closeSession timed out after session removal" : "[browser] Agent Browser closeSession timed out; forgetting session", {
241885
+ sessionId: input.hostIdentity.sessionId,
241886
+ ...input.sessionInput.browserId ? { browserId: input.sessionInput.browserId } : {}
241887
+ });
241888
+ }
241889
+ async function closeHostBridgeSession(input) {
241890
+ const hostSessionId = sessionIdForThread(input.sessionInput.threadId);
241891
+ await require_Schema$1.runPromise(input.options?.displayPeers?.closeForSession({ sessionId: hostSessionId }) ?? require_Schema$1.void_);
241892
+ const hostIdentity = {
241893
+ sessionId: hostSessionId,
241894
+ ...input.sessionInput.browserId ? { browserId: input.sessionInput.browserId } : {}
241895
+ };
241896
+ try {
241897
+ await withAgentBrowserBridgeTimeout("Agent Browser closeSession", input.bridge.closeSession(hostIdentity), agentBrowserCloseTimeoutMs());
241898
+ } catch (error) {
241899
+ if (!isAgentBrowserBridgeTimeoutError(error)) throw error;
241900
+ await verifyTimedOutClose({
241901
+ ...input,
241902
+ hostIdentity
241903
+ });
241904
+ }
241905
+ forgetHostSession({
241906
+ ...input,
241907
+ hostSessionId
241908
+ });
241909
+ }
241910
+ function createAgentBrowserHostBridgeManager(bridge, options) {
241911
+ const state = createAgentBrowserHostBridgeState(options);
241912
+ return {
241913
+ openSession: (input) => require_Schema$1.tryPromise({
241914
+ try: () => openHostBridgeSession({
241915
+ bridge,
241916
+ options,
241917
+ state,
241918
+ sessionInput: input
241919
+ }),
241920
+ catch: (error) => toBrowserError(error, "Failed to open Agent Browser session.")
241921
+ }),
241922
+ closeSession: (input) => require_Schema$1.tryPromise({
241923
+ try: () => closeHostBridgeSession({
241924
+ bridge,
241925
+ options,
241926
+ state,
241927
+ sessionInput: input
241928
+ }),
241929
+ catch: (error) => toBrowserError(error, "Failed to close Agent Browser session.")
241930
+ }),
241931
+ clearBrowsingData: () => require_Schema$1.succeed({
241932
+ closedSessionCount: 0,
241933
+ deletedSessionCount: 0,
241934
+ deletedCaptureCount: 0
241935
+ }),
241936
+ getSession: (input) => require_Schema$1.tryPromise({
241937
+ try: async () => {
241938
+ const snapshot = await withAgentBrowserBridgeTimeout("Agent Browser getSession", bridge.getSession({
241939
+ sessionId: sessionIdForThread(input.threadId),
241940
+ ...input.browserId ? { browserId: input.browserId } : {}
241941
+ }), agentBrowserFastTimeoutMs());
241942
+ return snapshot ? toForgeSession({
241943
+ snapshot,
241944
+ threadId: input.threadId,
241945
+ environmentId: input.environmentId,
241946
+ userId: input.userId,
241947
+ display: state.displayForSnapshot(snapshot),
241948
+ controlState: state.controlStateFor(snapshot.sessionId)
241949
+ }) : null;
241950
+ },
241951
+ catch: (error) => toBrowserError(error, "Failed to read Agent Browser session.")
241952
+ }),
241953
+ listSessions: (input) => require_Schema$1.tryPromise({
241954
+ try: async () => {
241955
+ return (await withAgentBrowserBridgeTimeout("Agent Browser listSessions", bridge.listSessions(input.browserId ? { browserId: input.browserId } : void 0), agentBrowserFastTimeoutMs())).map((snapshot) => toForgeSession({
241956
+ snapshot,
241957
+ threadId: input.threadId ?? ThreadId.make(snapshot.sessionId),
241958
+ environmentId: input.environmentId,
241959
+ userId: input.userId,
241960
+ display: state.displayForSnapshot(snapshot),
241961
+ controlState: state.controlStateFor(snapshot.sessionId)
241962
+ }));
241963
+ },
241964
+ catch: (error) => toBrowserError(error, "Failed to list Agent Browser sessions.")
241965
+ }),
241966
+ command: (input) => require_Schema$1.tryPromise({
241967
+ try: () => executeOrderedHostCommand({
241968
+ bridge,
241969
+ options,
241970
+ state,
241971
+ commandInput: input
241972
+ }),
241973
+ catch: (error) => toBrowserError(error, "Failed to run Agent Browser command.")
241974
+ }),
241975
+ attachDisplay: (input) => {
241976
+ const displayPeers = options?.displayPeers;
241977
+ if (!displayPeers) return require_Schema$1.fail(remoteWebRtcUnavailableError(input));
241978
+ return require_Schema$1.tryPromise({
241979
+ try: async () => {
241980
+ const iceServers = await resolveDisplayIceServers(options);
241981
+ if (options?.requiresWebRtcRelay === true && !hasTurnRelay(iceServers)) throw remoteWebRtcRelayRequiredError(input);
241982
+ const result = await require_Schema$1.runPromise(displayPeers.attach(input, {
241983
+ display: {
241984
+ transport: "remote-webrtc",
241985
+ status: "connecting",
241986
+ ...input.viewport ? { viewport: input.viewport } : {},
241987
+ devicePixelRatio: 1,
241988
+ ...iceServers.length > 0 ? { iceServers } : {},
241989
+ reconnect: {
241990
+ status: "ready",
241991
+ attempt: 0
241992
+ }
241993
+ },
241994
+ onIceCandidate: state.publishWebRtcIceCandidate
241995
+ }));
241996
+ state.connectedDisplays.set(input.sessionId, {
241997
+ tabId: input.tabId,
241998
+ display: result.display
241999
+ });
242000
+ return result;
242001
+ },
242002
+ catch: (error) => {
242003
+ if (isBrowserError(error)) return error;
242004
+ return toBrowserError(error, "Failed to attach WebRTC browser display.");
242005
+ }
242006
+ });
242007
+ },
242008
+ addDisplayIceCandidate: (input) => {
242009
+ const displayPeers = options?.displayPeers;
242010
+ if (!displayPeers) return require_Schema$1.fail(remoteWebRtcUnavailableError(input));
242011
+ return displayPeers.addIceCandidate(input);
242012
+ },
242013
+ subscribe: (input, listener) => require_Schema$1.promise(async () => {
242014
+ const localSubscription = {
242015
+ input,
242016
+ listener
242017
+ };
242018
+ state.localSubscriptions.add(localSubscription);
242019
+ const threadId = input.threadId ?? ThreadId.make("unknown-thread");
242020
+ const browserId = browserIdFrom(input);
242021
+ const environmentId = input.environmentId ?? DEFAULT_BROWSER_ENVIRONMENT_ID;
242022
+ const userId = input.userId ?? "local-user";
242023
+ const subscribeInput = {
242024
+ ...input.threadId ? { sessionId: sessionIdForThread(input.threadId) } : {},
242025
+ ...input.browserId ? { browserId: input.browserId } : {}
242026
+ };
242027
+ const unsubscribeBridge = await bridge.subscribe(subscribeInput, (event) => {
242028
+ if (event.type === "session.closed") state.clearConnectedDisplay({ sessionId: event.payload.sessionId });
242029
+ if (event.type === "tab.closed") state.clearConnectedDisplay({
242030
+ sessionId: event.sessionId,
242031
+ tabId: event.payload.tabId
242032
+ });
242033
+ const mapped = toStreamEvent({
242034
+ event,
242035
+ threadId,
242036
+ browserId,
242037
+ environmentId,
242038
+ userId,
242039
+ agentId: null,
242040
+ ownerRole: "lead",
242041
+ display: event.type === "session.started" || event.type === "session.updated" ? state.displayForSnapshot(event.payload.snapshot) : DEFAULT_AGENT_BROWSER_DISPLAY,
242042
+ controlState: state.controlStateFor(event.type === "session.closed" ? event.payload.sessionId : event.sessionId)
242043
+ });
242044
+ if (mapped) require_Schema$1.runPromise(listener(mapped));
242045
+ if (event.type === "session.closed") state.controlStates.delete(event.payload.sessionId);
242046
+ });
242047
+ return () => {
242048
+ state.localSubscriptions.delete(localSubscription);
242049
+ unsubscribeBridge();
242050
+ };
242051
+ })
242052
+ };
242053
+ }
242054
+ //#endregion
240807
242055
  //#region ../../node_modules/.bun/debug@4.4.0+2b91fc17bf64bdfd/node_modules/debug/src/common.js
240808
242056
  var require_common$2 = /* @__PURE__ */ require_chunk.__commonJSMin(((exports, module) => {
240809
242057
  /**
@@ -283269,234 +284517,317 @@ async function createBrowserWebRtcDisplayCaptureRuntime(options = {}) {
283269
284517
  }
283270
284518
  }
283271
284519
  //#endregion
283272
- //#region src/sprite/spriteTaskHold.ts
284520
+ //#region src/browser/browserProfilePaths.ts
283273
284521
  /**
283274
- * Keeps a Fly.io Sprite awake for the duration of an agent turn.
283275
- *
283276
- * ── why this exists ─────────────────────────────────────────────────────────
283277
- * A sprite's container cgroup is frozen within ~1 second of the last inbound
283278
- * request. Processes are paused mid-instruction, not killed, and resume when
283279
- * traffic returns. Measured on a throwaway sprite with a 1 Hz ticker
283280
- * (`pocs/sprite-lifecycle-spike/`):
283281
- *
283282
- * detached, nothing connected 2 ticks / 121 s = 1.7%
283283
- * with a task hold 90 ticks / 90 s = 100%
283284
- *
283285
- * So an agent turn that nobody is watching effectively stops. fly.io names our
283286
- * previous approach — relying on a running process after disconnection — as an
283287
- * anti-pattern, and points at the Tasks API instead. See
283288
- * https://docs.sprites.dev/keeping-sprites-running/ and
283289
- * `docs/specs/2026-07-30-sprites-platform-ground-truth.md` §1.
283290
- *
283291
- * Note that Services (the platform supervisor) do NOT solve this: "a Sprite with
283292
- * ten services defined still pauses when it goes idle." Supervision and
283293
- * wakefulness are separate primitives, and this module is only the second one.
283294
- *
283295
- * ── why it lives in the guest ───────────────────────────────────────────────
283296
- * The Tasks API is guest-only. `GET https://api.sprites.dev/v1/sprites/{n}/tasks`
283297
- * answers 404; the socket at /.sprite/api.sock is the only surface. CMMD cannot
283298
- * take this hold on our behalf, which is why the env vars below are exported by
283299
- * CMMD's provider and consumed here.
283300
- *
283301
- * ── the ordering rules ──────────────────────────────────────────────────────
283302
- * 1. The refresher MUST be stopped before the DELETE is issued. A refresher that
283303
- * outlives the delete re-acquires the hold on its next tick; that was
283304
- * previously measured leaking 37 renewals over 18 minutes, pinning a sprite
283305
- * running — and billing — 33 minutes after its work had ended.
283306
- * 2. Stopping the interval only cancels FUTURE ticks. A PUT already on the wire
283307
- * must be JOINED before the DELETE, or it lands afterwards and recreates the
283308
- * hold for another full expiry window. fly.io's reference worker cancels and
283309
- * awaits its heartbeat for the same reason.
283310
- * 3. `release` can arrive while `acquire` is still awaiting its own PUT —
283311
- * `releaseAll` runs from runtime drain and the shutdown finalizer,
283312
- * concurrently with the provider event stream. The hold is registered before
283313
- * that await (so a duplicate turn.started cannot double-acquire), so release
283314
- * can remove the map entry first; `acquire` must then NOT install a
283315
- * refresher. One attached to an unmapped hold is unreachable by release,
283316
- * releaseThread and releaseAll alike, and re-PUTs every 60 s for the life of
283317
- * the process — an unbounded pin.
283318
- *
283319
- * All three are pinned by tests.
284522
+ * Canonical Agent Browser Chromium profile paths.
283320
284523
  *
283321
- * Pure and dependency-free on purpose, for the same reason as
283322
- * `provider/claudeCredentialContainment.ts`: this rule is cost-critical and must
283323
- * stay directly unit-testable without booting the Effect runtime.
283324
- */
283325
- /** Set by CMMD's Sprites provider on every project sprite. Absent elsewhere. */
283326
- const SPRITE_TASK_HOLD_SOCKET_ENV = "FORGE_CMMD_SPRITE_TASK_HOLD_SOCKET";
283327
- const SPRITE_TASK_HOLD_REFRESH_MS = 6e4;
283328
- /**
283329
- * Per-request deadline for the guest-socket calls. Must stay strictly under
283330
- * SPRITE_TASK_HOLD_REFRESH_MS: the manager serializes refreshes, so a PUT that
283331
- * never settles turns every later tick into a no-op and the hold expires
283332
- * mid-turn. Bounding the call means a hung socket settles as a rejection, the
283333
- * next 60 s tick retries, and release is not wedged joining it. From the last
283334
- * good refresh there are still four attempts (+60/+120/+180/+240 s) before the
283335
- * 300 s expiry. 10 s matches the `-m 10` cap CMMD already puts on every curl in
283336
- * its own hold preamble; the guest-side manager was the one path left unbounded.
284524
+ * Both the host bridge adapter (which launches Chromium with a per-session
284525
+ * `userDataDir`) and the orphan reaper (which reconciles live Chromium
284526
+ * processes against registered sessions) must agree byte-for-byte on this
284527
+ * mapping — the reaper matches processes to sessions by profile directory
284528
+ * basename.
283337
284529
  */
283338
- const SPRITE_TASK_HOLD_REQUEST_TIMEOUT_MS = 1e4;
284530
+ const AGENT_BROWSER_PROFILE_ROOT = "/tmp/forge-agent-browser-profiles";
284531
+ /** Sanitized directory basename for a session id. Lossy — never invert it. */
284532
+ function browserProfileSessionKey(sessionId) {
284533
+ return sessionId.replace(/[^a-zA-Z0-9_.-]/g, "_").slice(0, 160) || "default";
284534
+ }
284535
+ function browserProfileDirForSession(sessionId, root = AGENT_BROWSER_PROFILE_ROOT) {
284536
+ return `${root}/${browserProfileSessionKey(sessionId)}`;
284537
+ }
284538
+ //#endregion
284539
+ //#region src/browser/Layers/BrowserOrphanReaper.ts
283339
284540
  /**
283340
- * Resolve the hold configuration from the process environment.
284541
+ * E7 production orphan reaper (spec:
284542
+ * docs/specs/2026-06-10-windows-free-dev-and-browser-lifecycle-hardening-plan.md).
283341
284543
  *
283342
- * Returns null on every runtime that is not a project sprite — desktop-local and
283343
- * browser runtimes have no `/.sprite/api.sock`, so the whole feature no-ops
283344
- * there rather than erroring.
284544
+ * Periodically reconciles live Chromium browser-main processes (distinct
284545
+ * `--user-data-dir` under the Agent Browser profile root) against the set of
284546
+ * sessions the host bridge knows about, kills orphans, enforces a max-session
284547
+ * TTL through graceful closes, and removes stale profile directories.
284548
+ *
284549
+ * Guard-rails:
284550
+ * - never kills a registered live session;
284551
+ * - the first sweep *attempt* after boot is observe-only (cold-start grace)
284552
+ * -- this is consumed unconditionally by that one attempt, so a transient
284553
+ * read failure on it can never grant a second free pass (which would
284554
+ * otherwise let a restart-orphaned Chromium outlive two sweep intervals
284555
+ * instead of one);
284556
+ * - unregistered Chromium younger than `minOrphanAgeMs` is spared so
284557
+ * in-flight opens are never raced;
284558
+ * - if the session registry cannot be read, the sweep kills nothing --
284559
+ * distinct from a registry that reads back empty (e.g. a freshly booted
284560
+ * server with zero sessions), which is healthy and reapable;
284561
+ * - `protectedSessionKeys` (the shared "default" backend profile) are never
284562
+ * touched.
283345
284563
  */
283346
- function resolveSpriteTaskHoldConfig(env = process.env) {
283347
- const socketPath = env[SPRITE_TASK_HOLD_SOCKET_ENV]?.trim();
283348
- if (socketPath === void 0 || socketPath.length === 0) return null;
284564
+ const execFileAsync$2 = (0, node_util.promisify)(node_child_process.execFile);
284565
+ var BrowserReaperStepError = class extends require_Schema$1.TaggedError("BrowserReaperStepError") {};
284566
+ const DEFAULT_REAPER_INTERVAL_MS = 6e4;
284567
+ const DEFAULT_MAX_SESSION_TTL_MS = 14400 * 1e3;
284568
+ const DEFAULT_MIN_ORPHAN_AGE_MS = 120 * 1e3;
284569
+ const KILL_ESCALATION_TIMEOUT_MS = 5e3;
284570
+ const KILL_POLL_INTERVAL_MS = 100;
284571
+ function positiveIntFromEnv(value) {
284572
+ if (!value) return;
284573
+ const parsed = Number.parseInt(value, 10);
284574
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : void 0;
284575
+ }
284576
+ function browserOrphanReaperConfigFromEnv(env, profileRoot = "/tmp/forge-agent-browser-profiles") {
284577
+ const enabledRaw = env.FORGE_BROWSER_REAPER_ENABLED?.trim().toLowerCase();
283349
284578
  return {
283350
- socketPath,
283351
- namePrefix: env["FORGE_CMMD_SPRITE_TASK_HOLD_PREFIX"]?.trim() ?? ""
284579
+ profileRoot,
284580
+ intervalMs: positiveIntFromEnv(env.FORGE_BROWSER_REAPER_INTERVAL_MS) || DEFAULT_REAPER_INTERVAL_MS,
284581
+ maxSessionTtlMs: positiveIntFromEnv(env.FORGE_BROWSER_SESSION_MAX_TTL_MS) ?? DEFAULT_MAX_SESSION_TTL_MS,
284582
+ minOrphanAgeMs: positiveIntFromEnv(env.FORGE_BROWSER_REAPER_MIN_ORPHAN_AGE_MS) ?? DEFAULT_MIN_ORPHAN_AGE_MS,
284583
+ enabled: enabledRaw !== "0" && enabledRaw !== "false",
284584
+ protectedSessionKeys: new Set(["default"])
283352
284585
  };
283353
284586
  }
284587
+ /** [[dd-]hh:]mm:ss as printed by `ps -o etime=`. Returns null on malformed input. */
284588
+ function parseEtimeToMs(etime) {
284589
+ const match = /^(?:(\d+)-)?(?:(\d+):)?(\d+):(\d+)$/.exec(etime.trim());
284590
+ if (!match) return null;
284591
+ const [, days, hours, minutes, seconds] = match;
284592
+ return (Number(days ?? 0) * 24 * 60 * 60 + Number(hours ?? 0) * 60 * 60 + Number(minutes) * 60 + Number(seconds)) * 1e3;
284593
+ }
283354
284594
  /**
283355
- * Hold names must be unique per turn: several git worktrees inside one sprite
283356
- * can run turns concurrently, and each has to be released independently.
284595
+ * A Chromium browser-main owns a profile under our root and has no `--type=`
284596
+ * flag (renderer/gpu/utility children all carry one). Killing the main tears
284597
+ * down the whole Chromium process tree.
283357
284598
  */
283358
- function spriteTaskHoldName(namePrefix, turnId) {
283359
- return `${namePrefix}${turnId}`;
284599
+ function isChromiumMainCommand(command, profileRoot) {
284600
+ return command.includes(`--user-data-dir=${profileRoot}/`) && !/\s--type=/.test(` ${command}`);
283360
284601
  }
283361
- const DEFAULT_TIMER = {
283362
- setInterval: (handler, ms) => setInterval(handler, ms),
283363
- clearInterval: (handle) => {
283364
- clearInterval(handle);
284602
+ function extractUserDataDir(command) {
284603
+ const match = /--user-data-dir=(\S+)/.exec(command);
284604
+ return match ? match[1] ?? null : null;
284605
+ }
284606
+ function profileKeyForDir(userDataDir, profileRoot) {
284607
+ const resolved = node_path.default.resolve(userDataDir);
284608
+ const resolvedRoot = node_path.default.resolve(profileRoot);
284609
+ if (resolved === resolvedRoot || !resolved.startsWith(resolvedRoot + node_path.default.sep)) return null;
284610
+ const relative = resolved.slice(resolvedRoot.length + 1);
284611
+ return relative.includes(node_path.default.sep) ? null : relative;
284612
+ }
284613
+ function makeSweepState(deps, config, observeOnly) {
284614
+ return {
284615
+ deps,
284616
+ config,
284617
+ observeOnly,
284618
+ errors: [],
284619
+ wouldReapOrphans: [],
284620
+ wouldCloseTtlSessions: [],
284621
+ wouldRemoveStaleProfiles: [],
284622
+ killedOrphans: [],
284623
+ closedTtlSessions: [],
284624
+ removedStaleProfiles: []
284625
+ };
284626
+ }
284627
+ function sweepReport(state, registryUnavailable) {
284628
+ return {
284629
+ observeOnly: state.observeOnly,
284630
+ registryUnavailable,
284631
+ wouldReapOrphans: state.wouldReapOrphans,
284632
+ wouldCloseTtlSessions: state.wouldCloseTtlSessions,
284633
+ wouldRemoveStaleProfiles: state.wouldRemoveStaleProfiles,
284634
+ killedOrphans: state.killedOrphans,
284635
+ closedTtlSessions: state.closedTtlSessions,
284636
+ removedStaleProfiles: state.removedStaleProfiles,
284637
+ errors: state.errors
284638
+ };
284639
+ }
284640
+ /** Run a dep call; on failure record the error into the state and yield null. */
284641
+ function attemptOrNull(state, label, run) {
284642
+ return require_Schema$1.tryPromise({
284643
+ try: run,
284644
+ catch: (error) => new BrowserReaperStepError({
284645
+ message: `${label}: ${error instanceof Error ? error.message : String(error)}`,
284646
+ cause: error
284647
+ })
284648
+ }).pipe(require_Schema$1.map((value) => value), require_Schema$1.catch_((error) => require_Schema$1.sync(() => (state.errors.push(error.message), null))));
284649
+ }
284650
+ /** Like attemptOrNull, but collapse the outcome to a success flag. */
284651
+ function attemptFlag(state, label, run) {
284652
+ return attemptOrNull(state, label, async () => {
284653
+ await run();
284654
+ return true;
284655
+ }).pipe(require_Schema$1.map((value) => value === true));
284656
+ }
284657
+ function mapRegisteredByKey(registered) {
284658
+ const registeredByKey = /* @__PURE__ */ new Map();
284659
+ for (const sessionId of registered) registeredByKey.set(browserProfileSessionKey(sessionId), sessionId);
284660
+ return registeredByKey;
284661
+ }
284662
+ const enforceSessionTtl = (state, sessionId, main) => require_Schema$1.gen(function* () {
284663
+ if (state.config.maxSessionTtlMs <= 0 || main.ageMs <= state.config.maxSessionTtlMs) return;
284664
+ if (state.observeOnly) {
284665
+ state.wouldCloseTtlSessions.push(sessionId);
284666
+ return;
283365
284667
  }
283366
- };
283367
- /**
283368
- * Tracks one hold per active turn.
283369
- *
283370
- * Every method is failure-tolerant by design. Acquiring a hold is an
283371
- * optimisation, not a precondition: if it fails the turn still runs, just at the
283372
- * degraded speed it has today. Failing the turn instead would convert a billing
283373
- * inefficiency into an outage.
283374
- */
283375
- var SpriteTaskHoldManager = class {
283376
- #config;
283377
- #transport;
283378
- #timer;
283379
- #onError;
283380
- #holds = /* @__PURE__ */ new Map();
283381
- constructor(options) {
283382
- this.#config = options.config;
283383
- this.#transport = options.transport;
283384
- this.#timer = options.timer ?? DEFAULT_TIMER;
283385
- this.#onError = options.onError ?? (() => {});
284668
+ if (yield* attemptFlag(state, `closeSession(${sessionId})`, () => state.deps.closeSession(sessionId))) {
284669
+ state.closedTtlSessions.push(sessionId);
284670
+ yield* increment(browserSessionsReapedTotal, { reason: "ttl" });
283386
284671
  }
283387
- /** Turn ids that currently hold the sprite awake. Test/diagnostic surface. */
283388
- activeTurnIds() {
283389
- return [...this.#holds.keys()];
284672
+ });
284673
+ const reapOrphanMain = (state, key, main) => require_Schema$1.gen(function* () {
284674
+ if (main.ageMs < state.config.minOrphanAgeMs) return;
284675
+ if (state.observeOnly) {
284676
+ state.wouldReapOrphans.push(key);
284677
+ return;
283390
284678
  }
283391
- async acquire(turnId, threadId = null) {
283392
- if (this.#holds.has(turnId)) return;
283393
- const name = spriteTaskHoldName(this.#config.namePrefix, turnId);
283394
- const hold = {
283395
- name,
283396
- threadId,
283397
- refresher: null,
283398
- inFlight: /* @__PURE__ */ new Set(),
283399
- released: false
283400
- };
283401
- this.#holds.set(turnId, hold);
283402
- const put = this.#transport.put(name, 300);
283403
- const tracked = put.then(() => {}, () => {});
283404
- hold.inFlight.add(tracked);
283405
- try {
283406
- await put;
283407
- } catch (cause) {
283408
- hold.inFlight.delete(tracked);
283409
- this.#onError(`failed to acquire sprite task hold for turn ${turnId}`, cause);
283410
- if (hold.released || this.#holds.get(turnId) !== hold) return;
284679
+ if (!(yield* attemptFlag(state, `killProcess(${main.pid})`, () => state.deps.killProcess(main.pid)))) return;
284680
+ state.killedOrphans.push(key);
284681
+ yield* increment(browserSessionsReapedTotal, { reason: "orphan" });
284682
+ yield* attemptFlag(state, `removeProfileDir(${main.userDataDir})`, () => state.deps.removeProfileDir(main.userDataDir));
284683
+ });
284684
+ const sweepChromiumMains = (state, registeredByKey, mains) => require_Schema$1.gen(function* () {
284685
+ const liveKeys = /* @__PURE__ */ new Set();
284686
+ for (const main of mains) {
284687
+ const key = profileKeyForDir(main.userDataDir, state.config.profileRoot);
284688
+ if (key === null) continue;
284689
+ liveKeys.add(key);
284690
+ if (state.config.protectedSessionKeys.has(key)) continue;
284691
+ const registeredSessionId = registeredByKey.get(key);
284692
+ if (registeredSessionId !== void 0) yield* enforceSessionTtl(state, registeredSessionId, main);
284693
+ else yield* reapOrphanMain(state, key, main);
284694
+ }
284695
+ return liveKeys;
284696
+ });
284697
+ function isRetainedProfileDir(state, registeredByKey, liveKeys, entry) {
284698
+ const key = profileKeyForDir(entry.path, state.config.profileRoot);
284699
+ return key === null || state.config.protectedSessionKeys.has(key) || registeredByKey.has(key) || liveKeys.has(key) || entry.ageMs < state.config.minOrphanAgeMs;
284700
+ }
284701
+ const sweepStaleProfileDirs = (state, registeredByKey, liveKeys) => require_Schema$1.gen(function* () {
284702
+ const profileDirs = yield* attemptOrNull(state, "listProfileDirs", state.deps.listProfileDirs);
284703
+ if (profileDirs === null) return;
284704
+ for (const entry of profileDirs) {
284705
+ if (isRetainedProfileDir(state, registeredByKey, liveKeys, entry)) continue;
284706
+ if (state.observeOnly) {
284707
+ state.wouldRemoveStaleProfiles.push(entry.path);
284708
+ continue;
284709
+ }
284710
+ if (yield* attemptFlag(state, `removeProfileDir(${entry.path})`, () => state.deps.removeProfileDir(entry.path))) {
284711
+ state.removedStaleProfiles.push(entry.path);
284712
+ yield* increment(browserSessionsReapedTotal, { reason: "stale_profile" });
283411
284713
  }
283412
- hold.inFlight.delete(tracked);
283413
- if (hold.released || this.#holds.get(turnId) !== hold) return;
283414
- hold.refresher = this.#timer.setInterval(() => {
283415
- if (hold.inFlight.size > 0) return;
283416
- const refresh = this.#transport.put(name, 300).catch((cause) => this.#onError(`failed to refresh sprite task hold for turn ${turnId}`, cause));
283417
- hold.inFlight.add(refresh);
283418
- refresh.finally(() => hold.inFlight.delete(refresh));
283419
- }, SPRITE_TASK_HOLD_REFRESH_MS);
283420
284714
  }
283421
- async release(turnId) {
283422
- const hold = this.#holds.get(turnId);
283423
- if (!hold) return;
283424
- hold.released = true;
283425
- if (hold.refresher !== null) {
283426
- this.#timer.clearInterval(hold.refresher);
283427
- hold.refresher = null;
284715
+ });
284716
+ function sweepHasActivity(report) {
284717
+ 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;
284718
+ }
284719
+ const logSweepReportIfActive = (report) => sweepHasActivity(report) ? require_Schema$1.logInfo("browser orphan reaper sweep", {
284720
+ observeOnly: report.observeOnly,
284721
+ killedOrphans: report.killedOrphans,
284722
+ closedTtlSessions: report.closedTtlSessions,
284723
+ removedStaleProfiles: report.removedStaleProfiles,
284724
+ wouldReapOrphans: report.wouldReapOrphans,
284725
+ wouldCloseTtlSessions: report.wouldCloseTtlSessions,
284726
+ wouldRemoveStaleProfiles: report.wouldRemoveStaleProfiles,
284727
+ errors: report.errors
284728
+ }) : require_Schema$1.void_;
284729
+ function createBrowserOrphanReaper(deps, config) {
284730
+ let coldStartGraceConsumed = false;
284731
+ return { sweep: require_Schema$1.gen(function* () {
284732
+ const observeOnly = !coldStartGraceConsumed;
284733
+ coldStartGraceConsumed = true;
284734
+ const state = makeSweepState(deps, config, observeOnly);
284735
+ const registered = yield* attemptOrNull(state, "listRegisteredSessionIds", deps.listRegisteredSessionIds);
284736
+ if (registered === null) {
284737
+ yield* require_Schema$1.logWarning("browser orphan reaper: session registry unavailable; skipping", { errors: state.errors });
284738
+ return sweepReport(state, true);
283428
284739
  }
283429
- this.#holds.delete(turnId);
283430
- if (hold.inFlight.size > 0) await Promise.all(hold.inFlight);
284740
+ const mains = yield* attemptOrNull(state, "listChromiumMains", deps.listChromiumMains);
284741
+ if (mains === null) return sweepReport(state, false);
284742
+ const registeredByKey = mapRegisteredByKey(registered);
284743
+ yield* sweepStaleProfileDirs(state, registeredByKey, yield* sweepChromiumMains(state, registeredByKey, mains));
284744
+ const report = sweepReport(state, false);
284745
+ yield* logSweepReportIfActive(report);
284746
+ return report;
284747
+ }) };
284748
+ }
284749
+ function sleep(ms) {
284750
+ return new Promise((resolve) => {
284751
+ setTimeout(resolve, ms).unref?.();
284752
+ });
284753
+ }
284754
+ function isEsrch(error) {
284755
+ return typeof error === "object" && error !== null && "code" in error && error.code === "ESRCH";
284756
+ }
284757
+ /** SIGTERM, poll for exit, escalate to SIGKILL. Chromium tears down its own children. */
284758
+ async function killProcessWithEscalation(pid) {
284759
+ try {
284760
+ process.kill(pid, "SIGTERM");
284761
+ } catch (error) {
284762
+ if (isEsrch(error)) return;
284763
+ throw error;
284764
+ }
284765
+ const deadline = Date.now() + KILL_ESCALATION_TIMEOUT_MS;
284766
+ while (Date.now() < deadline) {
284767
+ await sleep(KILL_POLL_INTERVAL_MS);
283431
284768
  try {
283432
- await this.#transport.delete(hold.name);
283433
- } catch (cause) {
283434
- this.#onError(`failed to release sprite task hold for turn ${turnId}`, cause);
284769
+ process.kill(pid, 0);
284770
+ } catch {
284771
+ return;
283435
284772
  }
283436
284773
  }
283437
- /**
283438
- * Release every hold belonging to one thread.
283439
- *
283440
- * `session.exited` names only the thread whose provider process died, and a
283441
- * dead session's turn will never emit `turn.completed`. Releasing by thread —
283442
- * rather than releasing everything — is what keeps a crash in one worktree
283443
- * from silently dropping the holds of the other turns running beside it.
283444
- */
283445
- async releaseThread(threadId) {
283446
- const turnIds = [...this.#holds.entries()].filter(([, hold]) => hold.threadId === threadId).map(([turnId]) => turnId);
283447
- await Promise.all(turnIds.map((turnId) => this.release(turnId)));
283448
- }
283449
- /**
283450
- * Release everything. Runtime drain and server shutdown must call this, or a
283451
- * drained sprite keeps billing until each hold expires.
283452
- */
283453
- async releaseAll() {
283454
- await Promise.all([...this.#holds.keys()].map((turnId) => this.release(turnId)));
284774
+ try {
284775
+ process.kill(pid, "SIGKILL");
284776
+ } catch (error) {
284777
+ if (!isEsrch(error)) throw error;
283455
284778
  }
283456
- };
283457
- /**
283458
- * HTTP-over-unix-socket transport for the guest API.
283459
- *
283460
- * Bun's fetch takes a `unix` option; the URL host is ignored by the socket but
283461
- * still has to parse, which is why it is the literal `http://sprite` the
283462
- * platform docs use.
283463
- */
283464
- function makeSpriteTaskHoldTransport(config, fetchImpl = fetch) {
283465
- const call = async (path, init) => {
283466
- const controller = new AbortController();
283467
- const deadline = setTimeout(() => controller.abort(), SPRITE_TASK_HOLD_REQUEST_TIMEOUT_MS);
283468
- try {
283469
- const response = await fetchImpl(`http://sprite/v1/tasks${path}`, {
283470
- ...init,
283471
- signal: controller.signal,
283472
- unix: config.socketPath
284779
+ }
284780
+ function createSystemBrowserOrphanReaperDeps(input) {
284781
+ const { profileRoot } = input;
284782
+ return {
284783
+ listChromiumMains: async () => {
284784
+ const { stdout } = await execFileAsync$2("ps", ["-axo", "pid=,etime=,args="], { maxBuffer: 16 * 1024 * 1024 });
284785
+ const mains = [];
284786
+ for (const line of stdout.split("\n")) {
284787
+ const match = /^\s*(\d+)\s+(\S+)\s+(.+)$/.exec(line);
284788
+ if (!match) continue;
284789
+ const [, pidRaw, etime, command] = match;
284790
+ if (!isChromiumMainCommand(command, profileRoot)) continue;
284791
+ const userDataDir = extractUserDataDir(command);
284792
+ const ageMs = parseEtimeToMs(etime);
284793
+ if (userDataDir === null || ageMs === null) continue;
284794
+ mains.push({
284795
+ pid: Number(pidRaw),
284796
+ userDataDir,
284797
+ ageMs
284798
+ });
284799
+ }
284800
+ return mains;
284801
+ },
284802
+ listRegisteredSessionIds: async () => (await input.listSessions()).map((session) => session.sessionId),
284803
+ closeSession: input.closeSession,
284804
+ killProcess: killProcessWithEscalation,
284805
+ listProfileDirs: async () => {
284806
+ const entries = await node_fs_promises.default.readdir(profileRoot, { withFileTypes: true }).catch(() => []);
284807
+ const now = Date.now();
284808
+ const dirs = [];
284809
+ for (const entry of entries) {
284810
+ if (!entry.isDirectory()) continue;
284811
+ const dirPath = node_path.default.join(profileRoot, entry.name);
284812
+ const stat = await node_fs_promises.default.stat(dirPath).catch(() => null);
284813
+ if (stat === null) continue;
284814
+ dirs.push({
284815
+ path: dirPath,
284816
+ ageMs: Math.max(0, now - stat.mtimeMs)
284817
+ });
284818
+ }
284819
+ return dirs;
284820
+ },
284821
+ removeProfileDir: async (dir) => {
284822
+ const resolved = node_path.default.resolve(dir);
284823
+ const resolvedRoot = node_path.default.resolve(profileRoot);
284824
+ if (resolved === resolvedRoot || !resolved.startsWith(resolvedRoot + node_path.default.sep)) throw new Error(`refusing to remove path outside the profile root: ${dir}`);
284825
+ await node_fs_promises.default.rm(resolved, {
284826
+ recursive: true,
284827
+ force: true
283473
284828
  });
283474
- if (!response.ok) throw new Error(`sprite task API responded ${response.status}`);
283475
- } finally {
283476
- clearTimeout(deadline);
283477
284829
  }
283478
284830
  };
283479
- return {
283480
- put: (name, expireSeconds) => call(`/${encodeURIComponent(name)}`, {
283481
- method: "PUT",
283482
- headers: { "content-type": "application/json" },
283483
- body: JSON.stringify({ expire: `${expireSeconds}s` })
283484
- }),
283485
- delete: (name) => call(`/${encodeURIComponent(name)}`, { method: "DELETE" })
283486
- };
283487
- }
283488
- /**
283489
- * Build a manager for this process, or null when the runtime is not a sprite.
283490
- * Callers treat null as "this feature does not apply here".
283491
- */
283492
- function makeSpriteTaskHoldManager(env = process.env, onError) {
283493
- const config = resolveSpriteTaskHoldConfig(env);
283494
- if (config === null) return null;
283495
- return new SpriteTaskHoldManager({
283496
- config,
283497
- transport: makeSpriteTaskHoldTransport(config),
283498
- ...onError ? { onError } : {}
283499
- });
283500
284831
  }
283501
284832
  //#endregion
283502
284833
  //#region src/browser/Layers/displaySessionTaskHold.ts
@@ -283928,364 +285259,6 @@ function createWebRtcDisplaySessionManager(input = {}) {
283928
285259
  })
283929
285260
  };
283930
285261
  }
283931
- //#endregion
283932
- //#region src/browser/browserProfilePaths.ts
283933
- /**
283934
- * Canonical Agent Browser Chromium profile paths.
283935
- *
283936
- * Both the host bridge adapter (which launches Chromium with a per-session
283937
- * `userDataDir`) and the orphan reaper (which reconciles live Chromium
283938
- * processes against registered sessions) must agree byte-for-byte on this
283939
- * mapping — the reaper matches processes to sessions by profile directory
283940
- * basename.
283941
- */
283942
- const AGENT_BROWSER_PROFILE_ROOT = "/tmp/forge-agent-browser-profiles";
283943
- /** Sanitized directory basename for a session id. Lossy — never invert it. */
283944
- function browserProfileSessionKey(sessionId) {
283945
- return sessionId.replace(/[^a-zA-Z0-9_.-]/g, "_").slice(0, 160) || "default";
283946
- }
283947
- function browserProfileDirForSession(sessionId, root = AGENT_BROWSER_PROFILE_ROOT) {
283948
- return `${root}/${browserProfileSessionKey(sessionId)}`;
283949
- }
283950
- //#endregion
283951
- //#region src/browser/Layers/BrowserOrphanReaper.ts
283952
- /**
283953
- * E7 production orphan reaper (spec:
283954
- * docs/specs/2026-06-10-windows-free-dev-and-browser-lifecycle-hardening-plan.md).
283955
- *
283956
- * Periodically reconciles live Chromium browser-main processes (distinct
283957
- * `--user-data-dir` under the Agent Browser profile root) against the set of
283958
- * sessions the host bridge knows about, kills orphans, enforces a max-session
283959
- * TTL through graceful closes, and removes stale profile directories.
283960
- *
283961
- * Guard-rails:
283962
- * - never kills a registered live session;
283963
- * - the first sweep *attempt* after boot is observe-only (cold-start grace)
283964
- * -- this is consumed unconditionally by that one attempt, so a transient
283965
- * read failure on it can never grant a second free pass (which would
283966
- * otherwise let a restart-orphaned Chromium outlive two sweep intervals
283967
- * instead of one);
283968
- * - unregistered Chromium younger than `minOrphanAgeMs` is spared so
283969
- * in-flight opens are never raced;
283970
- * - if the session registry cannot be read, the sweep kills nothing --
283971
- * distinct from a registry that reads back empty (e.g. a freshly booted
283972
- * server with zero sessions), which is healthy and reapable;
283973
- * - `protectedSessionKeys` (the shared "default" backend profile) are never
283974
- * touched.
283975
- */
283976
- const execFileAsync$2 = (0, node_util.promisify)(node_child_process.execFile);
283977
- var BrowserReaperStepError = class extends require_Schema$1.TaggedError("BrowserReaperStepError") {};
283978
- const DEFAULT_REAPER_INTERVAL_MS = 6e4;
283979
- const DEFAULT_MAX_SESSION_TTL_MS = 14400 * 1e3;
283980
- const DEFAULT_MIN_ORPHAN_AGE_MS = 120 * 1e3;
283981
- const KILL_ESCALATION_TIMEOUT_MS = 5e3;
283982
- const KILL_POLL_INTERVAL_MS = 100;
283983
- function positiveIntFromEnv(value) {
283984
- if (!value) return;
283985
- const parsed = Number.parseInt(value, 10);
283986
- return Number.isFinite(parsed) && parsed >= 0 ? parsed : void 0;
283987
- }
283988
- function browserOrphanReaperConfigFromEnv(env, profileRoot = "/tmp/forge-agent-browser-profiles") {
283989
- const enabledRaw = env.FORGE_BROWSER_REAPER_ENABLED?.trim().toLowerCase();
283990
- return {
283991
- profileRoot,
283992
- intervalMs: positiveIntFromEnv(env.FORGE_BROWSER_REAPER_INTERVAL_MS) || DEFAULT_REAPER_INTERVAL_MS,
283993
- maxSessionTtlMs: positiveIntFromEnv(env.FORGE_BROWSER_SESSION_MAX_TTL_MS) ?? DEFAULT_MAX_SESSION_TTL_MS,
283994
- minOrphanAgeMs: positiveIntFromEnv(env.FORGE_BROWSER_REAPER_MIN_ORPHAN_AGE_MS) ?? DEFAULT_MIN_ORPHAN_AGE_MS,
283995
- enabled: enabledRaw !== "0" && enabledRaw !== "false",
283996
- protectedSessionKeys: new Set(["default"])
283997
- };
283998
- }
283999
- /** [[dd-]hh:]mm:ss as printed by `ps -o etime=`. Returns null on malformed input. */
284000
- function parseEtimeToMs(etime) {
284001
- const match = /^(?:(\d+)-)?(?:(\d+):)?(\d+):(\d+)$/.exec(etime.trim());
284002
- if (!match) return null;
284003
- const [, days, hours, minutes, seconds] = match;
284004
- return (Number(days ?? 0) * 24 * 60 * 60 + Number(hours ?? 0) * 60 * 60 + Number(minutes) * 60 + Number(seconds)) * 1e3;
284005
- }
284006
- /**
284007
- * A Chromium browser-main owns a profile under our root and has no `--type=`
284008
- * flag (renderer/gpu/utility children all carry one). Killing the main tears
284009
- * down the whole Chromium process tree.
284010
- */
284011
- function isChromiumMainCommand(command, profileRoot) {
284012
- return command.includes(`--user-data-dir=${profileRoot}/`) && !/\s--type=/.test(` ${command}`);
284013
- }
284014
- function extractUserDataDir(command) {
284015
- const match = /--user-data-dir=(\S+)/.exec(command);
284016
- return match ? match[1] ?? null : null;
284017
- }
284018
- function profileKeyForDir(userDataDir, profileRoot) {
284019
- const resolved = node_path.default.resolve(userDataDir);
284020
- const resolvedRoot = node_path.default.resolve(profileRoot);
284021
- if (resolved === resolvedRoot || !resolved.startsWith(resolvedRoot + node_path.default.sep)) return null;
284022
- const relative = resolved.slice(resolvedRoot.length + 1);
284023
- return relative.includes(node_path.default.sep) ? null : relative;
284024
- }
284025
- function makeSweepState(deps, config, observeOnly) {
284026
- return {
284027
- deps,
284028
- config,
284029
- observeOnly,
284030
- errors: [],
284031
- wouldReapOrphans: [],
284032
- wouldCloseTtlSessions: [],
284033
- wouldRemoveStaleProfiles: [],
284034
- killedOrphans: [],
284035
- closedTtlSessions: [],
284036
- removedStaleProfiles: []
284037
- };
284038
- }
284039
- function sweepReport(state, registryUnavailable) {
284040
- return {
284041
- observeOnly: state.observeOnly,
284042
- registryUnavailable,
284043
- wouldReapOrphans: state.wouldReapOrphans,
284044
- wouldCloseTtlSessions: state.wouldCloseTtlSessions,
284045
- wouldRemoveStaleProfiles: state.wouldRemoveStaleProfiles,
284046
- killedOrphans: state.killedOrphans,
284047
- closedTtlSessions: state.closedTtlSessions,
284048
- removedStaleProfiles: state.removedStaleProfiles,
284049
- errors: state.errors
284050
- };
284051
- }
284052
- /** Run a dep call; on failure record the error into the state and yield null. */
284053
- function attemptOrNull(state, label, run) {
284054
- return require_Schema$1.tryPromise({
284055
- try: run,
284056
- catch: (error) => new BrowserReaperStepError({
284057
- message: `${label}: ${error instanceof Error ? error.message : String(error)}`,
284058
- cause: error
284059
- })
284060
- }).pipe(require_Schema$1.map((value) => value), require_Schema$1.catch_((error) => require_Schema$1.sync(() => (state.errors.push(error.message), null))));
284061
- }
284062
- /** Like attemptOrNull, but collapse the outcome to a success flag. */
284063
- function attemptFlag(state, label, run) {
284064
- return attemptOrNull(state, label, async () => {
284065
- await run();
284066
- return true;
284067
- }).pipe(require_Schema$1.map((value) => value === true));
284068
- }
284069
- function mapRegisteredByKey(registered) {
284070
- const registeredByKey = /* @__PURE__ */ new Map();
284071
- for (const sessionId of registered) registeredByKey.set(browserProfileSessionKey(sessionId), sessionId);
284072
- return registeredByKey;
284073
- }
284074
- const enforceSessionTtl = (state, sessionId, main) => require_Schema$1.gen(function* () {
284075
- if (state.config.maxSessionTtlMs <= 0 || main.ageMs <= state.config.maxSessionTtlMs) return;
284076
- if (state.observeOnly) {
284077
- state.wouldCloseTtlSessions.push(sessionId);
284078
- return;
284079
- }
284080
- if (yield* attemptFlag(state, `closeSession(${sessionId})`, () => state.deps.closeSession(sessionId))) {
284081
- state.closedTtlSessions.push(sessionId);
284082
- yield* increment(browserSessionsReapedTotal, { reason: "ttl" });
284083
- }
284084
- });
284085
- const reapOrphanMain = (state, key, main) => require_Schema$1.gen(function* () {
284086
- if (main.ageMs < state.config.minOrphanAgeMs) return;
284087
- if (state.observeOnly) {
284088
- state.wouldReapOrphans.push(key);
284089
- return;
284090
- }
284091
- if (!(yield* attemptFlag(state, `killProcess(${main.pid})`, () => state.deps.killProcess(main.pid)))) return;
284092
- state.killedOrphans.push(key);
284093
- yield* increment(browserSessionsReapedTotal, { reason: "orphan" });
284094
- yield* attemptFlag(state, `removeProfileDir(${main.userDataDir})`, () => state.deps.removeProfileDir(main.userDataDir));
284095
- });
284096
- const sweepChromiumMains = (state, registeredByKey, mains) => require_Schema$1.gen(function* () {
284097
- const liveKeys = /* @__PURE__ */ new Set();
284098
- for (const main of mains) {
284099
- const key = profileKeyForDir(main.userDataDir, state.config.profileRoot);
284100
- if (key === null) continue;
284101
- liveKeys.add(key);
284102
- if (state.config.protectedSessionKeys.has(key)) continue;
284103
- const registeredSessionId = registeredByKey.get(key);
284104
- if (registeredSessionId !== void 0) yield* enforceSessionTtl(state, registeredSessionId, main);
284105
- else yield* reapOrphanMain(state, key, main);
284106
- }
284107
- return liveKeys;
284108
- });
284109
- function isRetainedProfileDir(state, registeredByKey, liveKeys, entry) {
284110
- const key = profileKeyForDir(entry.path, state.config.profileRoot);
284111
- return key === null || state.config.protectedSessionKeys.has(key) || registeredByKey.has(key) || liveKeys.has(key) || entry.ageMs < state.config.minOrphanAgeMs;
284112
- }
284113
- const sweepStaleProfileDirs = (state, registeredByKey, liveKeys) => require_Schema$1.gen(function* () {
284114
- const profileDirs = yield* attemptOrNull(state, "listProfileDirs", state.deps.listProfileDirs);
284115
- if (profileDirs === null) return;
284116
- for (const entry of profileDirs) {
284117
- if (isRetainedProfileDir(state, registeredByKey, liveKeys, entry)) continue;
284118
- if (state.observeOnly) {
284119
- state.wouldRemoveStaleProfiles.push(entry.path);
284120
- continue;
284121
- }
284122
- if (yield* attemptFlag(state, `removeProfileDir(${entry.path})`, () => state.deps.removeProfileDir(entry.path))) {
284123
- state.removedStaleProfiles.push(entry.path);
284124
- yield* increment(browserSessionsReapedTotal, { reason: "stale_profile" });
284125
- }
284126
- }
284127
- });
284128
- function sweepHasActivity(report) {
284129
- 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;
284130
- }
284131
- const logSweepReportIfActive = (report) => sweepHasActivity(report) ? require_Schema$1.logInfo("browser orphan reaper sweep", {
284132
- observeOnly: report.observeOnly,
284133
- killedOrphans: report.killedOrphans,
284134
- closedTtlSessions: report.closedTtlSessions,
284135
- removedStaleProfiles: report.removedStaleProfiles,
284136
- wouldReapOrphans: report.wouldReapOrphans,
284137
- wouldCloseTtlSessions: report.wouldCloseTtlSessions,
284138
- wouldRemoveStaleProfiles: report.wouldRemoveStaleProfiles,
284139
- errors: report.errors
284140
- }) : require_Schema$1.void_;
284141
- function createBrowserOrphanReaper(deps, config) {
284142
- let coldStartGraceConsumed = false;
284143
- return { sweep: require_Schema$1.gen(function* () {
284144
- const observeOnly = !coldStartGraceConsumed;
284145
- coldStartGraceConsumed = true;
284146
- const state = makeSweepState(deps, config, observeOnly);
284147
- const registered = yield* attemptOrNull(state, "listRegisteredSessionIds", deps.listRegisteredSessionIds);
284148
- if (registered === null) {
284149
- yield* require_Schema$1.logWarning("browser orphan reaper: session registry unavailable; skipping", { errors: state.errors });
284150
- return sweepReport(state, true);
284151
- }
284152
- const mains = yield* attemptOrNull(state, "listChromiumMains", deps.listChromiumMains);
284153
- if (mains === null) return sweepReport(state, false);
284154
- const registeredByKey = mapRegisteredByKey(registered);
284155
- yield* sweepStaleProfileDirs(state, registeredByKey, yield* sweepChromiumMains(state, registeredByKey, mains));
284156
- const report = sweepReport(state, false);
284157
- yield* logSweepReportIfActive(report);
284158
- return report;
284159
- }) };
284160
- }
284161
- function sleep(ms) {
284162
- return new Promise((resolve) => {
284163
- setTimeout(resolve, ms).unref?.();
284164
- });
284165
- }
284166
- function isEsrch(error) {
284167
- return typeof error === "object" && error !== null && "code" in error && error.code === "ESRCH";
284168
- }
284169
- /** SIGTERM, poll for exit, escalate to SIGKILL. Chromium tears down its own children. */
284170
- async function killProcessWithEscalation(pid) {
284171
- try {
284172
- process.kill(pid, "SIGTERM");
284173
- } catch (error) {
284174
- if (isEsrch(error)) return;
284175
- throw error;
284176
- }
284177
- const deadline = Date.now() + KILL_ESCALATION_TIMEOUT_MS;
284178
- while (Date.now() < deadline) {
284179
- await sleep(KILL_POLL_INTERVAL_MS);
284180
- try {
284181
- process.kill(pid, 0);
284182
- } catch {
284183
- return;
284184
- }
284185
- }
284186
- try {
284187
- process.kill(pid, "SIGKILL");
284188
- } catch (error) {
284189
- if (!isEsrch(error)) throw error;
284190
- }
284191
- }
284192
- function createSystemBrowserOrphanReaperDeps(input) {
284193
- const { profileRoot } = input;
284194
- return {
284195
- listChromiumMains: async () => {
284196
- const { stdout } = await execFileAsync$2("ps", ["-axo", "pid=,etime=,args="], { maxBuffer: 16 * 1024 * 1024 });
284197
- const mains = [];
284198
- for (const line of stdout.split("\n")) {
284199
- const match = /^\s*(\d+)\s+(\S+)\s+(.+)$/.exec(line);
284200
- if (!match) continue;
284201
- const [, pidRaw, etime, command] = match;
284202
- if (!isChromiumMainCommand(command, profileRoot)) continue;
284203
- const userDataDir = extractUserDataDir(command);
284204
- const ageMs = parseEtimeToMs(etime);
284205
- if (userDataDir === null || ageMs === null) continue;
284206
- mains.push({
284207
- pid: Number(pidRaw),
284208
- userDataDir,
284209
- ageMs
284210
- });
284211
- }
284212
- return mains;
284213
- },
284214
- listRegisteredSessionIds: async () => (await input.listSessions()).map((session) => session.sessionId),
284215
- closeSession: input.closeSession,
284216
- killProcess: killProcessWithEscalation,
284217
- listProfileDirs: async () => {
284218
- const entries = await node_fs_promises.default.readdir(profileRoot, { withFileTypes: true }).catch(() => []);
284219
- const now = Date.now();
284220
- const dirs = [];
284221
- for (const entry of entries) {
284222
- if (!entry.isDirectory()) continue;
284223
- const dirPath = node_path.default.join(profileRoot, entry.name);
284224
- const stat = await node_fs_promises.default.stat(dirPath).catch(() => null);
284225
- if (stat === null) continue;
284226
- dirs.push({
284227
- path: dirPath,
284228
- ageMs: Math.max(0, now - stat.mtimeMs)
284229
- });
284230
- }
284231
- return dirs;
284232
- },
284233
- removeProfileDir: async (dir) => {
284234
- const resolved = node_path.default.resolve(dir);
284235
- const resolvedRoot = node_path.default.resolve(profileRoot);
284236
- if (resolved === resolvedRoot || !resolved.startsWith(resolvedRoot + node_path.default.sep)) throw new Error(`refusing to remove path outside the profile root: ${dir}`);
284237
- await node_fs_promises.default.rm(resolved, {
284238
- recursive: true,
284239
- force: true
284240
- });
284241
- }
284242
- };
284243
- }
284244
- //#endregion
284245
- //#region src/browser/browserControlProtocol.ts
284246
- function resolveBrowserControlDecision(state, metadata) {
284247
- const hasSessionEpoch = metadata.sessionEpoch !== void 0;
284248
- const hasControlSequence = metadata.controlSequence !== void 0;
284249
- const expectedControlSequence = state.lastAppliedControlSequence + 1;
284250
- if (!hasSessionEpoch && !hasControlSequence) return {
284251
- type: "apply",
284252
- controlSequence: expectedControlSequence
284253
- };
284254
- if (!hasSessionEpoch || !hasControlSequence) return {
284255
- type: "reject",
284256
- reason: "incomplete_metadata",
284257
- expectedSessionEpoch: state.sessionEpoch,
284258
- expectedControlSequence,
284259
- acknowledgedControlSequence: state.lastAppliedControlSequence
284260
- };
284261
- if (metadata.sessionEpoch !== state.sessionEpoch) return {
284262
- type: "reject",
284263
- reason: "stale_epoch",
284264
- expectedSessionEpoch: state.sessionEpoch,
284265
- acknowledgedControlSequence: state.lastAppliedControlSequence
284266
- };
284267
- if (metadata.controlSequence <= state.lastAppliedControlSequence) return {
284268
- type: "duplicate",
284269
- acknowledgedControlSequence: state.lastAppliedControlSequence
284270
- };
284271
- if (metadata.controlSequence !== expectedControlSequence) return {
284272
- type: "reject",
284273
- reason: "sequence_gap",
284274
- expectedControlSequence,
284275
- acknowledgedControlSequence: state.lastAppliedControlSequence
284276
- };
284277
- return {
284278
- type: "apply",
284279
- controlSequence: metadata.controlSequence
284280
- };
284281
- }
284282
- function browserControlRejectionDetail(decision) {
284283
- switch (decision.reason) {
284284
- case "stale_epoch": return `Browser session is stale. Expected epoch ${decision.expectedSessionEpoch}.`;
284285
- case "sequence_gap": return `Browser control sequence is out of order. Expected ${decision.expectedControlSequence}.`;
284286
- case "incomplete_metadata": return "Browser control ordering requires both sessionEpoch and controlSequence.";
284287
- }
284288
- }
284289
285262
  const HOST_BRIDGE_RECONNECT_MAX_MS = 1e3;
284290
285263
  /**
284291
285264
  * Exponential backoff, capped.
@@ -284583,162 +285556,21 @@ function createNativeHostBrowserBridgeClientImplementation(options, deps) {
284583
285556
  }
284584
285557
  //#endregion
284585
285558
  //#region src/browser/AgentBrowserHostBridgeAdapter.ts
284586
- const DEFAULT_URL = "about:blank";
284587
- const DEFAULT_RENDER_MODE = "interactive";
284588
- const DEFAULT_AGENT_BROWSER_ID = "embedded";
284589
- const DEFAULT_AGENT_BROWSER_VIEWPORT = {
284590
- width: 1280,
284591
- height: 720
284592
- };
284593
- const ABSOLUTE_WEB_URL_PATTERN = /^[a-z][a-z\d+\-.]*:\/\//i;
284594
- const SPECIAL_URL_PATTERN = /^(about|blob|chrome|data|devtools|file):/i;
284595
- const LOCAL_URL_LIKE_PATTERN = /^(localhost|127\.0\.0\.1|\[::1\]|::1|[a-z0-9.-]+\.localhost)(?::\d+)?(?:[/?#].*)?$/i;
284596
- const DEFAULT_AGENT_BROWSER_DISPLAY = {
284597
- transport: "remote-webrtc",
284598
- status: "connected",
284599
- reconnect: {
284600
- status: "ready",
284601
- attempt: 0
284602
- }
284603
- };
284604
- const DEFAULT_AGENT_BROWSER_CONTROL_COMMAND_TIMEOUT_MS = 1500;
284605
- const DEFAULT_AGENT_BROWSER_FAST_COMMAND_TIMEOUT_MS = 1e4;
284606
- const DEFAULT_AGENT_BROWSER_NAVIGATION_COMMAND_TIMEOUT_MS = 9e4;
284607
- const DEFAULT_AGENT_BROWSER_CAPTURE_COMMAND_TIMEOUT_MS = 9e4;
284608
- const DEFAULT_AGENT_BROWSER_BRIDGE_OPEN_TIMEOUT_MS = 9e4;
284609
- const DEFAULT_AGENT_BROWSER_BRIDGE_CLOSE_TIMEOUT_MS = 3e4;
284610
- const DEFAULT_AGENT_BROWSER_PLAYWRIGHT_BACKEND_CLOSE_TIMEOUT_MS = 1e4;
284611
- 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.";
284612
- 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.";
284613
- function nowIso$2() {
284614
- return (/* @__PURE__ */ new Date()).toISOString();
284615
- }
284616
- function positiveTimeoutMs(value) {
284617
- if (!value) return;
284618
- const parsed = Number.parseInt(value, 10);
284619
- return Number.isFinite(parsed) && parsed > 0 ? parsed : void 0;
284620
- }
284621
- function agentBrowserCommandTimeoutMs(command, env = process.env) {
284622
- const explicitCommandTimeout = positiveTimeoutMs(env.FORGE_AGENT_BROWSER_BRIDGE_COMMAND_TIMEOUT_MS);
284623
- if (explicitCommandTimeout !== void 0) return explicitCommandTimeout;
284624
- if (isAgentBrowserBoundedControlCommand(command)) return positiveTimeoutMs(env.FORGE_AGENT_BROWSER_BRIDGE_CONTROL_TIMEOUT_MS) ?? DEFAULT_AGENT_BROWSER_CONTROL_COMMAND_TIMEOUT_MS;
284625
- switch (command.type) {
284626
- case "screenshot":
284627
- case "snapshot": return positiveTimeoutMs(env.FORGE_AGENT_BROWSER_BRIDGE_CAPTURE_TIMEOUT_MS) ?? DEFAULT_AGENT_BROWSER_CAPTURE_COMMAND_TIMEOUT_MS;
284628
- case "newTab":
284629
- case "navigate":
284630
- case "goBack":
284631
- case "goForward":
284632
- case "reload": return DEFAULT_AGENT_BROWSER_NAVIGATION_COMMAND_TIMEOUT_MS;
284633
- default: return DEFAULT_AGENT_BROWSER_FAST_COMMAND_TIMEOUT_MS;
284634
- }
284635
- }
284636
- function agentBrowserCloseTimeoutMs(env = process.env) {
284637
- return positiveTimeoutMs(env.FORGE_AGENT_BROWSER_BRIDGE_CLOSE_TIMEOUT_MS) ?? DEFAULT_AGENT_BROWSER_BRIDGE_CLOSE_TIMEOUT_MS;
284638
- }
284639
- function agentBrowserOpenTimeoutMs(env = process.env) {
284640
- return positiveTimeoutMs(env.FORGE_AGENT_BROWSER_BRIDGE_OPEN_TIMEOUT_MS) ?? DEFAULT_AGENT_BROWSER_BRIDGE_OPEN_TIMEOUT_MS;
284641
- }
284642
- function agentBrowserFastTimeoutMs(env = process.env) {
284643
- return positiveTimeoutMs(env.FORGE_AGENT_BROWSER_BRIDGE_COMMAND_TIMEOUT_MS) ?? DEFAULT_AGENT_BROWSER_FAST_COMMAND_TIMEOUT_MS;
284644
- }
284645
- function agentBrowserPlaywrightBackendCloseTimeoutMs(env = process.env) {
284646
- return positiveTimeoutMs(env.FORGE_AGENT_BROWSER_PLAYWRIGHT_BACKEND_CLOSE_TIMEOUT_MS) ?? DEFAULT_AGENT_BROWSER_PLAYWRIGHT_BACKEND_CLOSE_TIMEOUT_MS;
284647
- }
284648
- async function withAgentBrowserBridgeTimeout(label, promise, timeoutMs) {
284649
- let timeout;
284650
- try {
284651
- return await Promise.race([promise, new Promise((_, reject) => {
284652
- timeout = setTimeout(() => {
284653
- reject(/* @__PURE__ */ new Error(`${label} timed out after ${Math.ceil(timeoutMs / 1e3)}s.`));
284654
- }, timeoutMs);
284655
- })]);
284656
- } finally {
284657
- if (timeout !== void 0) clearTimeout(timeout);
284658
- }
284659
- }
284660
- async function openAgentBrowserSessionReady(bridge, input) {
284661
- let resolveStarted;
284662
- const started = new Promise((resolve) => {
284663
- resolveStarted = resolve;
284664
- });
284665
- const unsubscribe = await bridge.subscribe({
284666
- sessionId: input.sessionId,
284667
- ...input.browserId ? { browserId: input.browserId } : {}
284668
- }, (event) => {
284669
- if (event.type === "session.started" && event.sessionId === input.sessionId) resolveStarted(event.payload.snapshot);
284670
- });
284671
- try {
284672
- return await Promise.race([bridge.openSession(input), started]);
284673
- } finally {
284674
- unsubscribe();
284675
- }
284676
- }
284677
- function isAgentBrowserBridgeTimeoutError(error) {
284678
- const message = [
284679
- require_Schema$1.is(BrowserError)(error) ? error.detail : void 0,
284680
- error instanceof Error ? error.message : void 0,
284681
- error instanceof Error ? error.stack : void 0,
284682
- typeof error === "string" ? error : void 0,
284683
- (() => {
284684
- try {
284685
- return JSON.stringify(error);
284686
- } catch {
284687
- return;
284688
- }
284689
- })(),
284690
- String(error)
284691
- ].filter((part) => typeof part === "string" && part.length > 0).join("\n");
284692
- return /timed out after \d+s\./.test(message);
284693
- }
284694
- function isAgentBrowserBoundedControlCommand(command) {
284695
- switch (command.type) {
284696
- case "click":
284697
- case "clickPoint":
284698
- case "pressKey":
284699
- case "resetViewport":
284700
- case "scroll":
284701
- case "setViewport":
284702
- case "type":
284703
- case "typeText":
284704
- case "wheel": return true;
284705
- default: return false;
284706
- }
284707
- }
284708
- function snapshotAfterTimedOutControl(snapshot, command) {
284709
- switch (command.type) {
284710
- case "setViewport": return {
284711
- ...snapshot,
284712
- viewport: command.viewport,
284713
- updatedAt: nowIso$2()
284714
- };
284715
- case "resetViewport": return {
284716
- ...snapshot,
284717
- viewport: DEFAULT_AGENT_BROWSER_VIEWPORT,
284718
- updatedAt: nowIso$2()
284719
- };
284720
- default: return snapshot;
284721
- }
284722
- }
284723
- async function closeAgentBrowserBackendWithTimeout(closeBackend, env = process.env) {
284724
- try {
284725
- await withAgentBrowserBridgeTimeout("Agent Browser Playwright backend close", closeBackend(), agentBrowserPlaywrightBackendCloseTimeoutMs(env));
284726
- } catch (error) {
284727
- if (!isAgentBrowserBridgeTimeoutError(error)) throw error;
284728
- console.warn("[browser] Agent Browser Playwright backend close timed out; continuing", { timeoutMs: agentBrowserPlaywrightBackendCloseTimeoutMs(env) });
284729
- }
284730
- }
284731
285559
  function createNativeHostBrowserBridgeClient(options) {
284732
285560
  return createNativeHostBrowserBridgeClientImplementation(options, { closeSessionTimeoutMs: agentBrowserCloseTimeoutMs });
284733
285561
  }
285562
+ function browserBackendOptionsForSession(input) {
285563
+ return {
285564
+ ...input.baseOptions,
285565
+ userDataDir: browserProfileDirForSession(input.sessionId)
285566
+ };
285567
+ }
284734
285568
  /**
284735
285569
  * Builds the Playwright-backed backend from the lazily loaded module. The
284736
- * `playwright` package (and everything that statically imports it) must stay
284737
- * out of the standalone bundle's eager import graph: the bundle keeps
284738
- * `playwright` external, so a static import would hoist it to the top of
284739
- * `dist/bin.mjs` and the server would fail to boot on any machine that has
284740
- * not provisioned playwright yet. Deferring the import to backend construction
284741
- * keeps boot playwright-free and lets a later provision satisfy first use.
285570
+ * `playwright` package and everything that statically imports it must stay out
285571
+ * of the standalone bundle's eager import graph. Deferring the import to
285572
+ * backend construction keeps boot playwright-free and lets a later provision
285573
+ * satisfy first use.
284742
285574
  */
284743
285575
  function createForgeAgentBrowserPlaywrightBackend(PlaywrightBackend, options) {
284744
285576
  class ForgeAgentBrowserPlaywrightBackend extends PlaywrightBackend {
@@ -284748,757 +285580,6 @@ function createForgeAgentBrowserPlaywrightBackend(PlaywrightBackend, options) {
284748
285580
  }
284749
285581
  return new ForgeAgentBrowserPlaywrightBackend(options);
284750
285582
  }
284751
- function toBrowserError(error, fallback) {
284752
- console.warn("[browser] Agent Browser host bridge operation failed", {
284753
- operation: fallback,
284754
- error
284755
- });
284756
- if (require_Schema$1.is(BrowserError)(error)) return error;
284757
- if (error instanceof Error && error.message.trim().length > 0) return new BrowserError({ detail: error.message.trim() });
284758
- return new BrowserError({ detail: fallback });
284759
- }
284760
- function remoteWebRtcUnavailableError(_input) {
284761
- return new BrowserError({ detail: REMOTE_WEBRTC_UNAVAILABLE_DETAIL });
284762
- }
284763
- function remoteWebRtcRelayRequiredError(_input) {
284764
- return new BrowserError({ detail: REMOTE_WEBRTC_RELAY_REQUIRED_DETAIL });
284765
- }
284766
- function subscriptionMatchesEvent(subscription, event) {
284767
- if (subscription.input.threadId && subscription.input.threadId !== event.threadId) return false;
284768
- if (subscription.input.browserId && subscription.input.browserId !== event.browserId) return false;
284769
- if (subscription.input.environmentId && subscription.input.environmentId !== event.environmentId) return false;
284770
- if (subscription.input.userId && subscription.input.userId !== event.userId) return false;
284771
- return true;
284772
- }
284773
- function sessionIdForThread(threadId) {
284774
- return BrowserSessionId.make(threadId);
284775
- }
284776
- function browserBackendOptionsForSession(input) {
284777
- return {
284778
- ...input.baseOptions,
284779
- userDataDir: browserProfileDirForSession(input.sessionId)
284780
- };
284781
- }
284782
- function browserIdFrom(input) {
284783
- return BrowserId.make(input.browserId ?? DEFAULT_AGENT_BROWSER_ID);
284784
- }
284785
- function normalizeUserVisibleBrowserUrl(url) {
284786
- const trimmed = url.trim();
284787
- if (trimmed.length === 0) return trimmed;
284788
- if (ABSOLUTE_WEB_URL_PATTERN.test(trimmed) || SPECIAL_URL_PATTERN.test(trimmed) || trimmed.startsWith("//")) return trimmed;
284789
- if (LOCAL_URL_LIKE_PATTERN.test(trimmed)) return `http://${trimmed}`;
284790
- return `https://${trimmed}`;
284791
- }
284792
- function agentBrowserDisplayForRuntimeMode(input) {
284793
- if (input.mode !== "desktop") {
284794
- const hasRelay = hasTurnRelay(input.iceServers ?? []);
284795
- return {
284796
- transport: "remote-webrtc",
284797
- status: "degraded",
284798
- reason: input.requiresRelay === true && !hasRelay ? "network_unreachable" : "transport_unavailable",
284799
- detail: input.requiresRelay === true && !hasRelay ? REMOTE_WEBRTC_RELAY_REQUIRED_DETAIL : "Remote WebRTC browser display is not attached yet.",
284800
- ...input.viewport ? { viewport: input.viewport } : {},
284801
- ...input.iceServers && input.iceServers.length > 0 ? { iceServers: input.iceServers } : {},
284802
- reconnect: {
284803
- status: "ready",
284804
- attempt: 0
284805
- }
284806
- };
284807
- }
284808
- return {
284809
- transport: "native-embedded",
284810
- status: "connected",
284811
- ...input.viewport ? { viewport: input.viewport } : {},
284812
- reconnect: {
284813
- status: "ready",
284814
- attempt: 0
284815
- }
284816
- };
284817
- }
284818
- function hasTurnRelay(iceServers) {
284819
- return browserWebRtcIceServersIncludeRelay(iceServers);
284820
- }
284821
- async function resolveDisplayIceServers(options) {
284822
- return options?.resolveDisplayIceServers ? await options.resolveDisplayIceServers() : options?.displayIceServers ?? [];
284823
- }
284824
- function webRtcDisplayRequiresRelay(serverConfig) {
284825
- return serverConfig.mode === "web" && serverConfig.devUrl === void 0;
284826
- }
284827
- function toForgeTab(input) {
284828
- return {
284829
- tabId: BrowserTabId.make(input.tab.id),
284830
- restoreKey: input.tab.id,
284831
- ...input.tab.title ? { title: input.tab.title } : {},
284832
- url: input.tab.url ?? DEFAULT_URL,
284833
- isActive: input.tab.isActive,
284834
- canGoBack: input.tab.canGoBack ?? false,
284835
- canGoForward: input.tab.canGoForward ?? false,
284836
- renderMode: DEFAULT_RENDER_MODE,
284837
- status: "live",
284838
- ...input.tab.navigationStatus !== void 0 ? { navigationStatus: input.tab.navigationStatus } : {},
284839
- ...input.tab.requestedUrl !== void 0 ? { requestedUrl: input.tab.requestedUrl } : {},
284840
- ...input.tab.navigationError !== void 0 ? { navigationError: input.tab.navigationError } : {},
284841
- updatedAt: input.updatedAt
284842
- };
284843
- }
284844
- function toForgeSession(input) {
284845
- return {
284846
- browserId: BrowserId.make(input.snapshot.browserId),
284847
- threadId: input.threadId,
284848
- environmentId: EnvironmentId.make(input.environmentId ?? DEFAULT_BROWSER_ENVIRONMENT_ID),
284849
- userId: BrowserUserId.make(input.userId ?? "local-user"),
284850
- agentId: input.agentId ?? null,
284851
- ownerRole: input.ownerRole ?? "lead",
284852
- name: input.snapshot.name,
284853
- visibility: input.snapshot.visibility,
284854
- viewport: input.snapshot.viewport,
284855
- display: input.display ?? DEFAULT_AGENT_BROWSER_DISPLAY,
284856
- sessionId: BrowserSessionId.make(input.snapshot.sessionId),
284857
- ...input.controlState ? {
284858
- sessionEpoch: input.controlState.sessionEpoch,
284859
- lastAppliedControlSequence: input.controlState.lastAppliedControlSequence
284860
- } : {},
284861
- activeTabId: input.snapshot.activeTabId ? BrowserTabId.make(input.snapshot.activeTabId) : null,
284862
- tabs: input.snapshot.tabs.map((tab) => toForgeTab({
284863
- tab,
284864
- updatedAt: input.snapshot.updatedAt
284865
- })),
284866
- updatedAt: input.snapshot.updatedAt
284867
- };
284868
- }
284869
- function resolveDisplay(snapshot, options) {
284870
- if (typeof options?.display === "function") return options.display(snapshot);
284871
- return options?.display ?? DEFAULT_AGENT_BROWSER_DISPLAY;
284872
- }
284873
- function toHostCommand(command) {
284874
- switch (command.type) {
284875
- case "activateTab": return {
284876
- type: "activateTab",
284877
- tabId: command.tabId
284878
- };
284879
- case "click": return {
284880
- type: "click",
284881
- tabId: command.tabId,
284882
- selector: command.selector
284883
- };
284884
- case "clickPoint": return {
284885
- type: "clickPoint",
284886
- tabId: command.tabId,
284887
- x: command.x,
284888
- y: command.y,
284889
- ...command.button !== void 0 ? { button: command.button } : {},
284890
- ...command.clickCount !== void 0 ? { clickCount: command.clickCount } : {}
284891
- };
284892
- case "closeTab": return {
284893
- type: "closeTab",
284894
- tabId: command.tabId
284895
- };
284896
- case "consoleLogs": return {
284897
- type: "consoleLogs",
284898
- ...command.tabId !== void 0 ? { tabId: command.tabId } : {},
284899
- ...command.limit !== void 0 ? { limit: command.limit } : {},
284900
- ...command.levels !== void 0 ? { levels: command.levels } : {}
284901
- };
284902
- case "goBack": return {
284903
- type: "goBack",
284904
- tabId: command.tabId
284905
- };
284906
- case "goForward": return {
284907
- type: "goForward",
284908
- tabId: command.tabId
284909
- };
284910
- case "nameSession": return {
284911
- type: "nameSession",
284912
- name: command.name
284913
- };
284914
- case "navigate": return {
284915
- type: "navigate",
284916
- url: normalizeUserVisibleBrowserUrl(command.url),
284917
- ...command.tabId !== void 0 ? { tabId: command.tabId } : {}
284918
- };
284919
- case "newTab": return {
284920
- type: "newTab",
284921
- ...command.url !== void 0 ? { url: normalizeUserVisibleBrowserUrl(command.url) } : {},
284922
- ...command.makeActive !== void 0 ? { makeActive: command.makeActive } : {}
284923
- };
284924
- case "pressKey": return {
284925
- type: "pressKey",
284926
- tabId: command.tabId,
284927
- key: command.key
284928
- };
284929
- case "reload": return {
284930
- type: "reload",
284931
- tabId: command.tabId
284932
- };
284933
- case "resetViewport": return { type: "resetViewport" };
284934
- case "screenshot": return {
284935
- type: "screenshot",
284936
- tabId: command.tabId,
284937
- ...command.fullPage !== void 0 ? { fullPage: command.fullPage } : {}
284938
- };
284939
- case "scroll": return {
284940
- type: "scroll",
284941
- tabId: command.tabId,
284942
- scrollX: 0,
284943
- scrollY: command.toTop === true ? -900 : 900,
284944
- ...command.x !== void 0 ? { x: command.x } : {},
284945
- ...command.y !== void 0 ? { y: command.y } : {}
284946
- };
284947
- case "setViewport": return {
284948
- type: "setViewport",
284949
- viewport: command.viewport
284950
- };
284951
- case "setVisibility": return {
284952
- type: "setVisibility",
284953
- visibility: command.visibility
284954
- };
284955
- case "snapshot": return {
284956
- type: "snapshot",
284957
- tabId: command.tabId
284958
- };
284959
- case "type": return {
284960
- type: "type",
284961
- tabId: command.tabId,
284962
- selector: command.selector,
284963
- text: command.text,
284964
- ...command.append !== void 0 ? { append: command.append } : {}
284965
- };
284966
- case "typeText": return {
284967
- type: "typeText",
284968
- tabId: command.tabId,
284969
- text: command.text,
284970
- ...command.pressEnter !== void 0 ? { pressEnter: command.pressEnter } : {}
284971
- };
284972
- case "wheel": return {
284973
- type: "wheel",
284974
- tabId: command.tabId,
284975
- deltaX: command.deltaX,
284976
- deltaY: command.deltaY,
284977
- ...command.x !== void 0 ? { x: command.x } : {},
284978
- ...command.y !== void 0 ? { y: command.y } : {}
284979
- };
284980
- default: throw new Error(`Unsupported browser command: ${command.type}`);
284981
- }
284982
- }
284983
- function toStreamEvent(input) {
284984
- const base = {
284985
- eventId: EventId.make(crypto.randomUUID()),
284986
- browserId: input.browserId,
284987
- threadId: input.threadId,
284988
- environmentId: EnvironmentId.make(input.environmentId),
284989
- userId: BrowserUserId.make(input.userId),
284990
- agentId: input.agentId,
284991
- ...input.controlState ? { sessionEpoch: input.controlState.sessionEpoch } : {},
284992
- createdAt: nowIso$2()
284993
- };
284994
- switch (input.event.type) {
284995
- case "session.started": return {
284996
- ...base,
284997
- type: "session.started",
284998
- payload: { snapshot: toForgeSession({
284999
- snapshot: input.event.payload.snapshot,
285000
- threadId: input.threadId,
285001
- environmentId: input.environmentId,
285002
- userId: input.userId,
285003
- agentId: input.agentId,
285004
- ownerRole: input.ownerRole,
285005
- display: input.display,
285006
- controlState: input.controlState
285007
- }) }
285008
- };
285009
- case "session.updated": return {
285010
- ...base,
285011
- type: input.event.type,
285012
- payload: { snapshot: toForgeSession({
285013
- snapshot: input.event.payload.snapshot,
285014
- threadId: input.threadId,
285015
- environmentId: input.environmentId,
285016
- userId: input.userId,
285017
- agentId: input.agentId,
285018
- ownerRole: input.ownerRole,
285019
- display: input.display,
285020
- controlState: input.controlState
285021
- }) }
285022
- };
285023
- case "session.closed": return {
285024
- ...base,
285025
- type: "session.closed",
285026
- payload: { sessionId: BrowserSessionId.make(input.event.payload.sessionId) }
285027
- };
285028
- case "tab.closed": return {
285029
- ...base,
285030
- type: "tab.closed",
285031
- payload: {
285032
- sessionId: BrowserSessionId.make(input.event.sessionId),
285033
- tabId: BrowserTabId.make(input.event.payload.tabId)
285034
- }
285035
- };
285036
- case "snapshot.ready": return {
285037
- ...base,
285038
- type: "snapshot.ready",
285039
- payload: {
285040
- sessionId: BrowserSessionId.make(input.event.sessionId),
285041
- tabId: BrowserTabId.make(input.event.payload.tabId),
285042
- captureId: BrowserCaptureId.make(input.event.payload.captureId ?? crypto.randomUUID()),
285043
- capturedAt: input.event.payload.capturedAt,
285044
- url: input.event.payload.url ?? DEFAULT_URL,
285045
- viewport: input.event.payload.viewport,
285046
- content: input.event.payload.content
285047
- }
285048
- };
285049
- case "screenshot.ready": return {
285050
- ...base,
285051
- type: "screenshot.ready",
285052
- payload: {
285053
- sessionId: BrowserSessionId.make(input.event.sessionId),
285054
- tabId: BrowserTabId.make(input.event.payload.tabId),
285055
- captureId: BrowserCaptureId.make(input.event.payload.captureId ?? crypto.randomUUID()),
285056
- capturedAt: input.event.payload.capturedAt,
285057
- url: input.event.payload.url ?? DEFAULT_URL,
285058
- viewport: input.event.payload.viewport,
285059
- imageDataUrl: input.event.payload.imageDataUrl
285060
- }
285061
- };
285062
- case "console.logs.ready": return {
285063
- ...base,
285064
- type: "console.logs.ready",
285065
- payload: {
285066
- sessionId: BrowserSessionId.make(input.event.sessionId),
285067
- tabId: BrowserTabId.make(input.event.payload.tabId),
285068
- logs: input.event.payload.logs.map((log) => ({
285069
- level: log.level === "warn" ? "warning" : log.level === "log" ? "info" : log.level,
285070
- message: log.message,
285071
- createdAt: log.timestamp
285072
- }))
285073
- }
285074
- };
285075
- case "action.failed": return {
285076
- ...base,
285077
- type: "action.failed",
285078
- payload: {
285079
- commandType: input.event.payload.commandType,
285080
- message: input.event.payload.message,
285081
- ...input.event.payload.tabId ? { tabId: BrowserTabId.make(input.event.payload.tabId) } : {}
285082
- }
285083
- };
285084
- case "dom.visible.ready":
285085
- case "element.info.ready":
285086
- case "element.screenshot.ready":
285087
- case "clipboard.text.ready": return null;
285088
- }
285089
- }
285090
- function toCommandResult(input) {
285091
- return {
285092
- ok: input.result.ok,
285093
- snapshot: toForgeSession({
285094
- snapshot: input.result.snapshot,
285095
- threadId: input.command.threadId,
285096
- environmentId: input.command.environmentId,
285097
- userId: input.command.userId,
285098
- display: input.display,
285099
- controlState: input.controlState
285100
- }),
285101
- sessionEpoch: input.controlState.sessionEpoch,
285102
- acknowledgedControlSequence: input.acknowledgedControlSequence,
285103
- ...input.result.detail ? { detail: input.result.detail } : {},
285104
- ...input.latestCapture ? { latestCapture: input.latestCapture } : {}
285105
- };
285106
- }
285107
- function commandCaptureTabId(command) {
285108
- switch (command.type) {
285109
- case "snapshot":
285110
- case "screenshot": return command.tabId;
285111
- default: return;
285112
- }
285113
- }
285114
- function toCommandLatestCapture(input) {
285115
- const tabId = commandCaptureTabId(input.command.command);
285116
- 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;
285117
- const mapped = toStreamEvent({
285118
- event: input.event,
285119
- threadId: input.command.threadId,
285120
- browserId: input.browserId,
285121
- environmentId: input.command.environmentId ?? DEFAULT_BROWSER_ENVIRONMENT_ID,
285122
- userId: input.command.userId ?? "local-user",
285123
- agentId: null,
285124
- ownerRole: "lead",
285125
- display: input.display,
285126
- controlState: input.controlState
285127
- });
285128
- return mapped?.type === "snapshot.ready" || mapped?.type === "screenshot.ready" ? mapped : void 0;
285129
- }
285130
- function createAgentBrowserHostBridgeManager(bridge, options) {
285131
- const localSubscriptions = /* @__PURE__ */ new Set();
285132
- const connectedDisplays = /* @__PURE__ */ new Map();
285133
- const controlStates = /* @__PURE__ */ new Map();
285134
- const commandQueues = /* @__PURE__ */ new Map();
285135
- const controlStateFor = (sessionId) => {
285136
- const existing = controlStates.get(sessionId);
285137
- if (existing) return existing;
285138
- const created = {
285139
- sessionEpoch: crypto.randomUUID(),
285140
- lastAppliedControlSequence: 0
285141
- };
285142
- controlStates.set(sessionId, created);
285143
- return created;
285144
- };
285145
- const runSerializedCommand = async (sessionId, task) => {
285146
- const previous = commandQueues.get(sessionId) ?? Promise.resolve();
285147
- let releaseCurrent;
285148
- const current = new Promise((resolve) => {
285149
- releaseCurrent = resolve;
285150
- });
285151
- const queued = previous.catch(() => void 0).then(() => current);
285152
- commandQueues.set(sessionId, queued);
285153
- await previous.catch(() => void 0);
285154
- try {
285155
- return await task();
285156
- } finally {
285157
- releaseCurrent();
285158
- if (commandQueues.get(sessionId) === queued) commandQueues.delete(sessionId);
285159
- }
285160
- };
285161
- const clearConnectedDisplay = (input) => {
285162
- const active = connectedDisplays.get(input.sessionId);
285163
- if (!active) return;
285164
- if (input.tabId === void 0 || active.tabId === input.tabId) connectedDisplays.delete(input.sessionId);
285165
- };
285166
- const displayForSnapshot = (snapshot) => {
285167
- const fallback = resolveDisplay(snapshot, options);
285168
- const active = connectedDisplays.get(snapshot.sessionId);
285169
- if (!active) return fallback;
285170
- if (!snapshot.tabs.some((tab) => tab.id === active.tabId)) {
285171
- connectedDisplays.delete(snapshot.sessionId);
285172
- return fallback;
285173
- }
285174
- if (snapshot.activeTabId !== active.tabId) return fallback;
285175
- const { reason: _reason, detail: _detail, ...fallbackWithoutDegradedReason } = fallback;
285176
- return {
285177
- ...fallbackWithoutDegradedReason,
285178
- ...active.display,
285179
- reconnect: active.display.reconnect ?? fallback.reconnect
285180
- };
285181
- };
285182
- const duplicateControlResult = async (input, hostSessionId, controlState, acknowledgedControlSequence) => {
285183
- const snapshot = await withAgentBrowserBridgeTimeout("Agent Browser duplicate command session read", bridge.getSession({
285184
- sessionId: hostSessionId,
285185
- ...input.browserId ? { browserId: input.browserId } : {}
285186
- }), agentBrowserFastTimeoutMs());
285187
- if (!snapshot) throw new BrowserError({ detail: "Browser session not found." });
285188
- return {
285189
- ok: true,
285190
- snapshot: toForgeSession({
285191
- snapshot,
285192
- threadId: input.threadId,
285193
- environmentId: input.environmentId,
285194
- userId: input.userId,
285195
- display: displayForSnapshot(snapshot),
285196
- controlState
285197
- }),
285198
- sessionEpoch: controlState.sessionEpoch,
285199
- acknowledgedControlSequence,
285200
- detail: "Browser control was already applied"
285201
- };
285202
- };
285203
- const dispatchHostCommand = async (input, hostIdentity, preCommandSnapshot) => {
285204
- try {
285205
- return await withAgentBrowserBridgeTimeout(`Agent Browser command ${input.command.type}`, bridge.command({
285206
- ...hostIdentity,
285207
- command: toHostCommand(input.command)
285208
- }), agentBrowserCommandTimeoutMs(input.command));
285209
- } catch (error) {
285210
- if (!(isAgentBrowserBridgeTimeoutError(error) && isAgentBrowserBoundedControlCommand(input.command) && preCommandSnapshot !== null)) throw error;
285211
- console.warn("[browser] Agent Browser control command timed out after dispatch", {
285212
- commandType: input.command.type,
285213
- sessionId: hostIdentity.sessionId,
285214
- ...input.browserId ? { browserId: input.browserId } : {}
285215
- });
285216
- return {
285217
- ok: true,
285218
- snapshot: snapshotAfterTimedOutControl(preCommandSnapshot, input.command),
285219
- detail: "Agent Browser control command dispatch timed out before post-action observation completed."
285220
- };
285221
- }
285222
- };
285223
- const executeHostCommand = async (input, hostSessionId, controlState, controlSequence) => {
285224
- const captureTabId = commandCaptureTabId(input.command);
285225
- const hostIdentity = {
285226
- sessionId: hostSessionId,
285227
- ...input.browserId ? { browserId: input.browserId } : {}
285228
- };
285229
- 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;
285230
- let latestCaptureEvent;
285231
- const unsubscribeCapture = captureTabId ? await bridge.subscribe(hostIdentity, (event) => {
285232
- if ((event.type === "snapshot.ready" || event.type === "screenshot.ready") && event.sessionId === hostSessionId && event.payload.tabId === captureTabId) latestCaptureEvent = event;
285233
- }) : void 0;
285234
- let result;
285235
- try {
285236
- result = await dispatchHostCommand(input, hostIdentity, preCommandSnapshot);
285237
- } finally {
285238
- unsubscribeCapture?.();
285239
- }
285240
- if (input.command.type === "closeTab") {
285241
- await require_Schema$1.runPromise(options?.displayPeers?.closeForSession({
285242
- sessionId: hostSessionId,
285243
- tabId: input.command.tabId
285244
- }) ?? require_Schema$1.void_);
285245
- clearConnectedDisplay({
285246
- sessionId: hostSessionId,
285247
- tabId: input.command.tabId
285248
- });
285249
- }
285250
- const display = displayForSnapshot(result.snapshot);
285251
- controlState.lastAppliedControlSequence = controlSequence;
285252
- return toCommandResult({
285253
- result,
285254
- command: input,
285255
- display,
285256
- controlState,
285257
- acknowledgedControlSequence: controlSequence,
285258
- latestCapture: latestCaptureEvent ? toCommandLatestCapture({
285259
- event: latestCaptureEvent,
285260
- command: input,
285261
- browserId: BrowserId.make(result.snapshot.browserId),
285262
- display,
285263
- controlState
285264
- }) : void 0
285265
- });
285266
- };
285267
- const executeOrderedHostCommand = async (input) => {
285268
- const hostSessionId = sessionIdForThread(input.threadId);
285269
- return runSerializedCommand(hostSessionId, async () => {
285270
- const controlState = controlStateFor(hostSessionId);
285271
- const controlDecision = resolveBrowserControlDecision(controlState, input);
285272
- if (controlDecision.type === "reject") throw new BrowserError({ detail: browserControlRejectionDetail(controlDecision) });
285273
- if (controlDecision.type === "duplicate") return duplicateControlResult(input, hostSessionId, controlState, controlDecision.acknowledgedControlSequence);
285274
- return executeHostCommand(input, hostSessionId, controlState, controlDecision.controlSequence);
285275
- });
285276
- };
285277
- const publishLocalEvent = (event) => {
285278
- for (const subscription of localSubscriptions) {
285279
- if (!subscriptionMatchesEvent(subscription, event)) continue;
285280
- require_Schema$1.runPromise(subscription.listener(event)).catch((error) => {
285281
- console.warn("[browser] Failed to publish local browser event", { error });
285282
- });
285283
- }
285284
- };
285285
- const publishWebRtcIceCandidate = (event) => {
285286
- publishLocalEvent({
285287
- eventId: EventId.make(crypto.randomUUID()),
285288
- browserId: event.input.browserId ?? browserIdFrom(event.input),
285289
- threadId: event.input.threadId,
285290
- environmentId: event.input.environmentId ?? DEFAULT_BROWSER_ENVIRONMENT_ID,
285291
- userId: event.input.userId ?? "local-user",
285292
- agentId: null,
285293
- createdAt: nowIso$2(),
285294
- type: "display.iceCandidate",
285295
- payload: {
285296
- sessionId: event.input.sessionId,
285297
- tabId: event.input.tabId,
285298
- clientId: event.input.clientId,
285299
- candidate: event.candidate
285300
- }
285301
- });
285302
- };
285303
- return {
285304
- openSession: (input) => require_Schema$1.tryPromise({
285305
- try: async () => {
285306
- const hostSessionId = sessionIdForThread(input.threadId);
285307
- const initialUrl = input.initialUrl ? normalizeUserVisibleBrowserUrl(input.initialUrl) : void 0;
285308
- options?.prepareRuntimeSession?.({
285309
- sessionId: hostSessionId,
285310
- initialUrl: initialUrl ?? DEFAULT_URL
285311
- });
285312
- let snapshot;
285313
- try {
285314
- snapshot = await withAgentBrowserBridgeTimeout("Agent Browser openSession", openAgentBrowserSessionReady(bridge, {
285315
- sessionId: hostSessionId,
285316
- ...input.browserId ? { browserId: input.browserId } : {},
285317
- ...initialUrl ? { initialUrl } : {},
285318
- ...input.name ? { name: input.name } : {},
285319
- ...input.visibility ? { visibility: input.visibility } : {},
285320
- ...input.viewport ? { viewport: input.viewport } : {}
285321
- }), agentBrowserOpenTimeoutMs());
285322
- } catch (error) {
285323
- if (!isAgentBrowserBridgeTimeoutError(error)) {
285324
- options?.forgetRuntimeSession?.({ sessionId: hostSessionId });
285325
- throw error;
285326
- }
285327
- console.warn("[browser] Agent Browser openSession timed out; quarantining session", {
285328
- sessionId: hostSessionId,
285329
- ...input.browserId ? { browserId: input.browserId } : {}
285330
- });
285331
- await withAgentBrowserBridgeTimeout("Agent Browser openSession quarantine close", bridge.closeSession({
285332
- sessionId: hostSessionId,
285333
- ...input.browserId ? { browserId: input.browserId } : {}
285334
- }), agentBrowserCloseTimeoutMs()).catch(() => void 0);
285335
- clearConnectedDisplay({ sessionId: hostSessionId });
285336
- controlStates.delete(hostSessionId);
285337
- options?.forgetRuntimeSession?.({ sessionId: hostSessionId });
285338
- throw new Error(`${error instanceof Error ? error.message : String(error)} The pending browser session was quarantined and closed.`, { cause: error });
285339
- }
285340
- return toForgeSession({
285341
- snapshot,
285342
- threadId: input.threadId,
285343
- environmentId: input.environmentId,
285344
- userId: input.userId,
285345
- agentId: input.agentId,
285346
- ownerRole: input.ownerRole,
285347
- display: displayForSnapshot(snapshot),
285348
- controlState: controlStateFor(snapshot.sessionId)
285349
- });
285350
- },
285351
- catch: (error) => toBrowserError(error, "Failed to open Agent Browser session.")
285352
- }),
285353
- closeSession: (input) => require_Schema$1.tryPromise({
285354
- try: async () => {
285355
- await require_Schema$1.runPromise(options?.displayPeers?.closeForSession({ sessionId: sessionIdForThread(input.threadId) }) ?? require_Schema$1.void_);
285356
- const hostSessionId = sessionIdForThread(input.threadId);
285357
- const hostIdentity = {
285358
- sessionId: hostSessionId,
285359
- ...input.browserId ? { browserId: input.browserId } : {}
285360
- };
285361
- try {
285362
- await withAgentBrowserBridgeTimeout("Agent Browser closeSession", bridge.closeSession(hostIdentity), agentBrowserCloseTimeoutMs());
285363
- } catch (error) {
285364
- if (!isAgentBrowserBridgeTimeoutError(error)) throw error;
285365
- 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", {
285366
- sessionId: hostSessionId,
285367
- ...input.browserId ? { browserId: input.browserId } : {}
285368
- });
285369
- else console.warn("[browser] Agent Browser closeSession timed out; forgetting session", {
285370
- sessionId: hostSessionId,
285371
- ...input.browserId ? { browserId: input.browserId } : {}
285372
- });
285373
- clearConnectedDisplay({ sessionId: hostSessionId });
285374
- controlStates.delete(hostSessionId);
285375
- options?.forgetRuntimeSession?.({ sessionId: hostSessionId });
285376
- return;
285377
- }
285378
- clearConnectedDisplay({ sessionId: hostSessionId });
285379
- controlStates.delete(hostSessionId);
285380
- options?.forgetRuntimeSession?.({ sessionId: hostSessionId });
285381
- },
285382
- catch: (error) => toBrowserError(error, "Failed to close Agent Browser session.")
285383
- }),
285384
- clearBrowsingData: () => require_Schema$1.succeed({
285385
- closedSessionCount: 0,
285386
- deletedSessionCount: 0,
285387
- deletedCaptureCount: 0
285388
- }),
285389
- getSession: (input) => require_Schema$1.tryPromise({
285390
- try: async () => {
285391
- const snapshot = await withAgentBrowserBridgeTimeout("Agent Browser getSession", bridge.getSession({
285392
- sessionId: sessionIdForThread(input.threadId),
285393
- ...input.browserId ? { browserId: input.browserId } : {}
285394
- }), agentBrowserFastTimeoutMs());
285395
- return snapshot ? toForgeSession({
285396
- snapshot,
285397
- threadId: input.threadId,
285398
- environmentId: input.environmentId,
285399
- userId: input.userId,
285400
- display: displayForSnapshot(snapshot),
285401
- controlState: controlStateFor(snapshot.sessionId)
285402
- }) : null;
285403
- },
285404
- catch: (error) => toBrowserError(error, "Failed to read Agent Browser session.")
285405
- }),
285406
- listSessions: (input) => require_Schema$1.tryPromise({
285407
- try: async () => {
285408
- return (await withAgentBrowserBridgeTimeout("Agent Browser listSessions", bridge.listSessions(input.browserId ? { browserId: input.browserId } : void 0), agentBrowserFastTimeoutMs())).map((snapshot) => toForgeSession({
285409
- snapshot,
285410
- threadId: input.threadId ?? ThreadId.make(snapshot.sessionId),
285411
- environmentId: input.environmentId,
285412
- userId: input.userId,
285413
- display: displayForSnapshot(snapshot),
285414
- controlState: controlStateFor(snapshot.sessionId)
285415
- }));
285416
- },
285417
- catch: (error) => toBrowserError(error, "Failed to list Agent Browser sessions.")
285418
- }),
285419
- command: (input) => require_Schema$1.tryPromise({
285420
- try: () => executeOrderedHostCommand(input),
285421
- catch: (error) => toBrowserError(error, "Failed to run Agent Browser command.")
285422
- }),
285423
- attachDisplay: (input) => {
285424
- const displayPeers = options?.displayPeers;
285425
- if (!displayPeers) return require_Schema$1.fail(remoteWebRtcUnavailableError(input));
285426
- return require_Schema$1.tryPromise({
285427
- try: async () => {
285428
- const iceServers = await resolveDisplayIceServers(options);
285429
- if (options?.requiresWebRtcRelay === true && !hasTurnRelay(iceServers)) throw remoteWebRtcRelayRequiredError(input);
285430
- const result = await require_Schema$1.runPromise(displayPeers.attach(input, {
285431
- display: {
285432
- transport: "remote-webrtc",
285433
- status: "connecting",
285434
- ...input.viewport ? { viewport: input.viewport } : {},
285435
- devicePixelRatio: 1,
285436
- ...iceServers.length > 0 ? { iceServers } : {},
285437
- reconnect: {
285438
- status: "ready",
285439
- attempt: 0
285440
- }
285441
- },
285442
- onIceCandidate: publishWebRtcIceCandidate
285443
- }));
285444
- connectedDisplays.set(input.sessionId, {
285445
- tabId: input.tabId,
285446
- display: result.display
285447
- });
285448
- return result;
285449
- },
285450
- catch: (error) => {
285451
- if (require_Schema$1.is(BrowserError)(error)) return error;
285452
- return toBrowserError(error, "Failed to attach WebRTC browser display.");
285453
- }
285454
- });
285455
- },
285456
- addDisplayIceCandidate: (input) => {
285457
- const displayPeers = options?.displayPeers;
285458
- if (!displayPeers) return require_Schema$1.fail(remoteWebRtcUnavailableError(input));
285459
- return displayPeers.addIceCandidate(input);
285460
- },
285461
- subscribe: (input, listener) => require_Schema$1.promise(async () => {
285462
- const localSubscription = {
285463
- input,
285464
- listener
285465
- };
285466
- localSubscriptions.add(localSubscription);
285467
- const threadId = input.threadId ?? ThreadId.make("unknown-thread");
285468
- const browserId = browserIdFrom(input);
285469
- const environmentId = input.environmentId ?? DEFAULT_BROWSER_ENVIRONMENT_ID;
285470
- const userId = input.userId ?? "local-user";
285471
- const subscribeInput = {
285472
- ...input.threadId ? { sessionId: sessionIdForThread(input.threadId) } : {},
285473
- ...input.browserId ? { browserId: input.browserId } : {}
285474
- };
285475
- const unsubscribeBridge = await bridge.subscribe(subscribeInput, (event) => {
285476
- if (event.type === "session.closed") clearConnectedDisplay({ sessionId: event.payload.sessionId });
285477
- if (event.type === "tab.closed") clearConnectedDisplay({
285478
- sessionId: event.sessionId,
285479
- tabId: event.payload.tabId
285480
- });
285481
- const mapped = toStreamEvent({
285482
- event,
285483
- threadId,
285484
- browserId,
285485
- environmentId,
285486
- userId,
285487
- agentId: null,
285488
- ownerRole: "lead",
285489
- display: event.type === "session.started" || event.type === "session.updated" ? displayForSnapshot(event.payload.snapshot) : DEFAULT_AGENT_BROWSER_DISPLAY,
285490
- controlState: controlStateFor(event.type === "session.closed" ? event.payload.sessionId : event.sessionId)
285491
- });
285492
- if (mapped) require_Schema$1.runPromise(listener(mapped));
285493
- if (event.type === "session.closed") controlStates.delete(event.payload.sessionId);
285494
- });
285495
- return () => {
285496
- localSubscriptions.delete(localSubscription);
285497
- unsubscribeBridge();
285498
- };
285499
- })
285500
- };
285501
- }
285502
285583
  function shouldUseNativeHostBridge(serverConfig) {
285503
285584
  return serverConfig.mode === "desktop" && typeof serverConfig.nativeBrowserHostUrl === "string" && serverConfig.nativeBrowserHostUrl.trim().length > 0 && typeof serverConfig.nativeBrowserHostToken === "string" && serverConfig.nativeBrowserHostToken.trim().length > 0;
285504
285585
  }
@@ -285522,8 +285603,10 @@ const AgentBrowserManagerLive = require_Schema$1.effect(BrowserManager, require_
285522
285603
  const displayCaptureRuntime = yield* require_Schema$1.promise(() => createBrowserWebRtcDisplayCaptureRuntime({ chromiumExecutablePath: process.env.FORGE_PLAYWRIGHT_EXECUTABLE_PATH }));
285523
285604
  yield* require_Schema$1.addFinalizer(() => require_Schema$1.promise(() => displayCaptureRuntime.close()).pipe(require_Schema$1.orDie));
285524
285605
  if (!displayCaptureRuntime.available) yield* (!requiresWebRtcRelay && displayCaptureRuntime.unavailableDetail?.includes("root-owned /tmp/.X11-unix") === true ? require_Schema$1.logInfo : require_Schema$1.logWarning)("browser WebRTC display capture runtime unavailable", { detail: displayCaptureRuntime.unavailableDetail });
285606
+ const layerContext = yield* require_Schema$1.context();
285607
+ const runLayerFork = require_Schema$1.runForkWith(layerContext);
285525
285608
  const displayTaskHoldManager = makeSpriteTaskHoldManager(process.env, (message, cause) => {
285526
- require_Schema$1.runFork(require_Schema$1.logWarning(message, { cause: cause instanceof Error ? cause.message : cause }));
285609
+ runLayerFork(require_Schema$1.logWarning(message, { cause: cause instanceof Error ? cause.message : cause }));
285527
285610
  });
285528
285611
  const displayPeers = createWebRtcDisplaySessionManager({
285529
285612
  mediaSourceFactory: displayCaptureRuntime.mediaSourceFactory,
@@ -285535,7 +285618,6 @@ const AgentBrowserManagerLive = require_Schema$1.effect(BrowserManager, require_
285535
285618
  yield* require_Schema$1.addFinalizer(() => require_Schema$1.promise(() => displayTaskHoldManager?.releaseAll() ?? Promise.resolve()));
285536
285619
  const baseBackendOptions = displayCaptureRuntime.backendOptions;
285537
285620
  const sessionInitialUrls = /* @__PURE__ */ new Map();
285538
- const reaperContext = yield* require_Schema$1.context();
285539
285621
  let runtimeBuild = null;
285540
285622
  const lazyDisposers = [];
285541
285623
  const buildPlaywrightRuntime = async () => {
@@ -285578,13 +285660,12 @@ const AgentBrowserManagerLive = require_Schema$1.effect(BrowserManager, require_
285578
285660
  await hostBridge.closeSession({ sessionId: BrowserSessionId.make(sessionId) });
285579
285661
  }
285580
285662
  }), reaperConfig);
285581
- const runReaperFork = require_Schema$1.runForkWith(reaperContext);
285582
285663
  const reaperInterval = setInterval(() => {
285583
- runReaperFork(reaper.sweep);
285664
+ runLayerFork(reaper.sweep);
285584
285665
  }, reaperConfig.intervalMs);
285585
285666
  reaperInterval.unref?.();
285586
285667
  lazyDisposers.push(() => clearInterval(reaperInterval));
285587
- runReaperFork(reaper.sweep);
285668
+ runLayerFork(reaper.sweep);
285588
285669
  }
285589
285670
  return createAgentBrowserHostBridgeManager(hostBridge, {
285590
285671
  display: (snapshot) => agentBrowserDisplayForRuntimeMode({
@@ -297667,7 +297748,7 @@ function resolveBuildCommitFromEnv(env) {
297667
297748
  * environment descriptor down instead of reporting an honest "unknown".
297668
297749
  */
297669
297750
  function readBakedBuildCommit() {
297670
- return "e0ef2b5174f2f50934814b1865cabaa9c7de0ce7";
297751
+ return "22e4740c3b851434e13fd47fc22833c48d658450";
297671
297752
  }
297672
297753
  async function resolveServerBuildCommit(input) {
297673
297754
  if (isFullCommitSha(input.baked)) return input.baked;