@omercnet/paseo-shared-browser 4.1.1 → 4.1.2-next.362.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
@@ -40,7 +40,7 @@ organization names.
40
40
  controller when a takeover is needed, and continues the selected action after control is granted.
41
41
  Closing a tab or browser still requires its own confirmation. Actions that need a fresh frame wait
42
42
  for one before sending input.
43
- - The plugin owns `agent-browser` version `0.39.2`, its IPC directory, and the Chromium executable.
43
+ - The plugin owns `agent-browser` version `0.40.0`, its IPC directory, and the Chromium executable.
44
44
  It strips inherited `AGENT_BROWSER_*` variables and sets `AGENT_BROWSER_SOCKET_DIR`,
45
45
  `AGENT_BROWSER_IDLE_TIMEOUT_MS=0`, `AGENT_BROWSER_STREAM_PORT=0`, and
46
46
  `AGENT_BROWSER_NO_AUTO_DIALOG=1` itself.
@@ -1,18 +1,18 @@
1
1
  {
2
2
  "name": "@omercnet/paseo-shared-browser",
3
- "version": "4.1.1",
3
+ "version": "4.1.2",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@omercnet/paseo-shared-browser",
9
- "version": "4.1.1",
9
+ "version": "4.1.2",
10
10
  "dependencies": {
11
11
  "@getpaseo/plugin": "0.14.0",
12
- "@modelcontextprotocol/sdk": "1.30.0",
13
- "agent-browser": "0.39.2",
12
+ "@modelcontextprotocol/sdk": "1.32.0",
13
+ "agent-browser": "0.40.0",
14
14
  "esbuild": "0.28.2",
15
- "zod": "4.4.3"
15
+ "zod": "4.6.7"
16
16
  }
17
17
  },
18
18
  "node_modules/@agentclientprotocol/sdk": {
@@ -511,9 +511,9 @@
511
511
  }
512
512
  },
513
513
  "node_modules/@modelcontextprotocol/sdk": {
514
- "version": "1.30.0",
515
- "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.30.0.tgz",
516
- "integrity": "sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA==",
514
+ "version": "1.32.0",
515
+ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.32.0.tgz",
516
+ "integrity": "sha512-8BviX/hK4Gd2eL1KdTwm0gfl4d3wdoErW0Jd00h6nomUAtk6IJk5vwQJc6kfWtJTzN5OEV/lgIiJKI8fiDGAlA==",
517
517
  "license": "MIT",
518
518
  "dependencies": {
519
519
  "@hono/node-server": "^1.19.9 || ^2.0.5",
@@ -606,9 +606,9 @@
606
606
  }
607
607
  },
608
608
  "node_modules/agent-browser": {
609
- "version": "0.39.2",
610
- "resolved": "https://registry.npmjs.org/agent-browser/-/agent-browser-0.39.2.tgz",
611
- "integrity": "sha512-8zh5Dj4r/Lo14nDw+8jUViTlmZV5ON3qmrGp1wppmB87NhMqmSXUqVIeNith9xjIJNr/00wHxhCqH/2lATezYQ==",
609
+ "version": "0.40.0",
610
+ "resolved": "https://registry.npmjs.org/agent-browser/-/agent-browser-0.40.0.tgz",
611
+ "integrity": "sha512-uqDAR3ozwJKYzVENMr/ZKYTZT1LUZauI49rTiK/KsFBxQDVTdtmrWGWTMWZlaIIQWToEtZUfWVXBSSTY6aHzrA==",
612
612
  "hasInstallScript": true,
613
613
  "license": "Apache-2.0",
614
614
  "bin": {
@@ -1029,9 +1029,9 @@
1029
1029
  }
1030
1030
  },
1031
1031
  "node_modules/express-rate-limit": {
1032
- "version": "8.7.1",
1033
- "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.7.1.tgz",
1034
- "integrity": "sha512-2FXbqkygil3mZiXzEtM0IjE/UBmBNot0J1GEVrJT4JbrvwOSoEtqtO+wHbiMQELWe1eNvHoYRg2dkjOid+6JkA==",
1032
+ "version": "8.7.2",
1033
+ "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.7.2.tgz",
1034
+ "integrity": "sha512-U1ScocxDD9WI09LaBVypvNkOtNR5yWAmZLkuKJdAzIchA4YKFYF6GT5IKER7epG58oXtjvGlV0/6ii0F83EyZg==",
1035
1035
  "license": "MIT",
1036
1036
  "dependencies": {
1037
1037
  "debug": "^4.4.3",
@@ -1233,9 +1233,9 @@
1233
1233
  }
1234
1234
  },
