Skip to content

fix(agent-core): bound harness exec stdout/stderr at 16 MiB#95195

Closed
Pick-cat wants to merge 7 commits into
openclaw:mainfrom
Pick-cat:fix/agents-darwin-runtime-os-version
Closed

fix(agent-core): bound harness exec stdout/stderr at 16 MiB#95195
Pick-cat wants to merge 7 commits into
openclaw:mainfrom
Pick-cat:fix/agents-darwin-runtime-os-version

Conversation

@Pick-cat

@Pick-cat Pick-cat commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

What Problem This Solves

Fixes unbounded stdout/stderr string accumulation in NodeExecutionEnv.exec(). Child process output uses stdout += chunk with no byte cap — a runaway command can OOM the agent process.

Why This Change Was Made

Add a 16 MiB per-stream cap with a maxOutputBytes override on the shared ExecutionEnvExecOptions type. Design:

  • Per-stream independent caps: stdout and stderr each independently bounded
  • Byte-accurate trim: truncateToBytes() stops at exact UTF-8 byte boundary, not UTF-16 code units
  • Callbacks preserved after truncation: onStdout/onStderr continue receiving chunks even after capture is capped — streaming parsers are not interrupted
  • Process not killed: child runs to natural exit or timeout
  • Shared contract: maxOutputBytes on ExecutionEnvExecOptions so all Shell implementations share the option
  • Shared helper: appendBoundedChunk() removes the duplicated stdout/stderr bounding logic

User Impact

Runaway commands can no longer OOM the agent process. Normal commands are unaffected. Streaming callbacks work end-to-end regardless of capture size.

Real behavior proof

Environment: Linux, Node 24, local .mts proof script driving the real NodeExecutionEnv.exec() against real child processes. The proof script is run locally and is not committed to the repo.

Fix (19 passed, 0 failed):

[case 1] exec: 500-byte stdout with 64 KiB cap → full capture
  ok: result.ok is true
  ok: exit code 0
  ok: stdout = 500 bytes
  ok: not truncated
  stdout: 500 bytes | exitCode: 0

[case 2] exec: 10 KB stdout with 1 KiB cap → truncated, process lives
  ok: result.ok is true
  ok: exit code 0 (process not killed)
  ok: stdout truncated
  ok: stdout length <= cap + marker (~1100 bytes)
  stdout: 1043 bytes (capped at 1024) | exitCode: 0

[case 3] exec: 10 KB stderr with 1 KiB cap → truncated via stderr
  ok: result.ok is true
  ok: stderr truncated
  stderr: 1043 bytes (capped) | stdout: 0 bytes

[case 4] exec: 2 KB stdout + 2 KB stderr with 1 KiB cap → both bounded
  ok: result.ok is true
  ok: stdout or stderr truncated
  stdout: 1043 bytes (trunc=true) | stderr: 1043 bytes (trunc=true)

[case 5] exec: callbacks continue after capture truncation
  ok: result.ok is true
  ok: capture truncated
  ok: callback received all chunks
  ok: callback fired for every chunk
  capture: 1043 bytes | callback: 10000 bytes in 1 chunks

[case 6] exec: UTF-8 byte boundary truncation
  ok: result.ok is true
  ok: stdout truncated
  ok: byte length stays near cap
  stdout: 21 bytes (byteLength=23)

=== Proof Summary ===
default cap: 16 MiB | test cap: 1 KiB
ALL PROOF ASSERTIONS: 19 passed, 0 failed

Negative control (origin/main, same script):

[case 2] exec: 10 KB stdout with 1 KiB cap → truncated, process lives
  ok: result.ok is true
  ok: exit code 0 (process not killed)
  FAIL: stdout truncated
  FAIL: stdout length <= cap + marker (~1100 bytes)
  stdout: 10000 bytes (capped at 1024) | exitCode: 0

[case 3] exec: 10 KB stderr with 1 KiB cap → truncated via stderr
  ok: result.ok is true
  FAIL: stderr truncated
  stderr: 10000 bytes (capped) | stdout: 0 bytes

[case 4] exec: 2 KB stdout + 2 KB stderr with 1 KiB cap → both bounded
  ok: result.ok is true
  FAIL: stdout or stderr truncated
  stdout: 2000 bytes (trunc=false) | stderr: 2000 bytes (trunc=false)

