[GH-ISSUE #6231] [Bug] Source-enabled page inspection counts a late script without returning it or reporting its omission #3793

Closed
opened 2026-10-31 00:44:07 +00:00 by bsakaguchi · 2 comments
Owner

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

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

The registered inspect_web_page MCP tool returns scripts.total: 2 but only one script item when a second allowed-origin script is parsed during the first script's source read. In three controlled attempts, the late script has no returned metadata or source and no scripts or script_sources exclusion, truncation, or dropped-event count. The no-late-script control returns total 1 and one item. These results were observed with the production CDP provider and a simulated loopback HTTP/WebSocket peer, not a real browser.

Reproduction

Save the following test as tests/boundary/mcp/scriptInventoryRace.test.ts in the baseline checkout with its locked dependencies installed. It uses the repository's simulated CDP peer and the registered MCP tool, with no external website or installed browser. The verifier used an equivalent temporary filename; the code is reproduced in full.

import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import { expect, it } from "vitest";
import { CdpBrowserProvider } from "../../../src/browser/CdpBrowserProvider.js";
import { createServer } from "../../../src/server/createServer.js";
import { ok } from "../../../src/domain/result.js";
import { webPageInspectionSchema } from "../../../src/domain/browserObservationSchemas.js";
import { createTestBinarySession } from "../../fixtures/binarySession.js";
import { observed } from "../../fixtures/analysisExecution.js";
import { startFakeCdpBrowser } from "../../fixtures/fakeCdpBrowser.js";

it("records the late-script mismatch through the registered MCP tool", async () => {
  for (const [attempt, late] of [false, true, true, true].entries()) {
    const browser = await startFakeCdpBrowser({
      eventsBeforeReply: ["Runtime.enable", "Debugger.enable", "Debugger.getScriptSource"],
      commandResult: ({ method }) => method === "Debugger.getScriptSource"
        ? { scriptSource: "const first = 1;" } : undefined,
      commandEvents: (command, origin) => {
        const sessionId = command.sessionId;
        if (command.method === "Runtime.enable") return [{
          method: "Runtime.executionContextCreated", sessionId,
          params: { context: { id: 1, origin, auxData: { frameId: "frame-main", isDefault: true } } },
        }];
        const first = command.method === "Debugger.enable";
        if (first || (late && command.method === "Debugger.getScriptSource")) return [{
          method: "Debugger.scriptParsed", sessionId,
          params: {
            scriptId: first ? "first" : "late",
            url: `${origin}/${first ? "first" : "late"}.js`,
            hash: first ? "first-hash" : "late-hash", length: 16,
            isModule: false, executionContextId: 1, scriptLanguage: "JavaScript",
          },
        }];
        return [];
      },
    });
    const session = createTestBinarySession(() => ({
      execute: () => Promise.resolve(observed(null)),
      close: () => Promise.resolve(ok(null)),
    }));
    const server = createServer({ kind: "session", session }, {
      environment: process.env,
      browserObservation: new CdpBrowserProvider(),
      availabilityPolicy: () => ({ processCaptureEnabled: false, investigationInputRoots: 0 }),
    });
    const client = new Client({ name: "script-inventory-repro", version: "1" });
    const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
    try {
      await server.connect(serverTransport);
      await client.connect(clientTransport);
      const response = await client.callTool({ name: "inspect_web_page", arguments: {
        cdp_endpoint: browser.endpoint, target_id: "allowed-page",
        allowed_origins: [browser.allowedOrigin], observation_ms: 0,
        include_script_sources: true,
      } });
      expect(response.isError).not.toBe(true);
      const content = response.structuredContent as Record<string, unknown>;
      const result = webPageInspectionSchema.parse(content.normalized_result);
      const scriptExclusions = result.completeness.excluded.filter(({ section }) => section === "scripts" || section === "script_sources");
      console.log(JSON.stringify({ attempt, late, total: result.scripts.total,
        item_count: result.scripts.items.length,
        script_paths: result.scripts.items.map(({ url }) => new URL(url).pathname),
        script_exclusions: scriptExclusions,
        truncated_sections: result.completeness.truncated_sections,
        dropped_scripts: result.completeness.dropped_events.scripts,
        source_reads: browser.commands.filter(({ method }) => method === "Debugger.getScriptSource").map(({ params }) => params.scriptId),
      }));
      expect(result.scripts.total).toBe(late ? 2 : 1);
      expect(result.scripts.items).toHaveLength(1);
      expect(scriptExclusions).toEqual([]);
      expect(result.completeness.truncated_sections).toEqual([]);
      expect(result.completeness.dropped_events.scripts).toBe(0);
      expect(result.scripts.items[0]?.source.included).toBe(true);
    } finally {
      await client.close();
      await server.close();
      await session.close();
      await browser.close();
    }
  }
}, 20000);

Run from the checkout root with isolated configuration directories:

repro_root="$(mktemp -d)"
mkdir -p "$repro_root"/home "$repro_root"/dsh "$repro_root"/config "$repro_root"/cache
HOME="$repro_root/home" DSH_HOME="$repro_root/dsh" \
XDG_CONFIG_HOME="$repro_root/config" XDG_CACHE_HOME="$repro_root/cache" \
node node_modules/vitest/vitest.mjs run tests/boundary/mcp/scriptInventoryRace.test.ts --maxWorkers=1

The single diagnostic test passed. Within that run, all three late-event requests reproduced the mismatch; the one no-late-event control returned one item and total 1. The test deliberately asserts the defective output and is not a passing restoration test. Delete the temporary test after running it.

Expected behavior

The count and returned items should describe a consistent inventory boundary, or any script counted but omitted should be explicitly accounted for. The capture documentation promises admitted scripts and explicit coverage for omitted script metadata. This expectation applies to the two valid same-origin script observations in the fixture; legitimate reported exclusions can still make counts differ.

Impact and scope

P2: callers lose an admitted script record and its selected source while receiving an inconsistent total without script-specific omission accounting. Three of three controlled late-event requests reproduced it, and the no-late-event control succeeded. An equivalent workaround is unverified; recapture may repeat the interleaving, while disabling sources changes the requested result. Consumers can detect the numerical mismatch, so this report does not claim that all comparisons falsely report equivalence. Real-browser frequency and a separate analyze_web_bundle execution were not measured. No security boundary crossing was observed.

REA version

Source commit c465ff5a81 (main)

Runtime and platform

Node.js v26.12.0; macOS 27.0.1 arm64; Vitest v5.0.3 from installed locked dependencies. The repository pins Node 24.18.0; this reproduction used the available Node version. No dependency install or build was run.

Target and artifact context

Repository-owned simulated CDP peer serving one authorized page and two same-origin scriptParsed observations in one execution context. Only the first script source is read. No external website, credentials, or installed browser is required.

Interface

MCP

Earliest failure stage

Capture or observation

Relevant output or evidence

The test logs these reduced projections of actual MCP normalized results. The late-event row occurred three times; the control occurred once:

{"late":false,"total":1,"item_count":1,"script_paths":["/first.js"],"script_exclusions":[],"truncated_sections":[],"dropped_scripts":0,"source_reads":["first"]}
{"late":true,"total":2,"item_count":1,"script_paths":["/first.js"],"script_exclusions":[],"truncated_sections":[],"dropped_scripts":0,"source_reads":["first"]}

Unrelated default policy and attachment limitations remain in the overall completeness result; the demonstrated omission is specifically absent from script coverage accounting. No claim that the entire capture reports complete is needed.

Typed outcome or recovery expected

Replay the same request and event ordering. Return count and items from a consistent inventory boundary, or explicitly report the counted missing script. Preserve the control's first script and included source without introducing a script omission. No fix or restoration was tested.

Analysis or suspected cause

Source-derived explanation: captureScripts snapshots the retained script values, awaits source reads, then obtains the total from the live map. The page-capture listener remains active during those awaits, so the later valid script updates the live map after the items snapshot. The fixture does not navigate or exceed metadata/source budgets, so final navigation checks and budget-exclusion accounting do not report this omission. This source reasoning explains the observed registered MCP result rather than replacing runtime evidence.

Additional context

Searches included captureScripts scripts total, script inventory late, CdpPageCaptureScripts, "scripts.total", "scriptParsed" "source", "scripts" "incomplete", "script" "late", and "script" "inventory" across open and closed issues and PRs. Related #1500 and #5725 concern frame attribution and navigation failure ordering; #3879/#4014 and #513 concern comparison; #3217 concerns map-less bundle completeness; #4350 concerns reconciliation performance. No duplicate was found.

Fetched upstream main dd3497e133 has no changes to the cited capture implementation, event admission, or capture documentation relative to the baseline. This freshness check was source-only; main was not executed. No last-known-good revision was established. Prepared with AI assistance and independent local verification.

Originally created by @coygeek on GitHub (Oct 22, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/6231 ### 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 The registered `inspect_web_page` MCP tool returns `scripts.total: 2` but only one script item when a second allowed-origin script is parsed during the first script's source read. In three controlled attempts, the late script has no returned metadata or source and no `scripts` or `script_sources` exclusion, truncation, or dropped-event count. The no-late-script control returns total 1 and one item. These results were observed with the production CDP provider and a simulated loopback HTTP/WebSocket peer, not a real browser. ### Reproduction Save the following test as `tests/boundary/mcp/scriptInventoryRace.test.ts` in the baseline checkout with its locked dependencies installed. It uses the repository's simulated CDP peer and the registered MCP tool, with no external website or installed browser. The verifier used an equivalent temporary filename; the code is reproduced in full. ```ts import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; import { expect, it } from "vitest"; import { CdpBrowserProvider } from "../../../src/browser/CdpBrowserProvider.js"; import { createServer } from "../../../src/server/createServer.js"; import { ok } from "../../../src/domain/result.js"; import { webPageInspectionSchema } from "../../../src/domain/browserObservationSchemas.js"; import { createTestBinarySession } from "../../fixtures/binarySession.js"; import { observed } from "../../fixtures/analysisExecution.js"; import { startFakeCdpBrowser } from "../../fixtures/fakeCdpBrowser.js"; it("records the late-script mismatch through the registered MCP tool", async () => { for (const [attempt, late] of [false, true, true, true].entries()) { const browser = await startFakeCdpBrowser({ eventsBeforeReply: ["Runtime.enable", "Debugger.enable", "Debugger.getScriptSource"], commandResult: ({ method }) => method === "Debugger.getScriptSource" ? { scriptSource: "const first = 1;" } : undefined, commandEvents: (command, origin) => { const sessionId = command.sessionId; if (command.method === "Runtime.enable") return [{ method: "Runtime.executionContextCreated", sessionId, params: { context: { id: 1, origin, auxData: { frameId: "frame-main", isDefault: true } } }, }]; const first = command.method === "Debugger.enable"; if (first || (late && command.method === "Debugger.getScriptSource")) return [{ method: "Debugger.scriptParsed", sessionId, params: { scriptId: first ? "first" : "late", url: `${origin}/${first ? "first" : "late"}.js`, hash: first ? "first-hash" : "late-hash", length: 16, isModule: false, executionContextId: 1, scriptLanguage: "JavaScript", }, }]; return []; }, }); const session = createTestBinarySession(() => ({ execute: () => Promise.resolve(observed(null)), close: () => Promise.resolve(ok(null)), })); const server = createServer({ kind: "session", session }, { environment: process.env, browserObservation: new CdpBrowserProvider(), availabilityPolicy: () => ({ processCaptureEnabled: false, investigationInputRoots: 0 }), }); const client = new Client({ name: "script-inventory-repro", version: "1" }); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); try { await server.connect(serverTransport); await client.connect(clientTransport); const response = await client.callTool({ name: "inspect_web_page", arguments: { cdp_endpoint: browser.endpoint, target_id: "allowed-page", allowed_origins: [browser.allowedOrigin], observation_ms: 0, include_script_sources: true, } }); expect(response.isError).not.toBe(true); const content = response.structuredContent as Record<string, unknown>; const result = webPageInspectionSchema.parse(content.normalized_result); const scriptExclusions = result.completeness.excluded.filter(({ section }) => section === "scripts" || section === "script_sources"); console.log(JSON.stringify({ attempt, late, total: result.scripts.total, item_count: result.scripts.items.length, script_paths: result.scripts.items.map(({ url }) => new URL(url).pathname), script_exclusions: scriptExclusions, truncated_sections: result.completeness.truncated_sections, dropped_scripts: result.completeness.dropped_events.scripts, source_reads: browser.commands.filter(({ method }) => method === "Debugger.getScriptSource").map(({ params }) => params.scriptId), })); expect(result.scripts.total).toBe(late ? 2 : 1); expect(result.scripts.items).toHaveLength(1); expect(scriptExclusions).toEqual([]); expect(result.completeness.truncated_sections).toEqual([]); expect(result.completeness.dropped_events.scripts).toBe(0); expect(result.scripts.items[0]?.source.included).toBe(true); } finally { await client.close(); await server.close(); await session.close(); await browser.close(); } } }, 20000); ``` Run from the checkout root with isolated configuration directories: ```sh repro_root="$(mktemp -d)" mkdir -p "$repro_root"/home "$repro_root"/dsh "$repro_root"/config "$repro_root"/cache HOME="$repro_root/home" DSH_HOME="$repro_root/dsh" \ XDG_CONFIG_HOME="$repro_root/config" XDG_CACHE_HOME="$repro_root/cache" \ node node_modules/vitest/vitest.mjs run tests/boundary/mcp/scriptInventoryRace.test.ts --maxWorkers=1 ``` The single diagnostic test passed. Within that run, all three late-event requests reproduced the mismatch; the one no-late-event control returned one item and total 1. The test deliberately asserts the defective output and is not a passing restoration test. Delete the temporary test after running it. ### Expected behavior The count and returned items should describe a consistent inventory boundary, or any script counted but omitted should be explicitly accounted for. The [capture documentation](https://github.com/morluto/rea/blob/c465ff5a814b8e1a386fc547cbcd94df5c35f086/docs/browser-observation.md#L375) promises admitted scripts and explicit coverage for omitted script metadata. This expectation applies to the two valid same-origin script observations in the fixture; legitimate reported exclusions can still make counts differ. ### Impact and scope P2: callers lose an admitted script record and its selected source while receiving an inconsistent total without script-specific omission accounting. Three of three controlled late-event requests reproduced it, and the no-late-event control succeeded. An equivalent workaround is unverified; recapture may repeat the interleaving, while disabling sources changes the requested result. Consumers can detect the numerical mismatch, so this report does not claim that all comparisons falsely report equivalence. Real-browser frequency and a separate analyze_web_bundle execution were not measured. No security boundary crossing was observed. ### REA version Source commit c465ff5a814b8e1a386fc547cbcd94df5c35f086 (main) ### Runtime and platform Node.js v26.12.0; macOS 27.0.1 arm64; Vitest v5.0.3 from installed locked dependencies. The repository pins Node 24.18.0; this reproduction used the available Node version. No dependency install or build was run. ### Target and artifact context Repository-owned simulated CDP peer serving one authorized page and two same-origin scriptParsed observations in one execution context. Only the first script source is read. No external website, credentials, or installed browser is required. ### Interface MCP ### Earliest failure stage Capture or observation ### Relevant output or evidence The test logs these reduced projections of actual MCP normalized results. The late-event row occurred three times; the control occurred once: ```json {"late":false,"total":1,"item_count":1,"script_paths":["/first.js"],"script_exclusions":[],"truncated_sections":[],"dropped_scripts":0,"source_reads":["first"]} {"late":true,"total":2,"item_count":1,"script_paths":["/first.js"],"script_exclusions":[],"truncated_sections":[],"dropped_scripts":0,"source_reads":["first"]} ``` Unrelated default policy and attachment limitations remain in the overall completeness result; the demonstrated omission is specifically absent from script coverage accounting. No claim that the entire capture reports complete is needed. ### Typed outcome or recovery expected Replay the same request and event ordering. Return count and items from a consistent inventory boundary, or explicitly report the counted missing script. Preserve the control's first script and included source without introducing a script omission. No fix or restoration was tested. ### Analysis or suspected cause Source-derived explanation: [captureScripts](https://github.com/morluto/rea/blob/c465ff5a814b8e1a386fc547cbcd94df5c35f086/src/browser/CdpPageCaptureScripts.ts#L62) snapshots the retained script values, awaits source reads, then obtains the total from the live map. The [page-capture listener](https://github.com/morluto/rea/blob/c465ff5a814b8e1a386fc547cbcd94df5c35f086/src/browser/CdpPageCapture.ts#L76) remains active during those awaits, so the later valid script updates the live map after the items snapshot. The fixture does not navigate or exceed metadata/source budgets, so final navigation checks and budget-exclusion accounting do not report this omission. This source reasoning explains the observed registered MCP result rather than replacing runtime evidence. ### Additional context Searches included `captureScripts scripts total`, `script inventory late`, `CdpPageCaptureScripts`, `"scripts.total"`, `"scriptParsed" "source"`, `"scripts" "incomplete"`, `"script" "late"`, and `"script" "inventory"` across open and closed issues and PRs. Related #1500 and #5725 concern frame attribution and navigation failure ordering; #3879/#4014 and #513 concern comparison; #3217 concerns map-less bundle completeness; #4350 concerns reconciliation performance. No duplicate was found. Fetched upstream main dd3497e1338cda86c9ef792754d8a369bd009942 has no changes to the cited capture implementation, event admission, or capture documentation relative to the baseline. This freshness check was source-only; main was not executed. No last-known-good revision was established. Prepared with AI assistance and independent local verification.
Author
Owner

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

确认根因:items 快照与 total 取自不同时刻的同一 map——await source reads 期间 listener 仍活跃,导致 total 来自更新后的 live map 而 items 来自旧快照,计数必然不一致。建议修复保证一致性边界:要么在快照同一时刻读取 total(后续进入的 script 计入 dropped/omission 计数),要么把 source read 期间新 admitted 的 script 追加进返回项并保持 total 同步。无论哪种,都应让 scripts.total 与 items 数量 + 显式 omission 计数严格相等,这样下游对比不会静默失配。

<!-- gh-comment-id:6332736541 --> @XinwangTao commented on GitHub (Oct 22, 2026): 确认根因:items 快照与 total 取自不同时刻的同一 map——await source reads 期间 listener 仍活跃,导致 total 来自更新后的 live map 而 items 来自旧快照,计数必然不一致。建议修复保证一致性边界:要么在快照同一时刻读取 total(后续进入的 script 计入 dropped/omission 计数),要么把 source read 期间新 admitted 的 script 追加进返回项并保持 total 同步。无论哪种,都应让 scripts.total 与 items 数量 + 显式 omission 计数严格相等,这样下游对比不会静默失配。
Author
Owner

@gongfpp commented on GitHub (Oct 22, 2026):

I would like to work on the producer-side inventory race with regression coverage through the registered inspect_web_page / compare_web_captures MCP tools. After exploring the snapshot options, the implementation preserves the initial metadata/source snapshot and, after the final CDP read, counts unique newly retained script IDs as explicit dropped-script inventory. It does not append late metadata/source rows or extend the source-read pass. Existing source budgets, cancellation, origin filtering and cleanup remain in force. I checked #4014; its retained-source comparison changes are adjacent, not this CDP inventory producer. Please let me know if someone is already addressing this or if a different snapshot policy is preferred. This is coordination, not an assignment or scope approval; any PR will remain draft for the human owner’s review.

<!-- gh-comment-id:6345563758 --> @gongfpp commented on GitHub (Oct 22, 2026): I would like to work on the producer-side inventory race with regression coverage through the registered inspect_web_page / compare_web_captures MCP tools. After exploring the snapshot options, the implementation preserves the initial metadata/source snapshot and, after the final CDP read, counts unique newly retained script IDs as explicit dropped-script inventory. It does not append late metadata/source rows or extend the source-read pass. Existing source budgets, cancellation, origin filtering and cleanup remain in force. I checked #4014; its retained-source comparison changes are adjacent, not this CDP inventory producer. Please let me know if someone is already addressing this or if a different snapshot policy is preferred. This is coordination, not an assignment or scope approval; any PR will remain draft for the human owner’s review.
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#3793
No description provided.