[GH-ISSUE #5132] [Bug] analyze_web_bundle analyzes scripts in-process: a page loading react-dom 18.3.1 aborts the CLI and the MCP server with heap OOM #3157

Closed
opened 2026-10-31 00:37:19 +00:00 by bsakaguchi · 3 comments
Owner

Originally created by @csorodrigo on GitHub (Oct 18, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/5132

Before submitting

  • I searched the open and closed issues for a related report. The expansion itself is #4348 (fix proposed in #4364). This report is about the missing containment in analyze_web_bundle.
  • I removed credentials, tokens, private paths, and sensitive target data.

Area

Process capture or runtime observation

Observed behavior

analyze_web_bundle never returns for a page that loads the official react-dom@18.3.1 production build. The process heap grows to the V8 limit and the process aborts with FATAL ERROR: Ineffective mark-compacts near heap limit … JavaScript heap out of memory. Over MCP, the REA server itself aborts, so the client loses every tool and gets no JSON-RPC response.

analyze_javascript_application contains the same file: it exits 0 and records Owned JavaScript worker exhausted its heap while analyzing react-dom.production.min.js during static.

Reproduction

Executed:

  1. Create index.html next to the npm UMD production builds of react@18.3.1 and react-dom@18.3.1. The react-dom file has SHA-256 35f4f974f4b2bcd44da73963347f8952e341f83909e4498227d4e26b98f66f0d:

    <!doctype html>
    <html><head><meta charset="utf-8"><title>repro</title></head>
    <body><div id="root"></div>
    <script src="react.production.min.js"></script>
    <script src="react-dom.production.min.js"></script>
    <script>ReactDOM.createRoot(document.getElementById("root")).render(React.createElement("p", null, "ok"));</script>
    </body></html>
    
  2. Serve the page with python3 -m http.server 8765 --bind 127.0.0.1.

  3. Open it in Chrome started with --headless=new --remote-debugging-port=9339 --user-data-dir=<new dir>.

  4. Run rea list-browser-targets http://127.0.0.1:9339.

  5. Run rea analyze-web-bundle http://127.0.0.1:9339 <target_id> --observation-ms 1000 --json.

  6. Over MCP, send tools/call analyze_web_bundle with { "cdp_endpoint": "http://127.0.0.1:9339", "target_id": "<target_id>", "observation_ms": 1000 } to rea mcp over stdio.

Expected behavior

Per-script failures should be contained as analyze_javascript_application already does:

  • Run each captured script in the owned JavaScript analysis worker, with a heap budget and a per-source deadline.
  • Record a resource-constraint limitation for the script that exceeds a bound, and keep the findings from the other scripts.
  • Honor cancellation, so a client timeout or SIGTERM ends the call and the MCP server stays up.

Impact and scope

Every run, in both reproductions above. Any page that ships a React 18 production renderer can trigger it, which includes Next.js 14 App Router sites. On a real Next.js 14 site with 47 captured scripts, 46 scripts take 5–370 ms each in analyzeScript. Its react-dom chunk (fd9d1056-*.js, React 18.3.0-canary-178c267a4e-20241218) exceeds 60 s.

Over MCP, one call terminates the server for the whole agent session. On a host under heavy memory pressure, the same CLI call ran for up to 1 h 50 m without completing. SIGTERM from timeout 240 did not end it. That is consistent with the synchronous analysis keeping the event loop busy, but it is not proven.

Untested: Linux, Windows, and a build with #4364 applied.

REA version

rea-agents 6.7.0 (npm). The source path below is unchanged on main at 559af3d3f.

Runtime and platform

macOS 26.5.2 arm64, Node 22.22.0, NODE_OPTIONS=--max-old-space-size=4096, Chrome 155.0.8059.80 headless.

Target and artifact context

Public npm builds react@18.3.1 and react-dom@18.3.1 (UMD and CJS production .min.js).

Interface

CLI and MCP.

Earliest failure stage

Native analysis or reconstruction. This is the static and semantic analysis of captured scripts, after capture succeeds.

Relevant output or evidence

Surface Result
CLI RSS 1.4 GB at 15 s and 2.9 GB at 30 s. No output. Heap OOM abort at ~71 s
MCP (tools/call analyze_web_bundle) No response. The server exits with SIGABRT (heap OOM) at 80.8 s
analyzeScript from dist/domain/webBundleAnalyzerInspection.js, alone in a worker with a 2 GiB heap CJS build (SHA-256 cbf7fe6b…16f5f47): >120 s, then ERR_WORKER_OUT_OF_MEMORY. UMD build: >120 s
rea analyze-javascript-application <dir with the UMD react-dom> exit 0 in 22 s, with the Owned JavaScript worker exhausted its heap limitation

Source-derived cause (not executed beyond the profile):

  • CdpBrowserProvider.analyzeBundle calls analyzeCapturedWebBundle(...) synchronously after capture (src/browser/CdpBrowserProvider.ts). The MCP tool reaches the same path through src/server/registerBrowserTools.ts.
  • analyzeScript (src/domain/webBundleAnalyzerInspection.ts) drives indexJavaScriptBindingsWithReceiversSteps, indexAmbientWritesSteps and the lazy indexMemberMutationFactsSteps with completeSemanticSteps. That function loops to completion without yielding or checking a signal or deadline.
  • A 60 s V8 tick profile of the CLI run shows the hot chain resolveJavaScriptIntrinsicInvocation → intrinsicPath (createSemanticOpaqueGlobalObserver) → the createAmbientPathResolver(state, true) resolver → continueReturned → the createSemanticReturnedReferences expansion. SetConstructor, SetPrototypeAdd and ordered-hash lookups dominate. This matches part 2 of #4348. The observer resolves ambient paths per CallExpression with possibleWrites = true, which bypasses the per-node cache.

NOT_ENOUGH_INFO: why mayReferenceGlobalEval is set for react-dom, which activates the opaque-global observer.

Originally created by @csorodrigo on GitHub (Oct 18, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/5132 ### Before submitting - [x] I searched the open and closed issues for a related report. The expansion itself is #4348 (fix proposed in #4364). This report is about the missing containment in `analyze_web_bundle`. - [x] I removed credentials, tokens, private paths, and sensitive target data. ### Area Process capture or runtime observation ### Observed behavior `analyze_web_bundle` never returns for a page that loads the official `react-dom@18.3.1` production build. The process heap grows to the V8 limit and the process aborts with `FATAL ERROR: Ineffective mark-compacts near heap limit … JavaScript heap out of memory`. Over MCP, the REA server itself aborts, so the client loses every tool and gets no JSON-RPC response. `analyze_javascript_application` contains the same file: it exits 0 and records `Owned JavaScript worker exhausted its heap while analyzing react-dom.production.min.js during static`. ### Reproduction Executed: 1. Create `index.html` next to the npm UMD production builds of `react@18.3.1` and `react-dom@18.3.1`. The react-dom file has SHA-256 `35f4f974f4b2bcd44da73963347f8952e341f83909e4498227d4e26b98f66f0d`: ```html <!doctype html> <html><head><meta charset="utf-8"><title>repro</title></head> <body><div id="root"></div> <script src="react.production.min.js"></script> <script src="react-dom.production.min.js"></script> <script>ReactDOM.createRoot(document.getElementById("root")).render(React.createElement("p", null, "ok"));</script> </body></html> ``` 2. Serve the page with `python3 -m http.server 8765 --bind 127.0.0.1`. 3. Open it in Chrome started with `--headless=new --remote-debugging-port=9339 --user-data-dir=<new dir>`. 4. Run `rea list-browser-targets http://127.0.0.1:9339`. 5. Run `rea analyze-web-bundle http://127.0.0.1:9339 <target_id> --observation-ms 1000 --json`. 6. Over MCP, send `tools/call` `analyze_web_bundle` with `{ "cdp_endpoint": "http://127.0.0.1:9339", "target_id": "<target_id>", "observation_ms": 1000 }` to `rea mcp` over stdio. ### Expected behavior Per-script failures should be contained as `analyze_javascript_application` already does: - Run each captured script in the owned JavaScript analysis worker, with a heap budget and a per-source deadline. - Record a resource-constraint limitation for the script that exceeds a bound, and keep the findings from the other scripts. - Honor cancellation, so a client timeout or SIGTERM ends the call and the MCP server stays up. ### Impact and scope Every run, in both reproductions above. Any page that ships a React 18 production renderer can trigger it, which includes Next.js 14 App Router sites. On a real Next.js 14 site with 47 captured scripts, 46 scripts take 5–370 ms each in `analyzeScript`. Its react-dom chunk (`fd9d1056-*.js`, React `18.3.0-canary-178c267a4e-20241218`) exceeds 60 s. Over MCP, one call terminates the server for the whole agent session. On a host under heavy memory pressure, the same CLI call ran for up to 1 h 50 m without completing. SIGTERM from `timeout 240` did not end it. That is consistent with the synchronous analysis keeping the event loop busy, but it is not proven. Untested: Linux, Windows, and a build with #4364 applied. ### REA version rea-agents 6.7.0 (npm). The source path below is unchanged on `main` at `559af3d3f`. ### Runtime and platform macOS 26.5.2 arm64, Node 22.22.0, `NODE_OPTIONS=--max-old-space-size=4096`, Chrome 155.0.8059.80 headless. ### Target and artifact context Public npm builds `react@18.3.1` and `react-dom@18.3.1` (UMD and CJS production `.min.js`). ### Interface CLI and MCP. ### Earliest failure stage Native analysis or reconstruction. This is the static and semantic analysis of captured scripts, after capture succeeds. ### Relevant output or evidence | Surface | Result | | --- | --- | | CLI | RSS 1.4 GB at 15 s and 2.9 GB at 30 s. No output. Heap OOM abort at ~71 s | | MCP (`tools/call analyze_web_bundle`) | No response. The server exits with `SIGABRT` (heap OOM) at 80.8 s | | `analyzeScript` from `dist/domain/webBundleAnalyzerInspection.js`, alone in a worker with a 2 GiB heap | CJS build (SHA-256 `cbf7fe6b…16f5f47`): >120 s, then `ERR_WORKER_OUT_OF_MEMORY`. UMD build: >120 s | | `rea analyze-javascript-application <dir with the UMD react-dom>` | exit 0 in 22 s, with the `Owned JavaScript worker exhausted its heap` limitation | Source-derived cause (not executed beyond the profile): - `CdpBrowserProvider.analyzeBundle` calls `analyzeCapturedWebBundle(...)` synchronously after capture (`src/browser/CdpBrowserProvider.ts`). The MCP tool reaches the same path through `src/server/registerBrowserTools.ts`. - `analyzeScript` (`src/domain/webBundleAnalyzerInspection.ts`) drives `indexJavaScriptBindingsWithReceiversSteps`, `indexAmbientWritesSteps` and the lazy `indexMemberMutationFactsSteps` with `completeSemanticSteps`. That function loops to completion without yielding or checking a signal or deadline. - A 60 s V8 tick profile of the CLI run shows the hot chain `resolveJavaScriptIntrinsicInvocation` → `intrinsicPath` (`createSemanticOpaqueGlobalObserver`) → the `createAmbientPathResolver(state, true)` resolver → `continueReturned` → the `createSemanticReturnedReferences` expansion. `SetConstructor`, `SetPrototypeAdd` and ordered-hash lookups dominate. This matches part 2 of #4348. The observer resolves ambient paths per `CallExpression` with `possibleWrites = true`, which bypasses the per-node cache. `NOT_ENOUGH_INFO`: why `mayReferenceGlobalEval` is set for react-dom, which activates the opaque-global observer.
Author
Owner

@morluto commented on GitHub (Oct 19, 2026):

Related root-cause fixes are published in e4c3d7239: returned-reference projection joins and full-binding callback cycle closure. They recover the exact CJS source from #5138 and both #4348 Playwright bundles through the owned application-file worker, with explicit precision and semantic-capacity unknowns.

Current captured-web analysis still calls the synchronous analyzer in the CLI/MCP process. Per-script child ownership, source-allocation and cumulative bounds, cancellation, partial results and cleanup remain to be addressed. I have not reproduced the UMD/browser workflow or established host survival for arbitrary captured scripts. Keeping this issue open for that containment work.

<!-- gh-comment-id:6259673599 --> @morluto commented on GitHub (Oct 19, 2026): Related root-cause fixes are published in [`e4c3d7239`](https://github.com/morluto/rea/commit/e4c3d72396bf925a45724ce8b18b238b0d23d787): returned-reference projection joins and full-binding callback cycle closure. They recover the exact CJS source from #5138 and both #4348 Playwright bundles through the owned application-file worker, with explicit precision and semantic-capacity unknowns. Current captured-web analysis still calls the synchronous analyzer in the CLI/MCP process. Per-script child ownership, source-allocation and cumulative bounds, cancellation, partial results and cleanup remain to be addressed. I have not reproduced the UMD/browser workflow or established host survival for arbitrary captured scripts. Keeping this issue open for that containment work.
Author
Owner

@XinwangTao commented on GitHub (Oct 19, 2026):

The MCP impact is the part that hurts most: one bundle call taking down the whole server (and every tool with it) turns a per-script problem into a session-level outage. Containing each captured script in an owned worker with a heap budget and deadline, mirroring analyze_javascript_application, directly fixes that — I'd prioritize containment over further root-cause tuning. The two efforts are complementary anyway: #4364-style fixes improve precision, while containment guarantees the CLI/MCP host survives pathological input.

Two additions worth considering for the containment plan: (1) a cheap pre-check (source length / captured-script count) before analysis so a pathological bundle fails fast with a resource-constraint limitation instead of spending 60+ s on a script that will OOM regardless; (2) make the deadline observable to the caller (per-script time in the limitation record), which helps users identify the offending chunk — the react-dom chunk on real Next.js sites is a good example of why that matters.

<!-- gh-comment-id:6259697363 --> @XinwangTao commented on GitHub (Oct 19, 2026): The MCP impact is the part that hurts most: one bundle call taking down the whole server (and every tool with it) turns a per-script problem into a session-level outage. Containing each captured script in an owned worker with a heap budget and deadline, mirroring analyze_javascript_application, directly fixes that — I'd prioritize containment over further root-cause tuning. The two efforts are complementary anyway: #4364-style fixes improve precision, while containment guarantees the CLI/MCP host survives pathological input. Two additions worth considering for the containment plan: (1) a cheap pre-check (source length / captured-script count) before analysis so a pathological bundle fails fast with a resource-constraint limitation instead of spending 60+ s on a script that will OOM regardless; (2) make the deadline observable to the caller (per-script time in the limitation record), which helps users identify the offending chunk — the react-dom chunk on real Next.js sites is a good example of why that matters.
Author
Owner

@morluto commented on GitHub (Oct 19, 2026):

Fixed on main in 715d93e95 and 9aca543f4.

The root problem was synchronous parsing/semantic inspection in the CLI/MCP host. Captured scripts now run sequentially in an owned child, with per-script and aggregate deadlines, bounded source/result transfer, host result admission before allocation, and explicit script-specific unknowns. Completed findings and caller-selected controls survive cancellation and cleanup failures. Shared retries close the entire owning resource unit before removing its root; late cleanup errors remain tied to the old root lifetime. The matching page/Electron source loops now share the existing 32 MiB retention budget and a 20-second source-collection deadline, preserving metadata and omission reasons.

Validation: a broader run passed 1,414 tests with six skips; the final affected owner/public-path rerun passed 82 tests. Full npm run check, documentation checks (including five README consistency tests), and discovery of all 1,628 test files passed. Regressions cover real owned Node child heap exhaustion, stall/deadline/restart, cancellation during late startup and final cleanup, mixed descriptor/process cleanup failure, cross-consumer retry, deferred-close ordering, stale cleanup binding after runtime reuse, and public CLI/MCP retained Evidence plus subsequent healthy requests.

The official npm ReactDOM 18.3.1 UMD matches the reported SHA-256 35f4f974f4b2bcd44da73963347f8952e341f83909e4498227d4e26b98f66f0d. Public CLI analysis through a controlled CDP HTTP/WebSocket peer finishes at both 128 MiB and 1024 MiB worker old-space: one parsed script, zero parse failures, 44,856 AST visits. Coverage is explicitly partial because returned-reference projections were conservatively widened. This verifies the inert source and production analysis boundary, not real Chrome/Electron or Windows/Bun behavior.

Isolation has a cost: six alternating small completed CLI controls had identical normalized results and reply tokens; repeated-process medians changed from 0.360 to 1.154 seconds elapsed, 0.552 to 1.216 seconds CPU, and approximately 331 to 491 MiB sampled process-family RSS. Selected-tool schema text adds an estimated 198 cl100k tokens; reply estimates remain 964 tokens for that fixture. These are samples/tokenizer estimates, not a speedup, full agent-context measurement, or process-family RSS hard limit. The V8 setting bounds old-space; one CDP response can still materialize up to the separate transport cap before aggregate source admission. The owning browser/Electron guides document these limits.

<!-- gh-comment-id:6260315670 --> @morluto commented on GitHub (Oct 19, 2026): Fixed on `main` in [715d93e95](https://github.com/morluto/rea/commit/715d93e954f238e009d9a425b6c430ce131eb51b) and [9aca543f4](https://github.com/morluto/rea/commit/9aca543f46085f502dbc5445002e129e5d2974e9). The root problem was synchronous parsing/semantic inspection in the CLI/MCP host. Captured scripts now run sequentially in an owned child, with per-script and aggregate deadlines, bounded source/result transfer, host result admission before allocation, and explicit script-specific unknowns. Completed findings and caller-selected controls survive cancellation and cleanup failures. Shared retries close the entire owning resource unit before removing its root; late cleanup errors remain tied to the old root lifetime. The matching page/Electron source loops now share the existing 32 MiB retention budget and a 20-second source-collection deadline, preserving metadata and omission reasons. Validation: a broader run passed 1,414 tests with six skips; the final affected owner/public-path rerun passed 82 tests. Full `npm run check`, documentation checks (including five README consistency tests), and discovery of all 1,628 test files passed. Regressions cover real owned Node child heap exhaustion, stall/deadline/restart, cancellation during late startup and final cleanup, mixed descriptor/process cleanup failure, cross-consumer retry, deferred-close ordering, stale cleanup binding after runtime reuse, and public CLI/MCP retained Evidence plus subsequent healthy requests. The official npm ReactDOM 18.3.1 UMD matches the reported SHA-256 `35f4f974f4b2bcd44da73963347f8952e341f83909e4498227d4e26b98f66f0d`. Public CLI analysis through a controlled CDP HTTP/WebSocket peer finishes at both 128 MiB and 1024 MiB worker old-space: one parsed script, zero parse failures, 44,856 AST visits. Coverage is explicitly partial because returned-reference projections were conservatively widened. This verifies the inert source and production analysis boundary, not real Chrome/Electron or Windows/Bun behavior. Isolation has a cost: six alternating small completed CLI controls had identical normalized results and reply tokens; repeated-process medians changed from 0.360 to 1.154 seconds elapsed, 0.552 to 1.216 seconds CPU, and approximately 331 to 491 MiB sampled process-family RSS. Selected-tool schema text adds an estimated 198 cl100k tokens; reply estimates remain 964 tokens for that fixture. These are samples/tokenizer estimates, not a speedup, full agent-context measurement, or process-family RSS hard limit. The V8 setting bounds old-space; one CDP response can still materialize up to the separate transport cap before aggregate source admission. The owning browser/Electron guides document these limits.
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#3157
No description provided.