[GH-ISSUE #7408] [Bug] inspect-electron-page uses a different unit for shared DOM node_value_length #4413

Closed
opened 2026-10-31 00:49:35 +00:00 by bsakaguchi · 0 comments
Owner

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

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 public CLI reports incompatible lengths for the same DOM text through its web and Electron observation surfaces. For text ééé😀, inspect-web-page returned node_value_length: 5, while inspect-electron-page returned node_value_length: 10. The ASCII control ASCII returned 5 through both. Both commands succeeded and used the same shared normalized DOM node field without identifying its unit. This was observed on real headless Chromium serving identical fixture bytes as HTTP and as a regular local file; a packaged Electron application was not tested.

Reproduction

  1. Use Node 24.22.1 and the assigned baseline checkout with its existing locked dependencies and compiled runtime. From an otherwise empty directory, make that checkout available through the relative path rea (a directory or directory symlink). The probe below uses the already-installed Playwright headless Chromium revision 1243 in the standard macOS cache; no browser installation is performed. Save the following exact script as probe.mjs.
import { createRequire } from 'node:module';
import { createServer } from 'node:http';
import { writeFile, readFile, mkdir } from 'node:fs/promises';
import { execFile } from 'node:child_process';
import { promisify } from 'node:util';
import { pathToFileURL } from 'node:url';
import { resolve, join } from 'node:path';
import { homedir } from 'node:os';
const repo = process.argv[2];
const require = createRequire(resolve(repo, 'package.json'));
const {chromium} = require('playwright-core');
const run = promisify(execFile);
const html = '<!doctype html><html><head><meta charset="utf-8"><title>unit probe</title></head><body><p id="unicode">ééé😀</p><p id="ascii">ASCII</p></body></html>';
await writeFile('unicode.html', html);
await mkdir('home', {recursive:true});
const server = createServer((req,res)=>{res.writeHead(200, {'Content-Type':'text/html; charset=utf-8'});res.end(html);});
await new Promise(r=>server.listen(0,'127.0.0.1',r));
let context;
try {
 const httpUrl = `http://127.0.0.1:${server.address().port}/unicode.html`;
 context = await chromium.launchPersistentContext(resolve('profile'), {headless:true, executablePath:join(homedir(),'Library','Caches','ms-playwright','chromium_headless_shell-1243','chrome-headless-shell-mac-arm64','chrome-headless-shell'), args:['--remote-debugging-port=0'], env:{...process.env, HOME:resolve('home')}});
 const port = (await readFile('profile/DevToolsActivePort','utf8')).split('\n')[0];
 const endpoint = `http://127.0.0.1:${port}`;
 const webPage = context.pages()[0];
 await webPage.goto(httpUrl);
 const filePage = await context.newPage();
 const fileUrl = pathToFileURL(resolve('unicode.html')).href;
 await filePage.goto(fileUrl);
 const targets = await (await fetch(`${endpoint}/json/list`)).json();
 const ids = [targets.find(t=>t.url===httpUrl).id, targets.find(t=>t.url===fileUrl).id];
 console.log('browser', context.browser()?.version());
 const summaries=[];
 for(let attempt=1;attempt<=2;attempt++){
  for(const [i,command] of ['inspect-web-page','inspect-electron-page'].entries()) {
   const args=[resolve(repo,'scripts/rea.mjs'),command,endpoint,ids[i],'--observation-ms','300','--json'];
   const {stdout,stderr}=await run(process.execPath,args,{cwd:process.cwd(),env:{PATH:process.env.PATH,HOME:resolve('home'),NO_COLOR:'1'},timeout:30000,maxBuffer:8000000});
   await writeFile(`${command}-${attempt}.json`,stdout);
   const result=JSON.parse(stdout).normalized_result;
   const text=result.dom.nodes.filter(n=>n.node_name==='#text');
   const summary={attempt,command,text_nodes:text.map(n=>({index:n.index,parent_index:n.parent_index,node_value_length:n.node_value_length})),stderr};
   console.log(JSON.stringify(summary));
   summaries.push(summary);
  }
 }
 await writeFile('summary.json',JSON.stringify(summaries,null,2));
} finally {
 if(context) await context.close();
 await new Promise((r,e)=>server.close(err=>err?e(err):r()));
 console.log('cleanup: browser and localhost server closed');
}

  1. Run node probe.mjs rea. The script creates the HTML inline, starts a loopback HTTP server on an ephemeral port, opens HTTP and local-file copies, discovers their target IDs, and invokes node rea/scripts/rea.mjs inspect-web-page <endpoint> <http-target-id> --observation-ms 300 --json and node rea/scripts/rea.mjs inspect-electron-page <endpoint> <file-target-id> --observation-ms 300 --json twice. Each CLI child has its HOME redirected to home. Both the owned browser and HTTP server close in finally.

  2. Observe the following relevant output. Both attempts produce these same node lengths; the attempt number is the only changing summary field. The final cleanup line appears after all four calls. I ran this two-round probe twice: four of four paired rounds showed the discrepancy, all eight CLI calls succeeded, and both probe runs closed their resources.

browser 153.0.8010.12
{"attempt":1,"command":"inspect-web-page","text_nodes":[{"index":6,"parent_index":5,"node_value_length":10},{"index":9,"parent_index":8,"node_value_length":5},{"index":11,"parent_index":10,"node_value_length":5}],"stderr":""}
{"attempt":1,"command":"inspect-electron-page","text_nodes":[{"index":6,"parent_index":5,"node_value_length":10},{"index":9,"parent_index":8,"node_value_length":10},{"index":11,"parent_index":10,"node_value_length":5}],"stderr":""}
cleanup: browser and localhost server closed

Expected behavior

The same normalized DOM length field should use a consistent unit or expose an explicit unit distinction. The shared schema describes these records as “Structural DOM facts shared by passive web and Electron observations.” In docs/browser-observation.md, under “Data minimization,” the contract says “DOM snapshots retain node types, node names, value lengths, and attribute names, but not text or attribute values.” In docs/electron-observation.md, under “Workflow,” the result is described as containing normalized DOM structure. Neither document nor the shared field schema documents a UTF-16-versus-UTF-8 distinction between these surfaces. These sources establish the shared fact and the absence of a unit distinction; they do not establish that either existing producer's particular unit must win.

Impact and scope

The observed result is a bounded evidence-semantics discrepancy: an identical non-ASCII value has length 5 or 10 depending on which observation command produces the shared field. It reproduced in all four paired rounds, while the ASCII control remained equal in every round. The text values are intentionally omitted from both results, so those result records alone do not provide the value needed to convert between units. Downstream consumer failures, incidence on other inputs, and behavior in a packaged Electron runtime were not tested.

REA version

6.8.0 (source commit a905a71e31)

Runtime and platform

Node 24.22.1; macOS 27.0.1 arm64; Chromium 153.0.8010.12 headless via locked playwright-core 1.64.1.

Target and artifact context

One inline UTF-8 HTML document with non-ASCII and ASCII text nodes, served by an owned localhost HTTP server and also opened as an existing local file in the same owned browser context; no packaged Electron application.

Interface

CLI

Earliest failure stage

Capture or observation

Relevant output or evidence

Observed public CLI evidence: the reproduction above reads the complete successful JSON results and prints retained DOM node positions and lengths. At index 9, the web result contains node_value_length: 5 and the Electron result contains node_value_length: 10; index 11 remains 5 in both. The complete replies are saved as inspect-web-page-1.json, inspect-electron-page-1.json, and their second-attempt equivalents.

Source evidence: src/domain/cdpObservationSchemas.ts:4 describes shared structural DOM facts and src/domain/cdpObservationSchemas.ts:10 defines the unitless integer field. Both output surfaces use that schema at src/domain/browserObservationSchemas.ts:278 and src/domain/javascript/electronObservation.ts:101. The public data-minimization contract is at docs/browser-observation.md:451; the Electron normalized-result description starts at docs/electron-observation.md:68.

Typed outcome or recovery expected

Return DOM value lengths whose unit can be interpreted consistently across both observation results. If the two surfaces intentionally retain different units, state those units in the public result contract so consumers can distinguish them.

Analysis or suspected cause

Source-derived reasoning: src/browser/CdpCaptureDocuments.ts:375 assigns JavaScript nodeValue.length, which counts UTF-16 code units, whereas src/browser/CdpElectronInspection.ts:777 assigns Buffer.byteLength(nodeValue), which counts UTF-8 bytes. The observed non-ASCII fixture has 5 UTF-16 units and 10 UTF-8 bytes. The built runtime retains the respective operations. Existing source-only (not executed) coverage at tests/boundary/cli/electronDomAdmissionCli.test.ts:112 expects byte length 4 for an emoji in Electron; that test does not compare the shared field across surfaces or document a separate unit.

Additional context

Related (closed):

https://github.com/morluto/rea/issues/4709 and https://github.com/morluto/rea/issues/4708 cover DOM parent validation and malformed resource sizes respectively, with different acceptance criteria; both are closed, and this independent length discrepancy still reproduces on the assigned baseline. The former's closure notes explicitly preserve Electron UTF-8 byte lengths; they do not define a separate public unit for the shared web/Electron field. No exact open duplicate was found in the all-state issue and PR searches. The npm registry reports latest rea-agents as 6.8.0, matching the tested source version; a separate published-package installation was not tested.

Originally created by @coygeek on GitHub (Oct 27, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/7408 ### 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 public CLI reports incompatible lengths for the same DOM text through its web and Electron observation surfaces. For text `ééé😀`, `inspect-web-page` returned `node_value_length: 5`, while `inspect-electron-page` returned `node_value_length: 10`. The ASCII control `ASCII` returned 5 through both. Both commands succeeded and used the same shared normalized DOM node field without identifying its unit. This was observed on real headless Chromium serving identical fixture bytes as HTTP and as a regular local file; a packaged Electron application was not tested. ### Reproduction 1. Use Node 24.22.1 and the assigned baseline checkout with its existing locked dependencies and compiled runtime. From an otherwise empty directory, make that checkout available through the relative path `rea` (a directory or directory symlink). The probe below uses the already-installed Playwright headless Chromium revision 1243 in the standard macOS cache; no browser installation is performed. Save the following exact script as `probe.mjs`. ```js import { createRequire } from 'node:module'; import { createServer } from 'node:http'; import { writeFile, readFile, mkdir } from 'node:fs/promises'; import { execFile } from 'node:child_process'; import { promisify } from 'node:util'; import { pathToFileURL } from 'node:url'; import { resolve, join } from 'node:path'; import { homedir } from 'node:os'; const repo = process.argv[2]; const require = createRequire(resolve(repo, 'package.json')); const {chromium} = require('playwright-core'); const run = promisify(execFile); const html = '<!doctype html><html><head><meta charset="utf-8"><title>unit probe</title></head><body><p id="unicode">ééé😀</p><p id="ascii">ASCII</p></body></html>'; await writeFile('unicode.html', html); await mkdir('home', {recursive:true}); const server = createServer((req,res)=>{res.writeHead(200, {'Content-Type':'text/html; charset=utf-8'});res.end(html);}); await new Promise(r=>server.listen(0,'127.0.0.1',r)); let context; try { const httpUrl = `http://127.0.0.1:${server.address().port}/unicode.html`; context = await chromium.launchPersistentContext(resolve('profile'), {headless:true, executablePath:join(homedir(),'Library','Caches','ms-playwright','chromium_headless_shell-1243','chrome-headless-shell-mac-arm64','chrome-headless-shell'), args:['--remote-debugging-port=0'], env:{...process.env, HOME:resolve('home')}}); const port = (await readFile('profile/DevToolsActivePort','utf8')).split('\n')[0]; const endpoint = `http://127.0.0.1:${port}`; const webPage = context.pages()[0]; await webPage.goto(httpUrl); const filePage = await context.newPage(); const fileUrl = pathToFileURL(resolve('unicode.html')).href; await filePage.goto(fileUrl); const targets = await (await fetch(`${endpoint}/json/list`)).json(); const ids = [targets.find(t=>t.url===httpUrl).id, targets.find(t=>t.url===fileUrl).id]; console.log('browser', context.browser()?.version()); const summaries=[]; for(let attempt=1;attempt<=2;attempt++){ for(const [i,command] of ['inspect-web-page','inspect-electron-page'].entries()) { const args=[resolve(repo,'scripts/rea.mjs'),command,endpoint,ids[i],'--observation-ms','300','--json']; const {stdout,stderr}=await run(process.execPath,args,{cwd:process.cwd(),env:{PATH:process.env.PATH,HOME:resolve('home'),NO_COLOR:'1'},timeout:30000,maxBuffer:8000000}); await writeFile(`${command}-${attempt}.json`,stdout); const result=JSON.parse(stdout).normalized_result; const text=result.dom.nodes.filter(n=>n.node_name==='#text'); const summary={attempt,command,text_nodes:text.map(n=>({index:n.index,parent_index:n.parent_index,node_value_length:n.node_value_length})),stderr}; console.log(JSON.stringify(summary)); summaries.push(summary); } } await writeFile('summary.json',JSON.stringify(summaries,null,2)); } finally { if(context) await context.close(); await new Promise((r,e)=>server.close(err=>err?e(err):r())); console.log('cleanup: browser and localhost server closed'); } ``` 2. Run `node probe.mjs rea`. The script creates the HTML inline, starts a loopback HTTP server on an ephemeral port, opens HTTP and local-file copies, discovers their target IDs, and invokes `node rea/scripts/rea.mjs inspect-web-page <endpoint> <http-target-id> --observation-ms 300 --json` and `node rea/scripts/rea.mjs inspect-electron-page <endpoint> <file-target-id> --observation-ms 300 --json` twice. Each CLI child has its HOME redirected to `home`. Both the owned browser and HTTP server close in `finally`. 3. Observe the following relevant output. Both attempts produce these same node lengths; the attempt number is the only changing summary field. The final cleanup line appears after all four calls. I ran this two-round probe twice: four of four paired rounds showed the discrepancy, all eight CLI calls succeeded, and both probe runs closed their resources. ```text browser 153.0.8010.12 {"attempt":1,"command":"inspect-web-page","text_nodes":[{"index":6,"parent_index":5,"node_value_length":10},{"index":9,"parent_index":8,"node_value_length":5},{"index":11,"parent_index":10,"node_value_length":5}],"stderr":""} {"attempt":1,"command":"inspect-electron-page","text_nodes":[{"index":6,"parent_index":5,"node_value_length":10},{"index":9,"parent_index":8,"node_value_length":10},{"index":11,"parent_index":10,"node_value_length":5}],"stderr":""} cleanup: browser and localhost server closed ``` ### Expected behavior The same normalized DOM length field should use a consistent unit or expose an explicit unit distinction. The shared schema describes these records as “Structural DOM facts shared by passive web and Electron observations.” In `docs/browser-observation.md`, under “Data minimization,” the contract says “DOM snapshots retain node types, node names, value lengths, and attribute names, but not text or attribute values.” In `docs/electron-observation.md`, under “Workflow,” the result is described as containing normalized DOM structure. Neither document nor the shared field schema documents a UTF-16-versus-UTF-8 distinction between these surfaces. These sources establish the shared fact and the absence of a unit distinction; they do not establish that either existing producer's particular unit must win. ### Impact and scope The observed result is a bounded evidence-semantics discrepancy: an identical non-ASCII value has length 5 or 10 depending on which observation command produces the shared field. It reproduced in all four paired rounds, while the ASCII control remained equal in every round. The text values are intentionally omitted from both results, so those result records alone do not provide the value needed to convert between units. Downstream consumer failures, incidence on other inputs, and behavior in a packaged Electron runtime were not tested. ### REA version 6.8.0 (source commit a905a71e31f71457cb21fd056b234f872126974e) ### Runtime and platform Node 24.22.1; macOS 27.0.1 arm64; Chromium 153.0.8010.12 headless via locked playwright-core 1.64.1. ### Target and artifact context One inline UTF-8 HTML document with non-ASCII and ASCII text nodes, served by an owned localhost HTTP server and also opened as an existing local file in the same owned browser context; no packaged Electron application. ### Interface CLI ### Earliest failure stage Capture or observation ### Relevant output or evidence Observed public CLI evidence: the reproduction above reads the complete successful JSON results and prints retained DOM node positions and lengths. At index 9, the web result contains `node_value_length: 5` and the Electron result contains `node_value_length: 10`; index 11 remains 5 in both. The complete replies are saved as `inspect-web-page-1.json`, `inspect-electron-page-1.json`, and their second-attempt equivalents. Source evidence: `src/domain/cdpObservationSchemas.ts:4` describes shared structural DOM facts and `src/domain/cdpObservationSchemas.ts:10` defines the unitless integer field. Both output surfaces use that schema at `src/domain/browserObservationSchemas.ts:278` and `src/domain/javascript/electronObservation.ts:101`. The public data-minimization contract is at `docs/browser-observation.md:451`; the Electron normalized-result description starts at `docs/electron-observation.md:68`. ### Typed outcome or recovery expected Return DOM value lengths whose unit can be interpreted consistently across both observation results. If the two surfaces intentionally retain different units, state those units in the public result contract so consumers can distinguish them. ### Analysis or suspected cause Source-derived reasoning: `src/browser/CdpCaptureDocuments.ts:375` assigns JavaScript `nodeValue.length`, which counts UTF-16 code units, whereas `src/browser/CdpElectronInspection.ts:777` assigns `Buffer.byteLength(nodeValue)`, which counts UTF-8 bytes. The observed non-ASCII fixture has 5 UTF-16 units and 10 UTF-8 bytes. The built runtime retains the respective operations. Existing source-only (not executed) coverage at `tests/boundary/cli/electronDomAdmissionCli.test.ts:112` expects byte length 4 for an emoji in Electron; that test does not compare the shared field across surfaces or document a separate unit. ### Additional context Related (closed): https://github.com/morluto/rea/issues/4709 and https://github.com/morluto/rea/issues/4708 cover DOM parent validation and malformed resource sizes respectively, with different acceptance criteria; both are closed, and this independent length discrepancy still reproduces on the assigned baseline. The former's closure notes explicitly preserve Electron UTF-8 byte lengths; they do not define a separate public unit for the shared web/Electron field. No exact open duplicate was found in the all-state issue and PR searches. The npm registry reports latest `rea-agents` as 6.8.0, matching the tested source version; a separate published-package installation was not tested.
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#4413
No description provided.