@hraness/slopcamera 3.10.1 → 3.10.2

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/PRIVACY.md CHANGED
@@ -54,6 +54,8 @@ from vulnerability reports.
54
54
  Report suspected vulnerabilities through GitHub private vulnerability
55
55
  reporting at https://github.com/hraness/slopcamera/security/advisories/new.
56
56
 
57
+ ## Website analytics
58
+
57
59
  The public slopcamera.com website sends cookieless page views, page exits,
58
60
  performance measurements, explicit link and install-copy actions, missing-page
59
61
  reports, and rate-limited redacted client errors to PostHog. It keeps no person
@@ -61,3 +63,8 @@ profiles, analytics cookies, or session recordings and respects Do Not Track.
61
63
  Public campaign parameters are retained; other query values and credentials
62
64
  are removed. Sensitive paths collapse to `/private` and their campaign
63
65
  attribution is removed. Preview builds and the inert preview page send nothing.
66
+
67
+ The site checks the shared regional analytics policy service. Analytics
68
+ waits for acceptance where that policy requires consent, or when the region is
69
+ unknown. Elsewhere it can run without cookies before a visitor responds.
70
+ A stored refusal or Do Not Track keeps analytics off.
package/README.md CHANGED
@@ -34,11 +34,11 @@ for prompt-only hosted images; model usage is billed separately.
34
34
  ## Install SlopCamera
35
35
 
