@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.
Files changed (46) hide show
  1. package/README.md +30 -5
  2. package/dist/browser-session-state.d.ts +39 -0
  3. package/dist/browser-session-state.d.ts.map +1 -1
  4. package/dist/browser-session-state.js +63 -1
  5. package/dist/commands/act.d.ts.map +1 -1
  6. package/dist/commands/act.js +539 -535
  7. package/dist/commands/attach.d.ts.map +1 -1
  8. package/dist/commands/attach.js +5 -10
  9. package/dist/commands/browser-connection-failure.d.ts +9 -0
  10. package/dist/commands/browser-connection-failure.d.ts.map +1 -0
  11. package/dist/commands/browser-connection-failure.js +15 -0
  12. package/dist/commands/browser-status.d.ts.map +1 -1
  13. package/dist/commands/browser-status.js +26 -30
  14. package/dist/commands/close.d.ts.map +1 -1
  15. package/dist/commands/close.js +5 -0
  16. package/dist/commands/extract.d.ts.map +1 -1
  17. package/dist/commands/extract.js +147 -144
  18. package/dist/commands/launch.d.ts.map +1 -1
  19. package/dist/commands/launch.js +11 -8
  20. package/dist/commands/navigate.d.ts.map +1 -1
  21. package/dist/commands/navigate.js +79 -73
  22. package/dist/commands/observe-inventory.d.ts +1 -0
  23. package/dist/commands/observe-inventory.d.ts.map +1 -1
  24. package/dist/commands/observe-inventory.js +15 -3
  25. package/dist/commands/observe.d.ts.map +1 -1
  26. package/dist/commands/observe.js +260 -272
  27. package/dist/commands/screenshot.d.ts.map +1 -1
  28. package/dist/commands/screenshot.js +50 -64
  29. package/dist/library.d.ts +2 -1
  30. package/dist/library.d.ts.map +1 -1
  31. package/dist/library.js +2 -1
  32. package/dist/protected-fill.d.ts.map +1 -1
  33. package/dist/protected-fill.js +46 -7
  34. package/dist/sticky-owner-host-entry.d.ts +2 -0
  35. package/dist/sticky-owner-host-entry.d.ts.map +1 -0
  36. package/dist/sticky-owner-host-entry.js +97 -0
  37. package/dist/sticky-owner.d.ts +15 -0
  38. package/dist/sticky-owner.d.ts.map +1 -0
  39. package/dist/sticky-owner.js +431 -0
  40. package/docs/configuration.md +36 -4
  41. package/docs/getting-started.md +28 -4
  42. package/docs/troubleshooting.md +42 -6
  43. package/package.json +1 -1
  44. package/dist/protected-fill-browser.d.ts +0 -22
  45. package/dist/protected-fill-browser.d.ts.map +0 -1
  46. 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
+ }
@@ -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
- // Always check a restored session before using it the browser it points
80
- // at may already be gone.
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.success) {
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(...)`.
@@ -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 and runtime state together between calls. A session
23
- stays valid while the underlying browser connection is live; call
24
- `status(session)` to check if you need to.
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)
@@ -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 browser process died, the next `act(...)` / `observe(...)` call
37
- will fail with a connection error. Use `status(session)` to confirm, then
38
- call `launch(...)` or `attach(...)` again.
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, discard it and start fresh.
42
- - `close(session)` called twice is safe — the second call is a no-op.
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@mercuryo-ai/agentbrowse",
3
- "version": "0.2.60",
3
+ "version": "0.2.61",
4
4
  "type": "module",
5
5
  "description": "Browser automation primitives library for AI agents",
6
6
  "license": "MIT",
@@ -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"}