1235
1235
  "node_modules/i18next": {
1236
- "version": "26.5.11",
1237
- "resolved": "https://registry.npmjs.org/i18next/-/i18next-26.5.11.tgz",
1238
- "integrity": "sha512-Wc0klBPKxqZ2tL2qdAnF8xvvnWgr1IMGSY2ikTiAvaFVbz7xiFARtkoU4RidBDw1SyziGROp0qxu6qnJ7l2Wug==",
1236
+ "version": "26.6.0",
1237
+ "resolved": "https://registry.npmjs.org/i18next/-/i18next-26.6.0.tgz",
1238
+ "integrity": "sha512-jpb1n5386s9mbBLNF6tSNIVhbBUUNiNeDyjWte+uxHP/Wr+CEy7WBM8q29TkfQ46VbwKeJFYdQJK3pSUhLp4CQ==",
1239
1239
  "funding": [
1240
1240
  {
1241
1241
  "type": "individual",
@@ -1874,9 +1874,9 @@
1874
1874
  "license": "ISC"
1875
1875
  },
1876
1876
  "node_modules/ws": {
1877
- "version": "8.22.0",
1878
- "resolved": "https://registry.npmjs.org/ws/-/ws-8.22.0.tgz",
1879
- "integrity": "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==",
1877
+ "version": "8.22.1",
1878
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.22.1.tgz",
1879
+ "integrity": "sha512-oyL6OPYme/CmiFvNuGzeoxCJ4nOrZE2O1FwM9MWM1AsGri6nLXB2pg+lNA8aLeXMSav0J6eIVS1j6vUPzUtK9g==",
1880
1880
  "license": "MIT",
1881
1881
  "peer": true,
1882
1882
  "engines": {
@@ -1896,9 +1896,9 @@
1896
1896
  }
1897
1897
  },
1898
1898
  "node_modules/zod": {
1899
- "version": "4.4.3",
1900
- "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
1901
- "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
1899
+ "version": "4.6.7",
1900
+ "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.7.tgz",
1901
+ "integrity": "sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==",
1902
1902
  "license": "MIT",
1903
1903
  "funding": {
1904
1904
  "url": "https://github.com/sponsors/colinhacks"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omercnet/paseo-shared-browser",
3
- "version": "4.1.1",
3
+ "version": "4.1.2-next.362.1",
4
4
  "description": "One real Chromium session per Paseo workspace, shared live across every connected client.",
5
5
  "keywords": [
6
6
  "browser",
@@ -56,10 +56,10 @@
56
56
  },
57
57
  "dependencies": {
58
58
  "@getpaseo/plugin": "0.14.0",
59
- "@modelcontextprotocol/sdk": "1.30.0",
60
- "agent-browser": "0.39.2",
59
+ "@modelcontextprotocol/sdk": "1.32.0",
60
+ "agent-browser": "0.40.0",
61
61
  "esbuild": "0.28.2",
62
- "zod": "4.4.3"
62
+ "zod": "4.6.7"
63
63
  },
64
64
  "devDependencies": {
65
65
  "@biomejs/biome": "^2.5.10",
@@ -1,10 +1,12 @@
1
- import { execFileSync } from "node:child_process";
1
+ import { execSync } from "node:child_process";
2
2
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
3
3
  import { createRequire } from "node:module";
4
4
  import { dirname, join } from "node:path";
5
5
  import { fileURLToPath, pathToFileURL } from "node:url";
6
6
 
7
7
  const projectRoot = dirname(dirname(fileURLToPath(import.meta.url)));
8
+ // Run through the shell so Windows resolves npm.cmd: Node refuses to spawn .cmd files without one.
9
+ const INSTALL_COMMAND = "npm ci --omit=dev --ignore-scripts --workspaces=false";
8
10
 
9
11
  function loadRootCatalog(root) {
10
12
  for (let directory = root; ; directory = dirname(directory)) {
@@ -28,6 +30,8 @@ function stageManifest(root, catalog, install) {
28
30
  manifest.dependencies[name] = catalog[name];
29
31
  }
30
32
  delete manifest.devDependencies;
33
+ // npm-shrinkwrap.json is generated without overrides, so npm ci rejects a manifest that keeps them.
34
+ delete manifest.overrides;
31
35
 
32
36
  writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
33
37
  try {
@@ -41,18 +45,11 @@ export function resolveDependencyRoot(packageName, from = import.meta.url) {
41
45
  return dirname(createRequire(from).resolve(`${packageName}/package.json`));
42
46
  }
43
47
 
44
- export function prepareDependencies(root = projectRoot, execute = execFileSync) {
48
+ export function prepareDependencies(root = projectRoot, execute = execSync) {
45
49
  const catalog = loadRootCatalog(root);
46
50
  if (!catalog) return false;
47
51
  stageManifest(root, catalog, () => {
48
- execute(
49
- process.platform === "win32" ? "npm.cmd" : "npm",
50
- ["ci", "--omit=dev", "--ignore-scripts", "--workspaces=false"],
51
- {
52
- cwd: root,
53
- stdio: "inherit",
54
- },
55
- );
52
+ execute(INSTALL_COMMAND, { cwd: root, stdio: "inherit" });
56
53
  });
57
54
  return true;
58
55
  }
@@ -18,7 +18,7 @@ function resolveDependencyRoot(packageName) {
18
18
  }
19
19
 
20
20
  const execFileAsync = promisify(execFile);
21
- const expectedVersion = "0.39.2";
21
+ const expectedVersion = "0.40.0";
22
22
  const hostPlatform = platform();
23
23
  const hostArch = arch();
24
24
  const projectRoot = dirname(dirname(fileURLToPath(import.meta.url)));
@@ -57,7 +57,7 @@ import { createScreencastSourceTime } from "./screencast-source-time";
57
57
  import { createVideoSourceRecovery } from "./video-source-recovery";
58
58
 
59
59
  const execFileAsync = promisify(execFile);
60
- export const AGENT_BROWSER_VERSION = "0.39.2";
60
+ export const AGENT_BROWSER_VERSION = "0.40.0";
61
61
  const PRIVATE_DIRECTORY_MODE = 0o700;
62
62
  const PRIVATE_FILE_MODE = 0o600;
63
63
  const DEFAULT_TIMEOUT_MS = 15_000;
@@ -379,22 +379,30 @@ export class AgentBrowserRuntime {
379
379
  // From here a nonce-owned daemon may exist even if open rejects (e.g. navigation failure).
380
380
  this.daemonLaunched = true;
381
381
  this.daemon = null;
382
- const opened = await this.invoke([
383
- "--session",
384
- this.session,
385
- "--profile",
386
- this.profilePath,
387
- "--executable-path",
388
- this.executablePath,
389
- ...(videoExtension ? ["--extension", videoExtension] : []),
390
- "--hide-scrollbars",
391
- "false",
392
- ...(this.headed ? ["--headed"] : []),
393
- ...(chromiumArguments ? ["--args", chromiumArguments] : []),
394
- "--json",
395
- "open",
396
- this.initialUrl,
397
- ]);
382
+ let opened: unknown;
383
+ try {
384
+ opened = await this.invoke([
385
+ "--session",
386
+ this.session,
387
+ "--profile",
388
+ this.profilePath,
389
+ "--executable-path",
390
+ this.executablePath,
391
+ ...(videoExtension ? ["--extension", videoExtension] : []),
392
+ "--hide-scrollbars",
393
+ "false",
394
+ ...(this.headed ? ["--headed"] : []),
395
+ ...(chromiumArguments ? ["--args", chromiumArguments] : []),
396
+ "--json",
397
+ "open",
398
+ this.initialUrl,
399
+ ]);
400
+ } catch (error) {
401
+ // Record the daemon a failed open left behind: since agent-browser 0.40.0 `close`
402
+ // removes the PID file before returning, after which its exit is unobservable.
403
+ await this.captureDaemon().catch(() => undefined);
404
+ throw error;
405
+ }
398
406
  this.assertRuntimeOpen();
399
407
  const targetId = findString(opened, ["targetId", "target_id"]);
400
408
  await this.protectIpcMetadata();
@@ -1,10 +1,11 @@
1
1
  /** The session daemon is signalled only while it is still the exact process this runtime launched. */
2
2
  import { type ChildProcess, spawn } from "node:child_process";
3
+ import { rmSync, writeFileSync } from "node:fs";
3
4
  import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
4
5
  import { tmpdir } from "node:os";
5
6
  import { join } from "node:path";
6
7
  import { afterEach, describe, expect, it, vi } from "vitest";
7
- import { AgentBrowserRuntime } from "./agent-browser-runtime";
8
+ import { AGENT_BROWSER_VERSION, AgentBrowserRuntime } from "./agent-browser-runtime";
8
9
  import { RUNTIME_OWNER_VARIABLE } from "./process-identity";
9
10
 
10
11
  const probe = vi.hoisted(() => ({
@@ -32,20 +33,31 @@ vi.mock("./process-identity", async (importOriginal) => {
32
33
  };
33
34
  });
34
35
 
35
- const cli = vi.hoisted(() => ({ onClose: null as null | (() => void), closes: 0 }));
36
+ const cli = vi.hoisted(() => ({
37
+ onClose: null as null | (() => void),
38
+ closes: 0,
39
+ /** Set to make `open` fail after its side effect, like a navigation failure. */
40
+ onOpen: null as null | (() => void),
41
+ version: "0.37.1",
42
+ }));
36
43
  vi.mock("node:child_process", async (importOriginal) => ({
37
44
  ...(await importOriginal<typeof import("node:child_process")>()),
38
45
  execFile: (
39
46
  _file: string,
40
47
  args: string[],
41
48
  _options: unknown,
42
- callback: (error: null, result: { stdout: string }) => void,
49
+ callback: (error: Error | null, result?: { stdout: string }) => void,
43
50
  ) => {
44
51
  if (args.includes("close")) {
45
52
  cli.closes += 1;
46
53
  cli.onClose?.();
47
54
  }
48
- callback(null, { stdout: args.includes("--version") ? "0.37.1\n" : "{}\n" });
55
+ if (args.includes("open") && cli.onOpen) {
56
+ cli.onOpen();
57
+ callback(new Error("Navigation failed: net::ERR_CONNECTION_REFUSED"));
58
+ return;
59
+ }
60
+ callback(null, { stdout: args.includes("--version") ? `${cli.version}\n` : "{}\n" });
49
61
  },
50
62
  }));
51
63
 
@@ -58,6 +70,8 @@ afterEach(async () => {
58
70
  );
59
71
  cli.onClose = null;
60
72
  cli.closes = 0;
73
+ cli.onOpen = null;
74
+ cli.version = "0.37.1";
61
75
  probe.unreadable = false;
62
76
  probe.simulated = null;
63
77
  vi.restoreAllMocks();
@@ -273,6 +287,27 @@ describe.each(ownershipModes)(
273
287
  expect(daemon.pid && alive(daemon.pid)).toBe(false);
274
288
  });
275
289
 
290
+ it("confirms the exit of a daemon left by a failed open even when close removes its PID file", async () => {
291
+ const f = await fixture();
292
+ const daemon = await ownChild(f.native.ownerNonce);
293
+ own(daemon);
294
+ const pidFile = join(f.ipc, "owned.pid");
295
+ cli.version = AGENT_BROWSER_VERSION;
296
+ // The daemon starts and records its PID, then the initial navigation fails.
297
+ cli.onOpen = () => writeFileSync(pidFile, String(daemon.pid));
298
+ // agent-browser >= 0.40.0: close returns only after the daemon removed its PID file.
299
+ cli.onClose = () => {
300
+ daemon.kill("SIGTERM");
301
+ rmSync(pidFile, { force: true });
302
+ };
303
+ await expect(f.runtime.launch()).rejects.toThrow("agent-browser command failed");
304
+ const kill = vi.spyOn(process, "kill");
305
+ await f.runtime.shutdown();
306
+ await exited(daemon);
307
+ expect(kill).not.toHaveBeenCalledWith(daemon.pid, "SIGKILL");
308
+ expect(daemon.pid && alive(daemon.pid)).toBe(false);
309
+ });
310
+
276
311
  it("shares one in-flight shutdown and never reports success after a failed one", async () => {
277
312
  const f = await fixture(150);
278
313
  const daemon = await ownChild();
@@ -17,7 +17,7 @@ async function fixture(failFirstEncoder = false) {
17
17
  }),
18
18
  cancel: async () => deliver({ done: true }),
19
19
  };
20
- const pending: { timestamp: number; type: string }[] = [];
20
+ const pending: { timestamp: number; type: string; queued: boolean }[] = [];
21
21
  const encoded: { timestamp: number; keyFrame: boolean }[] = [];
22
22
  let encoder: Encoder;
23
23
  const instances: Encoder[] = [];
@@ -37,14 +37,31 @@ async function fixture(failFirstEncoder = false) {
37
37
  if (failFirstEncoder && this === instances[0]) throw new Error("Codec stopped");
38
38
  this.encodeQueueSize++;
39
39
  encoded.push({ timestamp: frame.timestamp, keyFrame: options.keyFrame });
40
- pending.push({ timestamp: frame.timestamp, type: options.keyFrame ? "key" : "delta" });
40
+ pending.push({
41
+ timestamp: frame.timestamp,
42
+ type: options.keyFrame ? "key" : "delta",
43
+ queued: true,
44
+ });
45
+ }
46
+ /** WebCodecs lowers encodeQueueSize once the codec starts a frame, before its output. */
47
+ dequeue() {
48
+ const value = pending.find((entry) => entry.queued);
49
+ if (!value) throw new Error("No queued fixture frame");
50
+ value.queued = false;
51
+ this.encodeQueueSize--;
41
52
  }
53
+ /** Emits in submission order, as an in-order realtime codec does. */
42
54
  output() {
43
55
  const value = pending.shift();
44
56
  if (!value) throw new Error("No queued fixture frame");
45
- this.encodeQueueSize--;
57
+ if (value.queued) this.encodeQueueSize--;
46
58
  this.callbacks.output(
47
- { ...value, byteLength: 10, copyTo: (bytes: Uint8Array) => bytes.fill(1) },
59
+ {
60
+ timestamp: value.timestamp,
61
+ type: value.type,
62
+ byteLength: 10,
63
+ copyTo: (bytes: Uint8Array) => bytes.fill(1),
64
+ },
48
65
  {},
49
66
  );
50
67
  }
@@ -94,6 +111,7 @@ async function fixture(failFirstEncoder = false) {
94
111
  await turn();
95
112
  },
96
113
  output: () => encoder!.output(),
114
+ dequeue: () => encoder!.dequeue(),
97
115
  ack: () =>
98
116
  runInContext(
99
117
  'for(const p of fixturePackets) acknowledge("stream",p.sequence)',
@@ -189,6 +207,28 @@ it("still suppresses dependencies after an actual encoded output is dropped", as
189
207
  }
190
208
  });
191
209
 
210
+ it("emits outputs from a codec that lags further behind than its dequeued queue size", async () => {
211
+ const f = await fixture();
212
+ try {
213
+ const timestamps = [0, 33334, 66668, 100002, 133336, 166670];
214
+ for (const timestamp of timestamps) {
215
+ await f.frame(timestamp);
216
+ f.dequeue();
217
+ }
218
+ expect(f.encoded).toHaveLength(timestamps.length);
219
+ for (const _ of timestamps) {
220
+ f.output();
221
+ f.ack();
222
+ }
223
+ expect(f.packets.map((p) => [p.timestampUs, p.type])).toEqual(
224
+ timestamps.map((timestamp, index) => [timestamp, index ? "delta" : "key"]),
225
+ );
226
+ expect(f.encoded.filter((e) => e.keyFrame)).toHaveLength(1);
227
+ } finally {
228
+ await f.stop();
229
+ }
230
+ });
231
+
192
232
  it("shipped packet conversion is exact with native and legacy byte APIs", () => {
193
233
  for (const native of [true, false]) {
194
234
  const context = createContext({
@@ -132,7 +132,12 @@ async function startCapture(targetId, width, height, generation) {
132
132
  continue;
133
133
  state.lastEncodedTimestampUs = frame.timestamp;
134
134
  state.frameGenerations.set(frame.timestamp, current.generation);
135
- while (state.frameGenerations.size > 4)
135
+ // encodeQueueSize drops when the codec dequeues a frame, not when it
136
+ // outputs, so a slow encode keeps more frames in flight than the gate
137
+ // above admits. Bound by one second at the 60fps ceiling, the host's
138
+ // packet age limit, so an in-flight output keeps its generation
139
+ // for as long as the host would still accept it.
140
+ while (state.frameGenerations.size > 60)
136
141
  state.frameGenerations.delete(state.frameGenerations.keys().next().value);
137
142
  const keyFrame =
138
143
  state.forceKey || frame.timestamp - state.lastKeyTimestamp >= 1000000;
@@ -23,7 +23,7 @@ vi.mock("node:child_process", () => ({
23
23
  callback: (error: null, result: { stdout: string }) => void,
24
24
  ) => {
25
25
  exec.calls.push({ file, args, env: options.env });
26
- callback(null, { stdout: args.includes("--version") ? "0.39.2\n" : "{}\n" });
26
+ callback(null, { stdout: args.includes("--version") ? "0.40.0\n" : "{}\n" });
27
27
  },
28
28
  }));
29
29
 
@@ -15,7 +15,7 @@ vi.mock("node:child_process", async (importOriginal) => ({
15
15
  _args: string[],
16
16
  _options: unknown,
17
17
  callback: (error: null, result: { stdout: string }) => void,
18
- ) => callback(null, { stdout: "0.39.2\n" }),
18
+ ) => callback(null, { stdout: "0.40.0\n" }),
19
19
  }));
20
20
 
21
21
  const temporary: string[] = [];