Skip to content

fix(flows): ignore EPIPE on shell action stdin - #529

Merged
steipete merged 3 commits into
openclaw:mainfrom
SebTardif:fix/f003-flow-shell-stdin-epipe
Aug 31, 2026
Merged

fix(flows): ignore EPIPE on shell action stdin#529
steipete merged 3 commits into
openclaw:mainfrom
SebTardif:fix/f003-flow-shell-stdin-epipe

Conversation

@SebTardif

Copy link
Copy Markdown
Contributor

What Problem This Solves

Fixes an issue where users running defineFlow / shell() / acpx flow with ShellActionExecution.stdin would lose the whole CLI when the child exited without reading that stdin. Node treats an error event on the child stdin pipe as fatal when no listener is attached, so a large write (1 MiB) to a child that does setImmediate(() => process.exit(0)) raised uncaught EPIPE and killed the host.

Why This Change Was Made

runShellAction already pipes stdin and settles the action from the child exit event. It did not attach an error listener on child.stdin, and it called write / end even after the stream had already ended. This change ignores pipe-death codes on stdin (EPIPE, EIO, ECONNRESET, ERR_STREAM_DESTROYED) and skips write / end when the stream is no longer writable. The existing exit handler still records status.

This is not the CLI process.stdout path from closed #441. That helper can throw or exit. Child stdin must not. Open #507 covers terminal stdout/stderr listeners. Merged #501 timed out hung process-list helpers. Merged #498 covers finalize persist errors. None of those touch flow shell stdin.

The missing listener dates to #179 (697ee1f, 2026-03-26, "feat: add experimental acpx flows runtime and examples"), 156 days on main.

User Impact

A flow that feeds stdin into a short-lived shell child keeps the ACP host alive. The shell action still completes from the child's exit status. Operators no longer lose the CLI because leftover stdin raised EPIPE.

Evidence

terminal output from live node against compiled public defineFlow / shell() / FlowRunner (dist/flows.js).

Same script: 1 MiB stdin to process.execPath -e "setImmediate(() => process.exit(0))".

Before, on unpatched src/flows/executors/shell.ts compiled to dist, the host throws and exits 1:

$ node /tmp/acpx-F003-proof.mjs
node:events:505
    throw er; // Unhandled 'error' event
    ^

Error: write EPIPE
    at WriteWrap.onWriteComplete [as oncomplete] (node:internal/stream_base_commons:87:19)
Emitted 'error' event on Socket instance at:
    at emitErrorNT (node:internal/streams/destroy:170:8)
    at emitErrorCloseNT (node:internal/streams/destroy:129:3)
    at process.processTicksAndRejections (node:internal/process/task_queues:90:21) {
  errno: -32,
  code: 'EPIPE',
  syscall: 'write'
}

Node.js v26.7.0

After, on the patched compiled runShellAction path, the same public flow stays in-process and exits 0:

$ node /tmp/acpx-F003-proof.mjs
{
  "hostAlive": true,
  "hostPid": 90462,
  "status": "completed",
  "error": null,
  "exitCode": 0,
  "signal": null,
  "stdinBytes": 1048576
}

Real behavior proof

  • Behavior or issue addressed: Flow shell() stdin write to a child that exits without reading used to raise uncaught EPIPE and kill the host process.

  • Real environment tested: macOS, Node v26.7.0, acpx compiled from this branch at /tmp/oc-pr-acpx-F003 (dist/flows.js).

  • Exact steps or command run after this patch:

    node /tmp/acpx-F003-proof.mjs

    The script imports defineFlow, shell, and FlowRunner from compiled dist/flows.js, then runs a one-node flow whose exec returns command: process.execPath, args: ["-e", "setImmediate(() => process.exit(0))"], and stdin of 1 MiB.

  • Evidence after fix: terminal output from the patched compiled public API (shown above). The host prints hostAlive: true, status: "completed", exitCode: 0, stdinBytes: 1048576, and the Node process exits 0 with no EPIPE on stderr.

  • Observed result after fix: The same 1 MiB stdin write that previously emitted Unhandled 'error' event / write EPIPE now completes inside FlowRunner. The host PID stays alive through the flow result print.

  • What was not tested: Windows cmd.exe spawn, and a child that reads only part of stdin before exiting.

