[GH-ISSUE #6438] [Bug] inspect-python-pyc treats caller-selected decode limits as memory or active-analysis failures #3851

Closed
opened 2026-10-31 00:44:38 +00:00 by bsakaguchi · 0 comments
Owner

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

Before submitting

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

Area

CLI

Observed behavior

inspect-python-pyc correctly refuses a valid CPython 3.11 file when a caller-selected decode budget is too low, but returns recovery advice for a different resource. --max-depth 1 returns retryable: true and tells the caller to wait for active analysis. --max-objects 1 and --max-string-bytes 2 recommend worker/host-memory changes. None of these actions names the existing option that admits the same file when raised.

The exact driver below ran each refusal twice with identical stdout, exit 1 and empty stderr. Each corresponding raised-option control exited 0 with complete coverage on the same artifact.

Reproduction

From a fresh checkout at 9eb829aca34be36ff112826337996f6139f82eff, prepare locked dependencies and runtime with npm ci && npm run build:cached. That preparation was already provided for the observed runs; it was not rerun. Then, from the checkout root, run the following with Python 3.11 and Node on PATH. It builds a small local fixture without executing its bytecode and isolates REA's HOME.

WORK="$(mktemp -d)"
export WORK
python3.11 - <<'PYTHON'
import json, os, pathlib, py_compile, subprocess

work = pathlib.Path(os.environ['WORK']).resolve()
(work / 'home').mkdir(exist_ok=True)
source = work / 'sample.py'
source.write_text('"""Example module."""\ndef f(x):\n    return x + 1\n')
artifact = work / 'sample.pyc'
py_compile.compile(str(source), cfile=str(artifact), doraise=True)
env = dict(os.environ, HOME=str(work / 'home'))
for label, options in [
    ('objects', ['--max-objects', '1']),
    ('strings', ['--max-string-bytes', '2']),
    ('depth', ['--max-depth', '1']),
]:
    results = [subprocess.run(
        ['node', 'scripts/rea.mjs', 'inspect-python-pyc', str(artifact),
         *options, '--json'], env=env, capture_output=True, text=True,
        timeout=30) for _ in range(2)]
    assert all(r.returncode == 1 and not r.stderr for r in results)
    assert results[0].stdout == results[1].stdout
    value = json.loads(results[0].stdout)
    print(json.dumps({'case': label, 'exits': [r.returncode for r in results],
        'identical_stdout': True, 'code': value['code'],
        'retryable': value['retryable'], 'remediation': value['remediation'],
        'details': value['details']}))
for option, value in [('--max-objects', '256'),
                      ('--max-string-bytes', '1048576'),
                      ('--max-depth', '64')]:
    result = subprocess.run(
        ['node', 'scripts/rea.mjs', 'inspect-python-pyc', str(artifact),
         option, value, '--json'], env=env, capture_output=True, text=True,
        timeout=30)
    assert result.returncode == 0 and not result.stderr
    output = json.loads(result.stdout)
    assert output['normalized_result']['coverage']['status'] == 'complete'
    print(json.dumps({'control': option + ' ' + value, 'exit': result.returncode,
        'coverage': output['normalized_result']['coverage']}))
PYTHON

Expected behavior

Preserve the intentional refusals and their effective numeric budgets. Identify the affected per-call control in remediation.action, explain that a larger accepted value applies to the next call, and avoid implying that host memory or waiting raises it. A deterministic depth refusal should not be classified as retryable active-analysis occupancy.

The input schema advertises all three adjustable budgets; the CLI maps them to the same fields. The decode documentation promises bounded failures. The recovery contract requires the returned action to identify the control and tells callers to change request fields only when that action recommends it.

Impact and scope

P2: the depth refusal exposes incorrect machine-readable retryability and transient-capacity advice for a deterministic input budget. The other two cases also misdirect recovery. A safe workaround is available: raise the named option within its advertised range. The controls demonstrated complete inspection with 29 objects, two code objects and nesting depth 4.

Observed scope is the source CLI and this compiler-produced fixture. No client retry loop, data loss, incorrect successful analysis or security-boundary crossing was observed. Real-world frequency, other platforms and live MCP behavior were not measured; shared CLI/MCP implications are source-derived.

REA version

6.7.0 (source 9eb829aca3)

Runtime and platform

Node.js v26.12.1; macOS 27.0.1; arm64; CPython 3.11.17 for fixture generation only. The same failures and successful controls were also observed with installed Node.js v24.22.1. The exact repository pin v24.18.0 was not tested.

Target and artifact context

A CPython 3.11 pyc compiled from a module docstring and one function returning x + 1. The same bytes are used for the refusal and raised-budget controls. No Python installation is required by REA itself.

Interface

CLI

Earliest failure stage

Transport or result projection

Relevant output or evidence

All three refusals returned code: "resource_constraint", the correct exceeded option/value in details.reported_limits, exit 1 and empty stderr:

Option details.resource retryable remediation.action
--max-objects 1 memory false Review the reported worker memory limits and available host memory. Retry with sufficient memory or a smaller artifact; REA retains tighter inherited limits.
--max-string-bytes 2 memory false Same worker/host-memory action.
--max-depth 1 capacity true Wait for an active analysis to complete, then retry within the reported admission limits.

The reasons were respectively Marshal object count exceeds max_objects 1., Marshal string of 16 bytes exceeds max_string_bytes 2., and Marshal nesting exceeds max_depth 1. Repeating each unchanged call produced identical stdout. Raising only its option to 256 objects, 1048576 string bytes, or depth 64 returned complete coverage and exit 0.

Typed outcome or recovery expected

Keep resource_constraint and the actual reported budget. Return non-transient recovery for deterministic decode refusals, with the relevant CLI option/MCP field, effective value and accepted range. Preserve the current successful raised-budget controls; no numeric guard needs to be removed. These are proposed restoration criteria; no fix was tested.

Analysis or suspected cause

Source-derived: the provider mapping converts depth to capacity and other parser budgets to memory, without a remediation or retryability override. The shared presentation therefore chooses generic active-analysis or worker-memory recovery. The projection defaults capacity to retryable. The reached guards are ordinary object/depth and string-size comparisons against the caller's selected values.

Additional context

Related #5504 and #5556 cover other producers' deterministic limits; #5847 concerns binary plists. #5808 and #5910 track broader recovery work. Read-only open/closed issue searches found no report matching this pyc producer. Last known good version: NOT_ENOUGH_INFO; none established. No current-main runtime, installed-package or live MCP reproduction is claimed.

Originally created by @coygeek on GitHub (Oct 23, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/6438 ### 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 CLI ### Observed behavior `inspect-python-pyc` correctly refuses a valid CPython 3.11 file when a caller-selected decode budget is too low, but returns recovery advice for a different resource. `--max-depth 1` returns `retryable: true` and tells the caller to wait for active analysis. `--max-objects 1` and `--max-string-bytes 2` recommend worker/host-memory changes. None of these actions names the existing option that admits the same file when raised. The exact driver below ran each refusal twice with identical stdout, exit 1 and empty stderr. Each corresponding raised-option control exited 0 with complete coverage on the same artifact. ### Reproduction From a fresh checkout at `9eb829aca34be36ff112826337996f6139f82eff`, prepare locked dependencies and runtime with `npm ci && npm run build:cached`. That preparation was already provided for the observed runs; it was not rerun. Then, from the checkout root, run the following with Python 3.11 and Node on PATH. It builds a small local fixture without executing its bytecode and isolates REA's HOME. ```sh WORK="$(mktemp -d)" export WORK python3.11 - <<'PYTHON' import json, os, pathlib, py_compile, subprocess work = pathlib.Path(os.environ['WORK']).resolve() (work / 'home').mkdir(exist_ok=True) source = work / 'sample.py' source.write_text('"""Example module."""\ndef f(x):\n return x + 1\n') artifact = work / 'sample.pyc' py_compile.compile(str(source), cfile=str(artifact), doraise=True) env = dict(os.environ, HOME=str(work / 'home')) for label, options in [ ('objects', ['--max-objects', '1']), ('strings', ['--max-string-bytes', '2']), ('depth', ['--max-depth', '1']), ]: results = [subprocess.run( ['node', 'scripts/rea.mjs', 'inspect-python-pyc', str(artifact), *options, '--json'], env=env, capture_output=True, text=True, timeout=30) for _ in range(2)] assert all(r.returncode == 1 and not r.stderr for r in results) assert results[0].stdout == results[1].stdout value = json.loads(results[0].stdout) print(json.dumps({'case': label, 'exits': [r.returncode for r in results], 'identical_stdout': True, 'code': value['code'], 'retryable': value['retryable'], 'remediation': value['remediation'], 'details': value['details']})) for option, value in [('--max-objects', '256'), ('--max-string-bytes', '1048576'), ('--max-depth', '64')]: result = subprocess.run( ['node', 'scripts/rea.mjs', 'inspect-python-pyc', str(artifact), option, value, '--json'], env=env, capture_output=True, text=True, timeout=30) assert result.returncode == 0 and not result.stderr output = json.loads(result.stdout) assert output['normalized_result']['coverage']['status'] == 'complete' print(json.dumps({'control': option + ' ' + value, 'exit': result.returncode, 'coverage': output['normalized_result']['coverage']})) PYTHON ``` ### Expected behavior Preserve the intentional refusals and their effective numeric budgets. Identify the affected per-call control in `remediation.action`, explain that a larger accepted value applies to the next call, and avoid implying that host memory or waiting raises it. A deterministic depth refusal should not be classified as retryable active-analysis occupancy. The [input schema](https://github.com/morluto/rea/blob/9eb829aca34be36ff112826337996f6139f82eff/src/domain/python/pythonPyc.ts#L81) advertises all three adjustable budgets; [the CLI maps them to the same fields](https://github.com/morluto/rea/blob/9eb829aca34be36ff112826337996f6139f82eff/src/cli/pythonPycCommands.ts#L23). The [decode documentation](https://github.com/morluto/rea/blob/9eb829aca34be36ff112826337996f6139f82eff/docs/python-pyc.md#L61) promises bounded failures. The [recovery contract](https://github.com/morluto/rea/blob/9eb829aca34be36ff112826337996f6139f82eff/docs/design/limits-and-recovery.md#L65) requires the returned action to identify the control and tells callers to change request fields only when that action recommends it. ### Impact and scope P2: the depth refusal exposes incorrect machine-readable retryability and transient-capacity advice for a deterministic input budget. The other two cases also misdirect recovery. A safe workaround is available: raise the named option within its advertised range. The controls demonstrated complete inspection with 29 objects, two code objects and nesting depth 4. Observed scope is the source CLI and this compiler-produced fixture. No client retry loop, data loss, incorrect successful analysis or security-boundary crossing was observed. Real-world frequency, other platforms and live MCP behavior were not measured; shared CLI/MCP implications are source-derived. ### REA version 6.7.0 (source 9eb829aca34be36ff112826337996f6139f82eff) ### Runtime and platform Node.js v26.12.1; macOS 27.0.1; arm64; CPython 3.11.17 for fixture generation only. The same failures and successful controls were also observed with installed Node.js v24.22.1. The exact repository pin v24.18.0 was not tested. ### Target and artifact context A CPython 3.11 pyc compiled from a module docstring and one function returning `x + 1`. The same bytes are used for the refusal and raised-budget controls. No Python installation is required by REA itself. ### Interface CLI ### Earliest failure stage Transport or result projection ### Relevant output or evidence All three refusals returned `code: "resource_constraint"`, the correct exceeded option/value in `details.reported_limits`, exit 1 and empty stderr: | Option | `details.resource` | `retryable` | `remediation.action` | | --- | --- | --- | --- | | `--max-objects 1` | `memory` | `false` | Review the reported worker memory limits and available host memory. Retry with sufficient memory or a smaller artifact; REA retains tighter inherited limits. | | `--max-string-bytes 2` | `memory` | `false` | Same worker/host-memory action. | | `--max-depth 1` | `capacity` | `true` | Wait for an active analysis to complete, then retry within the reported admission limits. | The reasons were respectively `Marshal object count exceeds max_objects 1.`, `Marshal string of 16 bytes exceeds max_string_bytes 2.`, and `Marshal nesting exceeds max_depth 1.` Repeating each unchanged call produced identical stdout. Raising only its option to 256 objects, 1048576 string bytes, or depth 64 returned complete coverage and exit 0. ### Typed outcome or recovery expected Keep `resource_constraint` and the actual reported budget. Return non-transient recovery for deterministic decode refusals, with the relevant CLI option/MCP field, effective value and accepted range. Preserve the current successful raised-budget controls; no numeric guard needs to be removed. These are proposed restoration criteria; no fix was tested. ### Analysis or suspected cause Source-derived: [the provider mapping](https://github.com/morluto/rea/blob/9eb829aca34be36ff112826337996f6139f82eff/src/python/PythonPycProvider.ts#L53) converts depth to `capacity` and other parser budgets to `memory`, without a remediation or retryability override. The [shared presentation](https://github.com/morluto/rea/blob/9eb829aca34be36ff112826337996f6139f82eff/src/domain/analysisErrorPresentation.ts#L85) therefore chooses generic active-analysis or worker-memory recovery. The [projection](https://github.com/morluto/rea/blob/9eb829aca34be36ff112826337996f6139f82eff/src/domain/analysisErrorProjection.ts#L121) defaults capacity to retryable. The reached guards are ordinary [object/depth](https://github.com/morluto/rea/blob/9eb829aca34be36ff112826337996f6139f82eff/src/python/PythonPycParser.ts#L294) and [string-size](https://github.com/morluto/rea/blob/9eb829aca34be36ff112826337996f6139f82eff/src/python/PythonPycParser.ts#L1347) comparisons against the caller's selected values. ### Additional context Related #5504 and #5556 cover other producers' deterministic limits; #5847 concerns binary plists. #5808 and #5910 track broader recovery work. Read-only open/closed issue searches found no report matching this pyc producer. Last known good version: NOT_ENOUGH_INFO; none established. No current-main runtime, installed-package or live MCP reproduction 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#3851
No description provided.