@mearl/native-host 2.6.1 → 2.7.1

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/README.md CHANGED
@@ -45,6 +45,8 @@ mearl-native-host [options]
45
45
  --daemon 独立 CDP daemon
46
46
  --cdp-port <port> 指定 CDP 端口
47
47
  --cdp-user-data-dir <dir> 指定 Chrome Profile
48
+ --agentbay-session-id <id> 连接 AgentBay CDP Session
49
+ --agentbay-idle-release-minutes <minutes> AgentBay 空闲回收时间(内部参数)
48
50
  --browser-id <id> 指定浏览器注册 ID
49
51
  --browser-name <name> 指定浏览器显示名称
50
52
  --version, -v 查看版本
@@ -63,6 +65,15 @@ Chrome Extension
63
65
 
64
66
  `browser_launch` 会为每个托管 Chrome 启动一个带独立 CDP 配置的 native-host 子进程,使多个 headless/Profile/TDBank 账号实例可以同时被现有浏览器注册表寻址。设计细节见 [托管浏览器与 TDBank 多账号设计](../../docs/managed-browsers.md)。
65
67
 
68
+ AgentBay 会话创建时使用官方 `LifecyclePolicy`:默认 30 分钟没有 Mearl 指令时由本地宿主执行优雅关闭,120 分钟达到云端最大运行时长时由 AgentBay 强制释放。优雅关闭会先同步持久化 Browser Context,再删除 Session 和本地注册记录;执行中的 Mearl action 不计为空闲。正常任务仍应主动调用 `browser_close`,超时只用于异常退出兜底。
69
+
70
+ 运维环境可覆盖默认值;两项均为正整数分钟,空闲时间至少为 3,最大运行时长不得小于空闲时间:
71
+
72
+ ```bash
73
+ MEARL_AGENTBAY_IDLE_RELEASE_MINUTES=30
74
+ MEARL_AGENTBAY_MAX_RUNTIME_MINUTES=120
75
+ ```
76
+
66
77
  ## 侧边栏 Agent Chat
67
78
 
68
79
  扩展侧边栏通过 Native Messaging 调用 native-host,再按用户选择启动持久的 Codex app-server 或 Qoder ACP 进程。两种 Provider 都支持流式消息、思考与工具进度、中断和审批,并复用各自 CLI 已保存的本地登录态。