[case 5] exec: callbacks continue after capture truncation
  ok: result.ok is true
  FAIL: capture truncated
  ok: callback received all chunks
  ok: callback fired for every chunk
  capture: 10000 bytes | callback: 10000 bytes in 1 chunks

[case 6] exec: UTF-8 byte boundary truncation
  ok: result.ok is true
  FAIL: stdout truncated
  FAIL: byte length stays near cap
  stdout: 1010 bytes (byteLength=1020)

=== Proof Summary ===
ALL PROOF ASSERTIONS: 12 passed, 7 failed
EXIT=1

Evidence

Vitest (real child process spawn)

 Test Files  1 passed (1)
      Tests  9 passed (9)

Review fixes applied

Review Fix
[P2] Shared truncation drops stderr Per-stream independent CapturedStream state
[P2] UTF-16 slice overshoots byte cap truncateToBytes() char-level Buffer.byteLength iterator
[P3] maxOutputBytes not on shared type Added to ExecutionEnvExecOptions
[P1] Callbacks stop after truncation onStdout/onStderr fire for every chunk regardless of cap
[P1] Contract ambiguous JSDoc: per-stream independent caps, callbacks always fire
Quality: duplicated stdout/stderr logic Extracted appendBoundedChunk() helper
Quality: missing UTF-8 boundary test Added emoji truncation test
Quality: missing callback continuation test Added streaming callback coverage

@openclaw-barnacle openclaw-barnacle Bot added agents Agent runtime and tooling size: S labels Jun 20, 2026
@clawsweeper

clawsweeper Bot commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs maintainer review before merge. Reviewed July 10, 2026, 5:39 AM ET / 09:39 UTC.

Summary
The PR adds independent 16 MiB stdout and stderr capture limits to NodeExecutionEnv.exec, exposes a shared maxOutputBytes override, and adds focused real-child-process regression coverage.

PR surface: Source +43, Tests +49. Total +92 across 4 files.

Reproducibility: yes. Current main directly accumulates every child-output chunk without a limit, and the PR supplies a real-process negative control demonstrating that behavior.

Review metrics: 1 noteworthy metric.

  • Shared exec options: 1 option added. maxOutputBytes expands the exported agent-core execution contract while the new default changes large-output return behavior.

Merge readiness
Overall: 🐚 platinum hermit
Proof: 🦞 diamond lobster
Patch quality: 🐚 platinum hermit
Result: ready for maintainer review.

Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch.

Rank-up moves:

  • none.

Risk before merge

  • [P1] Existing consumers that intentionally inspect command output above 16 MiB per stream will receive truncated results after upgrade unless they provide a larger override or consume the complete stream through callbacks.
  • [P1] The PR head is behind current main, so the compatibility decision and focused checks should be refreshed against the eventual merge head.

Maintainer options:

  1. Accept the bounded default (recommended)
    Merge after a feature owner explicitly accepts that returned output above 16 MiB per stream will be truncated while callbacks remain complete.
  2. Preserve the existing default
    Revise the patch so the owning caller opts into bounding rather than changing every existing NodeExecutionEnv.exec result.
  3. Pause the API expansion
    Defer or close the PR if agent-core should not expose a configurable capture-limit contract at this time.

Next step before merge

  • [P2] A feature owner should accept or reject the deliberate default truncation contract and refresh the behind branch before merge; no mechanical code defect is established.

Maintainer decision needed

  • Question: Should NodeExecutionEnv.exec intentionally change its default contract from returning complete output to truncating each stream after 16 MiB?
  • Rationale: The implementation is technically sound, but accepting a default that changes returned results for existing large-output consumers requires maintainer ownership rather than an automated compatibility judgment.
  • Likely owner: steipete — He introduced the current agent-core execution contract and is the strongest history-backed decision owner for changing its default semantics.
  • Options:
    • Accept bounded default (recommended): Adopt the 16 MiB per-stream default, retain full callback streaming, and let callers explicitly request a different capture limit.
    • Require explicit opt-in: Preserve complete capture by default and require owning callers to request bounds, avoiding upgrade behavior change but leaving existing callers exposed.

Security
Cleared: The source-and-test diff adds no dependency, workflow, permission, secret, downloaded-artifact, or supply-chain execution surface.

Review details

Best possible solution:

Keep capture bounding in the Node execution environment with independent stream limits and uninterrupted callbacks, then land it after the feature owner explicitly accepts and documents the large-output default change.

