fix(js-sdk): stop CommandHandle.disconnect() leaking the output subscription#1474
fix(js-sdk): stop CommandHandle.disconnect() leaking the output subscription#1474mishushakov wants to merge 4 commits into
Conversation
…ription disconnect() now cooperatively ends event handling and resolves only after it has fully stopped, so onStdout/onStderr/onPty are guaranteed not to fire for output that arrives after the call resolves. An in-flight callback is abandoned rather than awaited, preventing a deadlock when disconnect() is awaited from inside a callback. Adds JS regression tests and Python tests verifying the async/sync handles are already correct. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
🦋 Changeset detectedLatest commit: a8b072c The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
PR SummaryLow Risk Overview
Python async and sync command handles get the same end-event ordering fix. Regression tests cover late output after disconnect, prompt disconnect on idle streams, and result recording after disconnect. Reviewed by Cursor Bugbot for commit a8b072c. Bugbot is set up for automated code reviews on this repo. Configure here. |
Package ArtifactsBuilt from b4fc3bb. Download artifacts from this workflow run. JS SDK ( npm install ./e2b-2.30.6-mishushakov-verify-bug-repro.0.tgzCLI ( npm install ./e2b-cli-2.12.3-mishushakov-verify-bug-repro.0.tgzPython SDK ( pip install ./e2b-2.29.5+mishushakov.verify.bug.repro-py3-none-any.whl |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 168e31e7cc
ℹ️ About Codex in GitHub
Codex has been enabled to automatically 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 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
Cast the controllable events fixture to the expected AsyncGenerator parameter type; the stand-in is intentionally not a real async generator so its aclose() does not unblock a suspended read, keeping the disconnect test discriminating. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…the handler Addresses review feedback: awaiting the event handler in disconnect() could hang indefinitely for an idle command whose stream produces no further output and whose transport abort does not unblock the pending read. The cooperative disconnected flag, checked before every dispatch, already guarantees no callback fires for post-disconnect output, so disconnect() no longer awaits the handler. Adds an idle-stream regression test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 2c8a348. Configure here.
A disconnected JS consumer breaks out of handleEvents on the first chunk yielded by iterateEvents. When an end event carried trailing decoder bytes, the flush was yielded before this.result was assigned, so the break aborted the generator and wait() failed with "Process exited without a result" even though the process had exited. Record the result before yielding the flushed chunks. Apply the equivalent reorder to the Python sync and async handles for parity. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Description
This PR fixes two related issues in the command handle's event handling.
1. JS
CommandHandle.disconnect()leaked the output subscriptiondisconnect()was fire-and-forget — it only triggered the transport abort and relied entirely on HTTP/2 abort propagation to stop events, which is unreliable under keepalive:onStdout/onStderr/onPtycould keep firing for output produced afterdisconnect()returned.disconnect()now sets a cooperativedisconnectedflag and aborts the transport. The flag is checked before every callback dispatch in the event loop, so oncedisconnect()returns no callback fires for output that arrives (or was buffered) after the call — even if the underlying abort hasn't torn the stream down yet. It does not wait for the event handler to drain, so it returns promptly even for an idle command (e.g.sleep) whose stream produces no further output, never blocks on an in-flight callback, and does not deadlock when awaited from inside a callback.The async Python SDK was already correct here (
disconnect()cancels the event-handling task), and the sync Python SDK has no background subscription (events are consumed only while the caller iterates). The added Python tests confirm both.2. Exit code was lost when a disconnected consumer stopped on a flushed
end-event chunkWhen the
endevent flushes trailing decoder bytes (an incomplete multibyte sequence → replacement character) and the consumer stops iterating on the first flushed chunk, the generator was aborted before the result was assigned, sowait()failed as if the process never produced a result. Theendhandler now records the result before yielding the flushed chunks, across the JS, async Python, and sync Python SDKs.Usage
🤖 Generated with Claude Code