Related: #179 (origin), #507 (terminal stdout/stderr, different file), #501, #498, #441. Same missing-listener class in openclaw/openclaw #102047 and #100410.

Attach an error listener on the flow shell child stdin pipe and skip
writes after the stream has ended. A large stdin to a child that
exits without reading used to raise uncaught EPIPE and kill the host.

Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
@SebTardif
SebTardif requested a review from a team as a code owner August 29, 2026 18:18
@clawsweeper

clawsweeper Bot commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

🦞👀
ClawSweeper picked this up.

Pull request received. I will update this pull request when review starts.

@clawsweeper clawsweeper Bot added merge-risk: 🚨 other 🚨 Merging this PR has meaningful risk outside the owned taxonomy. P2 Normal priority bug or improvement with limited blast radius. proof: sufficient Contributor real behavior proof is sufficient. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. labels Aug 29, 2026
@clawsweeper

clawsweeper Bot commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs maintainer review before merge. Reviewed August 31, 2026, 5:50 AM ET / 09:50 UTC.

ClawSweeper review

What this changes

The branch handles early child-stdin closure in Flow shell actions, adds a host-process regression test, and records the user-facing fix in the changelog.

Merge readiness

⚠️ Ready for maintainer review - 1 item remains

Keep open: the final head contains a focused fix for a real Flow shell-action host crash, resolves the prior review findings, and has maintainer-prepared landing evidence.

Priority: P2
Reviewed head: f921482a8aeefbfe3f55867ee8128d200ee928ad

Review scores

Measure Result What it means
Overall readiness 🐚 platinum hermit (4/6) Strong before/after compiled-flow proof supports a focused, regression-tested repair with no remaining actionable finding.
Proof confidence 🦞 diamond lobster (5/6) Sufficient (terminal): The changed production owner is the shell executor’s child-stdin write path; supplied terminal evidence exercises the compiled public FlowRunner and shell() entrypoint with a 1 MiB write to an early-exiting Node child, showing current-main EPIPE failure and after-fix completed exit-0 recovery.
Patch quality 🐚 platinum hermit (4/6) No actionable review findings were identified.

Verification

Check Result Evidence
Real behavior Verified Sufficient (terminal): The changed production owner is the shell executor’s child-stdin write path; supplied terminal evidence exercises the compiled public FlowRunner and shell() entrypoint with a 1 MiB write to an early-exiting Node child, showing current-main EPIPE failure and after-fix completed exit-0 recovery.
Evidence reviewed 6 items Focused production fix: The introduced helper installs the child-stdin error listener before writing and only writes or ends a still-writable stream; the existing child exit handler remains the action-result authority.
Regression coverage: The new test runs runShellAction in a separate Node host with 1 MiB stdin and asserts a zero exit with no EPIPE or unhandled-error output.
Prior CI-coverage finding resolved: The repository test command executes every compiled test file before coverage collection, so the new flows-shell regression is included in the CI Test job.
Findings None None.
Security None None.

How this fits together

Flow shell actions launch local child processes from a workflow definition and pass optional stdin to them. The executor captures the child result and returns it to the FlowRunner as the step outcome.

flowchart LR
  A[Flow definition] --> B[Shell action]
  B --> C[Shell executor]
  C --> D[Child process]
  C --> E[Child stdin pipe]
  E --> F{Input closes early?}
  F --> G[Child exit status]
  G --> H[Flow step result]
Loading

Before merge

  • Complete next step (P2) - No repair lane is needed: the prior findings are resolved on the exact head, leaving normal required-check completion and merge handling.
Agent review details

Security

None.

Review metrics

Metric Value Why it matters
Patch scope 1 production file, 1 test file, 1 changelog entry The 69 additions and 5 deletions remain focused on one shell-action failure mode.
Production versus test delta production +18/-5, tests +50, docs +1 The small runtime change is accompanied by an isolated process-level regression test.