Do we have a high-confidence way to reproduce the issue?

Yes. Current main directly accumulates every child-output chunk without a limit, and the PR supplies a real-process negative control demonstrating that behavior.

Is this the best way to solve the issue?

Yes technically. The execution environment is the narrowest owner that can bound retained output while preserving complete streaming callbacks, but maintainers must accept the deliberate default compatibility change.

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning high; reviewed against abbd5ae3ea73.

Label changes

Label justifications:

  • P2: This addresses a real agent-process memory-exhaustion path with limited harness-level blast radius.
  • merge-risk: 🚨 compatibility: Merging changes existing large-output calls from complete stdout and stderr results to truncated results by default.
  • rating: 🐚 platinum hermit: Overall readiness is 🐚 platinum hermit; proof is 🦞 diamond lobster and patch quality is 🐚 platinum hermit.
  • status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (terminal): The PR body convincingly shows after-fix behavior with real Node child processes and a same-script current-main negative control for both streams, callback continuation, process lifetime, and UTF-8 handling.
  • proof: sufficient: Contributor real behavior proof is sufficient. The PR body convincingly shows after-fix behavior with real Node child processes and a same-script current-main negative control for both streams, callback continuation, process lifetime, and UTF-8 handling.
Evidence reviewed

PR surface:

Source +43, Tests +49. Total +92 across 4 files.

View PR surface stats
Area Files Added Removed Net
Source 2 54 11 +43
Tests 2 113 64 +49
Docs 0 0 0 0
Config 0 0 0 0
Generated 0 0 0 0
Other 0 0 0 0
Total 4 167 75 +92

What I checked:

Likely related people:

  • steipete: Peter Steinberger introduced the current agent-core Node execution environment and its stdout/stderr capture contract, making him the strongest history-backed owner for accepting the default behavior change. (role: introduced behavior; confidence: high; commits: 0d63f46498e5; files: packages/agent-core/src/harness/env/nodejs.ts, packages/agent-core/src/harness/types.ts)
What the crustacean ranks mean
  • 🦀 challenger crab: rare, exceptional readiness with strong proof, clean implementation, and convincing validation.
  • 🦞 diamond lobster: very strong readiness with only minor maintainer review expected.
  • 🐚 platinum hermit: good normal PR, likely mergeable with ordinary maintainer review.
  • 🦐 gold shrimp: useful signal, but proof or patch confidence is still limited.
  • 🦪 silver shellfish: thin signal; proof, validation, or implementation needs work.
  • 🧂 unranked krab: not merge-ready because proof is missing/unusable or there are serious correctness or safety concerns.
  • 🌊 off-meta tidepool: rating does not apply to this item.

Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

How this review workflow works
  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.
Review history (6 earlier review cycles)
  • reviewed 2026-07-02T15:24:44.646Z sha af08225 :: needs maintainer review before merge. :: none
  • reviewed 2026-07-05T14:52:24.191Z sha af08225 :: needs maintainer review before merge. :: none
  • reviewed 2026-07-05T16:13:51.358Z sha af08225 :: needs real behavior proof before merge. :: none
  • reviewed 2026-07-07T08:52:34.168Z sha 9cc84d8 :: needs maintainer review before merge. :: none
  • reviewed 2026-07-07T08:59:42.357Z sha 9cc84d8 :: needs maintainer review before merge. :: none
  • reviewed 2026-07-09T07:43:16.001Z sha 4159a28 :: needs maintainer review before merge. :: none

@clawsweeper clawsweeper Bot added rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. P2 Normal backlog priority with limited blast radius. merge-risk: 🚨 compatibility 🚨 May break existing users, config, migrations, defaults, or upgrade paths. labels Jun 20, 2026
@Pick-cat

Copy link
Copy Markdown
Contributor Author

Pushed 101fb73887 addressing the review:

On proof: I don't have a live macOS 26 (Tahoe) host. The off-Darwin path is verified on a real host, the sw_vers Darwin mapping is locked by the regression test, and the issue's own triage notes live Tahoe proof isn't required to start the fix. Happy to fold in live mac output if a maintainer or another contributor can capture it.

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: the existing ClawSweeper review comment will be edited in place when the review finishes.