36
36
  Install [Bun 1.3.14 or newer](https://bun.sh), then install the
37
- [SlopCamera v3.9.2 release](https://github.com/hraness/slopcamera/releases/tag/v3.9.2)
37
+ [SlopCamera v3.10.1 release](https://github.com/hraness/slopcamera/releases/tag/v3.10.1)
38
38
  and its matching Agent Skill:
39
39
 
40
40
  ```sh
41
- bun add --global https://github.com/hraness/slopcamera/releases/download/v3.9.2/hraness-slopcamera-3.9.2.tgz
41
+ bun add --global https://github.com/hraness/slopcamera/releases/download/v3.10.1/hraness-slopcamera-3.10.1.tgz
42
42
  slopcamera skill install --target agents
43
43
  ```
44
44
 
@@ -4,7 +4,7 @@ import type { z } from "zod";
4
4
 
5
5
  import type { ApplicationContext } from "./context";
6
6
 
7
- export const SLOPCAMERA_APPLICATION_TOOL_VERSION = "slopcamera-3.10.1" as const;
7
+ export const SLOPCAMERA_APPLICATION_TOOL_VERSION = "slopcamera-3.10.2" as const;
8
8
 
9
9
  export const OPERATION_KINDS = [
10
10
  "scene.audit",
@@ -365,7 +365,7 @@ import {
365
365
  workflowRunStore,
366
366
  } from "./workflow-runs";
367
367
 
368
- export const SLOPCAMERA_VERSION = "3.10.1";
368
+ export const SLOPCAMERA_VERSION = "3.10.2";
369
369
 
370
370
  // Legacy direct renders predate per-target output contracts. Keep them
371
371
  // bounded generously enough for long-form production while preventing one
@@ -0,0 +1,184 @@
1
+ import { fstatSync, lstatSync, type BigIntStats } from "node:fs";
2
+ import { endianness } from "node:os";
3
+ import { basename, join } from "node:path";
4
+ import { ApplicationError } from "../application/errors";
5
+
6
+ export interface BrowserWatchIdentity {
7
+ readonly path: string;
8
+ readonly dev: string;
9
+ readonly ino: string;
10
+ readonly mode: number;
11
+ readonly size?: string;
12
+ readonly ctimeNs?: string;
13
+ readonly mtimeNs?: string;
14
+ readonly uid?: string;
15
+ readonly gid?: string;
16
+ readonly nlink?: string;
17
+ }
18
+
19
+ const metadataKeys = ["size", "ctimeNs", "mtimeNs", "uid", "gid", "nlink"] as const;
20
+ function matchesIdentity(details: BigIntStats, identity: BrowserWatchIdentity): boolean {
21
+ return details.dev.toString() === identity.dev && details.ino.toString() === identity.ino
22
+ && Number(details.mode & 0o177777n) === identity.mode
23
+ && metadataKeys.every(key => identity[key] === undefined || details[key].toString() === identity[key]);
24
+ }
25
+
26
+ interface WatchTarget {
27
+ readonly absolute: string;
28
+ readonly identity: BrowserWatchIdentity;
29
+ readonly label: string;
30
+ readonly kind: "anchor" | "container" | "payload" | "runtime-root";
31
+ }
32
+
33
+ /** Kernel vnode notes persist until read, including writes whose bytes were restored. */
34
+ export async function createBrowserRuntimeKqueue(options: {
35
+ readonly anchor: string;
36
+ readonly anchorIdentity: BrowserWatchIdentity;
37
+ readonly container: string;
38
+ readonly containerIdentity: BrowserWatchIdentity;
39
+ readonly runtimeRoot: string;
40
+ readonly identities: readonly BrowserWatchIdentity[];
41
+ }): Promise<{ poll: (mutations: Set<string>) => void; close: () => void }> {
42
+ if (process.platform !== "darwin") throw new ApplicationError("unavailable", "Browser vnode watches require macOS.");
43
+ if (!["arm64", "x64"].includes(process.arch) || endianness() !== "LE") throw new ApplicationError("unavailable", "Unsupported browser vnode watch ABI.");
44
+ const { dlopen, FFIType: T, ptr } = await import("bun:ffi");
45
+ const native = dlopen("/usr/lib/libSystem.B.dylib", {
46
+ kqueue: { args: [], returns: T.i32 },
47
+ kevent64: { args: [T.i32, T.ptr, T.i32, T.ptr, T.i32, T.u32, T.ptr], returns: T.i32 },
48
+ open: { args: [T.ptr, T.i32], returns: T.i32 },
49
+ close: { args: [T.i32], returns: T.i32 },
50
+ fcntl: { args: [T.i32, T.i32, T.i32], returns: T.i32 },
51
+ getrlimit: { args: [T.i32, T.ptr], returns: T.i32 },
52
+ fgetattrlist: { args: [T.i32, T.ptr, T.ptr, T.u64, T.u32], returns: T.i32 },
53
+ });
54
+ const targets: WatchTarget[] = [
55
+ { absolute: options.anchor, identity: options.anchorIdentity, label: "<snapshot-anchor>", kind: "anchor" },
56
+ { absolute: options.container, identity: options.containerIdentity, label: "<snapshot-container>", kind: "container" },
57
+ ...options.identities.map(identity => ({ absolute: identity.path === "." ? options.runtimeRoot : join(options.runtimeRoot, identity.path), identity, label: identity.path, kind: identity.path === "." ? "runtime-root" as const : "payload" as const })),
58
+ ];
59
+ let queue = -1;
60
+ let closed = false;
61
+ let closeFailure: unknown;
62
+ let failure: unknown;
63
+ const descriptors = new Map<number, WatchTarget>();
64
+ const payloadAccess = new Map<number, { atimeNs: bigint; readonly flags: number }>();
65
+ const immutableFlags = (descriptor: number): number => {
66
+ // Darwin attrlist: five attribute groups, requesting only ATTR_CMN_FLAGS.
67
+ const request = Buffer.alloc(24), result = Buffer.alloc(8);
68
+ request.writeUInt16LE(5, 0);
69
+ request.writeUInt32LE(0x00040000, 4);
70
+ if (native.symbols.fgetattrlist(descriptor, ptr(request), ptr(result), 8n, 0) !== 0
71
+ || result.readUInt32LE(0) !== 8) {
72
+ throw new ApplicationError("unavailable", "Browser vnode immutable flags could not be read completely.");
73
+ }
74
+ return result.readUInt32LE(4);
75
+ };
76
+ const close = () => {
77
+ if (closed) { if (closeFailure !== undefined) throw closeFailure; return; }
78
+ // A failed close may already have released its numeric descriptor. Never
79
+ // retry it: the number could belong to a different resource afterwards.
80
+ closed = true;
81
+ let failed = false;
82
+ for (const descriptor of descriptors.keys()) {
83
+ if (native.symbols.close(descriptor) === 0) descriptors.delete(descriptor);
84
+ else failed = true;
85
+ }
86
+ if (queue !== -1) {
87
+ if (native.symbols.close(queue) === 0) queue = -1;
88
+ else failed = true;
89
+ }
90
+ if (failed) {
91
+ closeFailure = new ApplicationError("unavailable", "Browser vnode descriptor collection is unproven.", { unresolvedDescriptors: descriptors.size + (queue === -1 ? 0 : 1) });
92
+ throw closeFailure;
93
+ }
94
+ native.close();
95
+ };
96
+ const reject = (message: string): never => { throw new ApplicationError("conflict", message); };
97
+ try {
98
+ const limits = new BigUint64Array(2);
99
+ // RLIMIT_NOFILE; never modify process or host resource limits.
100
+ if (targets.length > 8192 || native.symbols.getrlimit(8, ptr(limits)) !== 0 || limits[0]! < BigInt(targets.length + 64)) {
101
+ reject("Insufficient bounded descriptor capacity for complete browser vnode watches.");
102
+ }
103
+ queue = native.symbols.kqueue();
104
+ if (queue < 0) reject("Could not create browser vnode event queue.");
105
+ if (native.symbols.fcntl(queue, 2, 1) !== 0) reject("Could not prevent browser vnode queue inheritance.");
106
+ for (const target of targets) {
107
+ const path = Buffer.from(`${target.absolute}\0`);
108
+ // O_EVTONLY | O_SYMLINK | O_CLOEXEC. Symlinks bind their own vnode.
109
+ const descriptor = native.symbols.open(ptr(path), 0x8000 | 0x200000 | 0x1000000);
110
+ if (descriptor < 0) reject(`Could not open browser vnode watch: ${target.label}`);
111
+ descriptors.set(descriptor, target);
112
+ const details = fstatSync(descriptor, { bigint: true });
113
+ const identity = target.identity;
114
+ if (!matchesIdentity(details, identity)) {
115
+ reject(`Browser vnode identity changed before registration: ${target.label}`);
116
+ }
117
+ if (target.kind === "payload" && details.isFile()) {
118
+ payloadAccess.set(descriptor, { atimeNs: details.atimeNs, flags: immutableFlags(descriptor) });
119
+ }
120
+ const change = Buffer.alloc(48);
121
+ const receipt = Buffer.alloc(48);
122
+ change.writeBigUInt64LE(BigInt(descriptor), 0);
123
+ change.writeInt16LE(-4, 8); // EVFILT_VNODE
124
+ change.writeUInt16LE(1 | 0x20 | 0x40, 10); // EV_ADD | EV_CLEAR | EV_RECEIPT
125
+ // Unrelated children of /private/tmp are not this snapshot. Its own
126
+ // replacement/revocation/attribute changes remain observed; container
127
+ // and all payload directories independently retain child-write notes.
128
+ change.writeUInt32LE(target.kind === "anchor" ? 0x69 : 0x7f, 12);
129
+ const count = native.symbols.kevent64(queue, ptr(change), 1, ptr(receipt), 1, 1, null);
130
+ if (count !== 1 || receipt.readBigUInt64LE(0) !== BigInt(descriptor)
131
+ || receipt.readInt16LE(8) !== -4 || !(receipt.readUInt16LE(10) & 0x4000)
132
+ || receipt.readBigInt64LE(16) !== 0n) reject(`Browser vnode registration failed: ${target.label}`);
133
+ }
134
+ return {
135
+ close,
136
+ poll(mutations) {
137
+ if (failure !== undefined) throw failure;
138
+ if (closed) reject("Browser vnode event queue was closed before verification.");
139
+ try {
140
+ // One pending entry per registered vnode; aggregate flags are sticky
141
+ // in the caller before the next read can clear kernel state.
142
+ const output = Buffer.alloc(48 * targets.length);
143
+ const count = native.symbols.kevent64(queue, null, 0, ptr(output), targets.length, 1, null);
144
+ if (count < 0 || count > targets.length) reject("Browser vnode event queue could not be read completely.");
145
+ for (let index = 0; index < count; index += 1) {
146
+ const offset = index * 48;
147
+ const descriptor = Number(output.readBigUInt64LE(offset));
148
+ const target = descriptors.get(descriptor);
149
+ const flags = output.readUInt16LE(offset + 10);
150
+ const notes = output.readUInt32LE(offset + 12);
151
+ if (target === undefined) throw new ApplicationError("conflict", "Browser vnode event has an unknown descriptor.");
152
+ if (output.readInt16LE(offset + 8) !== -4 || flags & (0x4000 | 0x8000) || notes === 0 || notes & ~0x7f) {
153
+ reject("Browser vnode event queue returned an untrusted event.");
154
+ }
155
+ // Preserve the existing launch-managed app-root ATTRIB exception.
156
+ // No WRITE/RENAME/DELETE/LINK combination becomes a metadata event.
157
+ if (target.kind === "runtime-root" && notes === 8) {
158
+ mutations.add(`change:${basename(options.runtimeRoot)}`);
159
+ continue;
160
+ }
161
+ const access = payloadAccess.get(descriptor);
162
+ if (target.kind === "payload" && notes === 8 && access !== undefined
163
+ && (access.flags & 2) !== 0 && metadataKeys.every(key => target.identity[key] !== undefined)) {
164
+ const details = fstatSync(descriptor, { bigint: true });
165
+ const pathDetails = lstatSync(target.absolute, { bigint: true });
166
+ // mmap can emit pure ATTRIB for access-time updates on immutable
167
+ // files. Original ctime and all status identity remain fixed:
168
+ // restored flags, xattrs, permissions, and times still reject.
169
+ // The renderer also retains its full content-manifest checks.
170
+ if (details.isFile() && pathDetails.isFile()
171
+ && matchesIdentity(details, target.identity) && matchesIdentity(pathDetails, target.identity)
172
+ && details.atimeNs > access.atimeNs && pathDetails.atimeNs >= details.atimeNs
173
+ && immutableFlags(descriptor) === access.flags) {
174
+ access.atimeNs = pathDetails.atimeNs;
175
+ continue;
176
+ }
177
+ }
178
+ mutations.add(`vnode-${String(notes)}:${target.label}`);
179
+ }
180
+ } catch (error) { failure = error; throw error; }
181
+ },
182
+ };
183
+ } catch (error) { close(); throw error; }
184
+ }
@@ -24,6 +24,7 @@ import { basename, dirname, join } from "node:path";
24
24
  import { z } from "zod";
25
25
 
26
26
  import { verifyStableBrowserWatcherBaseline } from "./html-overlay-watcher-baseline";
27
+ import { createBrowserRuntimeKqueue } from "./html-overlay-kqueue";
27
28
 
28
29
  import {
29
30
  chromium,
@@ -391,10 +392,14 @@ interface SnapshotAnchorIdentity {
391
392
  interface SnapshotPathIdentity {
392
393
  readonly ctimeNs: string;
393
394
  readonly dev: string;
395
+ readonly gid: string;
394
396
  readonly ino: string;
395
397
  readonly mode: number;
398
+ readonly mtimeNs: string;
399
+ readonly nlink: string;
396
400
  readonly path: string;
397
401
  readonly size: string;
402
+ readonly uid: string;
398
403
  }
399
404
 
400
405
  interface SnapshotDirectChildIdentity {
@@ -545,10 +550,14 @@ async function captureBrowserRuntimeSnapshotIdentity(
545
550
  identities.push({
546
551
  ctimeNs: details.ctimeNs.toString(),
547
552
  dev: details.dev.toString(),
553
+ gid: details.gid.toString(),
548
554
  ino: details.ino.toString(),
549
555
  mode: Number(details.mode & 0o177777n),
556
+ mtimeNs: details.mtimeNs.toString(),
557
+ nlink: details.nlink.toString(),
550
558
  path: entry.path,
551
559
  size: details.size.toString(),
560
+ uid: details.uid.toString(),
552
561
  });
553
562
  }
554
563
  return identities;
@@ -569,10 +578,14 @@ async function captureBrowserRuntimeSnapshotContainerIdentity(
569
578
  return {
570
579
  ctimeNs: details.ctimeNs.toString(),
571
580
  dev: details.dev.toString(),
581
+ gid: details.gid.toString(),
572
582
  ino: details.ino.toString(),
573
583
  mode: Number(details.mode & 0o177777n),
584
+ mtimeNs: details.mtimeNs.toString(),
585
+ nlink: details.nlink.toString(),
574
586
  path: "<snapshot-container>",
575
587
  size: details.size.toString(),
588
+ uid: details.uid.toString(),
576
589
  };
577
590
  }
578
591
 
@@ -1458,8 +1471,10 @@ export async function removeBrowserRuntimeSnapshot(
1458
1471
  async function assertNoBrowserRuntimeMutationEvents(
1459
1472
  mutations: ReadonlySet<string>,
1460
1473
  label: string,
1474
+ poll?: () => void,
1461
1475
  ): Promise<void> {
1462
1476
  await new Promise<void>(resolve => setImmediate(resolve));
1477
+ poll?.();
1463
1478
  if (mutations.size > 0) {
1464
1479
  throw new ApplicationError(
1465
1480
  "conflict",
@@ -2011,6 +2026,7 @@ export class PlaywrightHtmlOverlayRenderer implements HtmlOverlayRenderer {
2011
2026
  let browserRuntimeSnapshot: PreparedBrowserRuntimeSnapshot | undefined;
2012
2027
  const browserRuntimeWatchers: FSWatcher[] = [];
2013
2028
  const browserRuntimeMutations = new Set<string>();
2029
+ let kernelWatcher: Awaited<ReturnType<typeof createBrowserRuntimeKqueue>> | undefined;
2014
2030
  let renderFailure: Readonly<{ error: unknown }> | undefined;
2015
2031
  let renderResult: HtmlOverlayFrameRenderResult | undefined;
2016
2032
  let gpuEvidence: HtmlOverlayGpuEvidence | undefined;
@@ -2026,71 +2042,82 @@ export class PlaywrightHtmlOverlayRenderer implements HtmlOverlayRenderer {
2026
2042
  let watcherFailure: Error | undefined;
2027
2043
  const runtimeName = basename(preparedBrowserRuntime.runtimeRoot);
2028
2044
  const snapshotName = basename(preparedBrowserRuntime.directory);
2029
- const anchorWatcher = watch(
2030
- preparedBrowserRuntime.anchorPath,
2031
- { persistent: false },
2032
- (eventType, filename) => {
2033
- const path = filename?.toString() ?? "<snapshot-anchor>";
2034
- if (path === snapshotName) {
2045
+ if (process.platform === "darwin") {
2046
+ kernelWatcher = await createBrowserRuntimeKqueue({
2047
+ anchor: preparedBrowserRuntime.anchorPath,
2048
+ anchorIdentity: preparedBrowserRuntime.anchorIdentity,
2049
+ container: preparedBrowserRuntime.directory,
2050
+ containerIdentity: preparedBrowserRuntime.containerIdentity,
2051
+ runtimeRoot: preparedBrowserRuntime.runtimeRoot,
2052
+ identities: preparedBrowserRuntime.identity,
2053
+ });
2054
+ } else {
2055
+ const anchorWatcher = watch(
2056
+ preparedBrowserRuntime.anchorPath,
2057
+ { persistent: false },
2058
+ (eventType, filename) => {
2059
+ const path = filename?.toString() ?? "<snapshot-anchor>";
2060
+ if (path === snapshotName) {
2061
+ watcherVersion += 1;
2062
+ browserRuntimeMutations.add(`anchor-${eventType}:${path}`);
2063
+ }
2064
+ },
2065
+ );
2066
+ anchorWatcher.on("error", error => {
2067
+ watcherFailure = error;
2068
+ browserRuntimeMutations.add(`anchor-watch-error:${error.message}`);
2069
+ });
2070
+ browserRuntimeWatchers.push(anchorWatcher);
2071
+ const containerWatcher = watch(
2072
+ preparedBrowserRuntime.directory,
2073
+ { persistent: false, recursive: false },
2074
+ (eventType, filename) => {
2075
+ const path = filename?.toString().replaceAll("\\", "/")
2076
+ ?? "<snapshot-container>";
2077
+ if (
2078
+ path === "home"
2079
+ || path.startsWith("home/")
2080
+ || path === "tmp"
2081
+ || path.startsWith("tmp/")
2082
+ ) return;
2083
+ if (path === runtimeName && eventType === "change") {
2084
+ // Gatekeeper may change the app root's first-launch metadata. The
2085
+ // immutable parent and its exact ctime identity independently
2086
+ // prove that this pathname was never removed or replaced.
2087
+ return;
2088
+ }
2089
+ if (
2090
+ request.browserRuntime.provenance.kind === "test-only-unverified"
2091
+ && request.browserRuntime.manifest.layout === "single-executable"
2092
+ && path === runtimeName
2093
+ && eventType === "rename"
2094
+ ) {
2095
+ // Darwin reports synthetic rename events for accesses to the
2096
+ // test-only single-file fixture. Production rejects this layout;
2097
+ // its exact path identity still detects fixture substitution.
2098
+ return;
2099
+ }
2035
2100
  watcherVersion += 1;
2036
- browserRuntimeMutations.add(`anchor-${eventType}:${path}`);
2037
- }
2038
- },
2039
- );
2040
- anchorWatcher.on("error", error => {
2041
- watcherFailure = error;
2042
- browserRuntimeMutations.add(`anchor-watch-error:${error.message}`);
2043
- });
2044
- browserRuntimeWatchers.push(anchorWatcher);
2045
- const containerWatcher = watch(
2046
- preparedBrowserRuntime.directory,
2047
- { persistent: false, recursive: process.platform === "darwin" },
2048
- (eventType, filename) => {
2049
- const path = filename?.toString().replaceAll("\\", "/")
2050
- ?? "<snapshot-container>";
2051
- if (
2052
- path === "home"
2053
- || path.startsWith("home/")
2054
- || path === "tmp"
2055
- || path.startsWith("tmp/")
2056
- ) return;
2057
- if (path === runtimeName && eventType === "change") {
2058
- // Gatekeeper may change the app root's first-launch metadata. The
2059
- // immutable parent and its exact ctime identity independently
2060
- // prove that this pathname was never removed or replaced.
2061
- return;
2062
- }
2063
- if (
2064
- request.browserRuntime.provenance.kind === "test-only-unverified"
2065
- && request.browserRuntime.manifest.layout === "single-executable"
2066
- && path === runtimeName
2067
- && eventType === "rename"
2068
- ) {
2069
- // Darwin reports synthetic rename events for accesses to the
2070
- // test-only single-file fixture. Production rejects this layout;
2071
- // its exact path identity still detects fixture substitution.
2072
- return;
2073
- }
2074
- watcherVersion += 1;
2075
- if (path === runtimeName || path.startsWith(`${runtimeName}/`)) {
2101
+ if (path === runtimeName || path.startsWith(`${runtimeName}/`)) {
2102
+ browserRuntimeMutations.add(`${eventType}:${path}`);
2103
+ return;
2104
+ }
2076
2105
  browserRuntimeMutations.add(`${eventType}:${path}`);
2077
- return;
2078
- }
2079
- browserRuntimeMutations.add(`${eventType}:${path}`);
2080
- },
2081
- );
2082
- containerWatcher.on("error", error => {
2083
- watcherFailure = error;
2084
- browserRuntimeMutations.add(`watch-error:${error.message}`);
2085
- });
2086
- browserRuntimeWatchers.push(containerWatcher);
2106
+ },
2107
+ );
2108
+ containerWatcher.on("error", error => {
2109
+ watcherFailure = error;
2110
+ browserRuntimeMutations.add(`watch-error:${error.message}`);
2111
+ });
2112
+ browserRuntimeWatchers.push(containerWatcher);
2113
+ }
2087
2114
  let browser: Browser | undefined;
2088
2115
  let browserFailure: Readonly<{ error: unknown }> | undefined;
2089
2116
  try {
2090
2117
  // Delayed preparation notifications can arrive during full verification.
2091
2118
  // Recheck the SAME pre-wait identities until one pass is stable. Nothing
2092
2119
  // after the synchronous baseline commit is discarded.
2093
- await verifyStableBrowserWatcherBaseline({
2120
+ const baselineOptions = {
2094
2121
  signal,
2095
2122
  version: () => watcherVersion,
2096
2123
  failure: () => watcherFailure,
@@ -2121,7 +2148,16 @@ export class PlaywrightHtmlOverlayRenderer implements HtmlOverlayRenderer {
2121
2148
  );
2122
2149
  },
2123
2150
  commit: () => { browserRuntimeMutations.clear(); },
2124
- });
2151
+ };
2152
+ if (kernelWatcher !== undefined) {
2153
+ // Vnode notes begin at registration, not an asynchronous history
2154
+ // stream. Never clear a payload event, including during baseline.
2155
+ await baselineOptions.verify();
2156
+ kernelWatcher.poll(browserRuntimeMutations);
2157
+ } else {
2158
+ await verifyStableBrowserWatcherBaseline(baselineOptions);
2159
+ }
2160
+ kernelWatcher?.poll(browserRuntimeMutations);
2125
2161
  discardProvenLaunchManagedRootMetadataEvent(
2126
2162
  browserRuntimeMutations,
2127
2163
  runtimeName,
@@ -2129,6 +2165,7 @@ export class PlaywrightHtmlOverlayRenderer implements HtmlOverlayRenderer {
2129
2165
  await assertNoBrowserRuntimeMutationEvents(
2130
2166
  browserRuntimeMutations,
2131
2167
  "before browser launch",
2168
+ () => kernelWatcher?.poll(browserRuntimeMutations),
2132
2169
  );
2133
2170
  const launchedBrowser = await boundedBrowserStep(
2134
2171
  async () => {
@@ -2188,6 +2225,7 @@ export class PlaywrightHtmlOverlayRenderer implements HtmlOverlayRenderer {
2188
2225
  "during browser launch",
2189
2226
  signal,
2190
2227
  );
2228
+ kernelWatcher?.poll(browserRuntimeMutations);
2191
2229
  discardProvenLaunchManagedRootMetadataEvent(
2192
2230
  browserRuntimeMutations,
2193
2231
  runtimeName,
@@ -2195,6 +2233,7 @@ export class PlaywrightHtmlOverlayRenderer implements HtmlOverlayRenderer {
2195
2233
  await assertNoBrowserRuntimeMutationEvents(
2196
2234
  browserRuntimeMutations,
2197
2235
  "during browser launch",
2236
+ () => kernelWatcher?.poll(browserRuntimeMutations),
2198
2237
  );
2199
2238
  const contextContract = HTML_OVERLAY_RENDERER_CONTRACT.browserContext;
2200
2239
  const context = await boundedBrowserStep(
@@ -2489,6 +2528,7 @@ export class PlaywrightHtmlOverlayRenderer implements HtmlOverlayRenderer {
2489
2528
  "through browser shutdown",
2490
2529
  signal,
2491
2530
  );
2531
+ kernelWatcher?.poll(browserRuntimeMutations);
2492
2532
  discardProvenLaunchManagedRootMetadataEvent(
2493
2533
  browserRuntimeMutations,
2494
2534
  runtimeName,
@@ -2496,6 +2536,7 @@ export class PlaywrightHtmlOverlayRenderer implements HtmlOverlayRenderer {
2496
2536
  await assertNoBrowserRuntimeMutationEvents(
2497
2537
  browserRuntimeMutations,
2498
2538
  "through browser shutdown",
2539
+ () => kernelWatcher?.poll(browserRuntimeMutations),
2499
2540
  );
2500
2541
  renderResult = {
2501
2542
  ...(gpuEvidence === undefined ? {} : { gpuEvidence }),
@@ -2510,6 +2551,12 @@ export class PlaywrightHtmlOverlayRenderer implements HtmlOverlayRenderer {
2510
2551
  .catch(() => undefined);
2511
2552
  } finally {
2512
2553
  for (const watcher of browserRuntimeWatchers) watcher.close();
2554
+ try { kernelWatcher?.close(); } catch (error) {
2555
+ renderFailure = { error: new ApplicationError("unavailable", "Browser vnode watch cleanup failed; frames were not published.", {
2556
+ cause: error,
2557
+ ...(renderFailure === undefined ? {} : { renderFailure: renderFailure.error }),
2558
+ }) };
2559
+ }
2513
2560
  if (browserRuntimeSnapshot !== undefined) {
2514
2561
  if (browserShutdownRequired) {
2515
2562
  const retainedError = new ApplicationError("unavailable", "HTML overlay browser shutdown is unproven; its active runtime snapshot was retained and frames were not published.", {