fix(coding-agent): contain bash spill stream errors - #1138
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 893ad173d4
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 74f6fd56b0
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
code-yeongyu
left a comment
There was a problem hiding this comment.
Verdict
REQUEST_CHANGES. The early spill-error listener fixes the reported process-level crash, but both close helpers still have a real error-masking path: a terminal write/close failure can arrive after finish, after the helper has already resolved and cleared the stream. That can return success with an incomplete fullOutputPath.
Blockers
packages/coding-agent/src/core/bash-executor.ts:105-116andpackages/coding-agent/src/core/tools/output-accumulator.ts:152-163—closeTempFileStream()/closeTempFile()resolve onfinish, while NodeWriteStreamcan emit a finalfs.closeerror afterfinishand before/withclose. I reproduced the ordering locally with a customfs.close:finish -> error:EIO -> close. The new persistent listener records that late error only after the promise has resolved andtempFileStreamhas been cleared, so the caller can return success and advertise a truncated or missing spill file. The same race also bypasses the promisedAggregateErrorwhen output finalization fails and spill cleanup fails late. Required change: make the close boundary await terminal stream completion (including lateclose/error), reconcile the captured first error before resolving, and remove/settle the early listener only after that terminal boundary; preserve the first storage error and aggregate it with a finalization error when both occur. Add deterministic regression coverage for thefinish -> error -> closeordering in both spill paths.
Non-blocking notes
- Listener lifecycle / duplicate handling: the early
on("error")listener is installed before writes and??=preserves the first failure. The close-timeonce("error")listener sees the same event and Promise rejection is idempotent, so there is no second rejection; its persistent sibling still needs the terminal lifecycle fix above. - First-failure preservation and abort semantics: failures emitted before or during the currently awaited close are preserved, and moving successful finalization outside the command-execution
catchcorrectly prevents a close failure from becoming a successful cancelled result. Genuine operation aborts still return the existing cancelled shape. The late-close blocker applies to both normal and abort finalization. - AggregateError: the message
Bash output finalization and spill cleanup failedis actionable and the[error, closeError]ordering is sensible when cleanup rejects beforefinish; the late terminal-error race above is the missing case. output-accumulator.tsmirrors the bash path correctly for early errors, but therefore shares the same late-close defect.- Tests: from a fresh
/tmpclone,npm ci --ignore-scriptssucceeded andcd packages/coding-agent && npm test -- test/suite/regressions/bash-spill-storage-error.test.tspassed (4/4). Temporarily removing both new production listeners made the suite RED (3 failures: both bash cases and the accumulator case); the mock emits its error on the imported production stream path, so this is not a mock-only assertion. The suite does not yet cover the late terminal-close ordering required above. The dynamicnode:streamimport inside the Vitest mock also conflicts with the repository's normal module-scope import convention, but is non-blocking. - CI:
Terminal tools (macos-latest)failed only in@earendil-works/pi-pty'stest/native-wait-threadpool.test.tswithnative wait did not report cancellation. This PR changes nopackages/ptyorcrates/senpi-ptyfiles, and the latest main CI run passed the same macOS PTY test (58/58), so this is clearly unrelated/flaky rather than a PR blocker. - Ledger/release metadata:
scripts/check-pr-changelog.mjs --base 4ec1acf1df269cea1b083599360394b7b71b1375passed with both changed production paths covered;audit-changes-md.mjsreported 247/247 covered. The two tracker blocks are dated and contain the four canonical headings, and the package CHANGELOG entry is under[Unreleased]. - Bot feedback: the connector's close-after-
finishsuggestion is valid and is the blocker above; its earlier abort-race suggestion is addressed by the finalization refactor; the module-scope dynamic-import suggestion is a style/policy note, not a correctness blocker.
code-yeongyu
left a comment
There was a problem hiding this comment.
Verdict
APPROVE. The round-1 blocker is genuinely resolved in both close helpers: each now waits for the WriteStream's terminal close event, records the first error through the close boundary, and settles/removes the temporary listeners exactly once. A finish -> error -> close mutation fails both new late-failure tests, while the restored implementation passes them.
Blockers
none
Non-blocking notes
- Fresh filtered clone and
npm cicompleted successfully. The new-commit diff is confined topackages/coding-agent/src/core/bash-executor.ts,packages/coding-agent/src/core/tools/output-accumulator.ts,packages/coding-agent/test/suite/regressions/bash-spill-storage-error.test.ts, the two requestedchanges.mdledgers, andpackages/coding-agent/CHANGELOG.md. - The dedicated regression file passes 6/6. The related suites pass 80 tests; the 2 Windows-only tests are skipped on this macOS host. Language-server diagnostics are clean for both production files and the regression file.
- Mutation check: restoring
finishas the close boundary while retaining the new listeners made both late-failure tests fail with a prematurely resolved promise; restoring the PR implementation made all 6 regression tests pass. - Adversarial checks passed: normal spill output remains truncated and its
fullOutputPathcontains the complete output; the abort spill path remains cancelled with a valid complete file; the OutputAccumulator spill file is complete after close; and finalization plus cleanup failures remain anAggregateErrorwith the original finalization error first. node scripts/audit-changes-md.mjsexited 0: 247/247 production paths covered, 0 uncovered.
|
Extended review continued after this PR merged. The verified lifecycle follow-up is #1142 at head It adds terminal-close settlement, causal dual-error preservation, callback-safe spill finalization, failure-only artifact cleanup, and deterministic Node/Bun + full-suite evidence. Final mass-ulw behavior, quality, evidence, and Architect audits all passed unconditionally. |
Summary
errorlisteners before the first write in both bash output pathsexecuteBashWithOperationsandOutputAccumulatorRoot cause
When large bash output crossed the in-memory limit, Senpi opened a
/tmpWriteStreamand started writing before installing its onlyerrorlistener. An earlyEDQUOT/ENOSPCevent therefore reached the process-leveluncaughtExceptionhandler and terminated the interactive session.Verification
npm run checknpm run buildenv -u KIMI_API_KEY CI=1 npm test(full hermetic workspace suite)SPILL_FAILURE=ENOTDIR:open, thenPROCESS_SURVIVED=1The first non-hermetic full-suite attempt activated legacy live Kimi E2E tests from ambient
KIMI_API_KEYand received account-level 403 usage-limit responses. Re-running with that opt-in credential removed completed cleanly.Summary by cubic
Fixes bash output spill files so an early
EDQUOT/ENOSPCstream error fails only the tool call instead of terminating the interactive session throughuncaughtException.Bug Fixes
errorlisteners before the first write in both bash output paths.closeevent so late filesystem failures afterfinish(and cancellation racing command settlement) still reject through the awaited close boundary.EDQUOTregressions forexecuteBashWithOperationsandOutputAccumulator.Written for commit 7b47dba. Summary will update on new commits.