Skip to content

fix(agents): exec output shows escape codes when a sequence spans a chunk#111364

Merged
steipete merged 2 commits into
openclaw:mainfrom
Yigtwxx:fix/exec-ansi-split-chunks
Jul 20, 2026
Merged

fix(agents): exec output shows escape codes when a sequence spans a chunk#111364
steipete merged 2 commits into
openclaw:mainfrom
Yigtwxx:fix/exec-ansi-split-chunks

Conversation

@Yigtwxx

@Yigtwxx Yigtwxx commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

What Problem This Solves

Fixes an issue where users running the exec tool would see escape codes as
literal garbage in command output. Running anything with coloured or progress
output — npm install, a coloured test run, docker build — can split an escape
sequence across a stream read, and when that happens the tool prints the sequence
as visible text (\x1b[31mRED instead of RED). The corrupted text also lands in
the transcript the model reads, so the agent can misread the command's output.

Chunk boundaries are arbitrary, so this shows up intermittently on exactly the
long, noisy commands where output clarity matters most.

Why This Change Was Made

The remote bash path already solved this. #103706 replaced the stateless
sanitizeBinaryOutput with a per-stream parser in
src/agents/sessions/bash-executor.ts:51, and the helper it added carries the
intent in its own doc comment:

/** Keep one ANSI parser per process stream so control sequences can span callbacks. */

The local exec runtime was not converted and still called the stateless helper
on every chunk. That helper is behaving as documented — it deliberately escapes a
pending CSI so a split final byte "cannot leak from the following chunk"
(shell-utils.ts:359) — but that contract only makes sense when no parser state
is carried between calls. This change gives the local path the same per-stream
parser the remote path uses.

stdout and stderr get separate parsers. They are independent streams, and a
shared parser would let one stream consume the other's pending sequence.

Scope is limited to wiring the existing helper into the two handlers; no new
sanitization behavior is introduced and the detectCursorKeyMode probe still runs
on the raw chunk before stripping, as before.

User Impact

exec output no longer shows stray escape codes when a sequence spans a chunk
boundary. Users see the text the command actually produced, and the model reads
the same clean text. Commands whose escape sequences never straddle a read
boundary behave exactly as before.

Evidence

Real-behavior proof — real child process, real pipe, real OS-level split.

A real node child writes start:, then ESC [, then sleeps 250 ms, then writes
31mRED ESC [0m:end. The sleep guarantees the pipe delivers the sequence in two
separate chunks. The real runExecProcess consumes it through the real
ProcessSupervisor — nothing mocked.

Before (this branch with the source change reverted):

status          : completed
raw output      : "start:\x1b[31mRED:end"

FAIL - escape sequence leaked into visible output

After:

status          : completed
raw output      : "start:RED:end"

PASS - split sequence consumed cleanly

Regression tests. Three cases in a new focused suite: ANSI and OSC sequences
split across stdout chunks (mirroring the sibling suite added by #103706), a
sequence split across stderr chunks, and a case where both streams leave a
sequence dangling simultaneously — which fails if the two streams share one
parser.

The tests are load-bearing. Reverting only the source change (tests untouched)
turns all three red, showing the exact leaked text:

× sanitizes ANSI and OSC sequences split across stdout chunks
  Expected: "ABCDEFGH"
  Received: "A\x1b]0;title\x07BC\x1b[31mDE\x9d0;title\x1b\FG\x9b31mH"

× sanitizes escape sequences split across stderr chunks
  Expected: "warn: red"
  Received: "warn: \x1b[31mred"

× keeps stdout and stderr parser state independent
  Expected: "outerrOUTERR"
  Received: "out\x1b[err\x1b[32mOUT31mERR"

Local checks. New suite 3 passed; bash-tools.exec-runtime.pty-fallback.test.ts
passed; src/scripts/test-projects.test.ts 85 passed. oxlint clean,
oxfmt --check clean, check-max-lines-ratchet clean.

Two failures in neighbouring suites on this Windows machine
(bash-executor.test.ts "stores truncated full output in an owner-only temp file",
and shell-utils.test.ts detectRuntimeShell reporting an empty platform-gated
suite) are pre-existing: they reproduce identically with this branch's source
change reverted.

AI-assisted.

The local exec runtime sanitized each stdout/stderr chunk with the
stateless `sanitizeBinaryOutput`, so an escape sequence straddling a read
boundary was escaped into visible text instead of being consumed. The
remote bash path already uses a per-stream parser via
`createStreamingBinaryOutputSanitizer` (openclaw#103706); this applies the same
treatment to the local path, with separate parsers for stdout and stderr
so neither stream can consume the other's pending sequence.
@openclaw-barnacle openclaw-barnacle Bot added agents Agent runtime and tooling size: S labels Jul 19, 2026
@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. P2 Normal backlog priority with limited blast radius. labels Jul 19, 2026
@clawsweeper

clawsweeper Bot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs maintainer review before merge. Reviewed July 19, 2026, 6:55 AM ET / 10:55 UTC.

Summary
The branch replaces stateless per-chunk ANSI sanitization in local exec with independent streaming sanitizers for stdout and stderr, and adds split-chunk regression coverage.

PR surface: Source +4, Tests +126. Total +130 across 2 files.

Reproducibility: no. direct current-main execution was performed in this read-only review, but the stateless per-callback sanitizer path and the supplied split-chunk harness provide a high-confidence source reproduction path.

Review metrics: none identified.

Merge readiness
Overall: 🐚 platinum hermit
Proof: 🐚 platinum hermit
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.

Next step before merge

  • [P2] No concrete automated repair is needed; the remaining action is ordinary maintainer review and completion of the in-progress current-head checks.

Security
Cleared: The diff only reuses an existing local sanitization helper and adds tests; it introduces no dependency, permission, secret, network, or supply-chain surface.

Review details

Best possible solution:

Merge the local exec wiring after the current-head CI/maintainer review confirms the focused parser-state fix remains cleanly applicable.

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

No direct current-main execution was performed in this read-only review, but the stateless per-callback sanitizer path and the supplied split-chunk harness provide a high-confidence source reproduction path.

Is this the best way to solve the issue?

Yes. Reusing the already-merged per-stream streaming-sanitizer pattern is the narrowest maintainable fix; separate stdout and stderr instances preserve stream isolation.

AGENTS.md: found and applied where relevant.

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

Label changes

Label justifications:

  • P2: The patch fixes intermittent corrupted agent exec output and transcript text, with a bounded agent-runtime blast radius.
  • rating: 🐚 platinum hermit: Overall readiness is 🐚 platinum hermit; proof is 🐚 platinum hermit and patch quality is 🐚 platinum hermit.
  • status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (live_output): The PR body includes an after-fix real child-process/pipe split with observed clean output, supplemented by focused regression coverage.
  • proof: sufficient: Contributor real behavior proof is sufficient. The PR body includes an after-fix real child-process/pipe split with observed clean output, supplemented by focused regression coverage.
Evidence reviewed

PR surface:

Source +4, Tests +126. Total +130 across 2 files.

View PR surface stats
Area Files Added Removed Net
Source 1 8 4 +4
Tests 1 126 0 +126
Docs 0 0 0 0
Config 0 0 0 0
Generated 0 0 0 0
Other 0 0 0 0
Total 2 134 4 +130

What I checked:

Likely related people:

  • jincheng-xydt: Their merged agent-runtime change established the per-stream incremental sanitizer pattern that this local exec repair follows. (role: introduced the sibling streaming-sanitizer behavior; confidence: high; commits: 5a81e9fa8115; files: src/agents/sessions/bash-executor.ts, src/agents/shell-utils.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 (1 earlier review cycle)
  • reviewed 2026-07-19T10:22:33.294Z sha 0bb3590 :: needs maintainer review before merge. :: none

@steipete
steipete merged commit 301657f into openclaw:main Jul 20, 2026
117 checks passed
@steipete

Copy link
Copy Markdown
Contributor

Merged via squash.

github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request Jul 20, 2026
…nclaw#111364)

The local exec runtime sanitized each stdout/stderr chunk with the
stateless `sanitizeBinaryOutput`, so an escape sequence straddling a read
boundary was escaped into visible text instead of being consumed. The
remote bash path already uses a per-stream parser via
`createStreamingBinaryOutputSanitizer` (openclaw#103706); this applies the same
treatment to the local path, with separate parsers for stdout and stderr
so neither stream can consume the other's pending sequence.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling 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: S status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants