[GH-ISSUE #7582] [Bug] One-column process captures silently use a two-column rendered screen #4516

Closed
opened 2026-10-31 00:50:26 +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/7582

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

node scripts/rea.mjs capture-process "$scenario" --json accepts an initial terminal width of one column and completes successfully, but returns rendered frames with columns: 2. For /bin/echo ab, the manifest retains the selected one-column terminal while the rendered screen puts both ASCII characters on a single row: lines: ["ab", ""].

An independent /bin/sh -c 'stty size' fixture prints 2 1, confirming that the child sees two rows and one column. A two-column echo control keeps the selected and rendered widths aligned. Normalization transformations were disabled in these runs.

Reproduction

  1. Start in the built baseline checkout with its locked dependencies and Node v24.22.1. Use a supported host with a working native PTY backend. The following command creates isolated HOME/XDG directories and immediate-exit fixtures, calls the real CLI, and removes its temporary inputs afterward.
export CAPTURE_REPRO_ROOT="$(mktemp -d)"
node --input-type=module <<'JS'
import fs from 'node:fs';
import {spawnSync} from 'node:child_process';
const root = process.env.CAPTURE_REPRO_ROOT;
const env = {...process.env};
for (const key of ['HOME', 'XDG_CONFIG_HOME', 'XDG_CACHE_HOME', 'XDG_DATA_HOME', 'XDG_STATE_HOME', 'XDG_RUNTIME_DIR']) {
  env[key] = `${root}/${key}`;
  fs.mkdirSync(env[key], {recursive: true});
}
try {
  for (const [name, executable, args, columns] of [
    ['one', '/bin/echo', ['ab'], 1],
    ['size', '/bin/sh', ['-c', 'stty size'], 1],
    ['two', '/bin/echo', ['ab'], 2],
  ]) {
    const scenario = {executable, arguments: args, working_directory: root,
      terminal: {columns, rows: 2, scrollback: 0},
      timeout_ms: 1000, idle_timeout_ms: 1000, settle_ms: 20,
      normalization: {paths: false, pids: false, ports: false}};
    const file = `${root}/${name}.json`;
    fs.writeFileSync(file, JSON.stringify(scenario));
    const run = spawnSync(process.execPath,
      ['scripts/rea.mjs', 'capture-process', file, '--json'],
      {env, encoding: 'utf8', timeout: 20000, maxBuffer: 4 * 1024 * 1024});
    if (run.status !== 0) throw new Error(run.stderr || String(run.error));
    const c = JSON.parse(run.stdout).normalized_result;
    console.log(JSON.stringify({name, status: run.status,
      selected: c.manifest.scenario.terminal,
      raw: c.frames.map(f => f.raw_data ?? f.data).join(''),
      rendered: c.rendered_frames.map(f => ({columns: f.columns, rows: f.rows, lines: f.lines})),
      exit: c.exit}));
  }
} finally {
  fs.rmSync(root, {recursive: true, force: true});
}
JS
  1. Inspect selected.columns versus each rendered frame's columns. The one-column echo mismatch reproduced in all three verifier runs; the PTY-size and matching two-column controls were each run twice. The complete script above was replayed once as written, using a scratch-contained temporary directory.

Expected behavior

A capture admitted with one terminal column should render that selected cell geometry. If the renderer cannot represent one column, admission should report the unsupported dimension before launching the fixture. Successful capture should not silently substitute a different terminal width.

The process-capture documentation promises that requested dimensions are not clamped and defines rendered columns as terminal cell width. Its caveat about normalization and Unicode changing string lengths does not explain this case: the numerical width changes, and the input is unnormalized ASCII. Exact contract and implementation locators are listed under Evidence.

Impact and scope

P2: a valid minimum-width input produces rendered Evidence with geometry different from the terminal supplied to the child. The observed discrepancy includes both the width metadata and the layout of the retained ASCII line. Raw PTY text remains available.

Choosing two columns produced an aligned result in the control and is an available workaround when the caller can use that width. It does not provide faithful one-column rendering. These observations cover initial dimensions through the CLI on Darwin arm64. Resize events, MCP calls, downstream comparison outcomes, and other platforms were not tested.

REA version

Exact node scripts/rea.mjs --version output: 6.8.0.
Source baseline: 65f1c447344a423fa2785847dbcea1e9a9e2454a.

Runtime and platform

Node.js v24.22.1; Darwin arm64; installed locked @lydell/node-pty 1.1.0 and @xterm/headless 6.0.0. The existing build and dependencies were used without updates.

Target and artifact context

Local immediate-exit /bin/echo and /bin/sh -c 'stty size' fixtures; process-capture Evidence. No external analysis service or GUI provider was used.

Interface

CLI

Earliest failure stage

Capture or observation

Relevant output or evidence

The reproduction printed these summaries; every CLI and fixture exit code was 0:

one:  selected={columns:1,rows:2,scrollback:0}
      raw="ab\r\n"
      rendered=[{columns:2,rows:2,lines:["ab",""]}]
size: selected={columns:1,rows:2,scrollback:0}
      raw="2 1\r\n"
      rendered=[{columns:2,rows:2,lines:["1",""]}]
two:  selected={columns:2,rows:2,scrollback:0}
      raw="ab\r\n"
      rendered=[{columns:2,rows:2,lines:["ab",""]}]

stty size prints rows before columns. This establishes that the first two fixtures received a one-column PTY, whereas the returned rendered states use two columns.

At the baseline:

  • Expected source: docs/process-capture.md:237-241 preserves requested dimensions; docs/process-capture.md:336-343 defines normalized visible rows and terminal cell width. src/domain/process/processScenario.ts:177-186 admits initial columns: 1.
  • Producing boundary: src/process/capture/ProcessHarness.ts:238-253,307-323 supplies the selected dimensions to the renderer and native PTY. src/process/capture/TerminalRenderer.ts:53-60,140-203 constructs the headless terminal, reads its visible rows, and publishes its actual column count.
  • An independently executed installed-xterm constructor probe using cols: 1, rows: 2 returned cols: 2, rows: 2, consistent with the CLI result.

Typed outcome or recovery expected

Return rendered observations for the admitted one-column geometry, or reject that unsupported dimension explicitly before execution. Preserve the matching two-column control and original PTY output.

Analysis or suspected cause

Source-derived explanation supported by the constructor probe: the headless terminal selects a minimum width of two columns. The PTY receives the requested width of one, and REA publishes the headless terminal's selected geometry without detecting the divergence. The defect is the disagreement between the admitted terminal and its rendered observations, rather than JavaScript string-length padding.

Additional context

Related closed #3868 and merged #4142 corrected extra cells from Unicode string padding. Closed #1464 and merged #1465 clarified normalized text versus cell width. Their restoration conditions differ from this ASCII width substitution. #7295 concerns omitted-rendered-frame uncertainty during comparison; #7281 concerns vanished observer scratch paths. Neither reports this geometry mismatch. Open and closed issues, PRs, and relevant issue comments were searched; no matching report was found.

No last-known-good revision or regression boundary was established. This report was independently reproduced with AI assistance.

Originally created by @coygeek on GitHub (Oct 27, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/7582 ### 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 `node scripts/rea.mjs capture-process "$scenario" --json` accepts an initial terminal width of one column and completes successfully, but returns rendered frames with `columns: 2`. For `/bin/echo ab`, the manifest retains the selected one-column terminal while the rendered screen puts both ASCII characters on a single row: `lines: ["ab", ""]`. An independent `/bin/sh -c 'stty size'` fixture prints `2 1`, confirming that the child sees two rows and one column. A two-column echo control keeps the selected and rendered widths aligned. Normalization transformations were disabled in these runs. ### Reproduction 1. Start in the built baseline checkout with its locked dependencies and Node v24.22.1. Use a supported host with a working native PTY backend. The following command creates isolated HOME/XDG directories and immediate-exit fixtures, calls the real CLI, and removes its temporary inputs afterward. ```sh export CAPTURE_REPRO_ROOT="$(mktemp -d)" node --input-type=module <<'JS' import fs from 'node:fs'; import {spawnSync} from 'node:child_process'; const root = process.env.CAPTURE_REPRO_ROOT; const env = {...process.env}; for (const key of ['HOME', 'XDG_CONFIG_HOME', 'XDG_CACHE_HOME', 'XDG_DATA_HOME', 'XDG_STATE_HOME', 'XDG_RUNTIME_DIR']) { env[key] = `${root}/${key}`; fs.mkdirSync(env[key], {recursive: true}); } try { for (const [name, executable, args, columns] of [ ['one', '/bin/echo', ['ab'], 1], ['size', '/bin/sh', ['-c', 'stty size'], 1], ['two', '/bin/echo', ['ab'], 2], ]) { const scenario = {executable, arguments: args, working_directory: root, terminal: {columns, rows: 2, scrollback: 0}, timeout_ms: 1000, idle_timeout_ms: 1000, settle_ms: 20, normalization: {paths: false, pids: false, ports: false}}; const file = `${root}/${name}.json`; fs.writeFileSync(file, JSON.stringify(scenario)); const run = spawnSync(process.execPath, ['scripts/rea.mjs', 'capture-process', file, '--json'], {env, encoding: 'utf8', timeout: 20000, maxBuffer: 4 * 1024 * 1024}); if (run.status !== 0) throw new Error(run.stderr || String(run.error)); const c = JSON.parse(run.stdout).normalized_result; console.log(JSON.stringify({name, status: run.status, selected: c.manifest.scenario.terminal, raw: c.frames.map(f => f.raw_data ?? f.data).join(''), rendered: c.rendered_frames.map(f => ({columns: f.columns, rows: f.rows, lines: f.lines})), exit: c.exit})); } } finally { fs.rmSync(root, {recursive: true, force: true}); } JS ``` 2. Inspect `selected.columns` versus each rendered frame's `columns`. The one-column echo mismatch reproduced in all three verifier runs; the PTY-size and matching two-column controls were each run twice. The complete script above was replayed once as written, using a scratch-contained temporary directory. ### Expected behavior A capture admitted with one terminal column should render that selected cell geometry. If the renderer cannot represent one column, admission should report the unsupported dimension before launching the fixture. Successful capture should not silently substitute a different terminal width. The process-capture documentation promises that requested dimensions are not clamped and defines rendered `columns` as terminal cell width. Its caveat about normalization and Unicode changing string lengths does not explain this case: the numerical width changes, and the input is unnormalized ASCII. Exact contract and implementation locators are listed under Evidence. ### Impact and scope P2: a valid minimum-width input produces rendered Evidence with geometry different from the terminal supplied to the child. The observed discrepancy includes both the width metadata and the layout of the retained ASCII line. Raw PTY text remains available. Choosing two columns produced an aligned result in the control and is an available workaround when the caller can use that width. It does not provide faithful one-column rendering. These observations cover initial dimensions through the CLI on Darwin arm64. Resize events, MCP calls, downstream comparison outcomes, and other platforms were not tested. ### REA version Exact `node scripts/rea.mjs --version` output: `6.8.0`. Source baseline: `65f1c447344a423fa2785847dbcea1e9a9e2454a`. ### Runtime and platform Node.js v24.22.1; Darwin arm64; installed locked `@lydell/node-pty` 1.1.0 and `@xterm/headless` 6.0.0. The existing build and dependencies were used without updates. ### Target and artifact context Local immediate-exit `/bin/echo` and `/bin/sh -c 'stty size'` fixtures; process-capture Evidence. No external analysis service or GUI provider was used. ### Interface CLI ### Earliest failure stage Capture or observation ### Relevant output or evidence The reproduction printed these summaries; every CLI and fixture exit code was 0: ```text one: selected={columns:1,rows:2,scrollback:0} raw="ab\r\n" rendered=[{columns:2,rows:2,lines:["ab",""]}] size: selected={columns:1,rows:2,scrollback:0} raw="2 1\r\n" rendered=[{columns:2,rows:2,lines:["1",""]}] two: selected={columns:2,rows:2,scrollback:0} raw="ab\r\n" rendered=[{columns:2,rows:2,lines:["ab",""]}] ``` `stty size` prints rows before columns. This establishes that the first two fixtures received a one-column PTY, whereas the returned rendered states use two columns. At the baseline: - Expected source: `docs/process-capture.md:237-241` preserves requested dimensions; `docs/process-capture.md:336-343` defines normalized visible rows and terminal cell width. `src/domain/process/processScenario.ts:177-186` admits initial `columns: 1`. - Producing boundary: `src/process/capture/ProcessHarness.ts:238-253,307-323` supplies the selected dimensions to the renderer and native PTY. `src/process/capture/TerminalRenderer.ts:53-60,140-203` constructs the headless terminal, reads its visible rows, and publishes its actual column count. - An independently executed installed-xterm constructor probe using `cols: 1, rows: 2` returned `cols: 2, rows: 2`, consistent with the CLI result. ### Typed outcome or recovery expected Return rendered observations for the admitted one-column geometry, or reject that unsupported dimension explicitly before execution. Preserve the matching two-column control and original PTY output. ### Analysis or suspected cause Source-derived explanation supported by the constructor probe: the headless terminal selects a minimum width of two columns. The PTY receives the requested width of one, and REA publishes the headless terminal's selected geometry without detecting the divergence. The defect is the disagreement between the admitted terminal and its rendered observations, rather than JavaScript string-length padding. ### Additional context Related closed #3868 and merged #4142 corrected extra cells from Unicode string padding. Closed #1464 and merged #1465 clarified normalized text versus cell width. Their restoration conditions differ from this ASCII width substitution. #7295 concerns omitted-rendered-frame uncertainty during comparison; #7281 concerns vanished observer scratch paths. Neither reports this geometry mismatch. Open and closed issues, PRs, and relevant issue comments were searched; no matching report was found. No last-known-good revision or regression boundary was established. This report was independently reproduced with AI assistance.
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#4516
No description provided.