@@ -9,6 +9,7 @@ export interface AgentBayBrowserRecord extends DaemonRecord {
9
9
  name: string;
10
10
  sessionId: string;
11
11
  imageId?: string;
12
+ userAgentMode?: 'default' | 'desktop';
12
13
  persistent?: boolean;
13
14
  contextId?: string;
14
15
  contextName?: string;
@@ -93,12 +94,21 @@ export interface AgentBayBrowserSummary {
93
94
  copiedCookies?: AgentBayBrowserRecord['copiedCookies'];
94
95
  running: boolean;
95
96
  }
97
+ export interface AgentBayBrowserLaunchResult extends AgentBayBrowserSummary {
98
+ created: boolean;
99
+ reused: boolean;
100
+ }
96
101
  export interface AgentBayBrowserManagerDeps {
97
102
  nativeHostScript: string;
98
103
  exportCookies: (domains: string[]) => Promise<TransferableCookie[]>;
99
104
  getBrowserUserAgent: () => Promise<string>;
100
105
  log: (...args: any[]) => void;
101
106
  createAgentBayClient?: (apiKey: string) => Promise<AgentBayClientLike>;
107
+ connectCdp?: ConnectAgentBayCdp;
108
+ findBrowserRecord?: (selector: string) => {
109
+ record: AgentBayBrowserRecord;
110
+ running: boolean;
111
+ } | null;
102
112
  persistBrowserRecord?: (record: AgentBayBrowserRecord) => void;
103
113
  resolveApiKey?: () => string;
104
114
  }
@@ -124,7 +134,8 @@ export declare function getAgentBayAccessUrl(sessionId: string, dependencies?: P
124
134
  export declare function resolveAgentBayEndpoint(sessionId: string): Promise<string>;
125
135
  export declare function keepAgentBaySessionAlive(sessionId: string): Promise<void>;
126
136
  export declare function initializeAgentBayBrowserSession(session: Pick<AgentBaySessionLike, 'browser'>, input: Pick<BrowserLaunchParams, 'url'>, cookies: TransferableCookie[], options?: AgentBayBrowserSessionInitOptions): Promise<number>;
127
- export declare function launchAgentBayBrowser(input: BrowserLaunchParams, deps: AgentBayBrowserManagerDeps): Promise<AgentBayBrowserSummary>;
137
+ export declare function reuseAgentBayBrowserSession(session: Pick<AgentBaySessionLike, 'browser'>, input: Pick<BrowserLaunchParams, 'url'>, cookies: TransferableCookie[], options?: Pick<AgentBayBrowserSessionInitOptions, 'connectCdp'>): Promise<number>;
138
+ export declare function launchAgentBayBrowser(input: BrowserLaunchParams, deps: AgentBayBrowserManagerDeps): Promise<AgentBayBrowserLaunchResult>;
128
139
  export declare function releaseAgentBayBrowserResources(record: AgentBayBrowserRecord, agentBay: AgentBayClientLike, options?: {
129
140
  deleteProfile?: boolean;
130
141
  persistRecord?: (record: AgentBayBrowserRecord) => void;
@@ -0,0 +1,38 @@
1
+ export declare const MIN_AGENTBAY_IDLE_RELEASE_MINUTES = 3;
2
+ export declare const DEFAULT_AGENTBAY_IDLE_RELEASE_MINUTES = 30;
3
+ export declare const DEFAULT_AGENTBAY_MAX_RUNTIME_MINUTES = 120;
4
+ export declare const DEFAULT_AGENTBAY_LIFECYCLE_CHECK_INTERVAL_MS = 60000;
5
+ export interface AgentBayLifecycleConfig {
6
+ idleReleaseTimeout: number;
7
+ maxRuntime: number;
8
+ }
9
+ interface AgentBaySessionWatchdogOptions {
10
+ idleTimeoutMs: number;
11
+ checkIntervalMs?: number;
12
+ now?: () => number;
13
+ keepAlive: () => Promise<void>;
14
+ close: () => Promise<void>;
15
+ log: (...args: unknown[]) => void;
16
+ }
17
+ export declare function resolveAgentBayLifecycleConfig(env?: NodeJS.ProcessEnv): AgentBayLifecycleConfig;
18
+ export declare class AgentBaySessionWatchdog {
19
+ private readonly idleTimeoutMs;
20
+ private readonly checkIntervalMs;
21
+ private readonly now;
22
+ private readonly keepAlive;
23
+ private readonly close;
24
+ private readonly log;
25
+ private activeRequests;
26
+ private lastActivityAt;
27
+ private timer;
28
+ private checking;
29
+ private closing;
30
+ private stopped;
31
+ constructor(options: AgentBaySessionWatchdogOptions);
32
+ start(): void;
33
+ stop(): void;
34
+ beginActivity(): boolean;
35
+ endActivity(): void;
36
+ checkNow(): Promise<void>;
37
+ }
38
+ export {};
@@ -4,7 +4,7 @@
4
4
  * Contains: HTTP helper (nodeFetch), cookie utilities, page-load waiters,
5
5
  * viewport helpers, and serialized function sources for page injection.
6
6
  */
7
- import { type PageClickPointerType } from '@mearl/browser-core';
7
+ import { type PageClickPointerType, type PageDragPoint, type PageDragPointerType } from '@mearl/browser-core';
8
8
  import type { PageSession } from './cdp-session.js';
9
9
  export declare const TYPE_INTO_ELEMENT_SRC: string;
10
10
  export declare const CLICK_ELEMENT_VIA_JS_SRC: string;
@@ -61,4 +61,7 @@ export declare function toCdpInputPoint(session: PageSession, x: number, y: numb
61
61
  y: number;
62
62
  }>;
63
63
  export declare function dispatchCdpClick(session: PageSession, x: number, y: number, pointerType: PageClickPointerType): Promise<void>;
64
+ export declare function dispatchCdpDrag(session: PageSession, from: PageDragPoint, to: PageDragPoint, pointerType: PageDragPointerType, durationMs: number): Promise<{
65
+ steps: number;
66
+ }>;
64
67
  export declare function resolveBackendNodeObjectId(session: PageSession, backendNodeId: number): Promise<string>;
@@ -3,9 +3,10 @@
3
3
  *
4
4
  * Covers: click, type, scroll, eval, press, wait, navigate, upload, device emulation.
5
5
  */
6
- import { type PageClickParams, type PageEvalParams, type PageHoverParams, type PageNavigateParams, type PagePressParams, type PageScrollParams, type PageTypeParams, type PageUploadParams, type PageWaitParams, type SetDeviceEmulationParams } from '@mearl/browser-core';
6
+ import { type PageClickParams, type PageDragParams, type PageDragResultData, type PageEvalParams, type PageHoverParams, type PageNavigateParams, type PagePressParams, type PageScrollParams, type PageTypeParams, type PageUploadParams, type PageWaitParams, type SetDeviceEmulationParams } from '@mearl/browser-core';
7
7
  import type { PageSession } from './cdp-session.js';
8
8
  export declare function cdpPageClick(session: PageSession, data: PageClickParams): Promise<any>;
9
+ export declare function cdpPageDrag(session: PageSession, data: PageDragParams): Promise<PageDragResultData>;
9
10
  export declare function cdpPageType(session: PageSession, data: PageTypeParams): Promise<any>;
10
11
  export declare function cdpPageScroll(session: PageSession, data: PageScrollParams): Promise<any>;
11
12
  export declare function cdpPageEval(session: PageSession, data: PageEvalParams): Promise<any>;
package/dist/index.js CHANGED
@@ -2068,6 +2068,203 @@ function getClickCoordsFromNode() {
2068
2068
  };
2069
2069
  }
2070
2070
 
2071
+ // ../browser-core/dist/pageDragging.js
2072
+ var PAGE_DRAG_DEFAULT_DURATION_MS = 400;
2073
+ var PAGE_DRAG_MAX_DURATION_MS = 5e3;
2074
+ var PAGE_DRAG_STEP_INTERVAL_MS = 25;
2075
+ var PAGE_DRAG_MIN_STEPS = 2;
2076
+ var PAGE_DRAG_MAX_STEPS = 60;
2077
+ function isPageDragPoint(value) {
2078
+ if (!value || typeof value !== "object")
2079
+ return false;
2080
+ const point = value;
2081
+ return Number.isFinite(point.x) && Number.isFinite(point.y) && point.x >= 0 && point.y >= 0;
2082
+ }
2083
+ function resolveEndpoint(value, name) {
2084
+ if (typeof value === "string" && value.trim().length > 0)
2085
+ return value.trim();
2086
+ if (isPageDragPoint(value))
2087
+ return { x: value.x, y: value.y };
2088
+ throw new Error(`page_drag: "${name}" must be a non-empty CSS/@ref selector or { x, y } viewport point.`);
2089
+ }
2090
+ function resolvePageDragInput(data) {
2091
+ if (!data || typeof data !== "object") {
2092
+ throw new Error("page_drag: payload is required");
2093
+ }
2094
+ if (data.dragMode !== void 0 && !["auto", "dom", "mouse", "touch"].includes(data.dragMode)) {
2095
+ throw new Error('page_drag: "dragMode" must be one of auto, dom, mouse, touch');
2096
+ }
2097
+ const durationMs = data.durationMs ?? PAGE_DRAG_DEFAULT_DURATION_MS;
2098
+ if (!Number.isFinite(durationMs) || durationMs < 0 || durationMs > PAGE_DRAG_MAX_DURATION_MS) {
2099
+ throw new Error(`page_drag: "durationMs" must be between 0 and ${PAGE_DRAG_MAX_DURATION_MS}.`);
2100
+ }
2101
+ return {
2102
+ from: resolveEndpoint(data.from, "from"),
2103
+ to: resolveEndpoint(data.to, "to"),
2104
+ dragMode: data.dragMode ?? "auto",
2105
+ durationMs: Math.round(durationMs)
2106
+ };
2107
+ }
2108
+ function resolvePageDragDispatchPolicy(dragMode, visibilityState, touchEmulationEnabled) {
2109
+ const requestedMode = dragMode ?? "auto";
2110
+ const pointerType = requestedMode === "mouse" || requestedMode === "touch" ? requestedMode : touchEmulationEnabled ? "touch" : "mouse";
2111
+ if (requestedMode === "dom")
2112
+ return { dispatchMode: "dom", pointerType };
2113
+ if (requestedMode === "auto" && visibilityState === "hidden") {
2114
+ return { dispatchMode: "dom", pointerType, fallbackReason: "page-hidden" };
2115
+ }
2116
+ return { dispatchMode: "trusted-input", pointerType };
2117
+ }
2118
+ function buildPageDragPath(from, to, durationMs) {
2119
+ const steps = Math.max(PAGE_DRAG_MIN_STEPS, Math.min(PAGE_DRAG_MAX_STEPS, Math.ceil(durationMs / PAGE_DRAG_STEP_INTERVAL_MS)));
2120
+ const points = Array.from({ length: steps }, (_, index) => {
2121
+ const ratio = (index + 1) / steps;
2122
+ return {
2123
+ x: from.x + (to.x - from.x) * ratio,
2124
+ y: from.y + (to.y - from.y) * ratio
2125
+ };
2126
+ });
2127
+ return { points, intervalMs: durationMs / steps };
2128
+ }
2129
+ function dragBetweenPointsInPage(from, points, pointerType) {
2130
+ const target = document.elementFromPoint(from.x, from.y);
2131
+ if (!target) {
2132
+ return {
2133
+ dragged: false,
2134
+ steps: 0,
2135
+ error: `No element found at drag start point (${from.x}, ${from.y})`
2136
+ };
2137
+ }
2138
+ if (points.length === 0) {
2139
+ return { dragged: false, steps: 0, error: "Drag path must contain at least one point" };
2140
+ }
2141
+ const view = target.ownerDocument.defaultView ?? window;
2142
+ const realm = view;
2143
+ const pointerId = 1;
2144
+ const pointerInit = (point, buttons, pressure) => ({
2145
+ bubbles: true,
2146
+ cancelable: true,
2147
+ composed: true,
2148
+ view,
2149
+ clientX: point.x,
2150
+ clientY: point.y,
2151
+ screenX: point.x,
2152
+ screenY: point.y,
2153
+ button: 0,
2154
+ buttons,
2155
+ pointerId,
2156
+ pointerType,
2157
+ isPrimary: true,
2158
+ pressure,
2159
+ width: 1,
2160
+ height: 1
2161
+ });
2162
+ const dispatchPointer = (type, point, buttons, pressure) => {
2163
+ if (typeof realm.PointerEvent !== "function")
2164
+ return false;
2165
+ target.dispatchEvent(new realm.PointerEvent(type, pointerInit(point, buttons, pressure)));
2166
+ return true;
2167
+ };
2168
+ const dispatchMouse = (type, point, buttons) => {
2169
+ if (typeof realm.MouseEvent !== "function")
2170
+ return false;
2171
+ target.dispatchEvent(new realm.MouseEvent(type, {
2172
+ bubbles: true,
2173
+ cancelable: true,
2174
+ composed: true,
2175
+ view,
2176
+ clientX: point.x,
2177
+ clientY: point.y,
2178
+ screenX: point.x,
2179
+ screenY: point.y,
2180
+ button: 0,
2181
+ buttons
2182
+ }));
2183
+ return true;
2184
+ };
2185
+ const createTouch = (point) => {
2186
+ const init = {
2187
+ identifier: pointerId,
2188
+ target,
2189
+ clientX: point.x,
2190
+ clientY: point.y,
2191
+ screenX: point.x,
2192
+ screenY: point.y,
2193
+ pageX: point.x + view.scrollX,
2194
+ pageY: point.y + view.scrollY,
2195
+ radiusX: 1,
2196
+ radiusY: 1,
2197
+ rotationAngle: 0,
2198
+ force: 0.5
2199
+ };
2200
+ return typeof realm.Touch === "function" ? new realm.Touch(init) : init;
2201
+ };
2202
+ const dispatchTouch = (type, point) => {
2203
+ if (typeof realm.TouchEvent !== "function")
2204
+ return false;
2205
+ const touch = createTouch(point);
2206
+ const activeTouches = type === "touchend" ? [] : [touch];
2207
+ target.dispatchEvent(new realm.TouchEvent(type, {
2208
+ bubbles: true,
2209
+ cancelable: true,
2210
+ composed: true,
2211
+ touches: activeTouches,
2212
+ targetTouches: activeTouches,
2213
+ changedTouches: [touch]
2214
+ }));
2215
+ return true;
2216
+ };
2217
+ let dispatched = dispatchPointer("pointerdown", from, 1, 0.5);
2218
+ dispatched = pointerType === "touch" ? dispatchTouch("touchstart", from) || dispatched : dispatchMouse("mousedown", from, 1) || dispatched;
2219
+ for (const point of points) {
2220
+ dispatched = dispatchPointer("pointermove", point, 1, 0.5) || dispatched;
2221
+ dispatched = pointerType === "touch" ? dispatchTouch("touchmove", point) || dispatched : dispatchMouse("mousemove", point, 1) || dispatched;
2222
+ }
2223
+ const end = points[points.length - 1];
2224
+ dispatched = pointerType === "touch" ? dispatchTouch("touchend", end) || dispatched : dispatchMouse("mouseup", end, 0) || dispatched;
2225
+ dispatched = dispatchPointer("pointerup", end, 0, 0) || dispatched;
2226
+ return dispatched ? { dragged: true, steps: points.length } : { dragged: false, steps: 0, error: "This page does not support synthetic drag events" };
2227
+ }
2228
+ var DRAG_BETWEEN_POINTS_IN_PAGE_SOURCE = dragBetweenPointsInPage.toString();
2229
+ function buildPageDragDomExpression(from, points, pointerType) {
2230
+ return `(${DRAG_BETWEEN_POINTS_IN_PAGE_SOURCE})(${JSON.stringify(from)}, ${JSON.stringify(points)}, ${JSON.stringify(pointerType)})`;
2231
+ }
2232
+ function unwrapPageDragDomResult(result) {
2233
+ if (!result)
2234
+ throw new Error("page_drag: DOM simulation returned no result");
2235
+ if (!result.dragged)
2236
+ throw new Error(`page_drag: ${result.error}`);
2237
+ return result;
2238
+ }
2239
+ function getDragCoordsFromNode() {
2240
+ const el = this.nodeType === Node.TEXT_NODE ? this.parentElement : this;
2241
+ if (!el || typeof el.getBoundingClientRect !== "function")
2242
+ return null;
2243
+ const rect = el.getBoundingClientRect();
2244
+ const viewportWidth = window.visualViewport?.width ?? window.innerWidth;
2245
+ const viewportHeight = window.visualViewport?.height ?? window.innerHeight;
2246
+ if (rect.width <= 0 || rect.height <= 0) {
2247
+ return { x: 0, y: 0, tag: "", text: "", error: "Element has no visible bounds" };
2248
+ }
2249
+ if (rect.right <= 0 || rect.bottom <= 0 || rect.left >= viewportWidth || rect.top >= viewportHeight) {
2250
+ return {
2251
+ x: 0,
2252
+ y: 0,
2253
+ tag: "",
2254
+ text: "",
2255
+ error: "Element is outside the current viewport; scroll it into view before dragging"
2256
+ };
2257
+ }
2258
+ const x = Math.min(Math.max(rect.left + rect.width / 2, 0), Math.max(viewportWidth - 1, 0));
2259
+ const y = Math.min(Math.max(rect.top + rect.height / 2, 0), Math.max(viewportHeight - 1, 0));
2260
+ return {
2261
+ x,
2262
+ y,
2263
+ tag: el.tagName?.toLowerCase() ?? "",
2264
+ text: (el.textContent ?? "").trim().slice(0, 100)
2265
+ };
2266
+ }
2267
+
2071
2268
  // ../browser-core/dist/axSnapshot.js
2072
2269
  var PAGE_SNAPSHOT_QUERY_MAX_MATCHES = 100;
2073
2270
  var PAGE_SNAPSHOT_QUERY_MAX_BYTES = 256 * 1024;
@@ -3372,6 +3569,7 @@ var SEND_REQUEST_MAX_TOTAL_FILE_BYTES = 40 * 1024 * 1024;
3372
3569
  var SEND_REQUEST_MAX_TOTAL_BASE64_FILE_BYTES = 7 * 1024 * 1024;
3373
3570
  var PAGE_ACT_ACTIONS = [
3374
3571
  "page_click",
3572
+ "page_drag",
3375
3573
  "page_type",
3376
3574
  "page_hover",
3377
3575
  "page_scroll",
@@ -3402,6 +3600,7 @@ var BROWSER_COMMAND_ACTIONS = {
3402
3600
  tab_close: true,
3403
3601
  tab_list: true,
3404
3602
  page_click: true,
3603
+ page_drag: true,
3405
3604
  page_type: true,
3406
3605
  page_scroll: true,
3407
3606
  page_hover: true,
@@ -3442,6 +3641,7 @@ var EXTENSION_BROWSER_ACTIONS = {
3442
3641
  tab_close: true,
3443
3642
  tab_list: true,
3444
3643
  page_click: true,
3644
+ page_drag: true,
3445
3645
  page_type: true,
3446
3646
  page_scroll: true,
3447
3647
  page_hover: true,
@@ -4339,6 +4539,12 @@ function resolveSetCookieTarget(currentUrl, input) {
4339
4539
  }
4340
4540
 
4341
4541
  // ../browser-core/dist/controlledFavicon.js
4542
+ function normalizeControlSource(source) {
4543
+ return source === "remote" || source === "cloud" ? "remote" : "local";
4544
+ }
4545
+ function toControlSourceWireValue(source) {
4546
+ return source === "remote" ? "cloud" : "local";
4547
+ }
4342
4548
  var BASE_ICON = `
4343
4549
  <rect width="128" height="128" rx="28" fill="#3273E8"/>
4344
4550
  <path d="M36 90V40L64 68L92 40V90" stroke="#FFFFFF" stroke-width="14"
@@ -4348,7 +4554,7 @@ var SOURCE_BADGES = {
4348
4554
  local: `
4349
4555
  <circle cx="104" cy="104" r="21" fill="#16A34A" stroke="#FFFFFF" stroke-width="6"/>
4350
4556
  `,
4351
- cloud: `
4557
+ remote: `
4352
4558
  <circle cx="104" cy="104" r="21" fill="#FF7A00" stroke="#FFFFFF" stroke-width="6"/>
4353
4559
  `
4354
4560
  };
@@ -5949,6 +6155,85 @@ async function dispatchCdpClick(session, x, y, pointerType) {
5949
6155
  clickCount: 1
5950
6156
  });
5951
6157
  }
6158
+ async function dispatchCdpDrag(session, from, to, pointerType, durationMs) {
6159
+ const [start, end] = await Promise.all([
6160
+ toCdpInputPoint(session, from.x, from.y),
6161
+ toCdpInputPoint(session, to.x, to.y)
6162
+ ]);
6163
+ const path15 = buildPageDragPath(start, end, durationMs);
6164
+ const waitForNextPoint = async () => {
6165
+ if (path15.intervalMs <= 0) return;
6166
+ await new Promise((resolve2) => setTimeout(resolve2, path15.intervalMs));
6167
+ };
6168
+ if (pointerType === "touch") {
6169
+ await sendCdpInputCommand(session, "Input.dispatchTouchEvent", {
6170
+ type: "touchStart",
6171
+ touchPoints: [start]
6172
+ });
6173
+ let primaryError2;
6174
+ try {
6175
+ for (const point of path15.points) {
6176
+ await waitForNextPoint();
6177
+ await sendCdpInputCommand(session, "Input.dispatchTouchEvent", {
6178
+ type: "touchMove",
6179
+ touchPoints: [point]
6180
+ });
6181
+ }
6182
+ } catch (error48) {
6183
+ primaryError2 = error48;
6184
+ throw error48;
6185
+ } finally {
6186
+ await sendCdpInputCommand(session, "Input.dispatchTouchEvent", {
6187
+ type: "touchEnd",
6188
+ touchPoints: []
6189
+ }).catch((error48) => {
6190
+ if (primaryError2 === void 0) throw error48;
6191
+ });
6192
+ }
6193
+ return { steps: path15.points.length };
6194
+ }
6195
+ await sendCdpInputCommand(session, "Input.dispatchMouseEvent", {
6196
+ type: "mouseMoved",
6197
+ x: start.x,
6198
+ y: start.y
6199
+ });
6200
+ await sendCdpInputCommand(session, "Input.dispatchMouseEvent", {
6201
+ type: "mousePressed",
6202
+ x: start.x,
6203
+ y: start.y,
6204
+ button: "left",
6205
+ buttons: 1,
6206
+ clickCount: 1
6207
+ });
6208
+ let primaryError;
6209
+ try {
6210
+ for (const point of path15.points) {
6211
+ await waitForNextPoint();
6212
+ await sendCdpInputCommand(session, "Input.dispatchMouseEvent", {
6213
+ type: "mouseMoved",
6214
+ x: point.x,
6215
+ y: point.y,
6216
+ button: "left",
6217
+ buttons: 1
6218
+ });
6219
+ }
6220
+ } catch (error48) {
6221
+ primaryError = error48;
6222
+ throw error48;
6223
+ } finally {
6224
+ await sendCdpInputCommand(session, "Input.dispatchMouseEvent", {
6225
+ type: "mouseReleased",
6226
+ x: end.x,
6227
+ y: end.y,
6228
+ button: "left",
6229
+ buttons: 0,
6230
+ clickCount: 1
6231
+ }).catch((error48) => {
6232
+ if (primaryError === void 0) throw error48;
6233
+ });
6234
+ }
6235
+ return { steps: path15.points.length };
6236
+ }
5952
6237
  async function sendCdpInputCommand(session, method, params) {
5953
6238
  try {
5954
6239
  await session.client.sendCommand(method, params, PAGE_CLICK_INPUT_TIMEOUT_MS);
@@ -7940,6 +8225,7 @@ registerActions([
7940
8225
  ]);
7941
8226
 
7942
8227
  // src/cdp-backend/interaction-handlers.ts
8228
+ var GET_DRAG_COORDS_FROM_NODE_SRC = getDragCoordsFromNode.toString();
7943
8229
  function resolveSnapshotRef(session, ref) {
7944
8230
  const entry = resolvePageRef(session, ref);
7945
8231
  if (!entry) {
@@ -8251,6 +8537,136 @@ ${candidates}`
8251
8537
  )
8252
8538
  );
8253
8539
  }
8540
+ async function scrollDragSourceIntoView(session, endpoint) {
8541
+ if (typeof endpoint !== "string") return;
8542
+ validatePageElementSelector(endpoint, "page_drag from");
8543
+ if (endpoint.startsWith("@e")) {
8544
+ const entry = resolveSnapshotRef(session, endpoint);
8545
+ const objectId = await resolveBackendNodeObjectId(session, entry.backendNodeId);
8546
+ await session.client.sendCommand("Runtime.callFunctionOn", {
8547
+ objectId,
8548
+ functionDeclaration: `function() {
8549
+ const el = this.nodeType === Node.TEXT_NODE ? this.parentElement : this;
8550
+ if (!el) return false;
8551
+ el.scrollIntoView({ block: 'center', inline: 'center', behavior: 'instant' });
8552
+ return true;
8553
+ }`,
8554
+ returnByValue: true
8555
+ });
8556
+ return;
8557
+ }
8558
+ const result = await session.client.sendCommand("Runtime.evaluate", {
8559
+ expression: `(() => {
8560
+ const selector = ${JSON.stringify(endpoint)};
8561
+ const el = document.querySelector(selector);
8562
+ if (!el) return { error: 'Element not found: ' + selector };
8563
+ el.scrollIntoView({ block: 'center', inline: 'center', behavior: 'instant' });
8564
+ return { scrolled: true };
8565
+ })()`,
8566
+ returnByValue: true
8567
+ });
8568
+ const value = result.result?.value;
8569
+ if (value?.error) throw new Error(`page_drag: ${value.error}`);
8570
+ }
8571
+ function unwrapDragElementCoords(value, endpoint) {
8572
+ if (!value) throw new Error(`page_drag: cannot resolve element coordinates for "${endpoint}"`);
8573
+ if (value.error) throw new Error(`page_drag: ${value.error}: "${endpoint}"`);
8574
+ return value;
8575
+ }
8576
+ async function resolveDragEndpoint(session, endpoint) {
8577
+ if (typeof endpoint !== "string") {
8578
+ return { requested: endpoint, point: endpoint };
8579
+ }
8580
+ validatePageElementSelector(endpoint, "page_drag endpoint");
8581
+ let coords;
8582
+ if (endpoint.startsWith("@e")) {
8583
+ const entry = resolveSnapshotRef(session, endpoint);
8584
+ const objectId = await resolveBackendNodeObjectId(session, entry.backendNodeId);
8585
+ const result = await session.client.sendCommand("Runtime.callFunctionOn", {
8586
+ objectId,
8587
+ functionDeclaration: GET_DRAG_COORDS_FROM_NODE_SRC,
8588
+ returnByValue: true
8589
+ });
8590
+ coords = unwrapDragElementCoords(result.result?.value, endpoint);
8591
+ } else {
8592
+ const result = await session.client.sendCommand("Runtime.evaluate", {
8593
+ expression: `(() => {
8594
+ const selector = ${JSON.stringify(endpoint)};
8595
+ const el = document.querySelector(selector);
8596
+ if (!el) return { error: 'Element not found: ' + selector };
8597
+ return (${GET_DRAG_COORDS_FROM_NODE_SRC}).call(el);
8598
+ })()`,
8599
+ returnByValue: true
8600
+ });
8601
+ coords = unwrapDragElementCoords(result.result?.value, endpoint);
8602
+ }
8603
+ return {
8604
+ requested: endpoint,
8605
+ point: { x: coords.x, y: coords.y },
8606
+ tag: coords.tag,
8607
+ text: coords.text
8608
+ };
8609
+ }
8610
+ async function cdpPageDrag(session, data) {
8611
+ const input = resolvePageDragInput(data);
8612
+ const visibilityResult = input.dragMode === "auto" ? await session.client.sendCommand("Runtime.evaluate", {
8613
+ expression: "document.visibilityState",
8614
+ returnByValue: true
8615
+ }) : void 0;
8616
+ const touchEmulationEnabled = input.dragMode === "auto" || input.dragMode === "dom" ? await prefersTouchInput(session) : input.dragMode === "touch";
8617
+ const policy = resolvePageDragDispatchPolicy(
8618
+ input.dragMode,
8619
+ visibilityResult?.result?.value,
8620
+ touchEmulationEnabled
8621
+ );
8622
+ await scrollDragSourceIntoView(session, input.from);
8623
+ const from = await resolveDragEndpoint(session, input.from);
8624
+ const to = await resolveDragEndpoint(session, input.to);
8625
+ if (Math.hypot(to.point.x - from.point.x, to.point.y - from.point.y) < 1) {
8626
+ throw new Error("page_drag: resolved endpoints must be at least 1 viewport pixel apart");
8627
+ }
8628
+ if (policy.dispatchMode === "dom") {
8629
+ const path15 = buildPageDragPath(from.point, to.point, input.durationMs);
8630
+ const result = await session.client.sendCommand("Runtime.evaluate", {
8631
+ expression: buildPageDragDomExpression(from.point, path15.points, policy.pointerType),
8632
+ returnByValue: true
8633
+ });
8634
+ if (result.exceptionDetails) {
8635
+ throw new Error(
8636
+ result.exceptionDetails.exception?.description ?? result.exceptionDetails.text ?? "page_drag: DOM simulation failed"
8637
+ );
8638
+ }
8639
+ const { steps: steps2 } = unwrapPageDragDomResult(
8640
+ result.result?.value
8641
+ );
8642
+ return {
8643
+ dragged: true,
8644
+ from,
8645
+ to,
8646
+ dispatchMode: "dom",
8647
+ pointerType: policy.pointerType,
8648
+ durationMs: input.durationMs,
8649
+ steps: steps2,
8650
+ ...policy.fallbackReason ? { fallbackReason: policy.fallbackReason } : {}
8651
+ };
8652
+ }
8653
+ const { steps } = await dispatchCdpDrag(
8654
+ session,
8655
+ from.point,
8656
+ to.point,
8657
+ policy.pointerType,
8658
+ input.durationMs
8659
+ );
8660
+ return {
8661
+ dragged: true,
8662
+ from,
8663
+ to,
8664
+ dispatchMode: "trusted-input",
8665
+ pointerType: policy.pointerType,
8666
+ durationMs: input.durationMs,
8667
+ steps
8668
+ };
8669
+ }
8254
8670
  async function cdpPageType(session, data) {
8255
8671
  const { selector, text, clearFirst = true } = data;
8256
8672
  if (!selector || typeof selector !== "string") {
@@ -8571,6 +8987,7 @@ async function cdpPageHover(session, data) {
8571
8987
  }
8572
8988
  registerActions([
8573
8989
  { name: "page_click", session: "page", handler: cdpPageClick },
8990
+ { name: "page_drag", session: "page", handler: cdpPageDrag },
8574
8991
  { name: "page_type", session: "page", handler: cdpPageType },
8575
8992
  { name: "page_hover", session: "page", handler: cdpPageHover },
8576
8993
  { name: "page_scroll", session: "page", handler: cdpPageScroll },
@@ -9373,6 +9790,12 @@ function acquireLaunchLock(browserId) {
9373
9790
  };
9374
9791
  }
9375
9792
  async function launchManagedBrowser(input, deps) {
9793
+ if (input.reuse !== void 0 && input.reuse !== "never" && input.reuse !== "prefer" && input.reuse !== "require") {
9794
+ throw new Error('reuse must be "never", "prefer", or "require"');
9795
+ }
9796
+ if (input.reuse === "prefer" || input.reuse === "require") {
9797
+ throw new Error("reuse is currently supported only by the AgentBay browser provider");
9798
+ }
9376
9799
  if (input.imageId !== void 0) {
9377
9800
  throw new Error("imageId is only supported by the AgentBay browser provider");
9378
9801
  }
@@ -9477,7 +9900,11 @@ async function launchManagedBrowser(input, deps) {
9477
9900
  recordWritten = true;
9478
9901
  await waitForBrowserRegistration(browserId, hostPid);
9479
9902
  deps.log(`managed browser launched: ${browserId}, chrome=${chromePid}, host=${hostPid}`);
9480
- return browserSummary(record2, true);
9903
+ return {
9904
+ ...browserSummary(record2, true),
9905
+ created: true,
9906
+ reused: false
9907
+ };
9481
9908
  } catch (error48) {
9482
9909
  session?.close();
9483
9910
  if (hostPid) await stopProcess(hostPid, `--browser-id ${browserId}`);
@@ -9513,6 +9940,127 @@ import { spawn as spawn4 } from "node:child_process";
9513
9940
  import { homedir as homedir2 } from "node:os";
9514
9941
  import { dirname as dirname3, join as join4, parse, resolve } from "node:path";
9515
9942
  import { fileURLToPath } from "node:url";
9943
+
9944
+ // src/agentbay-lifecycle.ts
9945
+ var MIN_AGENTBAY_IDLE_RELEASE_MINUTES = 3;
9946
+ var DEFAULT_AGENTBAY_IDLE_RELEASE_MINUTES = 30;
9947
+ var DEFAULT_AGENTBAY_MAX_RUNTIME_MINUTES = 120;
9948
+ var DEFAULT_AGENTBAY_LIFECYCLE_CHECK_INTERVAL_MS = 6e4;
9949
+ function positiveIntegerMinutes(value, name, fallback) {
9950
+ if (value === void 0 || !value.trim()) return fallback;
9951
+ const minutes = Number(value);
9952
+ if (!Number.isInteger(minutes) || minutes <= 0) {
9953
+ throw new Error(`${name} must be a positive integer in minutes`);
9954
+ }
9955
+ return minutes;
9956
+ }
9957
+ function resolveAgentBayLifecycleConfig(env = process.env) {
9958
+ const idleReleaseTimeout = positiveIntegerMinutes(
9959
+ env.MEARL_AGENTBAY_IDLE_RELEASE_MINUTES,
9960
+ "MEARL_AGENTBAY_IDLE_RELEASE_MINUTES",
9961
+ DEFAULT_AGENTBAY_IDLE_RELEASE_MINUTES
9962
+ );
9963
+ const maxRuntime = positiveIntegerMinutes(
9964
+ env.MEARL_AGENTBAY_MAX_RUNTIME_MINUTES,
9965
+ "MEARL_AGENTBAY_MAX_RUNTIME_MINUTES",
9966
+ DEFAULT_AGENTBAY_MAX_RUNTIME_MINUTES
9967
+ );
9968
+ if (idleReleaseTimeout < MIN_AGENTBAY_IDLE_RELEASE_MINUTES) {
9969
+ throw new Error(
9970
+ `MEARL_AGENTBAY_IDLE_RELEASE_MINUTES must be at least ${MIN_AGENTBAY_IDLE_RELEASE_MINUTES}`
9971
+ );
9972
+ }
9973
+ if (maxRuntime < idleReleaseTimeout) {
9974
+ throw new Error(
9975
+ "MEARL_AGENTBAY_MAX_RUNTIME_MINUTES must be greater than or equal to MEARL_AGENTBAY_IDLE_RELEASE_MINUTES"
9976
+ );
9977
+ }
9978
+ return { idleReleaseTimeout, maxRuntime };
9979
+ }
9980
+ var AgentBaySessionWatchdog = class {
9981
+ idleTimeoutMs;
9982
+ checkIntervalMs;
9983
+ now;
9984
+ keepAlive;
9985
+ close;
9986
+ log;
9987
+ activeRequests = 0;
9988
+ lastActivityAt;
9989
+ timer = null;
9990
+ checking = false;
9991
+ closing = false;
9992
+ stopped = false;
9993
+ constructor(options) {
9994
+ if (!Number.isFinite(options.idleTimeoutMs) || options.idleTimeoutMs <= 0) {
9995
+ throw new Error("AgentBay idle timeout must be positive");
9996
+ }
9997
+ this.idleTimeoutMs = options.idleTimeoutMs;
9998
+ this.checkIntervalMs = options.checkIntervalMs ?? DEFAULT_AGENTBAY_LIFECYCLE_CHECK_INTERVAL_MS;
9999
+ if (!Number.isFinite(this.checkIntervalMs) || this.checkIntervalMs <= 0) {
10000
+ throw new Error("AgentBay lifecycle check interval must be positive");
10001
+ }
10002
+ this.now = options.now ?? Date.now;
10003
+ this.keepAlive = options.keepAlive;
10004
+ this.close = options.close;
10005
+ this.log = options.log;
10006
+ this.lastActivityAt = this.now();
10007
+ }
10008
+ start() {
10009
+ if (this.timer || this.stopped) return;
10010
+ this.timer = setInterval(() => void this.checkNow(), this.checkIntervalMs);
10011
+ this.timer.unref();
10012
+ }
10013
+ stop() {
10014
+ this.stopped = true;
10015
+ if (this.timer) {
10016
+ clearInterval(this.timer);
10017
+ this.timer = null;
10018
+ }
10019
+ }
10020
+ beginActivity() {
10021
+ if (this.stopped || this.closing) return false;
10022
+ this.activeRequests += 1;
10023
+ this.lastActivityAt = this.now();
10024
+ return true;
10025
+ }
10026
+ endActivity() {
10027
+ if (this.activeRequests > 0) this.activeRequests -= 1;
10028
+ this.lastActivityAt = this.now();
10029
+ }
10030
+ async checkNow() {
10031
+ if (this.stopped || this.checking || this.closing) return;
10032
+ this.checking = true;
10033
+ try {
10034
+ const idleForMs = this.now() - this.lastActivityAt;
10035
+ if (this.activeRequests > 0 || idleForMs < this.idleTimeoutMs) {
10036
+ try {
10037
+ await this.keepAlive();
10038
+ } catch (error48) {
10039
+ this.log(
10040
+ "AgentBay keepalive failed:",
10041
+ error48 instanceof Error ? error48.message : String(error48)
10042
+ );
10043
+ }
10044
+ return;
10045
+ }
10046
+ this.closing = true;
10047
+ this.log(`AgentBay browser reached the idle timeout after ${idleForMs}ms; closing`);
10048
+ try {
10049
+ await this.close();
10050
+ } catch (error48) {
10051
+ this.closing = false;
10052
+ this.log(
10053
+ "AgentBay idle close failed:",
10054
+ error48 instanceof Error ? error48.message : String(error48)
10055
+ );
10056
+ }
10057
+ } finally {
10058
+ this.checking = false;
10059
+ }
10060
+ }
10061
+ };
10062
+
10063
+ // src/agentbay-browser.ts
9516
10064
  var AGENTBAY_BROWSERS_SUBDIR = "agentbay-browsers";
9517
10065
  var DEFAULT_AGENTBAY_IMAGE_ID = "imgc-0aae4rgl8kvagdv1j";
9518
10066
  var sdkPromise = null;
@@ -9589,6 +10137,24 @@ function validateAgentBayInput(input) {
9589
10137
  throw new Error("TDBank login parameters are only supported by the local browser provider");
9590
10138
  }
9591
10139
  }
10140
+ function resolveReuseMode(value) {
10141
+ if (value === void 0) return "never";
10142
+ if (value === "never" || value === "prefer" || value === "require") return value;
10143
+ throw new Error('reuse must be "never", "prefer", or "require"');
10144
+ }
10145
+ function validateReusableAgentBayConfiguration(input, record2) {
10146
+ if (input.imageId !== void 0 && resolveAgentBayImageId(input.imageId) !== record2.imageId) {
10147
+ throw new Error(`Cannot change imageId while reusing AgentBay browser: ${record2.browserId}`);
10148
+ }
10149
+ if (input.persistent !== void 0 && input.persistent !== (record2.persistent === true)) {
10150
+ throw new Error(`Cannot change persistent while reusing AgentBay browser: ${record2.browserId}`);
10151
+ }
10152
+ if (input.userAgentMode !== void 0 && record2.userAgentMode !== void 0 && input.userAgentMode !== record2.userAgentMode) {
10153
+ throw new Error(
10154
+ `Cannot change userAgentMode while reusing AgentBay browser: ${record2.browserId}`
10155
+ );
10156
+ }
10157
+ }
9592
10158
  function resolveAgentBayImageId(value) {
9593
10159
  if (value === void 0) return DEFAULT_AGENTBAY_IMAGE_ID;
9594
10160
  const imageId = typeof value === "string" ? value.trim() : "";
@@ -9647,11 +10213,14 @@ function listRecords2() {
9647
10213
  }
9648
10214
  return records;
9649
10215
  }
9650
- function findRecord(selector) {
10216
+ function findRecordWithStatus(selector) {
9651
10217
  const normalized = selector.trim().toLowerCase();
9652
10218
  return listRecords2().find(
9653
10219
  ({ record: record2 }) => record2.browserId.toLowerCase() === normalized || record2.name.toLowerCase() === normalized
9654
- )?.record ?? null;
10220
+ ) ?? null;
10221
+ }
10222
+ function findRecord(selector) {
10223
+ return findRecordWithStatus(selector)?.record ?? null;
9655
10224
  }
9656
10225
  function hasAgentBayBrowser(selector) {
9657
10226
  return findRecord(selector) !== null;
@@ -9723,13 +10292,9 @@ async function keepAgentBaySessionAlive(sessionId) {
9723
10292
  const result = await session.keepAlive();
9724
10293
  if (result.success === false) throw new Error(result.errorMessage || "AgentBay keepalive failed");
9725
10294
  }
9726
- async function initializeAgentBayBrowserSession(session, input, cookies, options = {}) {
9727
- const initialized = await session.browser.initializeAsync(
9728
- options.userAgent ? { userAgent: options.userAgent } : {}
9729
- );
9730
- if (!initialized) throw new Error("AgentBay browser initialization failed");
10295
+ async function configureAgentBayBrowserSession(session, input, cookies, connectCdp = (url2) => CdpClient.connect(url2)) {
9731
10296
  const endpoint = await session.browser.getEndpointUrl();
9732
- const cdp = await (options.connectCdp ?? ((url2) => CdpClient.connect(url2)))(endpoint);
10297
+ const cdp = await connectCdp(endpoint);
9733
10298
  try {
9734
10299
  await cdp.sendCommand("Browser.getVersion", {}, 1e4);
9735
10300
  const copiedCookieCount = await cdpImportCookiesWithClient(cdp, cookies);
@@ -9741,32 +10306,76 @@ async function initializeAgentBayBrowserSession(session, input, cookies, options
9741
10306
  cdp.close();
9742
10307
  }
9743
10308
  }
10309
+ async function initializeAgentBayBrowserSession(session, input, cookies, options = {}) {
10310
+ const initialized = await session.browser.initializeAsync(
10311
+ options.userAgent ? { userAgent: options.userAgent } : {}
10312
+ );
10313
+ if (!initialized) throw new Error("AgentBay browser initialization failed");
10314
+ return configureAgentBayBrowserSession(session, input, cookies, options.connectCdp);
10315
+ }
10316
+ async function reuseAgentBayBrowserSession(session, input, cookies, options = {}) {
10317
+ return configureAgentBayBrowserSession(session, input, cookies, options.connectCdp);
10318
+ }
9744
10319
  async function launchAgentBayBrowser(input, deps) {
9745
10320
  const name = validateName(input.name);
9746
10321
  const browserId = agentBayBrowserId(name);
9747
10322
  validateAgentBayInput(input);
10323
+ const reuseMode = resolveReuseMode(input.reuse);
9748
10324
  const imageId = resolveAgentBayImageId(input.imageId);
9749
10325
  const persistent = input.persistent === true;
9750
10326
  const userAgentMode = resolveUserAgentMode(input.userAgentMode);
9751
10327
  const copyCookieDomains = input.copyCookieDomains === void 0 ? [] : normalizeCookieDomains(input.copyCookieDomains);
9752
10328
  const apiKey = (deps.resolveApiKey ?? resolveAgentBayApiKey)();
10329
+ const lifecycle = resolveAgentBayLifecycleConfig();
9753
10330
  const releaseLock = acquireLaunchLock(browserId);
9754
10331
  const persistBrowserRecord = deps.persistBrowserRecord ?? ((record2) => writeRecord(keyedConfigFile(AGENTBAY_BROWSERS_SUBDIR, record2.browserId), record2));
10332
+ const findBrowserRecord = deps.findBrowserRecord ?? findRecordWithStatus;
9755
10333
  let session = null;
9756
10334
  let context;
9757
10335
  let hostPid = 0;
9758
10336
  let recordWritten = false;
9759
10337
  try {
9760
- if (findRecord(browserId)) throw new Error(`AgentBay browser already exists: ${browserId}`);
10338
+ const existing = findBrowserRecord(browserId);
10339
+ if (existing) {
10340
+ if (reuseMode === "never") {
10341
+ throw new Error(`AgentBay browser already exists: ${browserId}`);
10342
+ }
10343
+ if (!existing.running) {
10344
+ throw new Error(`AgentBay browser is not running and cannot be reused: ${browserId}`);
10345
+ }
10346
+ validateReusableAgentBayConfiguration(input, existing.record);
10347
+ const cookies2 = copyCookieDomains.length > 0 ? await deps.exportCookies(copyCookieDomains) : [];
10348
+ const agentBay2 = await (deps.createAgentBayClient ?? createClient)(apiKey);
10349
+ const existingSession = await requireSessionFromClient(agentBay2, existing.record.sessionId);
10350
+ const copiedCookieCount2 = await reuseAgentBayBrowserSession(existingSession, input, cookies2, {
10351
+ connectCdp: deps.connectCdp
10352
+ });
10353
+ const record3 = {
10354
+ ...existing.record,
10355
+ ...copyCookieDomains.length > 0 ? { copiedCookies: { domains: copyCookieDomains, count: copiedCookieCount2 } } : {}
10356
+ };
10357
+ if (copyCookieDomains.length > 0) persistBrowserRecord(record3);
10358
+ deps.log(`AgentBay browser reused: ${browserId}, host=${record3.pid}`);
10359
+ return {
10360
+ ...browserSummary2(record3, true),
10361
+ created: false,
10362
+ reused: true
10363
+ };
10364
+ }
10365
+ if (reuseMode === "require") {
10366
+ throw new Error(`AgentBay browser is not running and reuse=require: ${browserId}`);
10367
+ }
9761
10368
  const [cookies, userAgent] = await Promise.all([
9762
10369
  copyCookieDomains.length > 0 ? deps.exportCookies(copyCookieDomains) : Promise.resolve([]),
9763
10370
  userAgentMode === "desktop" ? deps.getBrowserUserAgent().then(normalizeDesktopUserAgent) : Promise.resolve(void 0)
9764
10371
  ]);
9765
10372
  const agentBay = await (deps.createAgentBayClient ?? createClient)(apiKey);
9766
10373
  context = persistent ? await resolvePersistentContext(agentBay, browserId) : void 0;
10374
+ const { LifecyclePolicy } = await loadSdk();
9767
10375
  const created = await agentBay.create({
9768
10376
  imageId,
9769
10377
  framework: "mearl",
10378
+ lifecyclePolicy: new LifecyclePolicy(lifecycle),
9770
10379
  ...context ? {
9771
10380
  browserContext: {
9772
10381
  contextId: context.id,
@@ -9780,7 +10389,8 @@ async function launchAgentBayBrowser(input, deps) {
9780
10389
  }
9781
10390
  session = created.session;
9782
10391
  const copiedCookieCount = await initializeAgentBayBrowserSession(session, input, cookies, {
9783
- userAgent
10392
+ userAgent,
10393
+ connectCdp: deps.connectCdp
9784
10394
  });
9785
10395
  const host = spawn4(
9786
10396
  process.execPath,
@@ -9792,7 +10402,9 @@ async function launchAgentBayBrowser(input, deps) {
9792
10402
  "--browser-id",
9793
10403
  browserId,
9794
10404
  "--browser-name",
9795
- name
10405
+ name,
10406
+ "--agentbay-idle-release-minutes",
10407
+ String(lifecycle.idleReleaseTimeout)
9796
10408
  ],
9797
10409
  {
9798
10410
  detached: true,
@@ -9813,6 +10425,7 @@ async function launchAgentBayBrowser(input, deps) {
9813
10425
  name,
9814
10426
  sessionId: session.sessionId,
9815
10427
  imageId,
10428
+ userAgentMode,
9816
10429
  persistent,
9817
10430
  ...context ? { contextId: context.id, contextName: context.name } : {},
9818
10431
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -9822,7 +10435,11 @@ async function launchAgentBayBrowser(input, deps) {
9822
10435
  recordWritten = true;
9823
10436
  await waitForBrowserRegistration(browserId, hostPid);
9824
10437
  deps.log(`AgentBay browser launched: ${browserId}, host=${hostPid}`);
9825
- return browserSummary2(record2, true);
10438
+ return {
10439
+ ...browserSummary2(record2, true),
10440
+ created: true,
10441
+ reused: false
10442
+ };
9826
10443
  } catch (error48) {
9827
10444
  if (hostPid) await stopProcess(hostPid, `--browser-id ${browserId}`);
9828
10445
  if (recordWritten) {
@@ -9846,6 +10463,7 @@ async function launchAgentBayBrowser(input, deps) {
9846
10463
  name,
9847
10464
  sessionId: session.sessionId,
9848
10465
  imageId,
10466
+ userAgentMode,
9849
10467
  persistent,
9850
10468
  ...context ? { contextId: context.id, contextName: context.name } : {},
9851
10469
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -9940,31 +10558,38 @@ async function releaseAgentBayBrowserResources(record2, agentBay, options = {})
9940
10558
  };
9941
10559
  }
9942
10560
  async function closeAgentBayBrowser(selector, options = {}) {
9943
- const record2 = findRecord(selector);
10561
+ let record2 = findRecord(selector);
9944
10562
  if (!record2) throw new Error(`AgentBay browser not found: ${selector}`);
9945
10563
  const agentBay = await createClient(resolveAgentBayApiKey());
9946
- const released = await releaseAgentBayBrowserResources(record2, agentBay, {
9947
- ...options,
9948
- persistRecord: (updatedRecord) => writeRecord(
9949
- keyedConfigFile(AGENTBAY_BROWSERS_SUBDIR, updatedRecord.browserId),
9950
- updatedRecord
9951
- )
9952
- });
9953
- const closingOwnHost = record2.pid === process.pid;
9954
- if (!closingOwnHost) await stopProcess(record2.pid, `--browser-id ${record2.browserId}`);
9955
- removeRecord(keyedConfigFile(AGENTBAY_BROWSERS_SUBDIR, record2.browserId));
9956
- removeRecord(keyedConfigFile("browsers", record2.browserId));
9957
- if (closingOwnHost) {
9958
- const timer = setTimeout(() => process.kill(process.pid, "SIGTERM"), 250);
9959
- timer.unref();
10564
+ const releaseLock = acquireLaunchLock(record2.browserId);
10565
+ try {
10566
+ record2 = findRecord(record2.browserId);
10567
+ if (!record2) throw new Error(`AgentBay browser not found: ${selector}`);
10568
+ const released = await releaseAgentBayBrowserResources(record2, agentBay, {
10569
+ ...options,
10570
+ persistRecord: (updatedRecord) => writeRecord(
10571
+ keyedConfigFile(AGENTBAY_BROWSERS_SUBDIR, updatedRecord.browserId),
10572
+ updatedRecord
10573
+ )
10574
+ });
10575
+ const closingOwnHost = record2.pid === process.pid;
10576
+ if (!closingOwnHost) await stopProcess(record2.pid, `--browser-id ${record2.browserId}`);
10577
+ removeRecord(keyedConfigFile(AGENTBAY_BROWSERS_SUBDIR, record2.browserId));
10578
+ removeRecord(keyedConfigFile("browsers", record2.browserId));
10579
+ if (closingOwnHost) {
10580
+ const timer = setTimeout(() => process.kill(process.pid, "SIGTERM"), 250);
10581
+ timer.unref();
10582
+ }
10583
+ return {
10584
+ browserId: record2.browserId,
10585
+ provider: "agentbay",
10586
+ closed: true,
10587
+ ...released,
10588
+ ...record2.persistent === true ? { profileDeleted: options.deleteProfile === true } : { profileDeleted: true }
10589
+ };
10590
+ } finally {
10591
+ releaseLock();
9960
10592
  }
9961
- return {
9962
- browserId: record2.browserId,
9963
- provider: "agentbay",
9964
- closed: true,
9965
- ...released,
9966
- ...record2.persistent === true ? { profileDeleted: options.deleteProfile === true } : { profileDeleted: true }
9967
- };
9968
10593
  }
9969
10594
 
9970
10595
  // src/browser-registry.ts
@@ -10163,7 +10788,7 @@ import os2 from "node:os";
10163
10788
  import { randomUUID as randomUUID4 } from "node:crypto";
10164
10789
 
10165
10790
  // src/version.ts
10166
- var NATIVE_HOST_VERSION = true ? "2.6.1" : "unknown";
10791
+ var NATIVE_HOST_VERSION = true ? "2.7.1" : "unknown";
10167
10792
 
10168
10793
  // src/analytics.ts
10169
10794
  var MEASUREMENT_ID = "G-W84LJDZ6PH";
@@ -30153,6 +30778,7 @@ var AgentSkillManager = class {
30153
30778
  "--agent",
30154
30779
  providerId,
30155
30780
  scope === "global" ? "--global" : "--project",
30781
+ "--full-depth",
30156
30782
  "--yes"
30157
30783
  ];
30158
30784
  const output = await this.run(executable, args, cwd);
@@ -30229,7 +30855,7 @@ function createExtensionRequestMessage(id, action, data, trustedInternal = false
30229
30855
  id,
30230
30856
  action,
30231
30857
  data,
30232
- controlSource,
30858
+ controlSource: toControlSourceWireValue(controlSource),
30233
30859
  ...trustedInternal ? { trustedInternal: true } : {}
30234
30860
  };
30235
30861
  }
@@ -30654,9 +31280,22 @@ function positivePort(value, fallback) {
30654
31280
  }
30655
31281
  return port;
30656
31282
  }
31283
+ function agentBayIdleReleaseMinutes(value) {
31284
+ if (value === void 0) return void 0;
31285
+ const minutes = Number(value);
31286
+ if (!Number.isInteger(minutes) || minutes < MIN_AGENTBAY_IDLE_RELEASE_MINUTES) {
31287
+ throw new Error(
31288
+ `Invalid --agentbay-idle-release-minutes: ${value}. Expected an integer of at least ${MIN_AGENTBAY_IDLE_RELEASE_MINUTES}.`
31289
+ );
31290
+ }
31291
+ return minutes;
31292
+ }
30657
31293
  var CDP_PORT = positivePort(cliOption("--cdp-port"), 9222);
30658
31294
  var CDP_USER_DATA_DIR = cliOption("--cdp-user-data-dir");
30659
31295
  var AGENTBAY_SESSION_ID = cliOption("--agentbay-session-id");
31296
+ var AGENTBAY_IDLE_RELEASE_MINUTES = agentBayIdleReleaseMinutes(
31297
+ cliOption("--agentbay-idle-release-minutes")
31298
+ );
30660
31299
  var CDP_BROWSER_ID = cliOption("--browser-id") ?? "cdp";
30661
31300
  var CDP_BROWSER_NAME = cliOption("--browser-name") ?? "CDP";
30662
31301
  if (cliArg === "--init" || cliArg === "--uninstall") {
@@ -30684,6 +31323,8 @@ Options:
30684
31323
  --cdp-port <port> CDP port for daemon mode (default 9222)
30685
31324
  --cdp-user-data-dir <dir> Read DevToolsActivePort from this Chrome profile
30686
31325
  --agentbay-session-id <id> Connect to an AgentBay browser session over CDP
31326
+ --agentbay-idle-release-minutes <minutes>
31327
+ Close an idle AgentBay browser (minimum 3 minutes)
30687
31328
  --browser-id <id> Registry id for this CDP browser
30688
31329
  --browser-name <name> Human-readable registry name
30689
31330
  --version, -v Show version
@@ -30818,6 +31459,7 @@ var socketServer = null;
30818
31459
  var legacyServer = null;
30819
31460
  var inventoryExitTimer = null;
30820
31461
  var cdpRegistrationTimer = null;
31462
+ var agentBaySessionWatchdog = null;
30821
31463
  function scheduleInventoryExit() {
30822
31464
  if (inventoryExitTimer) clearTimeout(inventoryExitTimer);
30823
31465
  inventoryExitTimer = setTimeout(() => cleanupAndExit(0), INVENTORY_IDLE_EXIT_MS);
@@ -31085,7 +31727,7 @@ function toTimeoutMs(action, data) {
31085
31727
  }
31086
31728
  return DEFAULT_TIMEOUT_MS;
31087
31729
  }
31088
- async function handleSocketLine(socket, line) {
31730
+ async function handleSocketRequest(socket, line) {
31089
31731
  let request;
31090
31732
  try {
31091
31733
  request = JSON.parse(line);
@@ -31096,7 +31738,7 @@ async function handleSocketLine(socket, line) {
31096
31738
  return;
31097
31739
  }
31098
31740
  const { id = "", action, data = {}, version: clientVersion } = request;
31099
- const controlSource = request.controlSource === "cloud" ? "cloud" : "local";
31741
+ const controlSource = normalizeControlSource(request.controlSource);
31100
31742
  if (action === "start_cloud_connector") {
31101
31743
  const serverUrl = data?.serverUrl;
31102
31744
  if (!serverUrl || typeof serverUrl !== "string") {
@@ -31386,6 +32028,33 @@ async function handleSocketLine(socket, line) {
31386
32028
  socket.write(JSON.stringify({ id, success: false, error: message }) + "\n");
31387
32029
  }
31388
32030
  }
32031
+ async function handleSocketLine(socket, line) {
32032
+ const watchdog = agentBaySessionWatchdog;
32033
+ if (!watchdog) {
32034
+ await handleSocketRequest(socket, line);
32035
+ return;
32036
+ }
32037
+ if (!watchdog.beginActivity()) {
32038
+ let id = "";
32039
+ try {
32040
+ id = JSON.parse(line)?.id ?? "";
32041
+ } catch {
32042
+ }
32043
+ socket.write(
32044
+ JSON.stringify({
32045
+ id,
32046
+ success: false,
32047
+ error: "AgentBay browser is closing after reaching its idle timeout"
32048
+ }) + "\n"
32049
+ );
32050
+ return;
32051
+ }
32052
+ try {
32053
+ await handleSocketRequest(socket, line);
32054
+ } finally {
32055
+ watchdog.endActivity();
32056
+ }
32057
+ }
31389
32058
  async function handleViaCdp(action, data, _timeoutMs, controlSource) {
31390
32059
  if (action === "set_rule" || action === "get_rules") {
31391
32060
  throw new Error(
@@ -31544,6 +32213,8 @@ function cleanupAndExit(code) {
31544
32213
  clearInterval(cdpRegistrationTimer);
31545
32214
  cdpRegistrationTimer = null;
31546
32215
  }
32216
+ agentBaySessionWatchdog?.stop();
32217
+ agentBaySessionWatchdog = null;
31547
32218
  cdpSession?.close();
31548
32219
  agentChatManager?.dispose();
31549
32220
  agentChatManager = null;
@@ -31630,15 +32301,17 @@ async function main() {
31630
32301
  await monitorDaemonBrowser();
31631
32302
  }
31632
32303
  if (AGENTBAY_SESSION_ID) {
31633
- const keepAliveTimer = setInterval(() => {
31634
- keepAgentBaySessionAlive(AGENTBAY_SESSION_ID).catch((error48) => {
31635
- log(
31636
- `AgentBay keepalive failed for ${AGENTBAY_SESSION_ID}:`,
31637
- error48 instanceof Error ? error48.message : String(error48)
31638
- );
31639
- });
31640
- }, 6e4);
31641
- keepAliveTimer.unref();
32304
+ const idleReleaseMinutes = AGENTBAY_IDLE_RELEASE_MINUTES ?? resolveAgentBayLifecycleConfig().idleReleaseTimeout;
32305
+ agentBaySessionWatchdog = new AgentBaySessionWatchdog({
32306
+ idleTimeoutMs: idleReleaseMinutes * 6e4,
32307
+ keepAlive: () => keepAgentBaySessionAlive(AGENTBAY_SESSION_ID),
32308
+ close: async () => {
32309
+ await closeAgentBayBrowser(CDP_BROWSER_ID);
32310
+ },
32311
+ log
32312
+ });
32313
+ agentBaySessionWatchdog.start();
32314
+ log(`AgentBay idle close enabled for ${AGENTBAY_SESSION_ID}: ${idleReleaseMinutes} minutes`);
31642
32315
  }
31643
32316
  process.on("SIGINT", () => cleanupAndExit(0));
31644
32317
  process.on("SIGTERM", () => cleanupAndExit(0));
@@ -1,8 +1,9 @@
1
+ import { type ControlSource, type ControlSourceWireValue } from '@mearl/browser-core';
1
2
  export declare function isInternalExtensionAction(action: unknown): boolean;
2
- export declare function createExtensionRequestMessage(id: string, action: string, data: any, trustedInternal?: boolean, controlSource?: 'local' | 'cloud'): {
3
+ export declare function createExtensionRequestMessage(id: string, action: string, data: any, trustedInternal?: boolean, controlSource?: ControlSource): {
3
4
  id: string;
4
5
  action: string;
5
6
  data: any;
6
- controlSource: 'local' | 'cloud';
7
+ controlSource: ControlSourceWireValue;
7
8
  trustedInternal?: true;
8
9
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mearl/native-host",
3
- "version": "2.6.1",
3
+ "version": "2.7.1",
4
4
  "description": "Native Messaging Host for Mearl — bridges Chrome Extension and local socket server",
5
5
  "type": "module",
6
6
  "main": "dist/install.js",
@@ -37,9 +37,9 @@
37
37
  "@types/ws": "^8.18.1",
38
38
  "esbuild": "^0.25.2",
39
39
  "typescript": "^5.8.3",
40
- "@mearl/browser-core": "2.6.1",
41
- "@mearl/setup": "2.6.1",
42
- "@mearl/daemon-core": "2.6.1"
40
+ "@mearl/browser-core": "2.7.1",
41
+ "@mearl/daemon-core": "2.7.1",
42
+ "@mearl/setup": "2.7.1"
43
43
  },
44
44
  "scripts": {
45
45
  "build": "rm -rf dist/ && node build.mjs",