[PR #7043] [MERGED] fix(browser): admit passive capture events against the shared retained budget #7599

Closed
opened 2026-11-03 13:39:01 +00:00 by bsakaguchi · 0 comments
Owner

📋 Pull Request Information

Original PR: https://github.com/morluto/rea/pull/7043
Author: @DeryFerd
Created: 10/24/2026
Status: ✅ Merged
Merged: 10/24/2026
Merged by: @morluto

Base: main ← Head: fix/cdp-capture-event-budget


📝 Commits (1)

  • fc03d0f fix(browser): admit passive capture events against the shared retained budget

📊 Changes

4 files changed (+202 additions, -3 deletions)

View changed files

📝 src/browser/CdpCaptureEventHandlers.ts (+52 -3)
📝 src/browser/CdpCaptureEventState.ts (+2 -0)
➕ src/browser/CdpCaptureEvents.eventBudget.test.ts (+146 -0)
📝 src/browser/CdpCaptureEvents.ts (+2 -0)

📄 Description

Summary

Passive page capture now admits each console entry and websocket frame against the shared 8 MiB retained-event budget before retaining it, and counts a refused entry as a dropped event in the capture's completeness report. Console entries and websocket frames were the two retention surfaces in this capture with no aggregate ceiling at all, so a page could grow the inspection's memory without limit until the process died, and the completeness report still said complete.

Every sibling retention surface already enforced the same constant: the observation session's pre-arm buffer and document-request identities, the browser runtime sources, requests, and metadata, the V8 inspector's retained metadata, and this same capture's own script metadata, which has had its own count-and-bytes budget with visible omissions for a while. This change closes the two paths that were left out, using the budget and the reporting vocabulary that already existed.

Problem and expected behavior

Smallest trigger: open a page with inspect_web_page whose script logs a few hundred megabytes of console text, or that sends websocket text frames whose JSON payloads carry hundreds of thousands of distinct keys.

Before this change, every Runtime.consoleAPICalled event pushed a retained entry with its captured text, and every Network.webSocketFrameSent or Received event pushed a retained frame carrying the payload's inferred JSON shape. Nothing summed what was retained. The console handler already computed a retained_bytes value for each entry's captured text, but that number was stored per entry and never compared against anything. The websocket shape summary grows with the payload's own keys, because the shape records every key name it sees, so a wide document made a wide retained shape. Both arrays grew until the process ran out of memory, and because the MCP response budget is enforced after the handler returns, the growth finished before any typed refusal could exist.

Expected behavior follows the rest of the browser runtime: retention is admitted against WEB_RUNTIME_LIMITS.retainedEventBytes, a refused entry is counted as a dropped event of its kind, and the inspection reports a truncated inventory with the dropped count rather than a complete one backed by unbounded memory. An inventory that never approaches the budget is byte-for-byte unchanged.

Change and scope

One counter, retainedEventBytes, now lives on the capture state alongside the existing script-metadata byte counter, resets in beginAuthorizedFrame together with the rest of the per-frame state, and is charged by the two handlers that admit events.

The console handler measures what the entry actually retains: the entry's type, its source url, its argument-type strings, and the text capture's retained bytes, which the handler was already computing per entry. A frame retains its shape summary rather than the payload, so the websocket handler charges the serialized size of the shape it keeps. A frame carrying a two-megabyte string costs a handful of bytes, because the shape records the type, not the value; a frame carrying a document with half a million keys costs the shape that names them.

Admission mirrors the script-metadata budget in the same file: an entry that does not fit in the remaining budget is not retained, and completeness.drop records it under the console_events or websocket_frames kind. That vocabulary already existed in the completeness tracker, carried both kinds, and reported a dropped_events count with a total, but only the scripts kind had ever used it. No new setting is introduced. The shared 8 MiB constant is reused as-is, so an operator who changes nothing sees the same number reported everywhere the budget appears.

Deliberately out of scope: response metadata and agent hints, whose rows are fixed-shape facts already bounded per response, and any per-caller knob for these two kinds. The scenario capture path already exposes caller-selected event budgets for its own events, and adding a second knob system to the passive path would be a new product decision rather than a gap fix.

Contract and boundary impact

  • Semantic owner and earliest changed stage: src/browser/CdpCaptureEventHandlers.ts owns passive-capture event ingestion; the change runs at admission time, before any entry is retained. src/browser/CdpCaptureEvents.ts and CdpCaptureEventState.ts own the state field and its per-frame reset.
  • Contribution scope fit and linked maintainer decision: routine fix inside an existing capture boundary; no target execution, injection, mutation, or protection recovery is added; not applicable.
  • CLI and MCP/tool-catalog contract: no tool schema, description, or catalog change. The completeness report can now show console_events or websocket_frames under dropped_events with a count and mark those sections truncated once the budget is exhausted, where the same capture previously reported them complete. Both fields already existed in the completeness schema and were already produced for late-dropped scripts, so no schema or catalog regeneration is involved.
  • Provider, bridge, target-format, or platform compatibility: CDP passive capture on all platforms. The active observation paths, the scenario capture path, and the script metadata budget are untouched.
  • Evidence, artifact, provenance, or reconstruction contract: Evidence provenance is unchanged. A capture that hits the budget now reports its inventory as truncated with a dropped count instead of presenting an unbounded inventory as complete, which is the direction the comparison rules already assume: omissions are never treated as equivalence.
  • Target selection, authority, upstream behavior, and cleanup details: not applicable.
  • Generated metadata (docs/public/product-catalog.json), package, or installation impact: none. No dependency, lockfile, generated catalog, or skill bundle change.

Expected effects (check known effects; use documentation-only on its own).

  • Read existing files or captured data.
  • Launch or attach to a process.
  • Inject instrumentation or run scripts/code.
  • Change process memory, registers, files, or configuration.
  • Contact network services.
  • Recover protected code or work around protections.
  • Documentation or usability only; no target interaction.
  • Not sure yet; investigation or maintainer guidance needed.

Evidence and regression coverage

  • Tests added or updated: src/browser/CdpCaptureEvents.eventBudget.test.ts, three cases. The console case sends megabyte-sized entries until the shared budget is exceeded and asserts that fewer entries than sent are retained, that the retained count is above zero, and that the completeness snapshot reports the exact dropped count for console_events and the same number in the total. A second console case keeps a small inventory far under the budget and asserts all sixteen entries are retained with zero dropped events, so ordinary captures are untouched. The websocket case retains a small frame, then sends one frame whose shape alone exceeds the budget, and asserts exactly the small frame survives with websocket_frames dropped at one.
  • Base reproduction or other evidence: on the base revision the console case fails with ten entries retained where the budget admits fewer, because nothing refused them, and the websocket case fails with both frames retained. Those are the two defects observed directly, one per kind.
  • User-visible CLI/MCP output (if applicable): an inspect_web_page capture whose console traffic exceeds the budget now reports the console section as truncated with a dropped_events.console_events count, and likewise for websocket frames, instead of returning a complete verdict over an inventory that only ended because the process stopped receiving.
  • Remaining proof gaps: no real browser session was driven on this host. The mechanism is exercised through the same synthetic CDP event ingestion the surrounding suites use, and the browser CI lane owns the live-page coverage.

For evidence-bearing changes:

  • Observed, derived, and inferred claims remain distinguishable.
  • Artifact identity, source provenance, and failed attempts remain preserved.
  • Unsupported, incomplete, unavailable, or uncertain outcomes remain visible.

Validation performed

  • npx vitest run src/browser/CdpCaptureEvents.eventBudget.test.ts — 3 passed, 2 of which failed on the base revision for the reasons quoted above.
  • npx vitest run over the neighboring console, websocket, coverage, context-retention, and completeness suites — 26 passed.
  • The wider browser project on this Windows host has 26 failures in three Electron and WebBundle worker-spawn files that predate this change. Verified by running the identical 45-file set twice: 26 failed, 644 passed on the base revision with this change stashed, and 26 failed, 647 passed with this change applied, the difference being exactly the three new tests. No file the change touches appears among the failures.
  • npx tsc -p .tmp-tsconfig.json --noEmit over the changed set and its consumers, narrowed from ./tsconfig.json — exit 0.
  • npx oxlint on the four changed files — 0 errors.
  • npx oxfmt --check on the four changed files — all matched files use the correct format.
  • npm run verify:module-boundaries — pass.

Compatibility, safety, and release

  • Breaking changes or migration steps: none. Captures that stay under the shared budget, which is every ordinary capture, produce identical output. A capture that exhausts the budget previously kept allocating and eventually returned whatever survived; it now stops retaining and says so with a count. The completeness fields it uses already existed. No ! marker is needed.
  • Real Hopper/Ghidra, browser, or OS coverage: not run locally on this host. The change is exercised through synthetic CDP events; the browser CI lane owns the live-page workflow.
  • Package or release metadata impact: none. No dependency, lockfile, catalog, or skill bundle change.
  • Security, privacy, process, or containment review: the change reduces what one observed page can make the inspection retain, and it adds no new input path, network path, or credential handling. The budget constant is an internal admission limit reused from the browser runtime, not a new setting, and nothing about token or origin handling changes.

Author readiness

Human owner: @DeryFerd

  • I reviewed the final diff and relevant existing code, understand the change and its edge cases, and take responsibility for the complete PR, including AI-assisted work.
  • I checked the evidence above, confirmed it supports the current change, and recorded meaningful validation gaps.
  • I declare this PR ready for initial maintainer review.

Before merge

  • This PR addresses a concrete problem or an agreed enhancement.
  • Material scope and authority questions are resolved, and any required maintainer scope decision covers the final diff.
  • The PR has one focused outcome and the title follows type(scope): outcome.
  • Related issue is linked, or the reason for not linking one is stated above.
  • Tests cover changed observable behavior and meaningful failure paths.
  • Owning docs, contracts, and generated metadata are updated where needed. No owning document describes passive-capture event retention, and the completeness contract this uses is unchanged.
  • User-visible CLI/MCP changes include representative output, quoted under Evidence and regression coverage.
  • I checked the final diff for secrets, unrelated cleanup, and unsupported claims.

🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/morluto/rea/pull/7043 **Author:** [@DeryFerd](https://github.com/DeryFerd) **Created:** 10/24/2026 **Status:** ✅ Merged **Merged:** 10/24/2026 **Merged by:** [@morluto](https://github.com/morluto) **Base:** `main` ← **Head:** `fix/cdp-capture-event-budget` --- ### 📝 Commits (1) - [`fc03d0f`](https://github.com/morluto/rea/commit/fc03d0fdbce0978dc01413f6e99d7f6847ec0b38) fix(browser): admit passive capture events against the shared retained budget ### 📊 Changes **4 files changed** (+202 additions, -3 deletions) <details> <summary>View changed files</summary> 📝 `src/browser/CdpCaptureEventHandlers.ts` (+52 -3) 📝 `src/browser/CdpCaptureEventState.ts` (+2 -0) ➕ `src/browser/CdpCaptureEvents.eventBudget.test.ts` (+146 -0) 📝 `src/browser/CdpCaptureEvents.ts` (+2 -0) </details> ### 📄 Description ## Summary Passive page capture now admits each console entry and websocket frame against the shared 8 MiB retained-event budget before retaining it, and counts a refused entry as a dropped event in the capture's completeness report. Console entries and websocket frames were the two retention surfaces in this capture with no aggregate ceiling at all, so a page could grow the inspection's memory without limit until the process died, and the completeness report still said complete. Every sibling retention surface already enforced the same constant: the observation session's pre-arm buffer and document-request identities, the browser runtime sources, requests, and metadata, the V8 inspector's retained metadata, and this same capture's own script metadata, which has had its own count-and-bytes budget with visible omissions for a while. This change closes the two paths that were left out, using the budget and the reporting vocabulary that already existed. ## Problem and expected behavior Smallest trigger: open a page with `inspect_web_page` whose script logs a few hundred megabytes of console text, or that sends websocket text frames whose JSON payloads carry hundreds of thousands of distinct keys. Before this change, every `Runtime.consoleAPICalled` event pushed a retained entry with its captured text, and every `Network.webSocketFrameSent` or `Received` event pushed a retained frame carrying the payload's inferred JSON shape. Nothing summed what was retained. The console handler already computed a `retained_bytes` value for each entry's captured text, but that number was stored per entry and never compared against anything. The websocket shape summary grows with the payload's own keys, because the shape records every key name it sees, so a wide document made a wide retained shape. Both arrays grew until the process ran out of memory, and because the MCP response budget is enforced after the handler returns, the growth finished before any typed refusal could exist. Expected behavior follows the rest of the browser runtime: retention is admitted against `WEB_RUNTIME_LIMITS.retainedEventBytes`, a refused entry is counted as a dropped event of its kind, and the inspection reports a truncated inventory with the dropped count rather than a complete one backed by unbounded memory. An inventory that never approaches the budget is byte-for-byte unchanged. ## Change and scope One counter, `retainedEventBytes`, now lives on the capture state alongside the existing script-metadata byte counter, resets in `beginAuthorizedFrame` together with the rest of the per-frame state, and is charged by the two handlers that admit events. The console handler measures what the entry actually retains: the entry's type, its source url, its argument-type strings, and the text capture's retained bytes, which the handler was already computing per entry. A frame retains its shape summary rather than the payload, so the websocket handler charges the serialized size of the shape it keeps. A frame carrying a two-megabyte string costs a handful of bytes, because the shape records the type, not the value; a frame carrying a document with half a million keys costs the shape that names them. Admission mirrors the script-metadata budget in the same file: an entry that does not fit in the remaining budget is not retained, and `completeness.drop` records it under the `console_events` or `websocket_frames` kind. That vocabulary already existed in the completeness tracker, carried both kinds, and reported a `dropped_events` count with a total, but only the scripts kind had ever used it. No new setting is introduced. The shared 8 MiB constant is reused as-is, so an operator who changes nothing sees the same number reported everywhere the budget appears. Deliberately out of scope: response metadata and agent hints, whose rows are fixed-shape facts already bounded per response, and any per-caller knob for these two kinds. The scenario capture path already exposes caller-selected event budgets for its own events, and adding a second knob system to the passive path would be a new product decision rather than a gap fix. ## Contract and boundary impact - Semantic owner and earliest changed stage: `src/browser/CdpCaptureEventHandlers.ts` owns passive-capture event ingestion; the change runs at admission time, before any entry is retained. `src/browser/CdpCaptureEvents.ts` and `CdpCaptureEventState.ts` own the state field and its per-frame reset. - Contribution scope fit and linked maintainer decision: routine fix inside an existing capture boundary; no target execution, injection, mutation, or protection recovery is added; not applicable. - CLI and MCP/tool-catalog contract: no tool schema, description, or catalog change. The completeness report can now show `console_events` or `websocket_frames` under `dropped_events` with a count and mark those sections truncated once the budget is exhausted, where the same capture previously reported them complete. Both fields already existed in the completeness schema and were already produced for late-dropped scripts, so no schema or catalog regeneration is involved. - Provider, bridge, target-format, or platform compatibility: CDP passive capture on all platforms. The active observation paths, the scenario capture path, and the script metadata budget are untouched. - Evidence, artifact, provenance, or reconstruction contract: Evidence provenance is unchanged. A capture that hits the budget now reports its inventory as truncated with a dropped count instead of presenting an unbounded inventory as complete, which is the direction the comparison rules already assume: omissions are never treated as equivalence. - Target selection, authority, upstream behavior, and cleanup details: not applicable. - Generated metadata (`docs/public/product-catalog.json`), package, or installation impact: none. No dependency, lockfile, generated catalog, or skill bundle change. Expected effects (check known effects; use documentation-only on its own). - [x] Read existing files or captured data. - [ ] Launch or attach to a process. - [ ] Inject instrumentation or run scripts/code. - [ ] Change process memory, registers, files, or configuration. - [ ] Contact network services. - [ ] Recover protected code or work around protections. - [ ] Documentation or usability only; no target interaction. - [ ] Not sure yet; investigation or maintainer guidance needed. ## Evidence and regression coverage - Tests added or updated: `src/browser/CdpCaptureEvents.eventBudget.test.ts`, three cases. The console case sends megabyte-sized entries until the shared budget is exceeded and asserts that fewer entries than sent are retained, that the retained count is above zero, and that the completeness snapshot reports the exact dropped count for `console_events` and the same number in the total. A second console case keeps a small inventory far under the budget and asserts all sixteen entries are retained with zero dropped events, so ordinary captures are untouched. The websocket case retains a small frame, then sends one frame whose shape alone exceeds the budget, and asserts exactly the small frame survives with `websocket_frames` dropped at one. - Base reproduction or other evidence: on the base revision the console case fails with ten entries retained where the budget admits fewer, because nothing refused them, and the websocket case fails with both frames retained. Those are the two defects observed directly, one per kind. - User-visible CLI/MCP output (if applicable): an `inspect_web_page` capture whose console traffic exceeds the budget now reports the console section as truncated with a `dropped_events.console_events` count, and likewise for websocket frames, instead of returning a complete verdict over an inventory that only ended because the process stopped receiving. - Remaining proof gaps: no real browser session was driven on this host. The mechanism is exercised through the same synthetic CDP event ingestion the surrounding suites use, and the browser CI lane owns the live-page coverage. For evidence-bearing changes: - [x] Observed, derived, and inferred claims remain distinguishable. - [x] Artifact identity, source provenance, and failed attempts remain preserved. - [x] Unsupported, incomplete, unavailable, or uncertain outcomes remain visible. ## Validation performed - `npx vitest run src/browser/CdpCaptureEvents.eventBudget.test.ts` — 3 passed, 2 of which failed on the base revision for the reasons quoted above. - `npx vitest run` over the neighboring console, websocket, coverage, context-retention, and completeness suites — 26 passed. - The wider browser project on this Windows host has 26 failures in three Electron and WebBundle worker-spawn files that predate this change. Verified by running the identical 45-file set twice: 26 failed, 644 passed on the base revision with this change stashed, and 26 failed, 647 passed with this change applied, the difference being exactly the three new tests. No file the change touches appears among the failures. - `npx tsc -p .tmp-tsconfig.json --noEmit` over the changed set and its consumers, narrowed from `./tsconfig.json` — exit 0. - `npx oxlint` on the four changed files — 0 errors. - `npx oxfmt --check` on the four changed files — all matched files use the correct format. - `npm run verify:module-boundaries` — pass. ## Compatibility, safety, and release - Breaking changes or migration steps: none. Captures that stay under the shared budget, which is every ordinary capture, produce identical output. A capture that exhausts the budget previously kept allocating and eventually returned whatever survived; it now stops retaining and says so with a count. The completeness fields it uses already existed. No `!` marker is needed. - Real Hopper/Ghidra, browser, or OS coverage: not run locally on this host. The change is exercised through synthetic CDP events; the browser CI lane owns the live-page workflow. - Package or release metadata impact: none. No dependency, lockfile, catalog, or skill bundle change. - Security, privacy, process, or containment review: the change reduces what one observed page can make the inspection retain, and it adds no new input path, network path, or credential handling. The budget constant is an internal admission limit reused from the browser runtime, not a new setting, and nothing about token or origin handling changes. ## Author readiness Human owner: @DeryFerd - [x] I reviewed the final diff and relevant existing code, understand the change and its edge cases, and take responsibility for the complete PR, including AI-assisted work. - [x] I checked the evidence above, confirmed it supports the current change, and recorded meaningful validation gaps. - [x] I declare this PR ready for initial maintainer review. ## Before merge - [x] This PR addresses a concrete problem or an agreed enhancement. - [x] Material scope and authority questions are resolved, and any required maintainer scope decision covers the final diff. - [x] The PR has one focused outcome and the title follows `type(scope): outcome`. - [x] Related issue is linked, or the reason for not linking one is stated above. - [x] Tests cover changed observable behavior and meaningful failure paths. - [x] Owning docs, contracts, and generated metadata are updated where needed. No owning document describes passive-capture event retention, and the completeness contract this uses is unchanged. - [x] User-visible CLI/MCP changes include representative output, quoted under Evidence and regression coverage. - [x] I checked the final diff for secrets, unrelated cleanup, and unsupported claims. --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
bsakaguchi 2026-11-03 13:39:01 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#7599
No description provided.