[GH-ISSUE #5725] [Bug] Script-source reads mask the target_changed outcome after a page reload #3473

Closed
opened 2026-10-31 00:40:52 +00:00 by bsakaguchi · 1 comment
Owner

Originally created by @coygeek on GitHub (Oct 21, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/5725

Before submitting

  • I searched the open and closed issues for a related report.
  • I removed credentials, tokens, private paths, and sensitive target data.

Area

Process capture or runtime observation

Observed behavior

A main-frame reload during inspect-web-page --include-script-sources can return unreadable_output, with a missing CDP script-ID error and details.reason: protocol_error, before REA reports its existing target_changed outcome. The independently verified baseline reproduced this in all three settled 2000 ms source-enabled requests. The same target without sources returned target_changed; a separate stable page succeeded with sources enabled.

Reproduction

From the built source checkout, with Python 3 and Chrome/Chromium installed, set CHROME_BIN to its executable and select unused loopback ports. This fixture uses a disposable browser profile and isolated REA directories. Its reload page includes one external script and reloads every 600 ms; the control includes the same script without reloading.

work_dir="$(mktemp -d)"
mkdir -p "$work_dir"/{home,cfg,data,cache,state,tmp,site,profile}
export CHROME_BIN="${CHROME_BIN:-$(command -v google-chrome || command -v chromium)}"
export SITE_PORT=8123 CDP_PORT=9223
cat > "$work_dir/site/app.js" <<'JS'
window.appLoaded = true;
JS
cat > "$work_dir/site/index.html" <<'HTML'
<!doctype html><html><head><script src="app.js"></script>
<script>setTimeout(function () { location.reload(); }, 600);</script>
</head><body>reload</body></html>
HTML
cat > "$work_dir/site/stable.html" <<'HTML'
<!doctype html><html><head><script src="app.js"></script></head><body>stable</body></html>
HTML
rea_isolated() {
  env -i PATH="$PATH" HOME="$work_dir/home" \
    XDG_CONFIG_HOME="$work_dir/cfg" XDG_DATA_HOME="$work_dir/data" \
    XDG_CACHE_HOME="$work_dir/cache" XDG_STATE_HOME="$work_dir/state" \
    TMPDIR="$work_dir/tmp" REA_LOG_LEVEL=silent \
    node scripts/rea.mjs "$@"
}
python3 -I -m http.server "$SITE_PORT" --bind 127.0.0.1 \
  -d "$work_dir/site" > "$work_dir/server.log" 2>&1 &
server_pid=$!
"$CHROME_BIN" --headless=new --user-data-dir="$work_dir/profile" \
  --remote-debugging-port="$CDP_PORT" --no-first-run --no-default-browser-check \
  "http://127.0.0.1:$SITE_PORT/index.html" > "$work_dir/chrome.log" 2>&1 &
chrome_pid=$!
cleanup() {
  kill "$chrome_pid" "$server_pid" 2>/dev/null || true
  wait "$chrome_pid" "$server_pid" 2>/dev/null || true
}
trap cleanup EXIT
python3 - <<'PY'
import json, os, time, urllib.request
endpoint = 'http://127.0.0.1:' + os.environ['CDP_PORT']
for attempt in range(100):
    try:
        with urllib.request.urlopen(endpoint + '/json/list', timeout=1) as r:
            pages = json.load(r)
        if any(p.get('url', '').endswith('/index.html') for p in pages):
            break
    except OSError:
        pass
    time.sleep(0.1)
else:
    raise SystemExit('Disposable Chrome did not become ready')
url = endpoint + '/json/new?http://127.0.0.1:' + os.environ['SITE_PORT'] + '/stable.html'
with urllib.request.urlopen(urllib.request.Request(url, method='PUT'), timeout=3) as r:
    json.load(r)
time.sleep(25)
PY
endpoint="http://127.0.0.1:$CDP_PORT"
rea_isolated list-browser-targets "$endpoint" --json > "$work_dir/targets.json"
reload_id=$(python3 -c 'import json,sys; print(next(t["target_id"] for t in json.load(open(sys.argv[1]))["normalized_result"]["targets"] if t["url"].endswith("/index.html")))' "$work_dir/targets.json")
stable_id=$(python3 -c 'import json,sys; print(next(t["target_id"] for t in json.load(open(sys.argv[1]))["normalized_result"]["targets"] if t["url"].endswith("/stable.html")))' "$work_dir/targets.json")
for attempt in 1 2 3; do
  rea_isolated inspect-web-page "$endpoint" "$reload_id" \
    --observation-ms 2000 --include-script-sources --json \
    > "$work_dir/reload-$attempt.json"
  echo "reload $attempt exit: $?"
  cat "$work_dir/reload-$attempt.json"
done
rea_isolated inspect-web-page "$endpoint" "$reload_id" \
  --observation-ms 2000 --json > "$work_dir/no-sources.json"
echo "without sources exit: $?"
cat "$work_dir/no-sources.json"
rea_isolated inspect-web-page "$endpoint" "$stable_id" \
  --observation-ms 2000 --include-script-sources --json > "$work_dir/stable.json"
echo "stable exit: $?"

The baseline verifier executed the equivalent isolated fixture with dynamically selected unused ports. The submission replay also waits for the HTTP fixture to respond and for browser startup to settle before inspection. Initial requests made before the browser committed its first document returned target_not_allowed; those startup requests are excluded from the settled sample.

Expected behavior

A main-frame reload must preserve the existing target_changed outcome rather than expose a missing-script protocol error. At the baseline, docs/browser-observation.md:289 documents discarding a mixed final capture with target_changed, and src/browser/CdpCaptureEvents.ts:90-99 explicitly preserves navigation observed during the observation window for that refusal. A successful mixed-document capture or partial Evidence during navigation is not required. The stable-page control must continue returning the selected script sources.

Impact and scope

P2: this bounded passive-capture failure replaces a meaningful navigation refusal with an internal protocol failure. The baseline's three settled long-window attempts reproduced it. A shorter 300 ms window still failed once in three baseline attempts, so reducing the window is timing-dependent. Omitting sources loses selected data and still returns a refusal. An equivalent workaround is unverified; other browsers, operating systems, MCP transport, and frequency outside these fixtures were not established. No security boundary crossing or target mutation by REA was observed.

REA version

rea-agents 6.7.0, source baseline 9d8e7803326a382cbbb3ffdb900f5e93e5442d68. Rebuilt and rechecked against current main 9258df02cc414112d4fa91a425f0d599f43e2357, also 6.7.0; the defect still reproduces. The lockfile was unchanged from the baseline. These are source-checkout reproductions; 6.7.0 is the latest published release.

Runtime and platform

Node.js v26.12.0; macOS 27.0.1 arm64; Google Chrome 155.0.8059.85, headless with a throwaway profile and explicit remote-debugging port.

Target and artifact context

Generated loopback HTTP pages with one external JavaScript file, a same-origin 600 ms main-frame reload, and a separate non-reloading control. No private target or external service is involved.

Interface

CLI

Earliest failure stage

Capture or observation

Relevant output or evidence

The independently reproduced baseline's three source-enabled 2000 ms calls exited 1 with this error, with script IDs 3, 7, and 13:

{"code":"unreadable_output","category":"execution_failure","message":"CDP Debugger.getScriptSource failed (-32000): No script for id: 3","retryable":false,"details":{"operation":"inspect_web_page","reason":"protocol_error"}}

The baseline no-source control exited 1 with code: target_unavailable and details.reason: target_changed. The stable control exited 0, returned an included script source, and reported completeness.status: policy_filtered. On freshly built main, all three settled 2000 ms/source requests again exited 1 with the same error, using missing IDs 3, 9, and 13. The no-source request returned target_changed, and the stable-page control exited 0 with one included source and policy_filtered. Three supplemental 300 ms/source requests succeeded on main; these were not synchronized to the reload timer and do not establish a reliable workaround.

Typed outcome or recovery expected

Return target_unavailable with details.reason: target_changed when a reload invalidates capture. Preserve stable-page source collection. A proposed restoration check is to rerun the 2000 ms/source-enabled case, the no-source control, and the stable control, plus a shorter observation that crosses a reload. No fix or restoration has been tested.

Analysis or suspected cause

Source-derived reasoning: at the baseline, src/browser/CdpPageCapture.ts:140-176 collects sources before checking recorded navigation. src/browser/CdpPageCaptureScripts.ts:61-102,155-161 snapshots retained script IDs, requests their sources, and propagates non-deadline failures. src/browser/CdpCaptureEvents.ts:134-136 clears execution-context attribution on context reset but retains script records. An expired script ID can therefore abort before the existing navigation outcome is produced. Current main retains this ordering. The evidence points to REA's capture lifecycle and error ordering; no bundled dependency was identified as the cause.

Additional context

A prior comment on closed #1500 reports the same missing-script symptom during same-origin navigation and says it is separate from that issue's frame-attribution bug. That issue was closed for frame attribution; this failure still reproduces after #1500. No last-known-good revision was established.

#3941 and #4040 concern false screenshot navigation detection and Electron frame inventory/identity, rather than a real reload masking the navigation refusal through stale source reads. The separate reload/doctor-advice report concerns presentation after target_changed has already been produced; this report concerns failing to produce that reason. See #5724 for that separate presentation defect.

Prepared with AI assistance. Baseline runtime evidence was independently verified and the draft received a separate semantic review; the main replay is reported separately above.

Originally created by @coygeek on GitHub (Oct 21, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/5725 ### Before submitting - [x] I searched the open and closed issues for a related report. - [x] I removed credentials, tokens, private paths, and sensitive target data. ### Area Process capture or runtime observation ### Observed behavior A main-frame reload during `inspect-web-page --include-script-sources` can return `unreadable_output`, with a missing CDP script-ID error and `details.reason: protocol_error`, before REA reports its existing `target_changed` outcome. The independently verified baseline reproduced this in all three settled 2000 ms source-enabled requests. The same target without sources returned `target_changed`; a separate stable page succeeded with sources enabled. ### Reproduction From the built source checkout, with Python 3 and Chrome/Chromium installed, set `CHROME_BIN` to its executable and select unused loopback ports. This fixture uses a disposable browser profile and isolated REA directories. Its reload page includes one external script and reloads every 600 ms; the control includes the same script without reloading. ```sh work_dir="$(mktemp -d)" mkdir -p "$work_dir"/{home,cfg,data,cache,state,tmp,site,profile} export CHROME_BIN="${CHROME_BIN:-$(command -v google-chrome || command -v chromium)}" export SITE_PORT=8123 CDP_PORT=9223 cat > "$work_dir/site/app.js" <<'JS' window.appLoaded = true; JS cat > "$work_dir/site/index.html" <<'HTML' <!doctype html><html><head><script src="app.js"></script> <script>setTimeout(function () { location.reload(); }, 600);</script> </head><body>reload</body></html> HTML cat > "$work_dir/site/stable.html" <<'HTML' <!doctype html><html><head><script src="app.js"></script></head><body>stable</body></html> HTML rea_isolated() { env -i PATH="$PATH" HOME="$work_dir/home" \ XDG_CONFIG_HOME="$work_dir/cfg" XDG_DATA_HOME="$work_dir/data" \ XDG_CACHE_HOME="$work_dir/cache" XDG_STATE_HOME="$work_dir/state" \ TMPDIR="$work_dir/tmp" REA_LOG_LEVEL=silent \ node scripts/rea.mjs "$@" } python3 -I -m http.server "$SITE_PORT" --bind 127.0.0.1 \ -d "$work_dir/site" > "$work_dir/server.log" 2>&1 & server_pid=$! "$CHROME_BIN" --headless=new --user-data-dir="$work_dir/profile" \ --remote-debugging-port="$CDP_PORT" --no-first-run --no-default-browser-check \ "http://127.0.0.1:$SITE_PORT/index.html" > "$work_dir/chrome.log" 2>&1 & chrome_pid=$! cleanup() { kill "$chrome_pid" "$server_pid" 2>/dev/null || true wait "$chrome_pid" "$server_pid" 2>/dev/null || true } trap cleanup EXIT python3 - <<'PY' import json, os, time, urllib.request endpoint = 'http://127.0.0.1:' + os.environ['CDP_PORT'] for attempt in range(100): try: with urllib.request.urlopen(endpoint + '/json/list', timeout=1) as r: pages = json.load(r) if any(p.get('url', '').endswith('/index.html') for p in pages): break except OSError: pass time.sleep(0.1) else: raise SystemExit('Disposable Chrome did not become ready') url = endpoint + '/json/new?http://127.0.0.1:' + os.environ['SITE_PORT'] + '/stable.html' with urllib.request.urlopen(urllib.request.Request(url, method='PUT'), timeout=3) as r: json.load(r) time.sleep(25) PY endpoint="http://127.0.0.1:$CDP_PORT" rea_isolated list-browser-targets "$endpoint" --json > "$work_dir/targets.json" reload_id=$(python3 -c 'import json,sys; print(next(t["target_id"] for t in json.load(open(sys.argv[1]))["normalized_result"]["targets"] if t["url"].endswith("/index.html")))' "$work_dir/targets.json") stable_id=$(python3 -c 'import json,sys; print(next(t["target_id"] for t in json.load(open(sys.argv[1]))["normalized_result"]["targets"] if t["url"].endswith("/stable.html")))' "$work_dir/targets.json") for attempt in 1 2 3; do rea_isolated inspect-web-page "$endpoint" "$reload_id" \ --observation-ms 2000 --include-script-sources --json \ > "$work_dir/reload-$attempt.json" echo "reload $attempt exit: $?" cat "$work_dir/reload-$attempt.json" done rea_isolated inspect-web-page "$endpoint" "$reload_id" \ --observation-ms 2000 --json > "$work_dir/no-sources.json" echo "without sources exit: $?" cat "$work_dir/no-sources.json" rea_isolated inspect-web-page "$endpoint" "$stable_id" \ --observation-ms 2000 --include-script-sources --json > "$work_dir/stable.json" echo "stable exit: $?" ``` The baseline verifier executed the equivalent isolated fixture with dynamically selected unused ports. The submission replay also waits for the HTTP fixture to respond and for browser startup to settle before inspection. Initial requests made before the browser committed its first document returned `target_not_allowed`; those startup requests are excluded from the settled sample. ### Expected behavior A main-frame reload must preserve the existing `target_changed` outcome rather than expose a missing-script protocol error. At the baseline, `docs/browser-observation.md:289` documents discarding a mixed final capture with `target_changed`, and `src/browser/CdpCaptureEvents.ts:90-99` explicitly preserves navigation observed during the observation window for that refusal. A successful mixed-document capture or partial Evidence during navigation is not required. The stable-page control must continue returning the selected script sources. ### Impact and scope P2: this bounded passive-capture failure replaces a meaningful navigation refusal with an internal protocol failure. The baseline's three settled long-window attempts reproduced it. A shorter 300 ms window still failed once in three baseline attempts, so reducing the window is timing-dependent. Omitting sources loses selected data and still returns a refusal. An equivalent workaround is unverified; other browsers, operating systems, MCP transport, and frequency outside these fixtures were not established. No security boundary crossing or target mutation by REA was observed. ### REA version `rea-agents` 6.7.0, source baseline `9d8e7803326a382cbbb3ffdb900f5e93e5442d68`. Rebuilt and rechecked against current main `9258df02cc414112d4fa91a425f0d599f43e2357`, also 6.7.0; the defect still reproduces. The lockfile was unchanged from the baseline. These are source-checkout reproductions; 6.7.0 is the latest published release. ### Runtime and platform Node.js v26.12.0; macOS 27.0.1 arm64; Google Chrome 155.0.8059.85, headless with a throwaway profile and explicit remote-debugging port. ### Target and artifact context Generated loopback HTTP pages with one external JavaScript file, a same-origin 600 ms main-frame reload, and a separate non-reloading control. No private target or external service is involved. ### Interface CLI ### Earliest failure stage Capture or observation ### Relevant output or evidence The independently reproduced baseline's three source-enabled 2000 ms calls exited 1 with this error, with script IDs 3, 7, and 13: ```json {"code":"unreadable_output","category":"execution_failure","message":"CDP Debugger.getScriptSource failed (-32000): No script for id: 3","retryable":false,"details":{"operation":"inspect_web_page","reason":"protocol_error"}} ``` The baseline no-source control exited 1 with `code: target_unavailable` and `details.reason: target_changed`. The stable control exited 0, returned an included script source, and reported `completeness.status: policy_filtered`. On freshly built main, all three settled 2000 ms/source requests again exited 1 with the same error, using missing IDs 3, 9, and 13. The no-source request returned `target_changed`, and the stable-page control exited 0 with one included source and `policy_filtered`. Three supplemental 300 ms/source requests succeeded on main; these were not synchronized to the reload timer and do not establish a reliable workaround. ### Typed outcome or recovery expected Return `target_unavailable` with `details.reason: target_changed` when a reload invalidates capture. Preserve stable-page source collection. A proposed restoration check is to rerun the 2000 ms/source-enabled case, the no-source control, and the stable control, plus a shorter observation that crosses a reload. No fix or restoration has been tested. ### Analysis or suspected cause Source-derived reasoning: at the baseline, `src/browser/CdpPageCapture.ts:140-176` collects sources before checking recorded navigation. `src/browser/CdpPageCaptureScripts.ts:61-102,155-161` snapshots retained script IDs, requests their sources, and propagates non-deadline failures. `src/browser/CdpCaptureEvents.ts:134-136` clears execution-context attribution on context reset but retains script records. An expired script ID can therefore abort before the existing navigation outcome is produced. Current main retains this ordering. The evidence points to REA's capture lifecycle and error ordering; no bundled dependency was identified as the cause. ### Additional context A [prior comment on closed #1500](https://github.com/morluto/rea/issues/1500#issuecomment-6099255835) reports the same missing-script symptom during same-origin navigation and says it is separate from that issue's frame-attribution bug. That issue was closed for frame attribution; this failure still reproduces after #1500. No last-known-good revision was established. #3941 and #4040 concern false screenshot navigation detection and Electron frame inventory/identity, rather than a real reload masking the navigation refusal through stale source reads. The separate reload/doctor-advice report concerns presentation after `target_changed` has already been produced; this report concerns failing to produce that reason. See #5724 for that separate presentation defect. Prepared with AI assistance. Baseline runtime evidence was independently verified and the draft received a separate semantic review; the main replay is reported separately above.
Author
Owner

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

Fixed in 590bab9ad. The owning capture boundary checks observed navigation even when script-source capture fails. target_changed takes precedence over the failed stale read; unchanged-document protocol-error and success controls remain distinct. Verified by tests/boundary/browser/cdpCaptureInvalidation.test.ts. Verification used loopback fake CDP and production provider/error projection; no real-browser run is claimed.

<!-- gh-comment-id:6376081625 --> @morluto commented on GitHub (Oct 23, 2026): Fixed in [590bab9ad](https://github.com/morluto/rea/commit/590bab9adac0bca82be0e4c39d7270575fa61a73). The owning capture boundary checks observed navigation even when script-source capture fails. target_changed takes precedence over the failed stale read; unchanged-document protocol-error and success controls remain distinct. Verified by `tests/boundary/browser/cdpCaptureInvalidation.test.ts`. Verification used loopback fake CDP and production provider/error projection; no real-browser run is claimed.
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#3473
No description provided.