Technical review

Best possible solution:

Merge the focused stdin-pipe handling and regression coverage while retaining child exit status as the shell action’s outcome authority.

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

Yes — the supplied built-CLI before/after proof exercises a FlowRunner shell action with 1 MiB stdin and an early-exiting child, reproducing the current-main EPIPE crash and the fixed result.

Is this the best way to solve the issue?

Yes — attaching the listener before writes and preserving the established child-exit result path is the narrowest fix; the final branch also covers the host-level failure mode.

AGENTS.md: found and applied where relevant.

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

Labels

Label changes:

  • add rating: 🐚 platinum hermit: Overall readiness is 🐚 platinum hermit; proof is 🦞 diamond lobster and patch quality is 🐚 platinum hermit.
  • add status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (terminal): The changed production owner is the shell executor’s child-stdin write path; supplied terminal evidence exercises the compiled public FlowRunner and shell() entrypoint with a 1 MiB write to an early-exiting Node child, showing current-main EPIPE failure and after-fix completed exit-0 recovery.
  • remove status: ⏳ waiting on author: Current PR status label is status: 👀 ready for maintainer look.
  • remove rating: 🦐 gold shrimp: Current PR rating is rating: 🐚 platinum hermit, so this older rating label is no longer current.
  • remove merge-risk: 🚨 other: Current PR review selected no merge-risk labels.

Label justifications:

  • P2: This fixes a real workflow host-crash path with limited scope and a focused repair.
  • 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 changed production owner is the shell executor’s child-stdin write path; supplied terminal evidence exercises the compiled public FlowRunner and shell() entrypoint with a 1 MiB write to an early-exiting Node child, showing current-main EPIPE failure and after-fix completed exit-0 recovery.
  • proof: sufficient: Contributor real behavior proof is sufficient. The changed production owner is the shell executor’s child-stdin write path; supplied terminal evidence exercises the compiled public FlowRunner and shell() entrypoint with a 1 MiB write to an early-exiting Node child, showing current-main EPIPE failure and after-fix completed exit-0 recovery.

Evidence

What I checked:

  • Focused production fix: The introduced helper installs the child-stdin error listener before writing and only writes or ends a still-writable stream; the existing child exit handler remains the action-result authority. (src/flows/executors/shell.ts:5, f921482a8aee)
  • Regression coverage: The new test runs runShellAction in a separate Node host with 1 MiB stdin and asserts a zero exit with no EPIPE or unhandled-error output. (test/flows-shell.test.ts:108, f921482a8aee)
  • Prior CI-coverage finding resolved: The repository test command executes every compiled test file before coverage collection, so the new flows-shell regression is included in the CI Test job. (package.json:69, f921482a8aee)
  • Real behavior proof and landing preparation: The maintainer triage comment records a built-CLI before/after run: current main exits 1 with unhandled EPIPE, while the prepared and reconciled branch exits 0 with a completed flow and child exit 0. (f921482a8aee)
  • Feature history: History identifies the merged flows introduction as the earliest relevant implementation lineage; the current PR is not already present on current main. (src/flows/executors/shell.ts:59, 697ee1f2a659)
  • Release and current-main status: The current main base is after v0.13.2, and no release tag contains that base commit; this unmerged fix is neither on current main nor in the latest release. (ea9a6c6859db)

Likely related people:

  • steipete: Suggested for follow-up; no historical authorship or introduction is verified. (role: unverified routing candidate; confidence: low)
  • osolmaz: Suggested for follow-up; no historical authorship or introduction is verified. (role: unverified routing candidate; confidence: low)

Rating scale

Score Internal tier Crab rank Meaning
6/6 S 🦀 challenger crab Exceptional readiness
5/6 A 🦞 diamond lobster Very strong readiness
4/6 B 🐚 platinum hermit Good normal PR; ordinary maintainer review
3/6 C 🦐 gold shrimp Useful, but confidence is limited
2/6 D 🦪 silver shellfish Proof or implementation needs work
1/6 F 🧂 unranked krab Not merge-ready
N/A NA 🌊 off-meta tidepool Rating does not apply

