[GH-ISSUE #3168] [Bug] Process capture exceeds the selected output budget after normalization without reporting truncation #1105

Closed
opened 2026-10-15 18:31:07 +00:00 by bsakaguchi · 0 comments
Owner

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

Before submitting

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

Area

Process capture or runtime observation

Observed behavior

A successful public capture-process call retained 50,002 bytes of normalized terminal chunk text with limits.output_bytes set to 1,000 and returned truncated: false. The scenario uses an accepted literal normalization rule that replaces each a with 500 X characters. The original PTY chunk contains 100 a characters followed by CRLF, totaling 102 bytes.

The retained normalized chunk contains exactly 50,000 X characters followed by CRLF. Raw and rendered observations each have one observed and one retained frame. The one-row terminal ends on a blank line, so the rendered observation retains zero bytes and does not independently exceed its budget. The CLI and captured process both exited with code 0, owned-process cleanup was verified, and the temporary capture root was removed.

An equal-length replacement control retained exactly 100 X characters followed by CRLF, totaling 102 normalized bytes, and returned truncated: false. A control without custom patterns also retained 102 normalized bytes.

Reproduction

Use a supported macOS capture host with a working native PTY backend and the documented Swift compiler prerequisite. Begin in a clean checkout of morluto/rea at 58300885207133422af235e0f2b10658cf958b03 with its pinned toolchain available. The final observed capture used the prepared baseline build and installed locked dependencies; the clean-checkout install/build commands below are setup instructions and were not rerun during verification.

  1. Install and build, then create isolated local configuration directories.

    npm ci
    npm run build:cached
    mkdir -p repro-output-budget/home/config repro-output-budget/home/cache repro-output-budget/home/data repro-output-budget/temp
    
  2. Create the complete scenario inline.

    node --input-type=module <<'JS'
    import { writeFileSync } from 'node:fs';
    writeFileSync('repro-output-budget/scenario.json', JSON.stringify({
      executable: 'echo',
      arguments: ['a'.repeat(100)],
      terminal: { columns: 80, rows: 1, scrollback: 0 },
      timeout_ms: 2000,
      idle_timeout_ms: 2000,
      settle_ms: 0,
      limits: { output_bytes: 1000 },
      normalization: {
        paths: false,
        pids: false,
        ports: false,
        patterns: [{ pattern: 'a', replacement: 'X'.repeat(500) }]
      }
    }));
    JS
    
  3. Capture the caller-selected command through the public CLI, keeping output separate from the input.

    env HOME="$PWD/repro-output-budget/home" \
      XDG_CONFIG_HOME="$PWD/repro-output-budget/home/config" \
      XDG_CACHE_HOME="$PWD/repro-output-budget/home/cache" \
      XDG_DATA_HOME="$PWD/repro-output-budget/home/data" \
      TMPDIR="$PWD/repro-output-budget/temp" \
      node scripts/rea.mjs capture-process repro-output-budget/scenario.json --json \
      > repro-output-budget/capture.json
    
  4. Measure the retained representations and the reported truncation flag.

    node --input-type=module <<'JS'
    import { readFileSync } from 'node:fs';
    const capture = JSON.parse(readFileSync('repro-output-budget/capture.json', 'utf8')).normalized_result;
    console.log(JSON.stringify({
      budget_bytes: capture.truncation_details.raw_terminal.budget_bytes,
      original_retained_bytes: capture.truncation_details.raw_terminal.retained_bytes,
      normalized_chunk_bytes: capture.frames.reduce((n, frame) => n + Buffer.byteLength(frame.data), 0),
      rendered_retained_bytes: capture.truncation_details.rendered_terminal.retained_bytes,
      truncated: capture.truncated
    }, null, 2));
    JS
    
  5. For the discriminating control, change only step 2's replacement to a single X, regenerate the scenario, and rerun steps 3 and 4. The observed control reports 102 normalized bytes, 102 original retained bytes, and truncated: false.

Expected behavior

docs/process-capture.md:222-226 defines limits.output_bytes as a bound on retained terminal observations, and docs/process-capture.md:420 calls it a bound on retained terminal content. Normalized chunk data is retained terminal content under docs/process-capture.md:244-250. The accepted caller-selected literal replacement semantics are documented at docs/process-capture.md:261-265.

The retained normalized representation needs a corresponding bound even when an accepted replacement expands it. Any omitted observations must appear in completeness accounting. Preserve the documented meanings of original-byte accounting and the independent rendered-frame budget, and keep valid literal normalization usable.

Impact and scope

This valid scenario retains roughly 50 times its selected terminal-content budget without reporting truncation. Callers cannot rely on that budget to bound the retained normalized Evidence content.

The observed failure concerns normalized PTY chunk text. The original-byte counter is accurate, and the rendered observation in this case remains within its independent budget. Equal-length replacement is an observed workaround for this fixture when it preserves the caller's comparison intent. Avoiding expansion does not preserve every supported expanding normalization request.

The verified path is the public CLI with a real native PTY. No crash, peak-memory measurement, secret exposure, privilege change, or security-boundary crossing was observed. An MCP call was not executed.

REA version

6.4.0 (main at 5830088520)

Runtime and platform

Node.js v24.21.0; macOS 27.0.1; arm64. The repository pins Node.js v24.18.0, which was unavailable during verification; the observed runtime differs from that pin.

Target and artifact context

Caller-selected echo command emitting 100 ASCII a characters and a PTY-translated CRLF; 80-column, one-row terminal with no scrollback; 1,000-byte output limit; literal a replacement with 500 ASCII X characters. The artifact inspected is the returned capture Evidence's normalized_result, including frames and truncation_details.

Interface

CLI

Earliest failure stage

Capture or observation

Relevant output or evidence

{
  "budget_bytes": 1000,
  "original_retained_bytes": 102,
  "normalized_chunk_bytes": 50002,
  "rendered_retained_bytes": 0,
  "truncated": false
}

Typed outcome or recovery expected

Enforce the bound on retained normalized terminal content and disclose resulting omissions through completeness and truncation accounting. Preserve original-byte and independent rendered-frame accounting. The equal-length control must still retain its complete 102-byte normalized chunk, apply the literal replacement, and report no spurious truncation. Rejecting all custom normalization or discarding every terminal observation would not restore this supported workflow.

Analysis or suspected cause

The following reasoning is source-derived, with locators at the reported baseline.

  • src/cli/processCommands.ts:18-31 and src/application/process/ProcessCli.ts:51-95 validate and execute the public scenario-file call. src/process/capture/ProcessHarness.ts:264-306 constructs the normalizer, spawns the real PTY, and registers collection.
  • src/process/capture/ProcessCaptureLifecycle.ts:1312-1337 admits a chunk using its original byte length before normalizing and retaining data. It preserves the original as raw_data when normalization changes the text.
  • src/process/capture/ProcessNormalization.ts:35-39 applies the accepted literal replacement with replaceAll, which expands this input into the retained normalized text.
  • src/domain/process/processCaptureValidation.ts:283-319 checks raw retention using raw_data ?? data and checks rendered bytes separately. These checks do not bound normalized chunk data when raw_data is present.

The missing bound concerns the additional normalized chunk representation. It is not an arithmetic error in the original-byte counter.

Additional context

Related reports are #1250, #1241, and #1464; they address per-dimension truncation diagnostics, preservation of numeric/original PTY text, and rendered-line documentation, while this report requires bounding expanded normalized chunk retention under the caller-selected output limit.

P2 is supported by consequential retained output growth and an unreliable caller-selected bound, with an observed equal-length replacement workaround for this fixture and no established crash or security-boundary impact.

Originally created by @coygeek on GitHub (Oct 14, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/3168 ### Before submitting - [x] I searched the open and closed issues for a related report. - [x] I removed credentials, tokens, private paths, and sensitive target data. ### Area Process capture or runtime observation ### Observed behavior A successful public `capture-process` call retained 50,002 bytes of normalized terminal chunk text with `limits.output_bytes` set to 1,000 and returned `truncated: false`. The scenario uses an accepted literal normalization rule that replaces each `a` with 500 `X` characters. The original PTY chunk contains 100 `a` characters followed by CRLF, totaling 102 bytes. The retained normalized chunk contains exactly 50,000 `X` characters followed by CRLF. Raw and rendered observations each have one observed and one retained frame. The one-row terminal ends on a blank line, so the rendered observation retains zero bytes and does not independently exceed its budget. The CLI and captured process both exited with code 0, owned-process cleanup was verified, and the temporary capture root was removed. An equal-length replacement control retained exactly 100 `X` characters followed by CRLF, totaling 102 normalized bytes, and returned `truncated: false`. A control without custom patterns also retained 102 normalized bytes. ### Reproduction Use a supported macOS capture host with a working native PTY backend and the documented Swift compiler prerequisite. Begin in a clean checkout of `morluto/rea` at `58300885207133422af235e0f2b10658cf958b03` with its pinned toolchain available. The final observed capture used the prepared baseline build and installed locked dependencies; the clean-checkout install/build commands below are setup instructions and were not rerun during verification. 1. Install and build, then create isolated local configuration directories. ```sh npm ci npm run build:cached mkdir -p repro-output-budget/home/config repro-output-budget/home/cache repro-output-budget/home/data repro-output-budget/temp ``` 2. Create the complete scenario inline. ```sh node --input-type=module <<'JS' import { writeFileSync } from 'node:fs'; writeFileSync('repro-output-budget/scenario.json', JSON.stringify({ executable: 'echo', arguments: ['a'.repeat(100)], terminal: { columns: 80, rows: 1, scrollback: 0 }, timeout_ms: 2000, idle_timeout_ms: 2000, settle_ms: 0, limits: { output_bytes: 1000 }, normalization: { paths: false, pids: false, ports: false, patterns: [{ pattern: 'a', replacement: 'X'.repeat(500) }] } })); JS ``` 3. Capture the caller-selected command through the public CLI, keeping output separate from the input. ```sh env HOME="$PWD/repro-output-budget/home" \ XDG_CONFIG_HOME="$PWD/repro-output-budget/home/config" \ XDG_CACHE_HOME="$PWD/repro-output-budget/home/cache" \ XDG_DATA_HOME="$PWD/repro-output-budget/home/data" \ TMPDIR="$PWD/repro-output-budget/temp" \ node scripts/rea.mjs capture-process repro-output-budget/scenario.json --json \ > repro-output-budget/capture.json ``` 4. Measure the retained representations and the reported truncation flag. ```sh node --input-type=module <<'JS' import { readFileSync } from 'node:fs'; const capture = JSON.parse(readFileSync('repro-output-budget/capture.json', 'utf8')).normalized_result; console.log(JSON.stringify({ budget_bytes: capture.truncation_details.raw_terminal.budget_bytes, original_retained_bytes: capture.truncation_details.raw_terminal.retained_bytes, normalized_chunk_bytes: capture.frames.reduce((n, frame) => n + Buffer.byteLength(frame.data), 0), rendered_retained_bytes: capture.truncation_details.rendered_terminal.retained_bytes, truncated: capture.truncated }, null, 2)); JS ``` 5. For the discriminating control, change only step 2's replacement to a single `X`, regenerate the scenario, and rerun steps 3 and 4. The observed control reports 102 normalized bytes, 102 original retained bytes, and `truncated: false`. ### Expected behavior `docs/process-capture.md:222-226` defines `limits.output_bytes` as a bound on retained terminal observations, and `docs/process-capture.md:420` calls it a bound on retained terminal content. Normalized chunk `data` is retained terminal content under `docs/process-capture.md:244-250`. The accepted caller-selected literal replacement semantics are documented at `docs/process-capture.md:261-265`. The retained normalized representation needs a corresponding bound even when an accepted replacement expands it. Any omitted observations must appear in completeness accounting. Preserve the documented meanings of original-byte accounting and the independent rendered-frame budget, and keep valid literal normalization usable. ### Impact and scope This valid scenario retains roughly 50 times its selected terminal-content budget without reporting truncation. Callers cannot rely on that budget to bound the retained normalized Evidence content. The observed failure concerns normalized PTY chunk text. The original-byte counter is accurate, and the rendered observation in this case remains within its independent budget. Equal-length replacement is an observed workaround for this fixture when it preserves the caller's comparison intent. Avoiding expansion does not preserve every supported expanding normalization request. The verified path is the public CLI with a real native PTY. No crash, peak-memory measurement, secret exposure, privilege change, or security-boundary crossing was observed. An MCP call was not executed. ### REA version 6.4.0 (main at 58300885207133422af235e0f2b10658cf958b03) ### Runtime and platform Node.js v24.21.0; macOS 27.0.1; arm64. The repository pins Node.js v24.18.0, which was unavailable during verification; the observed runtime differs from that pin. ### Target and artifact context Caller-selected `echo` command emitting 100 ASCII `a` characters and a PTY-translated CRLF; 80-column, one-row terminal with no scrollback; 1,000-byte output limit; literal `a` replacement with 500 ASCII `X` characters. The artifact inspected is the returned capture Evidence's `normalized_result`, including `frames` and `truncation_details`. ### Interface CLI ### Earliest failure stage Capture or observation ### Relevant output or evidence ```text { "budget_bytes": 1000, "original_retained_bytes": 102, "normalized_chunk_bytes": 50002, "rendered_retained_bytes": 0, "truncated": false } ``` ### Typed outcome or recovery expected Enforce the bound on retained normalized terminal content and disclose resulting omissions through completeness and truncation accounting. Preserve original-byte and independent rendered-frame accounting. The equal-length control must still retain its complete 102-byte normalized chunk, apply the literal replacement, and report no spurious truncation. Rejecting all custom normalization or discarding every terminal observation would not restore this supported workflow. ### Analysis or suspected cause The following reasoning is source-derived, with locators at the reported baseline. - `src/cli/processCommands.ts:18-31` and `src/application/process/ProcessCli.ts:51-95` validate and execute the public scenario-file call. `src/process/capture/ProcessHarness.ts:264-306` constructs the normalizer, spawns the real PTY, and registers collection. - `src/process/capture/ProcessCaptureLifecycle.ts:1312-1337` admits a chunk using its original byte length before normalizing and retaining `data`. It preserves the original as `raw_data` when normalization changes the text. - `src/process/capture/ProcessNormalization.ts:35-39` applies the accepted literal replacement with `replaceAll`, which expands this input into the retained normalized text. - `src/domain/process/processCaptureValidation.ts:283-319` checks raw retention using `raw_data ?? data` and checks rendered bytes separately. These checks do not bound normalized chunk `data` when `raw_data` is present. The missing bound concerns the additional normalized chunk representation. It is not an arithmetic error in the original-byte counter. ### Additional context Related reports are [#1250](https://github.com/morluto/rea/issues/1250), [#1241](https://github.com/morluto/rea/issues/1241), and [#1464](https://github.com/morluto/rea/issues/1464); they address per-dimension truncation diagnostics, preservation of numeric/original PTY text, and rendered-line documentation, while this report requires bounding expanded normalized chunk retention under the caller-selected output limit. P2 is supported by consequential retained output growth and an unreliable caller-selected bound, with an observed equal-length replacement workaround for this fixture and no established crash or security-boundary impact.
bsakaguchi 2026-10-15 18:31:07 +00:00
  • closed this issue
  • added the
    bug
    label
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#1105
No description provided.