@clawsweeper clawsweeper Bot added rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. and removed merge-risk: 🚨 compatibility 🚨 May break existing users, config, migrations, defaults, or upgrade paths. rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. labels Jun 20, 2026
@Pick-cat
Pick-cat force-pushed the fix/agents-darwin-runtime-os-version branch from 101fb73 to ce0d66c Compare July 2, 2026 09:27
@openclaw-barnacle openclaw-barnacle Bot added channel: discord Channel integration: discord and removed agents Agent runtime and tooling labels Jul 2, 2026
@Pick-cat Pick-cat changed the title fix(agents): report macOS product version in runtime prompt OS metadata fix(discord): bound gateway metadata response reads to prevent OOM Jul 2, 2026
@Pick-cat

Pick-cat commented Jul 2, 2026

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: the existing ClawSweeper review comment will be edited in place when the review finishes.

@clawsweeper clawsweeper Bot added rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. and removed rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. labels Jul 2, 2026
@openclaw-barnacle openclaw-barnacle Bot added triage: blank-template Candidate: PR template appears mostly untouched. triage: needs-pr-context Candidate: external PR body lacks required problem context or evidence. and removed triage: needs-pr-context Candidate: external PR body lacks required problem context or evidence. labels Jul 2, 2026
@Pick-cat
Pick-cat force-pushed the fix/agents-darwin-runtime-os-version branch from 9399fec to af08225 Compare July 2, 2026 15:09
@Pick-cat

Pick-cat commented Jul 2, 2026

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review

Branch rebased to latest main.

Proof and patch improvements since last review:

  • Removed the committed proof script; proof now runs locally and is pasted into the PR body.
  • Extracted appendBoundedChunk() helper to eliminate duplicated stdout/stderr bounding logic.
  • Added UTF-8 byte-boundary regression test (emoji truncation).
  • Added streaming-callback continuation regression test.
  • Fresh negative control included in the PR body: old code fails 7 assertions, fix passes all 19.

CI: pnpm test packages/agent-core/src/harness/env/nodejs.test.ts --run passes (9/9); lint passes.

@clawsweeper

clawsweeper Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: the existing ClawSweeper review comment will be edited in place when the review finishes.

@clawsweeper clawsweeper Bot added proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. merge-risk: 🚨 compatibility 🚨 May break existing users, config, migrations, defaults, or upgrade paths. and removed rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. status: 🛠️ actively grinding The PR author has acted after the latest ClawSweeper review and work remains. proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. merge-risk: 🚨 compatibility 🚨 May break existing users, config, migrations, defaults, or upgrade paths. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. labels Jul 2, 2026
@Pick-cat

Pick-cat commented Jul 5, 2026

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review

@clawsweeper clawsweeper Bot added proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. labels Jul 5, 2026
@Pick-cat

Pick-cat commented Jul 7, 2026

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: the existing ClawSweeper review comment will be edited in place when the review finishes.

Pick-cat and others added 7 commits July 10, 2026 10:00
Replace unbounded stdout/stderr string accumulation with a byte-capped
collector that truncates and appends "[output truncated]" when output
exceeds maxOutputBytes (default 16 MiB). Prevents OOM from runaway
child process output in the agent harness exec path.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
Stop accumulating output when cap is exceeded but let the process
continue to natural exit or timeout. Fixes the merge-risk: availability
concern — truncated output no longer takes down the child process.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
…t type

- Separate stdoutTruncated/stderrTruncated so stderr is not dropped
  after stdout crosses the cap
- Use truncateToBytes (UTF-8 character iterator) instead of
  chunk.slice(0, N) which slices by UTF-16 code units
- Expose maxOutputBytes on ExecutionEnvExecOptions so all Shell
  implementations share the contract

Co-Authored-By: Claude Opus 4.7 <[email protected]>
…process file

The output bounding tests use real child process spawn calls, but
the vi.mock("node:child_process") in nodejs.test.ts intercepted all
spawn calls and returned undefined, causing "Cannot read properties
of undefined (reading 'stdout')" errors.

Move them to a separate file (nodejs.exec-bounding.test.ts) that does
not mock child_process, keeping the mocked stream error tests and the
real-process output bounding tests isolated.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merge-risk: 🚨 compatibility 🚨 May break existing users, config, migrations, defaults, or upgrade paths. P2 Normal backlog priority with limited blast radius. proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. size: M status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. triage: blank-template Candidate: PR template appears mostly untouched.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant