Skip to content

fix(memory-host-sdk): handle stdout/stderr stream errors in runCliCommand#101402

Merged
steipete merged 6 commits into
openclaw:mainfrom
wings1029:fix/qmd-process-stream-error-handlers
Jul 7, 2026
Merged

fix(memory-host-sdk): handle stdout/stderr stream errors in runCliCommand#101402
steipete merged 6 commits into
openclaw:mainfrom
wings1029:fix/qmd-process-stream-error-handlers

Conversation

@wings1029

@wings1029 wings1029 commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Summary

Problem: runCliCommand() in packages/memory-host-sdk/src/host/qmd-process.ts spawns child processes with piped stdout/stderr but never registers error listeners on those streams. Per Node.js docs, a Readable stream that emits error with no listener throws an uncaught exception that crashes the process.

Solution: Add stream error handlers on child.stdout and child.stderr that kill the child process and reject the promise with a descriptive error. Follows the identical pattern already merged in docker.ts (#101031) and applied in agent-core (#101370).

What changed: qmd-process.tsrunCliCommand() (+15 lines): stream error guard. qmd-process.test.ts (+42 lines): two new test cases.

What did NOT change: checkQmdBinaryAvailability() — uses stdio: "ignore", no stream pipes to guard.

What Problem This Solves

Problem: When a child process spawned by runCliCommand() exits or is killed while pipes still have buffered data, the OS closes the pipe and the stream emits error: EPIPE. With no error listener, Node.js throws an uncaught exception.

Why This Change Was Made: The fix follows the same defensive pattern as #101031 (docker), #101370 (agent-core). The memory-host-sdk runs QMD CLI commands through this path — a pipe error here could crash the memory host.

User Impact: Normal operation unchanged. Prevents rare crash during QMD command execution.

Root Cause

Trigger condition: Child process killed (timeout/abort) while pipe data is still draining → EPIPE → no listener → uncaught exception.

Affected scope: packages/memory-host-sdk/src/host/qmd-process.tsrunCliCommand().

Linked context

Real behavior proof

Behavior addressed: Uncaught EPIPE on child stdout/stderr crashes the process; fix registers error listeners.

Real environment tested: Linux x86_64, Node.js 24

Exact steps or command run after this patch:

# Source verification
rg -c "onStreamError" packages/memory-host-sdk/src/host/qmd-process.ts
rg -c "child.stdout.on" packages/memory-host-sdk/src/host/qmd-process.ts
rg -c "child.stderr.on" packages/memory-host-sdk/src/host/qmd-process.ts
# Stream error proof — same mechanism as the fix
node --import tsx -e "
import { spawn } from \"node:child_process\";
const child = spawn(\"/bin/bash\", [\"-c\", \"echo before; sleep 0.05; echo after\"], {
  stdio: [\"ignore\", \"pipe\", \"pipe\"],
});
let errorCaught = false;
child.stdout.on(\"data\", () => {
  if (!errorCaught) { errorCaught = true; child.stdout.destroy(new Error(\"simulated EPIPE\")); }
});
child.stdout.on(\"error\", (err) => console.log(\"stdout error caught:\", err.message));
child.stderr.on(\"error\", (err) => console.log(\"stderr error caught:\", err.message));
child.on(\"close\", () => { console.log(\"NO uncaughtException\"); });
setTimeout(() => process.exit(1), 5000);
"

After-fix evidence:

onStreamError: 4 refs
child.stdout.on: 2 (data + error)
child.stderr.on: 2 (data + error)
stdout error caught: simulated EPIPE
NO uncaughtException
Pre-commit: Finished in 75ms on 2 files using 8 threads.

Observed result after the fix: Source has error handlers on both pipes. Stream error proof confirms the identical mechanism catches EPIPE without crashing.

What was not tested: Real EPIPE from a QMD child process — timing-dependent. Unit tests cover mock-based stream error scenarios using the existing createMockChild pattern.

Maintainer exact-head proof

The patched runCliCommand path and its real process-tree lifecycle test were run from immutable head 914a00cf7142aec7cd030913ae314dc1a8e6cf2f in a sanitized, public-network AWS Crabbox with no instance role, no Tailscale, and no hydrated credentials.

provider=aws lease=cbx_4d0b9f6a18a1 run=run_b2d1a469d603
pnpm test packages/memory-host-sdk/src/host/qmd-process.test.ts packages/memory-host-sdk/src/host/qmd-process.real.test.ts
qmd-process.test.ts: 23 passed
qmd-process.real.test.ts: 1 passed
[test] passed 2 Vitest shards
exitCode=0

This imports the patched memory-host SDK function, exercises stdout and stderr stream errors through its actual settlement and process-tree kill path, verifies the other stream remains guarded after settlement, and separately proves descendant cleanup with a real spawned process tree.

Tests and validation

Two new test cases in qmd-process.test.ts:

  1. rejects when stdout/stderr stream emits an error — param test for both streams
  2. keeps the other stream guarded after stdout error — late error must not throw

Risk checklist

  • This change is backwards compatible
  • This change has been tested with existing configurations
  • I have updated relevant documentation (N/A)
  • Breaking changes (if any) are documented in Summary (none)

Merge-risk: Low. Additive change only. Stream errors that previously crashed are now caught and reported.

Current review state

  • Self-review completed
  • At least one maintainer review
  • All CI checks passed

AI-assisted.

@wings1029
wings1029 force-pushed the fix/qmd-process-stream-error-handlers branch from 4c2893d to a5c46f6 Compare July 7, 2026 06:17
@clawsweeper

clawsweeper Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs maintainer review before merge. Reviewed July 7, 2026, 3:30 AM ET / 07:30 UTC.

Summary
The branch adds stdout/stderr error handling and regression coverage for runCliCommand in the memory-host SDK QMD process wrapper.

PR surface: Source +20, Tests +48. Total +68 across 2 files.

Reproducibility: yes. for the underlying source and stream-contract path: current main reads QMD stdout/stderr without error listeners, and a direct Node Readable error without a listener exits the process. I did not establish a deterministic live QMD EPIPE reproduction because the timing-dependent pipe failure is difficult to force reliably.

Review metrics: none identified.

Stored data model
Persistent data-model change detected: vector/embedding metadata: packages/memory-host-sdk/src/host/qmd-process.test.ts, vector/embedding metadata: packages/memory-host-sdk/src/host/qmd-process.ts. Confirm migration or upgrade compatibility proof before merge.

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

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

Next step before merge

  • [P2] No repair job is needed because the patch has no review findings and remaining work is normal CI and maintainer landing review.

Security
Cleared: The diff only adds child stream error listeners and tests, with no dependency, workflow, secret, permission, or packaging changes.

Review details

Best possible solution:

Land the focused stream-error guard after required CI completes, keeping the fix inside the QMD process wrapper and leaving release changelog updates to the release flow.

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

Yes for the underlying source and stream-contract path: current main reads QMD stdout/stderr without error listeners, and a direct Node Readable error without a listener exits the process. I did not establish a deterministic live QMD EPIPE reproduction because the timing-dependent pipe failure is difficult to force reliably.

Is this the best way to solve the issue?

Yes. Handling stdout/stderr errors in runCliCommand is the narrow owner-boundary fix; checkQmdBinaryAvailability uses stdio: "ignore", while sibling process wrappers handle output stream failures at their own capture boundary.

AGENTS.md: found and applied where relevant.

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

Label changes

Label changes:

  • add proof: sufficient: Contributor real behavior proof is sufficient. The PR body includes terminal Crabbox proof for the patched memory-host SDK path, and the only later commit removed a changelog entry, so the proof still covers current head.
  • add rating: 🦞 diamond lobster: Overall readiness is 🦞 diamond lobster; proof is 🦞 diamond lobster and patch quality is 🦞 diamond lobster.
  • add status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (terminal): The PR body includes terminal Crabbox proof for the patched memory-host SDK path, and the only later commit removed a changelog entry, so the proof still covers current head.

Label justifications:

  • P2: The PR fixes a rare memory-host subprocess crash path with limited blast radius and no config, API, or migration change.
  • rating: 🦞 diamond lobster: Overall readiness is 🦞 diamond lobster; proof is 🦞 diamond lobster and patch quality is 🦞 diamond lobster.
  • status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (terminal): The PR body includes terminal Crabbox proof for the patched memory-host SDK path, and the only later commit removed a changelog entry, so the proof still covers current head.
  • proof: sufficient: Contributor real behavior proof is sufficient. The PR body includes terminal Crabbox proof for the patched memory-host SDK path, and the only later commit removed a changelog entry, so the proof still covers current head.
Evidence reviewed

PR surface:

Source +20, Tests +48. Total +68 across 2 files.

View PR surface stats
Area Files Added Removed Net
Source 1 20 0 +20
Tests 1 48 0 +48
Docs 0 0 0 0
Config 0 0 0 0
Generated 0 0 0 0
Other 0 0 0 0
Total 2 68 0 +68

What I checked:

Likely related people:

  • vincentkoc: Live path history shows vincentkoc introduced the QMD availability/process wrapper work and made multiple later QMD process-tree and export-maintenance changes. (role: feature-history owner; confidence: high; commits: 8623c28f1d7d, 20534c57b7e8, 830691b2010b; files: packages/memory-host-sdk/src/host/qmd-process.ts, packages/memory-host-sdk/src/host/qmd-process.test.ts, packages/memory-host-sdk/src/engine-qmd.ts)
  • steipete: steipete is assigned to this PR and appears in recent QMD process timeout/output-cap and real-test cleanup history, including committer work on the abort-signal path. (role: recent area contributor and current assignee; confidence: high; commits: d92a0292a966, 9e1faf81abf1, 73dd758310e8; files: packages/memory-host-sdk/src/host/qmd-process.ts, packages/memory-host-sdk/src/host/qmd-process.test.ts, packages/memory-host-sdk/src/host/qmd-process.real.test.ts)
  • Alix-007: Alix-007 authored the abort-signal/process-tree change that introduced the current one-shot settlement structure this PR extends for stream errors. (role: adjacent behavior contributor; confidence: medium; commits: 78184ea7e42f; files: packages/memory-host-sdk/src/host/qmd-process.ts, packages/memory-host-sdk/src/host/qmd-process.test.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 (3 earlier review cycles)
  • reviewed 2026-07-07T06:23:35.932Z sha a5c46f6 :: needs real behavior proof before merge. :: none
  • reviewed 2026-07-07T06:30:44.214Z sha a5c46f6 :: needs real behavior proof before merge. :: none
  • reviewed 2026-07-07T07:03:56.531Z sha 81e6dcd :: needs real behavior proof before merge. :: none

@clawsweeper clawsweeper Bot added rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. P2 Normal backlog priority with limited blast radius. labels Jul 7, 2026
wings1029 and others added 2 commits July 7, 2026 14:53
…mand

Register error handlers on stdout and stderr streams in
runCliCommand() to prevent uncaught exceptions when a pipe
breaks (e.g. EPIPE after child process exit).

Without these listeners, Node.js throws an uncaught exception
that crashes the process.

Co-Authored-By: Claude <[email protected]>
Refactor the inline stream error handling into a reusable
listenForChildOutputErrors helper, matching the pattern
established in the merged openclaw#101370 (agent-core).

The helper registers error listeners on stdout/stderr via a
shared loop, keeping the pattern consistent across the codebase.

Co-Authored-By: Claude <[email protected]>
@wings1029
wings1029 force-pushed the fix/qmd-process-stream-error-handlers branch from a5c46f6 to 81e6dcd Compare July 7, 2026 06:54
@wings1029

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review — refactored to use reusable listenForChildOutputErrors helper matching the merged #101370 pattern.

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

@steipete steipete self-assigned this Jul 7, 2026
@clawsweeper clawsweeper Bot added proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🦞 diamond lobster Very strong PR readiness with only minor maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. and removed rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. labels Jul 7, 2026
@steipete

steipete commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Land-ready at exact head 3d4bda8c9c429c892d96e233bb0a091d12d9e0a6.

What changed:

  • Guard both QMD child stdout/stderr streams at the shared command-runner owner.
  • On capture failure, terminate the existing detached process tree, reject once with a stream-specific error, and preserve the original error as cause.
  • Keep both listeners installed after settlement so a late second stream error cannot become an uncaught exception.
  • Refactored away the one-use helper from the intermediate contributor revision; production code is smaller than that revision.

Proof:

  • pnpm test packages/memory-host-sdk/src/host/qmd-process.test.ts packages/memory-host-sdk/src/host/qmd-process.real.test.ts — 24 tests passed on sanitized direct AWS Crabbox cbx_778d0d678f2e (coral-prawn), run run_8b8af198d5f2.
  • Source-blind real-child contract on the same exact SHA — normal stdout/stderr preserved; unique stdout pipe failure rejected with its original cause; captured child PID exited; no uncaught exception. Same provider/lease, run run_cb849e2d8d1a.
  • Fresh structured autoreview after the complete final bundle: clean, no accepted/actionable findings.
  • Exact-head hosted CI: run 28848955783, successful.
  • OPENCLAW_TESTBOX=1 scripts/pr prepare-run 101402: passed with hosted exact-head gates; no bypasses.

Known proof gaps: none for this bounded subprocess failure path.

@steipete
steipete merged commit 5c4b639 into openclaw:main Jul 7, 2026
128 of 133 checks passed
@steipete

steipete commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Merged via squash.

github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request Jul 7, 2026
…mand (openclaw#101402)

* fix(memory-host-sdk): handle stdout/stderr stream errors in runCliCommand

Register error handlers on stdout and stderr streams in
runCliCommand() to prevent uncaught exceptions when a pipe
breaks (e.g. EPIPE after child process exit).

Without these listeners, Node.js throws an uncaught exception
that crashes the process.

Co-Authored-By: Claude <[email protected]>

* fix(memory-host-sdk): extract listenForChildOutputErrors helper

Refactor the inline stream error handling into a reusable
listenForChildOutputErrors helper, matching the pattern
established in the merged openclaw#101370 (agent-core).

The helper registers error listeners on stdout/stderr via a
shared loop, keeping the pattern consistent across the codebase.

Co-Authored-By: Claude <[email protected]>

* fix(memory-host-sdk): harden qmd stream failures

* fix(memory-host-sdk): harden qmd stream failures

* docs(changelog): note qmd stream hardening

* chore: keep release changelog out of contributor PR

---------

Co-authored-by: Claude <[email protected]>
Co-authored-by: Peter Steinberger <[email protected]>
@wings1029

Copy link
Copy Markdown
Contributor Author

Thank you @steipete for the thorough review and squash merge! 🙏

sheyanmin pushed a commit to sheyanmin/openclaw that referenced this pull request Jul 8, 2026
…mand (openclaw#101402)

* fix(memory-host-sdk): handle stdout/stderr stream errors in runCliCommand

Register error handlers on stdout and stderr streams in
runCliCommand() to prevent uncaught exceptions when a pipe
breaks (e.g. EPIPE after child process exit).

Without these listeners, Node.js throws an uncaught exception
that crashes the process.

Co-Authored-By: Claude <[email protected]>

* fix(memory-host-sdk): extract listenForChildOutputErrors helper

Refactor the inline stream error handling into a reusable
listenForChildOutputErrors helper, matching the pattern
established in the merged openclaw#101370 (agent-core).

The helper registers error listeners on stdout/stderr via a
shared loop, keeping the pattern consistent across the codebase.

Co-Authored-By: Claude <[email protected]>

* fix(memory-host-sdk): harden qmd stream failures

* fix(memory-host-sdk): harden qmd stream failures

* docs(changelog): note qmd stream hardening

* chore: keep release changelog out of contributor PR

---------

Co-authored-by: Claude <[email protected]>
Co-authored-by: Peter Steinberger <[email protected]>
giodl73-repo pushed a commit to giodl73-repo/openclaw that referenced this pull request Jul 8, 2026
…mand (openclaw#101402)

* fix(memory-host-sdk): handle stdout/stderr stream errors in runCliCommand

Register error handlers on stdout and stderr streams in
runCliCommand() to prevent uncaught exceptions when a pipe
breaks (e.g. EPIPE after child process exit).

Without these listeners, Node.js throws an uncaught exception
that crashes the process.

Co-Authored-By: Claude <[email protected]>

* fix(memory-host-sdk): extract listenForChildOutputErrors helper

Refactor the inline stream error handling into a reusable
listenForChildOutputErrors helper, matching the pattern
established in the merged openclaw#101370 (agent-core).

The helper registers error listeners on stdout/stderr via a
shared loop, keeping the pattern consistent across the codebase.

Co-Authored-By: Claude <[email protected]>

* fix(memory-host-sdk): harden qmd stream failures

* fix(memory-host-sdk): harden qmd stream failures

* docs(changelog): note qmd stream hardening

* chore: keep release changelog out of contributor PR

---------

Co-authored-by: Claude <[email protected]>
Co-authored-by: Peter Steinberger <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

P2 Normal backlog priority with limited blast radius. proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🦞 diamond lobster Very strong PR readiness with only minor 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