[GH-ISSUE #1500] fix(browser): keep a script on the frame it was parsed in #513

Closed
opened 2026-10-15 18:25:45 +00:00 by bsakaguchi · 4 comments
Owner

Originally created by @cursor[bot] on GitHub (Oct 10, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/1500

TL;DR: A page-capture script keeps its execution-context id, and the frame for that id can be replaced later. Frame attribution for an already parsed script follows the replacement.

Area

Browser page capture

Observed behavior

handleExecutionContextCreated in src/browser/CdpCaptureEventHandlers.ts does executionContextFrames.set(key, frameId) for String(context.id). A second created event with the same id overwrites the frame. There is no check that the previous frame was destroyed.

frameForScript in src/browser/CdpCaptureEvents.ts reads that map at use time. Scripts store executionContextKey from Debugger.scriptParsed and do not store the frame id they had when they were parsed. A reused context id moves frame_id on those scripts to the new frame.

The destroy path deletes the key, and a test covers destroy-then-recreate. An overwrite without destroy is not recorded as a limitation, and the script is not parsed again.

Expected behavior

A script's frame should be the frame recorded when that script was parsed. A second created event for an id that is still mapped should keep the original frame, or mark the affected scripts' frame attribution unknown.

Where

  • src/browser/CdpCaptureEventHandlers.ts — handleExecutionContextCreated
  • src/browser/CdpCaptureEvents.ts — frameForScript
  • src/browser/CdpPageCaptureScripts.ts — script frame assignment
Originally created by @cursor[bot] on GitHub (Oct 10, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/1500 **TL;DR:** A page-capture script keeps its execution-context id, and the frame for that id can be replaced later. Frame attribution for an already parsed script follows the replacement. ### Area Browser page capture ### Observed behavior `handleExecutionContextCreated` in `src/browser/CdpCaptureEventHandlers.ts` does `executionContextFrames.set(key, frameId)` for `String(context.id)`. A second created event with the same id overwrites the frame. There is no check that the previous frame was destroyed. `frameForScript` in `src/browser/CdpCaptureEvents.ts` reads that map at use time. Scripts store `executionContextKey` from `Debugger.scriptParsed` and do not store the frame id they had when they were parsed. A reused context id moves `frame_id` on those scripts to the new frame. The destroy path deletes the key, and a test covers destroy-then-recreate. An overwrite without destroy is not recorded as a limitation, and the script is not parsed again. ### Expected behavior A script's frame should be the frame recorded when that script was parsed. A second created event for an id that is still mapped should keep the original frame, or mark the affected scripts' frame attribution unknown. ### Where - `src/browser/CdpCaptureEventHandlers.ts` — `handleExecutionContextCreated` - `src/browser/CdpCaptureEvents.ts` — `frameForScript` - `src/browser/CdpPageCaptureScripts.ts` — script frame assignment
bsakaguchi 2026-10-15 18:25:45 +00:00
Author
Owner

@CassieFox23 commented on GitHub (Oct 10, 2026):

TL;DR: On main after b4db35f09 (#1513), a script keeps the frame it was parsed in. In my runs, scripts from iframes that moved to a new page now keep their iframe, where 7a56fe626 gave null. I could not reach the overwrite case itself with Chromium 154 (BSD-3-Clause).

Iframes that move while REA watches

Page C has two iframes. At 1,500 ms one moves to a same-site page and the other leaves for [::1]. At 3,000 ms the second comes back. I ran inspect-web-page during the moves, and the capture worked on both builds. The cells show scripts.items[].frame_id:

Script 7a56fe626 46c9cba03
c-f1.js (old page in iframe 1) null iframe 1
c-f2.js (old page in iframe 2) null iframe 2
c-f1b.js (new page in iframe 1) iframe 1 iframe 1
c-f2b.js (new page in iframe 2) iframe 2 iframe 2
c-main.js main page main page

Chromium destroyed each old iframe context before it made a new one, and the new ones got fresh ids 4 and 5.

Where ids do repeat

In my runs, Chromium reused ids only when the main page moved to another site. Page A on localhost moved itself to page B on [::1]:

Time Event Context id Frame
26 ms A's iframe context created 3 31829E83
2,524 ms executionContextsCleared all
2,542 ms B's second iframe context created 3 EA31EF39

No executionContextDestroyed came for id 3, only the clear. That main page move makes REA stop, and both builds returned this:

{"code": "target_unavailable", "details": {"operation": "inspect_web_page", "reason": "target_changed"}}

Not run

Edit: two of these ran, in the follow-up. Still not run: the overwrite case, Electron and other engines.

  • The overwrite case itself. No Chromium 154 event order I tried reached it.
  • Other commands that use page capture, like analyze-web-bundle and export-web-scripts.
  • The Electron page tools.
  • Back and forward cache, where an old page comes back from memory.
  • Other browser engines.
Raw CDP events, page C on 46c9cba03 (trimmed)
   24 ctxCreated id=2 frame=5747D9DD c-f1.html
   27 ctxCreated id=3 frame=FE6CDF4A c-f2.html
   27 scriptParsed script=5 ctx=2 c-f1.js
   27 scriptParsed script=6 ctx=3 c-f2.js
 1522 ctxDestroyed id=2
 1525 ctxCreated id=4 frame=5747D9DD c-f1b.html
 1527 frameDetached frame=FE6CDF4A swap
 1527 ctxDestroyed id=3
 3021 ctxCreated id=5 frame=FE6CDF4A c-f2b.html

REA frame_id on 46c9cba03: c-f1.js 5747D9DD, c-f2.js FE6CDF4A, c-f1b.js 5747D9DD, c-f2b.js FE6CDF4A.

Raw CDP events, page A to page B on 46c9cba03 (trimmed)
   17 ctxCreated id=2 frame=F117531C origin=http://localhost:8731
   26 ctxCreated id=3 frame=31829E83 origin=http://localhost:8731
 2524 ctxsCleared
 2532 ctxCreated id=1 frame=F117531C origin=http://[::1]:8732
 2539 ctxCreated id=2 frame=A0CF5F7F origin=http://[::1]:8732
 2542 ctxCreated id=3 frame=EA31EF39 origin=http://[::1]:8732
Commands
python3 -m http.server 8731 --bind 127.0.0.1
python3 -m http.server 8732 --bind ::1
chromium-browser --headless=new --remote-debugging-port=9335 \
  --remote-allow-origins='*' --password-store=basic \
  --user-data-dir=<fresh temp profile> about:blank
rea inspect-web-page http://127.0.0.1:9335 <target> --observation-ms 4500 \
  --allowed-origins 'http://[::1]:8732' --allowed-origins http://localhost:8731
Test setup
  • Box: Nobara 44 (Fedora 44 base), Linux 7.2.9
  • CPU: AMD Ryzen 9 5900X, 12 cores / 24 threads
  • Node: 24.18.0
  • Builds: 7a56fe626 and 46c9cba03 (main, before and after b4db35f09)
  • Other: Chromium 154 (BSD-3-Clause), the one Fedora 44 ships
  • Build: 154.0.8037.57
  • Pages: headless, served from 127.0.0.1 and ::1

🤖 Generated with Claude Code (Opus 5.5) 🦊

<!-- gh-comment-id:6096139454 --> @CassieFox23 commented on GitHub (Oct 10, 2026): **TL;DR:** On main after `b4db35f09` (#1513), a script keeps the frame it was parsed in. In my runs, scripts from iframes that moved to a new page now keep their iframe, where `7a56fe626` gave `null`. I could not reach the overwrite case itself with [Chromium 154](https://github.com/chromium/chromium) ([BSD-3-Clause](https://github.com/chromium/chromium/blob/main/LICENSE)). ### Iframes that move while REA watches Page C has two iframes. At 1,500 ms one moves to a same-site page and the other leaves for `[::1]`. At 3,000 ms the second comes back. I ran `inspect-web-page` during the moves, and the capture worked on both builds. The cells show `scripts.items[].frame_id`: | Script | `7a56fe626` | `46c9cba03` | |---|---|---| | `c-f1.js` (old page in iframe 1) | `null` | iframe 1 | | `c-f2.js` (old page in iframe 2) | `null` | iframe 2 | | `c-f1b.js` (new page in iframe 1) | iframe 1 | iframe 1 | | `c-f2b.js` (new page in iframe 2) | iframe 2 | iframe 2 | | `c-main.js` | main page | main page | Chromium destroyed each old iframe context before it made a new one, and the new ones got fresh ids 4 and 5. ### Where ids do repeat In my runs, Chromium reused ids only when the main page moved to another site. Page A on `localhost` moved itself to page B on `[::1]`: | Time | Event | Context id | Frame | |---|---|---|---| | 26 ms | A's iframe context created | 3 | `31829E83` | | 2,524 ms | `executionContextsCleared` | all | | | 2,542 ms | B's second iframe context created | 3 | `EA31EF39` | No `executionContextDestroyed` came for id 3, only the clear. That main page move makes REA stop, and both builds returned this: ```json {"code": "target_unavailable", "details": {"operation": "inspect_web_page", "reason": "target_changed"}} ``` ### Not run Edit: two of these ran, in [the follow-up](https://github.com/morluto/rea/issues/1500#issuecomment-6099255835). Still not run: the overwrite case, Electron and other engines. - The overwrite case itself. No Chromium 154 event order I tried reached it. - ~~Other commands that use page capture, like `analyze-web-bundle` and `export-web-scripts`.~~ - The Electron page tools. - ~~Back and forward cache, where an old page comes back from memory.~~ - Other browser engines. <details> <summary>Raw CDP events, page C on 46c9cba03 (trimmed)</summary> ```text 24 ctxCreated id=2 frame=5747D9DD c-f1.html 27 ctxCreated id=3 frame=FE6CDF4A c-f2.html 27 scriptParsed script=5 ctx=2 c-f1.js 27 scriptParsed script=6 ctx=3 c-f2.js 1522 ctxDestroyed id=2 1525 ctxCreated id=4 frame=5747D9DD c-f1b.html 1527 frameDetached frame=FE6CDF4A swap 1527 ctxDestroyed id=3 3021 ctxCreated id=5 frame=FE6CDF4A c-f2b.html ``` REA `frame_id` on `46c9cba03`: `c-f1.js` 5747D9DD, `c-f2.js` FE6CDF4A, `c-f1b.js` 5747D9DD, `c-f2b.js` FE6CDF4A. </details> <details> <summary>Raw CDP events, page A to page B on 46c9cba03 (trimmed)</summary> ```text 17 ctxCreated id=2 frame=F117531C origin=http://localhost:8731 26 ctxCreated id=3 frame=31829E83 origin=http://localhost:8731 2524 ctxsCleared 2532 ctxCreated id=1 frame=F117531C origin=http://[::1]:8732 2539 ctxCreated id=2 frame=A0CF5F7F origin=http://[::1]:8732 2542 ctxCreated id=3 frame=EA31EF39 origin=http://[::1]:8732 ``` </details> <details> <summary>Commands</summary> ```sh python3 -m http.server 8731 --bind 127.0.0.1 python3 -m http.server 8732 --bind ::1 chromium-browser --headless=new --remote-debugging-port=9335 \ --remote-allow-origins='*' --password-store=basic \ --user-data-dir=<fresh temp profile> about:blank rea inspect-web-page http://127.0.0.1:9335 <target> --observation-ms 4500 \ --allowed-origins 'http://[::1]:8732' --allowed-origins http://localhost:8731 ``` </details> <details> <summary>Test setup</summary> - Box: Nobara 44 (Fedora 44 base), Linux 7.2.9 - CPU: AMD Ryzen 9 5900X, 12 cores / 24 threads - Node: 24.18.0 - Builds: `7a56fe626` and `46c9cba03` (main, before and after `b4db35f09`) - Other: [Chromium 154](https://github.com/chromium/chromium) ([BSD-3-Clause](https://github.com/chromium/chromium/blob/main/LICENSE)), the one Fedora 44 ships - Build: `154.0.8037.57` - Pages: headless, served from 127.0.0.1 and ::1 </details> 🤖 Generated with Claude Code (Opus 5.5) 🦊
Author
Owner

@CassieFox23 commented on GitHub (Oct 10, 2026):

TL;DR: Follow-up on main 46c9cba03: export-web-scripts keeps each script's iframe right on my moving-iframe page. The analyze-web-bundle output has no frame field to check. REA stops when the main page moves, so a page coming back from the cache can't be caught in one run.

Moving iframes, other commands

This is the same page C as before, where both iframes load a new page during the run. Cells show each script's frame.

Script inspect-web-page export-web-scripts manifest
c-f1.js (old page in iframe 1) iframe 1 iframe 1
c-f1b.js (new page in iframe 1) iframe 1 iframe 1
c-f2.js (old page in iframe 2) iframe 2 iframe 2
c-f2b.js (new page in iframe 2) iframe 2 iframe 2
c-main.js and c.html main page main page

analyze-web-bundle found the same 6 scripts. Its output has no frame id per script, so there is nothing to compare.

Back and forward cache

Page D1 moves to page D2 on the same site, then calls history.back(). With the debugger on, headless Chromium 154 (BSD-3-Clause) did restore page D1 from the cache. I saw frameNavigated with type BackForwardCacheRestore, and pageshow had persisted set to true.

On restore, Chromium sent executionContextsCleared first. Then the old contexts came back with their old ids and old frames, and scriptParsed replayed the old script ids. So the ids repeat, but each one maps to the frame it had before.

REA runs on main, each across that away-and-back:

Run Result
inspect-web-page target_changed
inspect-web-page --include-script-sources protocol_error
analyze-web-bundle, 3 runs protocol_error each time
inspect-web-page after the restore was done ok, frames right

The protocol_error message was Debugger.getScriptSource failed (-32000): No script for id. It needs no cache restore. A plain move from page E1 to E2 on the same site gave it too, with no move back, in 2 of 2 analyze-web-bundle runs. A move to another site gave target_changed instead. In CdpPageCapture.ts, captureScripts reads the sources before the navigationDuringCapture check. That order fits what I saw. This is not the frame bug, so it may want its own issue.

Still not run

  • The overwrite case itself. No Chromium 154 event order I tried reached it.
  • The Electron page tools. They need a desktop app window.
  • Other browser engines.
Raw CDP events, page D1 restore (trimmed)
   22 ctxCreated id=61 frame=9698C913 d1.html
   29 ctxCreated id=62 frame=3B202A7E d1-frame.html
  846 ctxsCleared
  846 frameNavigated type=Navigation frame=9698C913 d2.html
  853 ctxCreated id=63 frame=9698C913 d2.html
 1658 ctxsCleared
 1658 ctxCreated id=62 frame=3B202A7E (same uniqueId as before)
 1658 ctxCreated id=61 frame=9698C913 (same uniqueId as before)
 1658 scriptParsed script=17, 18 ctx=61; script=19 ctx=62
 1658 frameNavigated type=BackForwardCacheRestore frame=9698C913
eval: pageshow persisted = [false, true]
Commands
rea inspect-web-page http://127.0.0.1:9341 <target> --observation-ms 4500 \
  --include-script-sources --allowed-origins http://127.0.0.1:8741 \
  --allowed-origins 'http://[::1]:8742' --format json > capture.json
rea export-web-scripts "$PWD/capture.json" "$PWD/export"
rea analyze-web-bundle http://127.0.0.1:9341 <target> --observation-ms 6000
Test setup
  • Box: Nobara 44 (Fedora 44 base), Linux 7.2.9
  • CPU: AMD Ryzen 9 5900X, 12 cores / 24 threads
  • Node: 24.18.0
  • REA: main 46c9cba03
  • Other: Chromium 154 (BSD-3-Clause), the one Fedora 44 ships
  • Build: 154.0.8037.57, run headless
  • Pages: served from 127.0.0.1 and ::1

🤖 Generated with Claude Code (Opus 5.5) 🦊

<!-- gh-comment-id:6099255835 --> @CassieFox23 commented on GitHub (Oct 10, 2026): **TL;DR:** Follow-up on main `46c9cba03`: `export-web-scripts` keeps each script's iframe right on my moving-iframe page. The `analyze-web-bundle` output has no frame field to check. REA stops when the main page moves, so a page coming back from the cache can't be caught in one run. ### Moving iframes, other commands This is the same page C as before, where both iframes load a new page during the run. Cells show each script's frame. | Script | `inspect-web-page` | `export-web-scripts` manifest | |---|---|---| | `c-f1.js` (old page in iframe 1) | iframe 1 | iframe 1 | | `c-f1b.js` (new page in iframe 1) | iframe 1 | iframe 1 | | `c-f2.js` (old page in iframe 2) | iframe 2 | iframe 2 | | `c-f2b.js` (new page in iframe 2) | iframe 2 | iframe 2 | | `c-main.js` and `c.html` | main page | main page | `analyze-web-bundle` found the same 6 scripts. Its output has no frame id per script, so there is nothing to compare. ### Back and forward cache Page D1 moves to page D2 on the same site, then calls `history.back()`. With the debugger on, headless [Chromium 154](https://github.com/chromium/chromium) ([BSD-3-Clause](https://github.com/chromium/chromium/blob/main/LICENSE)) did restore page D1 from the cache. I saw `frameNavigated` with type `BackForwardCacheRestore`, and `pageshow` had `persisted` set to true. On restore, Chromium sent `executionContextsCleared` first. Then the old contexts came back with their old ids and old frames, and `scriptParsed` replayed the old script ids. So the ids repeat, but each one maps to the frame it had before. REA runs on main, each across that away-and-back: | Run | Result | |---|---| | `inspect-web-page` | `target_changed` | | `inspect-web-page --include-script-sources` | `protocol_error` | | `analyze-web-bundle`, 3 runs | `protocol_error` each time | | `inspect-web-page` after the restore was done | ok, frames right | The `protocol_error` message was `Debugger.getScriptSource failed (-32000): No script for id`. It needs no cache restore. A plain move from page E1 to E2 on the same site gave it too, with no move back, in 2 of 2 `analyze-web-bundle` runs. A move to another site gave `target_changed` instead. In `CdpPageCapture.ts`, `captureScripts` reads the sources before the `navigationDuringCapture` check. That order fits what I saw. This is not the frame bug, so it may want its own issue. ### Still not run - The overwrite case itself. No Chromium 154 event order I tried reached it. - The Electron page tools. They need a desktop app window. - Other browser engines. <details> <summary>Raw CDP events, page D1 restore (trimmed)</summary> ```text 22 ctxCreated id=61 frame=9698C913 d1.html 29 ctxCreated id=62 frame=3B202A7E d1-frame.html 846 ctxsCleared 846 frameNavigated type=Navigation frame=9698C913 d2.html 853 ctxCreated id=63 frame=9698C913 d2.html 1658 ctxsCleared 1658 ctxCreated id=62 frame=3B202A7E (same uniqueId as before) 1658 ctxCreated id=61 frame=9698C913 (same uniqueId as before) 1658 scriptParsed script=17, 18 ctx=61; script=19 ctx=62 1658 frameNavigated type=BackForwardCacheRestore frame=9698C913 eval: pageshow persisted = [false, true] ``` </details> <details> <summary>Commands</summary> ```sh rea inspect-web-page http://127.0.0.1:9341 <target> --observation-ms 4500 \ --include-script-sources --allowed-origins http://127.0.0.1:8741 \ --allowed-origins 'http://[::1]:8742' --format json > capture.json rea export-web-scripts "$PWD/capture.json" "$PWD/export" rea analyze-web-bundle http://127.0.0.1:9341 <target> --observation-ms 6000 ``` </details> <details> <summary>Test setup</summary> - Box: Nobara 44 (Fedora 44 base), Linux 7.2.9 - CPU: AMD Ryzen 9 5900X, 12 cores / 24 threads - Node: 24.18.0 - REA: main `46c9cba03` - Other: [Chromium 154](https://github.com/chromium/chromium) ([BSD-3-Clause](https://github.com/chromium/chromium/blob/main/LICENSE)), the one Fedora 44 ships - Build: `154.0.8037.57`, run headless - Pages: served from 127.0.0.1 and ::1 </details> 🤖 Generated with Claude Code (Opus 5.5) 🦊
Author
Owner

@CassieFox23 commented on GitHub (Oct 10, 2026):

TL;DR: I think #1513 fixed this. It stores each script's frame when the script is parsed, which is what this issue asks for. If you agree, this can be closed.

My two comments above have the test runs. The one gap is the overwrite case itself, which I could not reach in Chromium 154 (BSD-3-Clause).

🤖 Generated with Claude Code (Opus 5.5) 🦊

<!-- gh-comment-id:6099535139 --> @CassieFox23 commented on GitHub (Oct 10, 2026): **TL;DR:** I think #1513 fixed this. It stores each script's frame when the script is parsed, which is what this issue asks for. If you agree, this can be closed. My two comments above have the test runs. The one gap is the overwrite case itself, which I could not reach in [Chromium 154](https://github.com/chromium/chromium) ([BSD-3-Clause](https://github.com/chromium/chromium/blob/main/LICENSE)). 🤖 Generated with Claude Code (Opus 5.5) 🦊
Author
Owner

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

Thanks for reporting this. Closing as resolved on main. Parsed scripts retain their frameId at the scriptParsed event; later execution-context reuse no longer reassigns their frame ownership.

Merged fix. I checked the current source against this report; no tests or real-provider workflows were run for this closure pass.

<!-- gh-comment-id:6099801155 --> @morluto commented on GitHub (Oct 10, 2026): Thanks for reporting this. Closing as resolved on main. Parsed scripts retain their frameId at the scriptParsed event; later execution-context reuse no longer reassigns their frame ownership. [Merged fix](https://github.com/morluto/rea/commit/b4db35f092d7166aebdf4387b48f7652b1d1c3b6). I checked the current source against this report; no tests or real-provider workflows were run for this closure pass.
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#513
No description provided.