[GH-ISSUE #5736] [Bug] WebMCP discovery marks a main-frame-only replay complete when subframes already have tools #3483

Closed
opened 2026-10-31 00:40:59 +00:00 by bsakaguchi · 2 comments
Owner

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

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

discover-webmcp-tools reports complete_within_window when Chrome replays only main-document registrations and omits tools already registered by a stable same-origin iframe. The page registry contains four registrations; raw WebMCP.toolsAdded replay and REA each return two. REA records no attach limitation, exclusion, unavailability, or dropped WebMCP event. Its two limitation strings describe trust and invocation policy, not replay coverage.

In a control, a new iframe registration made during discovery arrives through raw CDP and appears in REA with the correct child-frame ownership. The two older iframe registrations remain missing and coverage remains complete. This reproduced in independent baseline verification and a fresh submission replay on rebuilt main.

Reproduction

From the source checkout at the baseline below, with dependencies installed and the CLI built, use the pinned Node runtime on PATH and Chrome with the experimental WebMCP API enabled. The following runner creates a loopback fixture, starts a separate headless browser with a throwaway profile and explicit debug port, compares raw CDP with the CLI, and stops its owned browser and server. Page evaluation belongs only to the independent oracle; REA remains attach-only.

work_dir="$(mktemp -d)"
cat > "$work_dir/reproduce.mjs" <<'JS'
import http from 'node:http';
import net from 'node:net';
import {spawn} from 'node:child_process';
import {mkdir,writeFile} from 'node:fs/promises';
import {once} from 'node:events';
const scratch=process.argv[2], cwd=process.argv[3];
for(const d of ['home','cfg','data','cache','state','tmp','profile']) await mkdir(`${scratch}/${d}`,{recursive:true});
const schema={type:'object',properties:{value:{type:'string'}}};
function tool(name){return `document.modelContext.registerTool({name:${JSON.stringify(name)},description:'fixture tool',inputSchema:${JSON.stringify(schema)},execute:async()=>({content:[]})});`;}
const main=`<!doctype html><iframe src="/frame.html"></iframe><script>${tool('main-tool')}${tool('shared')}</script>`;
const frame=`<!doctype html><script>${tool('frame-tool')}${tool('shared')}addEventListener('message',()=>{${tool('late-frame-tool')}});</script>`;
const server=http.createServer((req,res)=>{res.setHeader('Content-Type','text/html');res.end(req.url==='/frame.html'?frame:main);});
server.listen(0,'127.0.0.1'); await once(server,'listening');
const port=server.address().port;
const portProbe=net.createServer(); portProbe.listen(0,'127.0.0.1'); await once(portProbe,'listening'); const debugPort=portProbe.address().port; await new Promise(r=>portProbe.close(r));
const url=`http://127.0.0.1:${port}/main.html`,endpoint=`http://127.0.0.1:${debugPort}`;
const chrome=spawn('/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',['--headless=new',`--remote-debugging-port=${debugPort}`,`--user-data-dir=${scratch}/profile`,'--no-first-run','--no-default-browser-check','--enable-features=WebMCPTesting',url],{stdio:['ignore','ignore','pipe']});
let chromeErr='';chrome.stderr.on('data',x=>chromeErr+=x);
await writeFile(`${scratch}/owned-pids.json`,JSON.stringify({script:process.pid,chrome:chrome.pid,port,debugPort},null,2));
const delay=ms=>new Promise(r=>setTimeout(r,ms));
let ws;
try{
 let target;
 for(let i=0;i<100;i++){try{target=(await(await fetch(`${endpoint}/json/list`)).json()).find(x=>x.url===url);if(target)break;}catch{}await delay(100);}
 if(!target) throw Error('No fixture target');
 ws=new WebSocket(target.webSocketDebuggerUrl); await once(ws,'open');
 let nextId=0; const pending=new Map(), events=[];
 ws.addEventListener('message',({data})=>{const m=JSON.parse(data);if(m.id){const p=pending.get(m.id);pending.delete(m.id);m.error?p.reject(Error(JSON.stringify(m.error))):p.resolve(m.result);}else events.push(m);});
 const send=(method,params={})=>new Promise((resolve,reject)=>{const id=++nextId;pending.set(id,{resolve,reject});ws.send(JSON.stringify({id,method,params}));});
 let registry;
 for(let i=0;i<50;i++){registry=await send('Runtime.evaluate',{expression:'document.modelContext.getTools().then(tools=>tools.map(t=>({name:t.name,frameId:t.frameId})))',awaitPromise:true,returnByValue:true}); if(registry.result?.value?.length===4)break;await delay(100);}
 const tree=await send('Page.getFrameTree');
 await send('WebMCP.enable'); await delay(500);
 await writeFile(`${scratch}/oracle-before.json`,JSON.stringify({endpoint,url,targetId:target.id,registry,tree,events},null,2));
 const env={PATH:process.env.PATH,HOME:`${scratch}/home`,XDG_CONFIG_HOME:`${scratch}/cfg`,XDG_DATA_HOME:`${scratch}/data`,XDG_CACHE_HOME:`${scratch}/cache`,XDG_STATE_HOME:`${scratch}/state`,TMPDIR:`${scratch}/tmp`,REA_LOG_LEVEL:'silent'};
 async function cli(ms,file,control=false){
  const args=['scripts/rea.mjs','discover-webmcp-tools',endpoint,target.id,'--observation-ms',String(ms),'--json'];
  const child=spawn('env',['-i',...Object.entries(env).map(([key,value])=>`${key}=${value}`),process.execPath,...args],{cwd,stdio:['ignore','pipe','pipe']});let out='',err=''; child.stdout.on('data',x=>out+=x);child.stderr.on('data',x=>err+=x);
  if(control){await delay(2000);await send('Runtime.evaluate',{expression:"frames[0].postMessage('register','*')"});}
  const [code,signal]=await once(child,'exit');await writeFile(`${scratch}/${file}`,out);await writeFile(`${scratch}/${file}.run.json`,JSON.stringify({args,code,signal,stderr:err},null,2));
 }
 await cli(500,'rea-before.json');
 events.length=0;
 await cli(5000,'rea-control.json',true);
 await writeFile(`${scratch}/oracle-control.json`,JSON.stringify({registry:await send('Runtime.evaluate',{expression:'document.modelContext.getTools().then(tools=>tools.map(t=>({name:t.name,frameId:t.frameId})))',awaitPromise:true,returnByValue:true}),events},null,2));
 console.log(JSON.stringify({endpoint,url,targetId:target.id,registry:registry.result?.value}));
}finally{
 ws?.close();chrome.kill('SIGTERM');await once(chrome,'exit');await new Promise(r=>server.close(r));await writeFile(`${scratch}/chrome.stderr.log`,chromeErr);await writeFile(`${scratch}/cleanup.json`,JSON.stringify({chromeExited:chrome.exitCode!==null||chrome.signalCode!==null,serverClosed:!server.listening}));
}
JS
node "$work_dir/reproduce.mjs" "$work_dir" "$PWD"
jq '.normalized_result | {tools: [.tools.items[] | {name, frame_url}], completeness, limitations}' "$work_dir/rea-before.json"
jq '{registry: .registry.result.value, replay: [.events[] | select(.method == "WebMCP.toolsAdded") | .params.tools[] | {name, frameId}]}' "$work_dir/oracle-before.json"
jq '.normalized_result | {tools: [.tools.items[] | {name, frame_url}], completeness}' "$work_dir/rea-control.json"

The runner waits until the page registry contains four registrations, then records the initial raw WebMCP replay and runs discovery with a 500 ms observation window. Its second discovery uses a 5000 ms window; after 2000 ms the fixture receives a message that registers one additional iframe tool. The isolated CLI subprocess uses env -i with PATH, HOME, XDG configuration/data/cache/state directories, TMPDIR, and REA_LOG_LEVEL set explicitly under the working directory.

The submission replay executed this same runner against rebuilt main using Node 24.18.0, once for the 500 ms observation and once for the 5000 ms control. Both CLI calls exited 0 with empty stderr. The runner's cleanup receipt confirmed that its owned Chrome exited and fixture server closed.

Expected behavior

Retain all available registrations with their frame ownership, and explicitly disclose that existing subframe registrations may be outside replay coverage. If Chrome cannot replay those descriptors, an incomplete inventory is acceptable; presenting it as complete current page inventory is not. Preserve REA's passive observation: no navigation, reload, page evaluation, or tool invocation is required.

At the verified baseline, browser-observation.md:21 promises every in-scope registration, browserToolContracts.ts:216-231 describes current inventory, and tool-design.md:51-58 requires honest coverage and limitations. The CDP-only qualification bounds recovery of unavailable descriptors; it does not establish complete replay.

Impact and scope

P2: the tested current-tool inventory silently omits two of four registrations and claims complete coverage, misleading consumers about available page tools. The demonstrated scope is one stable same-origin iframe in the stated Chrome release. Other browser releases, cross-origin/out-of-process frames, and MCP transport were not tested. No security boundary crossing, data loss, or broad outage was established. Workaround availability is unverified: observing a newly registered iframe tool does not establish a practical passive method for recovering existing registrations on arbitrary pages.

REA version

rea-agents 6.7.0, verified baseline 9d8e7803326a382cbbb3ffdb900f5e93e5442d68. Fetched, rebuilt with npm run build:cached, and replayed on main d0067ba34a39ea47dd7e11bb2e6169bc0938cc3f (also 6.7.0): still reproduces. The lockfile and discovery implementation are unchanged between these revisions. These are source-checkout observations.

Runtime and platform

Reproduction: Node.js v24.18.0; macOS 27.0.1; arm64; Google Chrome 155.0.8059.85, headless with --enable-features=WebMCPTesting. Submission build used Node.js v24.22.0; the CLI replay used the repository-pinned v24.18.0.

Target and artifact context

Two self-authored loopback HTML documents at one origin, with a stable iframe and no navigation during discovery. Every registration includes an object input schema. Separate throwaway browser profile and explicit loopback debug port; isolated CLI HOME/XDG/temp directories. Independent oracle evaluation reads the fixture registry and triggers its control registration; REA does not evaluate the page.

Interface

CLI

Earliest failure stage

Capture or observation

Relevant output or evidence

Observed on rebuilt main, matching the independent baseline verification:

Before attach registry: frame-tool, main-tool, shared, shared (4)
Raw WebMCP.enable replay: main-tool, shared (2, both main.html)
REA 500 ms: exit 0; tools.total = 2; both frame_url values end in /main.html
Control registry: frame-tool, late-frame-tool, main-tool, shared, shared (5)
Raw later event: late-frame-tool with the child frame ID
REA 5000 ms: exit 0; tools.total = 3; late-frame-tool belongs to /frame.html

Both REA outputs contain:

{
  "status": "complete_within_window",
  "conditions": ["complete_within_window"],
  "attach_limited_sections": [],
  "unavailable_sections": [],
  "excluded": []
}

dropped_events.webmcp_tools and dropped_events.total are both zero. Page.getFrameTree independently contains the main and child documents at the authorized origin. The missing descriptors were not received and dropped by REA. Evidence provenance: observed; baseline independently verified and current-main replay directly executed for submission.

Typed outcome or recovery expected

Return available tools while marking webmcp_tools coverage incomplete/attach-limited and explaining the pre-attach subframe limitation. If complete descriptors become available through the existing passive protocol, retain all four with correct frame ownership. Continue receiving the later iframe tool without clearing the limitation for the older missing registrations. Do not synthesize absent descriptors or expand REA's authority.

Analysis or suspected cause

Observed producer limitation: the underlying replay gap is Chromium's experimental WebMCP domain behavior. Its WebMCP.enable replay omits the pre-existing child-frame registrations in this fixture while later live child-frame registration events work.

Source-derived REA explanation: CdpWebMcpDiscovery.ts:49-123 initializes an unrestricted completeness ledger, captures both frames, enables replay, and marks attach uncertainty only for transient frames. This ordinary stable iframe does not satisfy that condition. Lines 542-550 return the retained subset with the unchanged completeness snapshot. The actionable REA defect is the false completeness claim, not failure to recover descriptors Chrome never supplied. No bundled dependency was identified as the cause.

Additional context

Related but distinct: merged #813 fixes identity collisions between registrations that CDP actually emits and explicitly leaves initial child-frame replay unchanged. Open PR #4040 addresses malformed/transient initial frame-scope uncertainty; the stable allowed-origin frames here are well formed. #3989 concerns offline comparison of recorded dropped events; these live results record zero drops. Closed #557 concerns transient frame navigation and pruning; this fixture never navigates. All fixture tools include input schemas, so optional-schema validation cannot explain the omission.

Compared three dispositions: accept CDP's subset as complete; require recovery of unavailable descriptors; or retain the subset with honest replay coverage. The third preserves both observed facts and the existing passive authority contract. No fix or successful restoration has been tested. Last known good version: NOT_ENOUGH_INFO; none established. Prepared with AI assistance and independent baseline verification.

Originally created by @coygeek on GitHub (Oct 21, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/5736 ### 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 `discover-webmcp-tools` reports `complete_within_window` when Chrome replays only main-document registrations and omits tools already registered by a stable same-origin iframe. The page registry contains four registrations; raw `WebMCP.toolsAdded` replay and REA each return two. REA records no attach limitation, exclusion, unavailability, or dropped WebMCP event. Its two limitation strings describe trust and invocation policy, not replay coverage. In a control, a new iframe registration made during discovery arrives through raw CDP and appears in REA with the correct child-frame ownership. The two older iframe registrations remain missing and coverage remains complete. This reproduced in independent baseline verification and a fresh submission replay on rebuilt main. ### Reproduction From the source checkout at the baseline below, with dependencies installed and the CLI built, use the pinned Node runtime on PATH and Chrome with the experimental WebMCP API enabled. The following runner creates a loopback fixture, starts a separate headless browser with a throwaway profile and explicit debug port, compares raw CDP with the CLI, and stops its owned browser and server. Page evaluation belongs only to the independent oracle; REA remains attach-only. ```sh work_dir="$(mktemp -d)" cat > "$work_dir/reproduce.mjs" <<'JS' import http from 'node:http'; import net from 'node:net'; import {spawn} from 'node:child_process'; import {mkdir,writeFile} from 'node:fs/promises'; import {once} from 'node:events'; const scratch=process.argv[2], cwd=process.argv[3]; for(const d of ['home','cfg','data','cache','state','tmp','profile']) await mkdir(`${scratch}/${d}`,{recursive:true}); const schema={type:'object',properties:{value:{type:'string'}}}; function tool(name){return `document.modelContext.registerTool({name:${JSON.stringify(name)},description:'fixture tool',inputSchema:${JSON.stringify(schema)},execute:async()=>({content:[]})});`;} const main=`<!doctype html><iframe src="/frame.html"></iframe><script>${tool('main-tool')}${tool('shared')}</script>`; const frame=`<!doctype html><script>${tool('frame-tool')}${tool('shared')}addEventListener('message',()=>{${tool('late-frame-tool')}});</script>`; const server=http.createServer((req,res)=>{res.setHeader('Content-Type','text/html');res.end(req.url==='/frame.html'?frame:main);}); server.listen(0,'127.0.0.1'); await once(server,'listening'); const port=server.address().port; const portProbe=net.createServer(); portProbe.listen(0,'127.0.0.1'); await once(portProbe,'listening'); const debugPort=portProbe.address().port; await new Promise(r=>portProbe.close(r)); const url=`http://127.0.0.1:${port}/main.html`,endpoint=`http://127.0.0.1:${debugPort}`; const chrome=spawn('/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',['--headless=new',`--remote-debugging-port=${debugPort}`,`--user-data-dir=${scratch}/profile`,'--no-first-run','--no-default-browser-check','--enable-features=WebMCPTesting',url],{stdio:['ignore','ignore','pipe']}); let chromeErr='';chrome.stderr.on('data',x=>chromeErr+=x); await writeFile(`${scratch}/owned-pids.json`,JSON.stringify({script:process.pid,chrome:chrome.pid,port,debugPort},null,2)); const delay=ms=>new Promise(r=>setTimeout(r,ms)); let ws; try{ let target; for(let i=0;i<100;i++){try{target=(await(await fetch(`${endpoint}/json/list`)).json()).find(x=>x.url===url);if(target)break;}catch{}await delay(100);} if(!target) throw Error('No fixture target'); ws=new WebSocket(target.webSocketDebuggerUrl); await once(ws,'open'); let nextId=0; const pending=new Map(), events=[]; ws.addEventListener('message',({data})=>{const m=JSON.parse(data);if(m.id){const p=pending.get(m.id);pending.delete(m.id);m.error?p.reject(Error(JSON.stringify(m.error))):p.resolve(m.result);}else events.push(m);}); const send=(method,params={})=>new Promise((resolve,reject)=>{const id=++nextId;pending.set(id,{resolve,reject});ws.send(JSON.stringify({id,method,params}));}); let registry; for(let i=0;i<50;i++){registry=await send('Runtime.evaluate',{expression:'document.modelContext.getTools().then(tools=>tools.map(t=>({name:t.name,frameId:t.frameId})))',awaitPromise:true,returnByValue:true}); if(registry.result?.value?.length===4)break;await delay(100);} const tree=await send('Page.getFrameTree'); await send('WebMCP.enable'); await delay(500); await writeFile(`${scratch}/oracle-before.json`,JSON.stringify({endpoint,url,targetId:target.id,registry,tree,events},null,2)); const env={PATH:process.env.PATH,HOME:`${scratch}/home`,XDG_CONFIG_HOME:`${scratch}/cfg`,XDG_DATA_HOME:`${scratch}/data`,XDG_CACHE_HOME:`${scratch}/cache`,XDG_STATE_HOME:`${scratch}/state`,TMPDIR:`${scratch}/tmp`,REA_LOG_LEVEL:'silent'}; async function cli(ms,file,control=false){ const args=['scripts/rea.mjs','discover-webmcp-tools',endpoint,target.id,'--observation-ms',String(ms),'--json']; const child=spawn('env',['-i',...Object.entries(env).map(([key,value])=>`${key}=${value}`),process.execPath,...args],{cwd,stdio:['ignore','pipe','pipe']});let out='',err=''; child.stdout.on('data',x=>out+=x);child.stderr.on('data',x=>err+=x); if(control){await delay(2000);await send('Runtime.evaluate',{expression:"frames[0].postMessage('register','*')"});} const [code,signal]=await once(child,'exit');await writeFile(`${scratch}/${file}`,out);await writeFile(`${scratch}/${file}.run.json`,JSON.stringify({args,code,signal,stderr:err},null,2)); } await cli(500,'rea-before.json'); events.length=0; await cli(5000,'rea-control.json',true); await writeFile(`${scratch}/oracle-control.json`,JSON.stringify({registry:await send('Runtime.evaluate',{expression:'document.modelContext.getTools().then(tools=>tools.map(t=>({name:t.name,frameId:t.frameId})))',awaitPromise:true,returnByValue:true}),events},null,2)); console.log(JSON.stringify({endpoint,url,targetId:target.id,registry:registry.result?.value})); }finally{ ws?.close();chrome.kill('SIGTERM');await once(chrome,'exit');await new Promise(r=>server.close(r));await writeFile(`${scratch}/chrome.stderr.log`,chromeErr);await writeFile(`${scratch}/cleanup.json`,JSON.stringify({chromeExited:chrome.exitCode!==null||chrome.signalCode!==null,serverClosed:!server.listening})); } JS node "$work_dir/reproduce.mjs" "$work_dir" "$PWD" jq '.normalized_result | {tools: [.tools.items[] | {name, frame_url}], completeness, limitations}' "$work_dir/rea-before.json" jq '{registry: .registry.result.value, replay: [.events[] | select(.method == "WebMCP.toolsAdded") | .params.tools[] | {name, frameId}]}' "$work_dir/oracle-before.json" jq '.normalized_result | {tools: [.tools.items[] | {name, frame_url}], completeness}' "$work_dir/rea-control.json" ``` The runner waits until the page registry contains four registrations, then records the initial raw WebMCP replay and runs discovery with a 500 ms observation window. Its second discovery uses a 5000 ms window; after 2000 ms the fixture receives a message that registers one additional iframe tool. The isolated CLI subprocess uses `env -i` with PATH, HOME, XDG configuration/data/cache/state directories, TMPDIR, and REA_LOG_LEVEL set explicitly under the working directory. The submission replay executed this same runner against rebuilt main using Node 24.18.0, once for the 500 ms observation and once for the 5000 ms control. Both CLI calls exited 0 with empty stderr. The runner's cleanup receipt confirmed that its owned Chrome exited and fixture server closed. ### Expected behavior Retain all available registrations with their frame ownership, and explicitly disclose that existing subframe registrations may be outside replay coverage. If Chrome cannot replay those descriptors, an incomplete inventory is acceptable; presenting it as complete current page inventory is not. Preserve REA's passive observation: no navigation, reload, page evaluation, or tool invocation is required. At the verified baseline, [browser-observation.md:21](https://github.com/morluto/rea/blob/9d8e7803326a382cbbb3ffdb900f5e93e5442d68/docs/browser-observation.md#L21) promises every in-scope registration, [browserToolContracts.ts:216-231](https://github.com/morluto/rea/blob/9d8e7803326a382cbbb3ffdb900f5e93e5442d68/src/contracts/browserToolContracts.ts#L216-L231) describes current inventory, and [tool-design.md:51-58](https://github.com/morluto/rea/blob/9d8e7803326a382cbbb3ffdb900f5e93e5442d68/docs/tool-design.md#L51-L58) requires honest coverage and limitations. The [CDP-only qualification](https://github.com/morluto/rea/blob/9d8e7803326a382cbbb3ffdb900f5e93e5442d68/docs/browser-observation.md#L220-L249) bounds recovery of unavailable descriptors; it does not establish complete replay. ### Impact and scope P2: the tested current-tool inventory silently omits two of four registrations and claims complete coverage, misleading consumers about available page tools. The demonstrated scope is one stable same-origin iframe in the stated Chrome release. Other browser releases, cross-origin/out-of-process frames, and MCP transport were not tested. No security boundary crossing, data loss, or broad outage was established. Workaround availability is unverified: observing a newly registered iframe tool does not establish a practical passive method for recovering existing registrations on arbitrary pages. ### REA version `rea-agents` 6.7.0, verified baseline `9d8e7803326a382cbbb3ffdb900f5e93e5442d68`. Fetched, rebuilt with `npm run build:cached`, and replayed on main `d0067ba34a39ea47dd7e11bb2e6169bc0938cc3f` (also 6.7.0): still reproduces. The lockfile and discovery implementation are unchanged between these revisions. These are source-checkout observations. ### Runtime and platform Reproduction: Node.js v24.18.0; macOS 27.0.1; arm64; Google Chrome 155.0.8059.85, headless with `--enable-features=WebMCPTesting`. Submission build used Node.js v24.22.0; the CLI replay used the repository-pinned v24.18.0. ### Target and artifact context Two self-authored loopback HTML documents at one origin, with a stable iframe and no navigation during discovery. Every registration includes an object input schema. Separate throwaway browser profile and explicit loopback debug port; isolated CLI HOME/XDG/temp directories. Independent oracle evaluation reads the fixture registry and triggers its control registration; REA does not evaluate the page. ### Interface CLI ### Earliest failure stage Capture or observation ### Relevant output or evidence Observed on rebuilt main, matching the independent baseline verification: ```text Before attach registry: frame-tool, main-tool, shared, shared (4) Raw WebMCP.enable replay: main-tool, shared (2, both main.html) REA 500 ms: exit 0; tools.total = 2; both frame_url values end in /main.html Control registry: frame-tool, late-frame-tool, main-tool, shared, shared (5) Raw later event: late-frame-tool with the child frame ID REA 5000 ms: exit 0; tools.total = 3; late-frame-tool belongs to /frame.html ``` Both REA outputs contain: ```json { "status": "complete_within_window", "conditions": ["complete_within_window"], "attach_limited_sections": [], "unavailable_sections": [], "excluded": [] } ``` `dropped_events.webmcp_tools` and `dropped_events.total` are both zero. `Page.getFrameTree` independently contains the main and child documents at the authorized origin. The missing descriptors were not received and dropped by REA. Evidence provenance: observed; baseline independently verified and current-main replay directly executed for submission. ### Typed outcome or recovery expected Return available tools while marking `webmcp_tools` coverage incomplete/attach-limited and explaining the pre-attach subframe limitation. If complete descriptors become available through the existing passive protocol, retain all four with correct frame ownership. Continue receiving the later iframe tool without clearing the limitation for the older missing registrations. Do not synthesize absent descriptors or expand REA's authority. ### Analysis or suspected cause Observed producer limitation: the underlying replay gap is Chromium's experimental WebMCP domain behavior. Its `WebMCP.enable` replay omits the pre-existing child-frame registrations in this fixture while later live child-frame registration events work. Source-derived REA explanation: [CdpWebMcpDiscovery.ts:49-123](https://github.com/morluto/rea/blob/d0067ba34a39ea47dd7e11bb2e6169bc0938cc3f/src/browser/CdpWebMcpDiscovery.ts#L49-L123) initializes an unrestricted completeness ledger, captures both frames, enables replay, and marks attach uncertainty only for transient frames. This ordinary stable iframe does not satisfy that condition. [Lines 542-550](https://github.com/morluto/rea/blob/d0067ba34a39ea47dd7e11bb2e6169bc0938cc3f/src/browser/CdpWebMcpDiscovery.ts#L542-L550) return the retained subset with the unchanged completeness snapshot. The actionable REA defect is the false completeness claim, not failure to recover descriptors Chrome never supplied. No bundled dependency was identified as the cause. ### Additional context Related but distinct: merged #813 fixes identity collisions between registrations that CDP actually emits and explicitly leaves initial child-frame replay unchanged. Open PR #4040 addresses malformed/transient initial frame-scope uncertainty; the stable allowed-origin frames here are well formed. #3989 concerns offline comparison of recorded dropped events; these live results record zero drops. Closed #557 concerns transient frame navigation and pruning; this fixture never navigates. All fixture tools include input schemas, so optional-schema validation cannot explain the omission. Compared three dispositions: accept CDP's subset as complete; require recovery of unavailable descriptors; or retain the subset with honest replay coverage. The third preserves both observed facts and the existing passive authority contract. No fix or successful restoration has been tested. Last known good version: NOT_ENOUGH_INFO; none established. Prepared with AI assistance and independent baseline verification.
Author
Owner

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

This is a real coverage-accuracy problem: complete_within_window is being decided from main-frame replay alone, so a stable same-origin iframe whose tools were registered earlier silently falls outside the coverage claim. The two limitation strings on the result describe trust/invocation policy, not the missing descriptors, which is why the failure looks like a silent gap instead of an explicit limitation. The control shows new iframe registrations during discovery do come through raw CDP with correct child-frame ownership, so the gap is specific to already-registered iframes — the replay path apparently only collects registrations it observes during the window and never reconciles against the iframe toolset it already knows. Suggest: when discovery completes, diff against known iframe registrations and either surface a replay-coverage limitation with the count of omitted tools, or attach them from the known registry before reporting complete. Also add the two older iframe fixtures to the contract so complete_within_window is pinned to actual coverage.

<!-- gh-comment-id:6305854021 --> @XinwangTao commented on GitHub (Oct 21, 2026): This is a real coverage-accuracy problem: complete_within_window is being decided from main-frame replay alone, so a stable same-origin iframe whose tools were registered earlier silently falls outside the coverage claim. The two limitation strings on the result describe trust/invocation policy, not the missing descriptors, which is why the failure looks like a silent gap instead of an explicit limitation. The control shows new iframe registrations during discovery do come through raw CDP with correct child-frame ownership, so the gap is specific to already-registered iframes — the replay path apparently only collects registrations it observes during the window and never reconciles against the iframe toolset it already knows. Suggest: when discovery completes, diff against known iframe registrations and either surface a replay-coverage limitation with the count of omitted tools, or attach them from the known registry before reporting complete. Also add the two older iframe fixtures to the contract so complete_within_window is pinned to actual coverage.
Author
Owner

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

Fixed in b29aa6043. Discovery now records a WebMCP attach limitation when initial subframes exist, explaining that replay cannot establish older subframe registrations. Replay and live child tools are retained. This fixes the unsupported completeness assertion rather than claiming recovery of previously omitted registrations. Verified by tests/boundary/browser/cdpWebMcpCoverage.test.ts. Verification used loopback fake CDP and production provider/error projection; no real-browser run is claimed.

<!-- gh-comment-id:6376078524 --> @morluto commented on GitHub (Oct 23, 2026): Fixed in [b29aa6043](https://github.com/morluto/rea/commit/b29aa6043f6469024b86ea28757dba464c04dbe2). Discovery now records a WebMCP attach limitation when initial subframes exist, explaining that replay cannot establish older subframe registrations. Replay and live child tools are retained. This fixes the unsupported completeness assertion rather than claiming recovery of previously omitted registrations. Verified by `tests/boundary/browser/cdpWebMcpCoverage.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#3483
No description provided.