@mercuryo-ai/agentbrowse 0.2.60 → 0.2.61
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 +30 -5
- package/dist/browser-session-state.d.ts +39 -0
- package/dist/browser-session-state.d.ts.map +1 -1
- package/dist/browser-session-state.js +63 -1
- package/dist/commands/act.d.ts.map +1 -1
- package/dist/commands/act.js +539 -535
- package/dist/commands/attach.d.ts.map +1 -1
- package/dist/commands/attach.js +5 -10
- package/dist/commands/browser-connection-failure.d.ts +9 -0
- package/dist/commands/browser-connection-failure.d.ts.map +1 -0
- package/dist/commands/browser-connection-failure.js +15 -0
- package/dist/commands/browser-status.d.ts.map +1 -1
- package/dist/commands/browser-status.js +26 -30
- package/dist/commands/close.d.ts.map +1 -1
- package/dist/commands/close.js +5 -0
- package/dist/commands/extract.d.ts.map +1 -1
- package/dist/commands/extract.js +147 -144
- package/dist/commands/launch.d.ts.map +1 -1
- package/dist/commands/launch.js +11 -8
- package/dist/commands/navigate.d.ts.map +1 -1
- package/dist/commands/navigate.js +79 -73
- package/dist/commands/observe-inventory.d.ts +1 -0
- package/dist/commands/observe-inventory.d.ts.map +1 -1
- package/dist/commands/observe-inventory.js +15 -3
- package/dist/commands/observe.d.ts.map +1 -1
- package/dist/commands/observe.js +260 -272
- package/dist/commands/screenshot.d.ts.map +1 -1
- package/dist/commands/screenshot.js +50 -64
- package/dist/library.d.ts +2 -1
- package/dist/library.d.ts.map +1 -1
- package/dist/library.js +2 -1
- package/dist/protected-fill.d.ts.map +1 -1
- package/dist/protected-fill.js +46 -7
- package/dist/sticky-owner-host-entry.d.ts +2 -0
- package/dist/sticky-owner-host-entry.d.ts.map +1 -0
- package/dist/sticky-owner-host-entry.js +97 -0
- package/dist/sticky-owner.d.ts +15 -0
- package/dist/sticky-owner.d.ts.map +1 -0
- package/dist/sticky-owner.js +431 -0
- package/docs/configuration.md +36 -4
- package/docs/getting-started.md +28 -4
- package/docs/troubleshooting.md +42 -6
- package/package.json +1 -1
- package/dist/protected-fill-browser.d.ts +0 -22
- package/dist/protected-fill-browser.d.ts.map +0 -1
- package/dist/protected-fill-browser.js +0 -52
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import { existsSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { chromium } from 'playwright-core';
|
|
8
|
+
import { buildStickyOwnerMetadata, isSessionAlive, resolveBrowserSessionId, } from './browser-session-state.js';
|
|
9
|
+
import { connectPlaywright, disconnectPlaywright } from './playwright-runtime.js';
|
|
10
|
+
const HOST_READY_TIMEOUT_MS = 5_000;
|
|
11
|
+
const HOST_READY_POLL_MS = 50;
|
|
12
|
+
const HOST_CONNECT_TIMEOUT_MS = 3_000;
|
|
13
|
+
const HOST_STOP_TIMEOUT_MS = 1_000;
|
|
14
|
+
const DEFAULT_STICKY_OWNER_TTL_MS = 30 * 60 * 1_000;
|
|
15
|
+
const STICKY_OWNER_HEARTBEAT_FLOOR_MS = 5_000;
|
|
16
|
+
const STICKY_OWNER_HEARTBEAT_CEILING_MS = 60_000;
|
|
17
|
+
const inProcessOwners = new WeakMap();
|
|
18
|
+
function resolveStickyOwnerBootstrapMode() {
|
|
19
|
+
const explicit = process.env.AGENTBROWSE_STICKY_OWNER_MODE?.trim().toLowerCase();
|
|
20
|
+
if (explicit === 'detached_process') {
|
|
21
|
+
return 'detached_process';
|
|
22
|
+
}
|
|
23
|
+
if (explicit === 'in_process') {
|
|
24
|
+
return 'in_process';
|
|
25
|
+
}
|
|
26
|
+
if (process.env.VITEST) {
|
|
27
|
+
return 'in_process';
|
|
28
|
+
}
|
|
29
|
+
return 'detached_process';
|
|
30
|
+
}
|
|
31
|
+
function createHostId() {
|
|
32
|
+
return `owner_${randomUUID().replace(/-/g, '')}`;
|
|
33
|
+
}
|
|
34
|
+
function resolveStickyOwnerTtlMs() {
|
|
35
|
+
const raw = process.env.AGENTBROWSE_STICKY_OWNER_TTL_MS?.trim();
|
|
36
|
+
if (!raw) {
|
|
37
|
+
return DEFAULT_STICKY_OWNER_TTL_MS;
|
|
38
|
+
}
|
|
39
|
+
const ttlMs = Number(raw);
|
|
40
|
+
if (!Number.isFinite(ttlMs) || ttlMs <= 0) {
|
|
41
|
+
return DEFAULT_STICKY_OWNER_TTL_MS;
|
|
42
|
+
}
|
|
43
|
+
return ttlMs;
|
|
44
|
+
}
|
|
45
|
+
function getStickyOwnerTouchPath(hostId) {
|
|
46
|
+
return join(tmpdir(), `agentbrowse-sticky-owner-${hostId}.touch`);
|
|
47
|
+
}
|
|
48
|
+
function buildInProcessStickyOwner(session, options = {}) {
|
|
49
|
+
return buildStickyOwnerMetadata({
|
|
50
|
+
hostId: options.hostId ?? session.stickyOwner?.hostId ?? createHostId(),
|
|
51
|
+
state: options.state ?? 'active',
|
|
52
|
+
startedAt: options.startedAt ?? session.stickyOwner?.startedAt ?? new Date().toISOString(),
|
|
53
|
+
browserSessionId: resolveBrowserSessionId(session),
|
|
54
|
+
transport: {
|
|
55
|
+
type: 'in_process',
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
function readStickyOwnerLastUsedMs(stickyOwner) {
|
|
60
|
+
if (stickyOwner.touchPath) {
|
|
61
|
+
try {
|
|
62
|
+
if (existsSync(stickyOwner.touchPath)) {
|
|
63
|
+
return statSync(stickyOwner.touchPath).mtimeMs;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// Fall through to metadata timestamps.
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const candidate = stickyOwner.lastUsedAt ?? stickyOwner.startedAt;
|
|
71
|
+
const parsed = Date.parse(candidate);
|
|
72
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
73
|
+
}
|
|
74
|
+
function isStickyOwnerExpired(stickyOwner) {
|
|
75
|
+
if (stickyOwner.transport.type !== 'playwright_bind') {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
const ttlMs = stickyOwner.ttlMs ?? DEFAULT_STICKY_OWNER_TTL_MS;
|
|
79
|
+
return Date.now() - readStickyOwnerLastUsedMs(stickyOwner) >= ttlMs;
|
|
80
|
+
}
|
|
81
|
+
function removeStickyOwnerTouchFile(stickyOwner) {
|
|
82
|
+
if (!stickyOwner?.touchPath) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
rmSync(stickyOwner.touchPath, { force: true });
|
|
86
|
+
}
|
|
87
|
+
function touchStickyOwnerLease(session) {
|
|
88
|
+
if (!session.stickyOwner || session.stickyOwner.transport.type !== 'playwright_bind') {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const touchedAt = new Date().toISOString();
|
|
92
|
+
if (session.stickyOwner.touchPath) {
|
|
93
|
+
writeFileSync(session.stickyOwner.touchPath, touchedAt);
|
|
94
|
+
}
|
|
95
|
+
session.stickyOwner = {
|
|
96
|
+
...session.stickyOwner,
|
|
97
|
+
lastUsedAt: touchedAt,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function startStickyOwnerLeaseHeartbeat(session) {
|
|
101
|
+
if (!session.stickyOwner || session.stickyOwner.transport.type !== 'playwright_bind') {
|
|
102
|
+
return () => undefined;
|
|
103
|
+
}
|
|
104
|
+
const ttlMs = session.stickyOwner.ttlMs ?? DEFAULT_STICKY_OWNER_TTL_MS;
|
|
105
|
+
const heartbeatMs = Math.max(STICKY_OWNER_HEARTBEAT_FLOOR_MS, Math.min(Math.floor(ttlMs / 3), STICKY_OWNER_HEARTBEAT_CEILING_MS));
|
|
106
|
+
const timer = setInterval(() => {
|
|
107
|
+
try {
|
|
108
|
+
touchStickyOwnerLease(session);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// Best-effort heartbeat; restore path will handle stale or dead owners later.
|
|
112
|
+
}
|
|
113
|
+
}, heartbeatMs);
|
|
114
|
+
timer.unref?.();
|
|
115
|
+
return () => clearInterval(timer);
|
|
116
|
+
}
|
|
117
|
+
function markStickyOwnerState(session, state) {
|
|
118
|
+
if (!session.stickyOwner) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
session.stickyOwner = {
|
|
122
|
+
...session.stickyOwner,
|
|
123
|
+
state,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function isProcessAlive(pid) {
|
|
127
|
+
if (typeof pid !== 'number' || !Number.isFinite(pid) || pid <= 0) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
try {
|
|
131
|
+
process.kill(pid, 0);
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
async function sleep(ms) {
|
|
139
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
140
|
+
}
|
|
141
|
+
async function waitForProcessExit(pid, timeoutMs) {
|
|
142
|
+
const deadline = Date.now() + timeoutMs;
|
|
143
|
+
while (Date.now() < deadline) {
|
|
144
|
+
if (!isProcessAlive(pid)) {
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
await sleep(50);
|
|
148
|
+
}
|
|
149
|
+
return !isProcessAlive(pid);
|
|
150
|
+
}
|
|
151
|
+
async function canConnectToStickyOwnerEndpoint(endpoint) {
|
|
152
|
+
let browser = null;
|
|
153
|
+
try {
|
|
154
|
+
browser = await chromium.connect(endpoint, {
|
|
155
|
+
timeout: HOST_CONNECT_TIMEOUT_MS,
|
|
156
|
+
});
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
finally {
|
|
163
|
+
if (browser) {
|
|
164
|
+
await disconnectPlaywright(browser);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
function getStickyOwnerHostEntryPath() {
|
|
169
|
+
const currentPath = fileURLToPath(import.meta.url);
|
|
170
|
+
const suffix = currentPath.endsWith('.ts') ? '.ts' : '.js';
|
|
171
|
+
return fileURLToPath(new URL(`./sticky-owner-host-entry${suffix}`, import.meta.url));
|
|
172
|
+
}
|
|
173
|
+
function getStickyOwnerHostSpawnArgs(entryPath, manifestPath, cdpUrl, hostId, browserSessionId, touchPath, ttlMs) {
|
|
174
|
+
const args = [
|
|
175
|
+
entryPath,
|
|
176
|
+
'--manifest',
|
|
177
|
+
manifestPath,
|
|
178
|
+
'--cdp-url',
|
|
179
|
+
cdpUrl,
|
|
180
|
+
'--host-id',
|
|
181
|
+
hostId,
|
|
182
|
+
'--browser-session-id',
|
|
183
|
+
browserSessionId,
|
|
184
|
+
'--touch-path',
|
|
185
|
+
touchPath,
|
|
186
|
+
'--ttl-ms',
|
|
187
|
+
String(ttlMs),
|
|
188
|
+
];
|
|
189
|
+
if (entryPath.endsWith('.ts')) {
|
|
190
|
+
return ['--import', 'tsx', ...args];
|
|
191
|
+
}
|
|
192
|
+
return args;
|
|
193
|
+
}
|
|
194
|
+
async function waitForStickyOwnerManifest(manifestPath) {
|
|
195
|
+
const deadline = Date.now() + HOST_READY_TIMEOUT_MS;
|
|
196
|
+
while (Date.now() < deadline) {
|
|
197
|
+
if (existsSync(manifestPath)) {
|
|
198
|
+
const payload = JSON.parse(readFileSync(manifestPath, 'utf-8'));
|
|
199
|
+
if (typeof payload.endpoint === 'string' &&
|
|
200
|
+
payload.endpoint.trim().length > 0 &&
|
|
201
|
+
typeof payload.pid === 'number' &&
|
|
202
|
+
Number.isFinite(payload.pid) &&
|
|
203
|
+
payload.pid > 0 &&
|
|
204
|
+
typeof payload.startedAt === 'string' &&
|
|
205
|
+
payload.startedAt.trim().length > 0) {
|
|
206
|
+
rmSync(manifestPath, { force: true });
|
|
207
|
+
return {
|
|
208
|
+
endpoint: payload.endpoint,
|
|
209
|
+
pid: payload.pid,
|
|
210
|
+
startedAt: payload.startedAt,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
await sleep(HOST_READY_POLL_MS);
|
|
215
|
+
}
|
|
216
|
+
throw new Error('sticky_owner_bootstrap_timeout');
|
|
217
|
+
}
|
|
218
|
+
async function bootstrapDetachedStickyOwner(session) {
|
|
219
|
+
const hostId = session.stickyOwner?.hostId ?? createHostId();
|
|
220
|
+
const browserSessionId = resolveBrowserSessionId(session);
|
|
221
|
+
const ttlMs = session.stickyOwner?.ttlMs ?? resolveStickyOwnerTtlMs();
|
|
222
|
+
const manifestPath = join(tmpdir(), `agentbrowse-sticky-owner-${hostId}.json`);
|
|
223
|
+
const touchPath = session.stickyOwner?.touchPath ?? getStickyOwnerTouchPath(hostId);
|
|
224
|
+
const touchedAt = new Date().toISOString();
|
|
225
|
+
writeFileSync(touchPath, touchedAt);
|
|
226
|
+
const entryPath = getStickyOwnerHostEntryPath();
|
|
227
|
+
const child = spawn(process.execPath, getStickyOwnerHostSpawnArgs(entryPath, manifestPath, session.cdpUrl, hostId, browserSessionId, touchPath, ttlMs), {
|
|
228
|
+
detached: true,
|
|
229
|
+
stdio: 'ignore',
|
|
230
|
+
});
|
|
231
|
+
child.unref();
|
|
232
|
+
try {
|
|
233
|
+
const ready = await waitForStickyOwnerManifest(manifestPath);
|
|
234
|
+
return buildStickyOwnerMetadata({
|
|
235
|
+
hostId,
|
|
236
|
+
state: 'active',
|
|
237
|
+
startedAt: ready.startedAt,
|
|
238
|
+
lastUsedAt: touchedAt,
|
|
239
|
+
ttlMs,
|
|
240
|
+
touchPath,
|
|
241
|
+
browserSessionId,
|
|
242
|
+
pid: ready.pid,
|
|
243
|
+
transport: {
|
|
244
|
+
type: 'playwright_bind',
|
|
245
|
+
endpoint: ready.endpoint,
|
|
246
|
+
mode: 'pipe',
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
catch (error) {
|
|
251
|
+
if (isProcessAlive(child.pid)) {
|
|
252
|
+
process.kill(child.pid, 'SIGTERM');
|
|
253
|
+
await waitForProcessExit(child.pid, HOST_STOP_TIMEOUT_MS).catch(() => undefined);
|
|
254
|
+
}
|
|
255
|
+
rmSync(manifestPath, { force: true });
|
|
256
|
+
rmSync(touchPath, { force: true });
|
|
257
|
+
throw error;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
async function ensureInProcessOwner(session) {
|
|
261
|
+
const existing = inProcessOwners.get(session);
|
|
262
|
+
if (existing) {
|
|
263
|
+
return existing;
|
|
264
|
+
}
|
|
265
|
+
const browser = await connectPlaywright(session.cdpUrl);
|
|
266
|
+
const owner = {
|
|
267
|
+
browser,
|
|
268
|
+
hostId: session.stickyOwner?.hostId ?? createHostId(),
|
|
269
|
+
startedAt: session.stickyOwner?.startedAt ?? new Date().toISOString(),
|
|
270
|
+
};
|
|
271
|
+
inProcessOwners.set(session, owner);
|
|
272
|
+
session.stickyOwner = buildInProcessStickyOwner(session, {
|
|
273
|
+
hostId: owner.hostId,
|
|
274
|
+
startedAt: owner.startedAt,
|
|
275
|
+
});
|
|
276
|
+
return owner;
|
|
277
|
+
}
|
|
278
|
+
export async function initializeBrowserSessionOwner(session) {
|
|
279
|
+
if (resolveStickyOwnerBootstrapMode() === 'in_process') {
|
|
280
|
+
session.stickyOwner = buildInProcessStickyOwner(session, {
|
|
281
|
+
state: 'active',
|
|
282
|
+
});
|
|
283
|
+
return session;
|
|
284
|
+
}
|
|
285
|
+
session.stickyOwner = await bootstrapDetachedStickyOwner(session);
|
|
286
|
+
return session;
|
|
287
|
+
}
|
|
288
|
+
export async function terminateBrowserSessionOwner(session) {
|
|
289
|
+
if (!session?.stickyOwner) {
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
if (session.stickyOwner.transport.type === 'in_process') {
|
|
293
|
+
const owner = inProcessOwners.get(session);
|
|
294
|
+
inProcessOwners.delete(session);
|
|
295
|
+
if (owner) {
|
|
296
|
+
await disconnectPlaywright(owner.browser);
|
|
297
|
+
}
|
|
298
|
+
markStickyOwnerState(session, 'dead');
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
const ownerPid = session.stickyOwner.pid;
|
|
302
|
+
if (isProcessAlive(ownerPid)) {
|
|
303
|
+
process.kill(ownerPid, 'SIGTERM');
|
|
304
|
+
const exited = await waitForProcessExit(ownerPid, HOST_STOP_TIMEOUT_MS);
|
|
305
|
+
if (!exited && isProcessAlive(ownerPid)) {
|
|
306
|
+
process.kill(ownerPid, 'SIGKILL');
|
|
307
|
+
await waitForProcessExit(ownerPid, HOST_STOP_TIMEOUT_MS).catch(() => undefined);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
removeStickyOwnerTouchFile(session.stickyOwner);
|
|
311
|
+
markStickyOwnerState(session, 'dead');
|
|
312
|
+
}
|
|
313
|
+
export async function restoreBrowserSessionOwner(session) {
|
|
314
|
+
const stickyOwner = session.stickyOwner;
|
|
315
|
+
if (!stickyOwner) {
|
|
316
|
+
if (resolveStickyOwnerBootstrapMode() === 'in_process') {
|
|
317
|
+
try {
|
|
318
|
+
await ensureInProcessOwner(session);
|
|
319
|
+
return {
|
|
320
|
+
success: true,
|
|
321
|
+
session,
|
|
322
|
+
restored: true,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
catch {
|
|
326
|
+
return {
|
|
327
|
+
success: false,
|
|
328
|
+
reason: 'sticky_owner_unrecoverable',
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
if (!(await isSessionAlive(session))) {
|
|
333
|
+
return {
|
|
334
|
+
success: false,
|
|
335
|
+
reason: 'sticky_owner_unrecoverable',
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
try {
|
|
339
|
+
session.stickyOwner = await bootstrapDetachedStickyOwner(session);
|
|
340
|
+
return {
|
|
341
|
+
success: true,
|
|
342
|
+
session,
|
|
343
|
+
restored: true,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
catch {
|
|
347
|
+
return {
|
|
348
|
+
success: false,
|
|
349
|
+
reason: 'sticky_owner_unrecoverable',
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
if (stickyOwner.transport.type === 'in_process') {
|
|
354
|
+
try {
|
|
355
|
+
await ensureInProcessOwner(session);
|
|
356
|
+
return {
|
|
357
|
+
success: true,
|
|
358
|
+
session,
|
|
359
|
+
restored: false,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
catch {
|
|
363
|
+
return {
|
|
364
|
+
success: false,
|
|
365
|
+
reason: 'sticky_owner_unrecoverable',
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
if (stickyOwner.state === 'active' &&
|
|
370
|
+
!isStickyOwnerExpired(stickyOwner) &&
|
|
371
|
+
isProcessAlive(stickyOwner.pid) &&
|
|
372
|
+
(await canConnectToStickyOwnerEndpoint(stickyOwner.transport.endpoint))) {
|
|
373
|
+
return {
|
|
374
|
+
success: true,
|
|
375
|
+
session,
|
|
376
|
+
restored: false,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
await terminateBrowserSessionOwner(session);
|
|
380
|
+
if (!(await isSessionAlive(session))) {
|
|
381
|
+
return {
|
|
382
|
+
success: false,
|
|
383
|
+
reason: 'sticky_owner_unrecoverable',
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
try {
|
|
387
|
+
markStickyOwnerState(session, 'recovering');
|
|
388
|
+
session.stickyOwner = await bootstrapDetachedStickyOwner(session);
|
|
389
|
+
return {
|
|
390
|
+
success: true,
|
|
391
|
+
session,
|
|
392
|
+
restored: true,
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
catch {
|
|
396
|
+
session.stickyOwner = {
|
|
397
|
+
...stickyOwner,
|
|
398
|
+
state: 'dead',
|
|
399
|
+
};
|
|
400
|
+
return {
|
|
401
|
+
success: false,
|
|
402
|
+
reason: 'sticky_owner_unrecoverable',
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
export async function withStickyOwnerBrowser(session, operation) {
|
|
407
|
+
const stickyOwner = session.stickyOwner;
|
|
408
|
+
if (!stickyOwner || stickyOwner.transport.type === 'playwright_bind') {
|
|
409
|
+
const restored = await restoreBrowserSessionOwner(session);
|
|
410
|
+
if (!restored.success || !session.stickyOwner) {
|
|
411
|
+
throw new Error('sticky_owner_unrecoverable');
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
if (session.stickyOwner?.transport.type === 'playwright_bind') {
|
|
415
|
+
touchStickyOwnerLease(session);
|
|
416
|
+
const browser = await chromium.connect(session.stickyOwner.transport.endpoint, {
|
|
417
|
+
timeout: HOST_CONNECT_TIMEOUT_MS,
|
|
418
|
+
});
|
|
419
|
+
const stopHeartbeat = startStickyOwnerLeaseHeartbeat(session);
|
|
420
|
+
try {
|
|
421
|
+
return await operation(browser);
|
|
422
|
+
}
|
|
423
|
+
finally {
|
|
424
|
+
stopHeartbeat();
|
|
425
|
+
touchStickyOwnerLease(session);
|
|
426
|
+
await disconnectPlaywright(browser);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
const owner = await ensureInProcessOwner(session);
|
|
430
|
+
return operation(owner.browser);
|
|
431
|
+
}
|
package/docs/configuration.md
CHANGED
|
@@ -15,6 +15,12 @@ Most applications can start with this mental model:
|
|
|
15
15
|
2. keep the returned `session` in memory
|
|
16
16
|
3. pass that `session` into later calls
|
|
17
17
|
|
|
18
|
+
Both bootstrap the same sticky-owner lifecycle. AgentBrowse may keep that
|
|
19
|
+
owner in-process or in an internal detached host, but consumers do not manage
|
|
20
|
+
that host directly. Detached hosts default to a 30 minute TTL and may be
|
|
21
|
+
recreated on the next browser command if the browser session itself is still
|
|
22
|
+
alive.
|
|
23
|
+
|
|
18
24
|
You only need more configuration when you want one of these:
|
|
19
25
|
|
|
20
26
|
- custom LLM integration
|
|
@@ -43,6 +49,10 @@ const attached = await attach(remoteCdpUrl, {
|
|
|
43
49
|
The provider label is metadata only — AgentBrowse treats the connection as
|
|
44
50
|
a generic CDP-attached browser session regardless of the label.
|
|
45
51
|
|
|
52
|
+
`attach(...)` creates the same sticky-owner metadata as `launch(...)`. Later
|
|
53
|
+
browser commands reuse that owner and only attempt a fresh root attach again
|
|
54
|
+
as a repair path after owner loss.
|
|
55
|
+
|
|
46
56
|
## Client Configuration
|
|
47
57
|
|
|
48
58
|
```ts
|
|
@@ -68,6 +78,9 @@ configuration is the cleaner embedded pattern.
|
|
|
68
78
|
Persistence is optional. Use it when you want to restore a browser session
|
|
69
79
|
after a process restart.
|
|
70
80
|
|
|
81
|
+
Persisted session files store browser identity plus versioned sticky-owner
|
|
82
|
+
metadata. They do not serialize a live Playwright connection.
|
|
83
|
+
|
|
71
84
|
### Default Store
|
|
72
85
|
|
|
73
86
|
```ts
|
|
@@ -76,12 +89,12 @@ import { loadBrowserSession, saveBrowserSession, status } from '@mercuryo-ai/age
|
|
|
76
89
|
saveBrowserSession(session);
|
|
77
90
|
const restored = loadBrowserSession();
|
|
78
91
|
|
|
79
|
-
//
|
|
80
|
-
//
|
|
92
|
+
// `null` means there is no usable persisted session. That includes
|
|
93
|
+
// incompatible reconnect-era records and incomplete owner metadata.
|
|
81
94
|
if (restored) {
|
|
82
95
|
const check = await status(restored);
|
|
83
|
-
if (!check.
|
|
84
|
-
// The session is no longer reachable. Discard and relaunch.
|
|
96
|
+
if (!check.alive) {
|
|
97
|
+
// The session is no longer reachable. Discard and relaunch or re-attach.
|
|
85
98
|
}
|
|
86
99
|
}
|
|
87
100
|
```
|
|
@@ -90,6 +103,21 @@ Default path:
|
|
|
90
103
|
|
|
91
104
|
`~/.agentbrowse/browse-session.json`
|
|
92
105
|
|
|
106
|
+
If the detached owner host is gone but the underlying browser session is still
|
|
107
|
+
alive, the first command after restore may repair ownership. If the browser is
|
|
108
|
+
gone, AgentBrowse fails closed and you should start a fresh session.
|
|
109
|
+
|
|
110
|
+
### Sticky Owner TTL
|
|
111
|
+
|
|
112
|
+
Detached sticky-owner hosts use a bounded lifetime by default:
|
|
113
|
+
|
|
114
|
+
- default TTL: `30` minutes
|
|
115
|
+
- env override: `AGENTBROWSE_STICKY_OWNER_TTL_MS=<milliseconds>`
|
|
116
|
+
|
|
117
|
+
This TTL is a resource guard for the detached owner host, not for the browser
|
|
118
|
+
session itself. If the TTL expires and the browser is still reachable, the next
|
|
119
|
+
browser command may bootstrap a fresh owner and continue.
|
|
120
|
+
|
|
93
121
|
### Custom Store
|
|
94
122
|
|
|
95
123
|
For embedded apps, prefer an explicit store root:
|
|
@@ -108,6 +136,10 @@ store.delete();
|
|
|
108
136
|
|
|
109
137
|
This avoids hidden machine-level coupling to `~/.agentbrowse`.
|
|
110
138
|
|
|
139
|
+
`store.load()` follows the same contract as `loadBrowserSession()`: it returns
|
|
140
|
+
`null` for missing files, incompatible old records, or unusable sticky-owner
|
|
141
|
+
metadata.
|
|
142
|
+
|
|
111
143
|
## Proxy Configuration
|
|
112
144
|
|
|
113
145
|
The clearest way to use a proxy is to pass it directly to `launch(...)`.
|
package/docs/getting-started.md
CHANGED
|
@@ -19,9 +19,18 @@ The normal flow is:
|
|
|
19
19
|
5. `close(session)` ends the browser session
|
|
20
20
|
|
|
21
21
|
The `session` is the key object in the whole API. It is the handle that keeps
|
|
22
|
-
the browser connection
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
the browser connection, runtime state, and sticky-owner metadata together
|
|
23
|
+
between calls. Healthy commands reuse that sticky owner instead of issuing a
|
|
24
|
+
fresh root attach on every call. If you persist the session and restart your
|
|
25
|
+
process, the next command may repair the owner while the underlying browser is
|
|
26
|
+
still alive; otherwise the session fails closed and you start fresh. Detached
|
|
27
|
+
sticky owners also have a bounded lifetime, so an idle or expired owner may be
|
|
28
|
+
recreated on the next browser command while the underlying browser session is
|
|
29
|
+
still live.
|
|
30
|
+
|
|
31
|
+
The sticky owner may live in-process or in an internal detached host. That is
|
|
32
|
+
an implementation detail of AgentBrowse, not a daemon you manage separately.
|
|
33
|
+
Detached hosts default to a 30 minute TTL.
|
|
25
34
|
|
|
26
35
|
Refs returned by `observe(...)` (target refs, scope refs, fill refs) are
|
|
27
36
|
valid for the page state that produced them, not forever. Any of these
|
|
@@ -116,6 +125,11 @@ Success result includes:
|
|
|
116
125
|
- current `url`
|
|
117
126
|
- current `title`
|
|
118
127
|
|
|
128
|
+
`attach(...)` bootstraps the same sticky-owner lifecycle as `launch(...)`.
|
|
129
|
+
After attach succeeds, later browser commands use that owner. A new provider-
|
|
130
|
+
level root attach is only attempted again as an explicit repair path after
|
|
131
|
+
owner loss.
|
|
132
|
+
|
|
119
133
|
### `observe(session, goal?)`
|
|
120
134
|
|
|
121
135
|
Reads the current page and returns what AgentBrowse found.
|
|
@@ -172,12 +186,17 @@ before calling it.
|
|
|
172
186
|
Returns local browser/runtime diagnostics for an existing session.
|
|
173
187
|
|
|
174
188
|
Use it when you want to know whether the browser is still reachable and what
|
|
175
|
-
page AgentBrowse believes it is on.
|
|
189
|
+
page AgentBrowse believes it is on. After restoring a persisted session,
|
|
190
|
+
`status(session)` is the cheapest explicit health check before more expensive
|
|
191
|
+
workflows.
|
|
176
192
|
|
|
177
193
|
### `close(session)`
|
|
178
194
|
|
|
179
195
|
Closes the browser session.
|
|
180
196
|
|
|
197
|
+
This also terminates the internal sticky owner. Repeated closes and already-
|
|
198
|
+
dead owner hosts are treated as idempotent.
|
|
199
|
+
|
|
181
200
|
## How To Handle Results
|
|
182
201
|
|
|
183
202
|
All main commands use the same broad pattern:
|
|
@@ -233,6 +252,11 @@ If you want to restore a browser session between process runs, use:
|
|
|
233
252
|
- `loadBrowserSession()`
|
|
234
253
|
- `createBrowserSessionStore({ rootDir })`
|
|
235
254
|
|
|
255
|
+
Persisted session records now require restorable sticky-owner metadata.
|
|
256
|
+
Incompatible reconnect-era records are rejected at load time instead of being
|
|
257
|
+
auto-migrated. Treat `loadBrowserSession() === null` as "no usable session",
|
|
258
|
+
not as a recoverable partial state.
|
|
259
|
+
|
|
236
260
|
See:
|
|
237
261
|
|
|
238
262
|
- [Configuration Guide](./configuration.md)
|
package/docs/troubleshooting.md
CHANGED
|
@@ -23,7 +23,9 @@ Inspect the `error` and `reason` fields on the `launch(...)` failure result
|
|
|
23
23
|
- **Unreachable CDP WebSocket URL.** Verify the URL with a simple
|
|
24
24
|
WebSocket client or `curl`.
|
|
25
25
|
- **Session already owned.** Some providers reject a second CDP attach for
|
|
26
|
-
the same session.
|
|
26
|
+
the same session. In the sticky-owner architecture this should only happen
|
|
27
|
+
during the initial `attach(...)` or an explicit repair attempt after the
|
|
28
|
+
original owner was lost, not during every healthy command.
|
|
27
29
|
- **Version mismatch.** Very old or very new Chrome versions may not match
|
|
28
30
|
Playwright's expected CDP shape; `status(session)` after attach returns
|
|
29
31
|
details in that case.
|
|
@@ -33,13 +35,47 @@ Inspect the `error` and `reason` fields on the `launch(...)` failure result
|
|
|
33
35
|
A `session` handle is valid only while the underlying browser connection
|
|
34
36
|
is live.
|
|
35
37
|
|
|
36
|
-
- If the
|
|
37
|
-
|
|
38
|
-
|
|
38
|
+
- If the detached sticky owner died but the browser is still alive,
|
|
39
|
+
AgentBrowse may repair ownership on the next command or explicit restore
|
|
40
|
+
path.
|
|
41
|
+
- If the detached sticky owner hit its TTL, the next browser command may
|
|
42
|
+
recycle the owner and continue as long as the browser session is still live.
|
|
43
|
+
- If the browser process or remote CDP session died, the next
|
|
44
|
+
`act(...)` / `observe(...)` call fails closed. Use `status(session)` to
|
|
45
|
+
confirm, then call `launch(...)` or `attach(...)` again.
|
|
39
46
|
- If you restored a session from `loadBrowserSession()` or a custom store,
|
|
40
47
|
call `status(session)` right after loading. If it reports the session is
|
|
41
|
-
no longer reachable,
|
|
42
|
-
|
|
48
|
+
no longer reachable, or a later command fails with
|
|
49
|
+
`sticky_owner_unrecoverable`, discard it and start fresh.
|
|
50
|
+
- `close(session)` called twice is safe — the second call is a no-op, and an
|
|
51
|
+
already-dead owner host is treated as already closed.
|
|
52
|
+
|
|
53
|
+
## Restored Session Loads As `null`
|
|
54
|
+
|
|
55
|
+
`loadBrowserSession()` and custom stores intentionally reject incompatible
|
|
56
|
+
older session records that do not contain persistable sticky-owner metadata.
|
|
57
|
+
|
|
58
|
+
If a session that used to restore now loads as `null`:
|
|
59
|
+
|
|
60
|
+
- treat it as a stale local record, not as a partial session you should patch
|
|
61
|
+
by hand;
|
|
62
|
+
- delete the old session file if it is still on disk;
|
|
63
|
+
- run `launch(...)` or `attach(...)` again to create a new session record.
|
|
64
|
+
|
|
65
|
+
AgentBrowse does not auto-migrate reconnect-era session files.
|
|
66
|
+
|
|
67
|
+
## `close(session)` Seems Stuck Or The Owner Host Already Died
|
|
68
|
+
|
|
69
|
+
`close(session)` first terminates the sticky owner and then closes the managed
|
|
70
|
+
browser when the session owns it.
|
|
71
|
+
|
|
72
|
+
- If the owner host is already gone, close remains idempotent.
|
|
73
|
+
- For detached owner hosts, AgentBrowse escalates from graceful termination to
|
|
74
|
+
a forced kill internally; there is no separate daemon command to run.
|
|
75
|
+
- In CLI wrappers, a successful close also clears the locally persisted browser
|
|
76
|
+
session and workflow bindings.
|
|
77
|
+
- If closing the managed browser itself fails, AgentBrowse reports that failure
|
|
78
|
+
and keeps the session record so you can inspect or retry.
|
|
43
79
|
|
|
44
80
|
## Why The Package Uses Both Puppeteer And Playwright
|
|
45
81
|
|
package/package.json
CHANGED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import type { BrowserSessionState } from './browser-session-state.js';
|
|
2
|
-
import { type ProtectedFillExecutionResult } from './secrets/protected-fill.js';
|
|
3
|
-
import type { PersistedFillableForm, StoredSecretFieldPolicies, StoredSecretFieldKey } from './secrets/types.js';
|
|
4
|
-
export type FillProtectedFormBrowserResult = {
|
|
5
|
-
success: true;
|
|
6
|
-
pageRef: string;
|
|
7
|
-
url: string;
|
|
8
|
-
title: string;
|
|
9
|
-
execution: ProtectedFillExecutionResult;
|
|
10
|
-
} | {
|
|
11
|
-
success: false;
|
|
12
|
-
error: 'browser_connection_failed' | 'page_resolution_failed';
|
|
13
|
-
message: string;
|
|
14
|
-
reason: string;
|
|
15
|
-
};
|
|
16
|
-
export declare function fillProtectedFormBrowser(params: {
|
|
17
|
-
session: BrowserSessionState;
|
|
18
|
-
fillableForm: PersistedFillableForm;
|
|
19
|
-
protectedValues: Partial<Record<StoredSecretFieldKey, string>>;
|
|
20
|
-
fieldPolicies?: StoredSecretFieldPolicies;
|
|
21
|
-
}): Promise<FillProtectedFormBrowserResult>;
|
|
22
|
-
//# sourceMappingURL=protected-fill-browser.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"protected-fill-browser.d.ts","sourceRoot":"","sources":["../src/protected-fill-browser.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAOtE,OAAO,EAEL,KAAK,4BAA4B,EAClC,MAAM,6BAA6B,CAAC;AACrC,OAAO,KAAK,EACV,qBAAqB,EACrB,yBAAyB,EACzB,oBAAoB,EACrB,MAAM,oBAAoB,CAAC;AAE5B,MAAM,MAAM,8BAA8B,GACtC;IACE,OAAO,EAAE,IAAI,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,4BAA4B,CAAC;CACzC,GACD;IACE,OAAO,EAAE,KAAK,CAAC;IACf,KAAK,EAAE,2BAA2B,GAAG,wBAAwB,CAAC;IAC9D,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEN,wBAAsB,wBAAwB,CAAC,MAAM,EAAE;IACrD,OAAO,EAAE,mBAAmB,CAAC;IAC7B,YAAY,EAAE,qBAAqB,CAAC;IACpC,eAAe,EAAE,OAAO,CAAC,MAAM,CAAC,oBAAoB,EAAE,MAAM,CAAC,CAAC,CAAC;IAC/D,aAAa,CAAC,EAAE,yBAAyB,CAAC;CAC3C,GAAG,OAAO,CAAC,8BAA8B,CAAC,CAqD1C"}
|