Overall follows the weaker of proof and patch quality.
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.

Workflow

  • 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.

History

Review history (1 earlier review cycle)
  • reviewed 2026-08-29T18:21:29.938Z sha fea44f1 :: needs changes before merge. :: [P1] Propagate stdin errors outside the pipe-death allowlist | [P2] Run the new EPIPE regression in the CI test command

@steipete

Copy link
Copy Markdown
Contributor

Maintainer triage: LAND recommended using the prepared branch triage/pr529-20260831, head 039fc2cf9dd203584ad6335bef5b8197c331679d. The original contributor commit is retained. The follow-up makes the existing child-exit ownership explicit, changes the regression harness to collect output on close, and adds changelog credit. The contributor branch is untouched; nothing was merged.

The built CLI reproduces the actual failure on current main and succeeds with the prepared patch:

node epipe-proof.mjs <main-checkout>
exit=1; stderr="Unhandled 'error' event ... Error: write EPIPE"
node epipe-proof.mjs <prepared-checkout>
exit=0; status=completed; outputs.child.childExit=0

The fixture runs a real flow shell action with 1 MiB stdin and a Node child that exits successfully without reading it. Separate source-blind CLI checks passed early exit 0, exact delivery of 1,441,792 Unicode bytes, empty input, and early exit 7. The latter returns a structured flow failure; none crashes with EPIPE.

Validation: eight focused shell tests; clean Codex autoreview of the final branch; full pnpm run check and pnpm run check:docs on AWS Crabbox cbx_f07d2cfa8b97, Node 24.18.1 / pnpm 10.34.5. Results: 966 tests passed, 140 coverage tests passed, 95.06% lines/statements, 88.81% branches, 96.63% functions; docs passed. No thresholds were changed.

Make child-exit ownership explicit and wait for stdio closure in the
subprocess regression harness. Add the user-facing changelog credit.

Co-authored-by: Sebastien Tardif <sebtardif@ncf.ca>
@steipete steipete self-assigned this Aug 31, 2026
Keep the prepared stdin behavior and regression intact, integrate the approved tooling and heartbeat changes, and append the credited fix to Unreleased.

Co-authored-by: Sebastien Tardif <sebtardif@ncf.ca>
@steipete

Copy link
Copy Markdown
Contributor

Applied the approved preparation to this contributor branch as new commits, retaining Sebastien’s original commit and co-author credit. Integrated current main without rewriting history; the only reconciliation conflict was the changelog, where the stdin fix is appended under Unreleased with thanks to @SebTardif.

Final head: f921482a8aeefbfe3f55867ee8128d200ee928ad.

The repeated built-CLI proof runs a real shell action with 1 MiB stdin and a child that exits before reading it: CLI exit 0, flow completed, child exit 0, empty stderr, no EPIPE. Original before/after evidence.

Validation: fresh reconciliation autoreview clean; pnpm run check passed on AWS Crabbox cbx_4f37d564a539 / run run_7678a0b1bb99 with 968 tests and 142 coverage tests. Coverage: 95.06% lines/statements, 88.81% branches, 96.63% functions for the configured target. pnpm run check:docs passed in the local Git worktree. An initial remote setup attempt hit an npm global-install permission error before checks; the successful retry used ephemeral pnpm 11.24.0. Exact-head CI is running before merge.

@clawsweeper clawsweeper Bot added 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. and removed status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. merge-risk: 🚨 other 🚨 Merging this PR has meaningful risk outside the owned taxonomy. labels Aug 31, 2026
@steipete
steipete merged commit 9b6e8ea into openclaw:main Aug 31, 2026
14 checks passed
@steipete

Copy link
Copy Markdown
Contributor

Landed as 9b6e8ea after all exact-head CI jobs passed.

The landing validation includes the repeated real EPIPE proof, full checks, documentation checks, and clean Codex review. The Unreleased changelog includes contributor thanks. Thanks @SebTardif for fixing this host-crash path!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

P2 Normal priority bug or improvement with limited blast radius. proof: sufficient Contributor real behavior proof is sufficient. 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.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants