Skip to content

fix(js-sdk): stop CommandHandle.disconnect() leaking the output subscription#1474

Merged
mishushakov merged 5 commits into
mainfrom
mishushakov/verify-bug-repro
Jun 24, 2026
Merged

fix(js-sdk): stop CommandHandle.disconnect() leaking the output subscription#1474
mishushakov merged 5 commits into
mainfrom
mishushakov/verify-bug-repro

Conversation

@mishushakov

@mishushakov mishushakov commented Jun 23, 2026

Copy link
Copy Markdown
Member

Description

This PR fixes two related issues in the command handle's event handling.

1. JS CommandHandle.disconnect() leaked the output subscription

disconnect() 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/onPty could keep firing for output produced after disconnect() returned.

disconnect() now sets a cooperative disconnected flag and aborts the transport. The flag is checked before every callback dispatch in the event loop, so once disconnect() 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 chunk

When the end event 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, so wait() failed as if the process never produced a result. The end handler now records the result before yielding the flushed chunks, across the JS, async Python, and sync Python SDKs.

Usage

const handle = await sandbox.commands.run(daemon, { background: true, stdin: true, onStdout })
await sandbox.commands.sendStdin(handle.pid, 'turn1\n')
await handle.disconnect() // resolves promptly; onStdout will not fire again
await sandbox.commands.sendStdin(handle.pid, 'turn2\n') // turn2 output never reaches onStdout

🤖 Generated with Claude Code

…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-bot

changeset-bot Bot commented Jun 23, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: f686b3f

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
e2b Patch
@e2b/python-sdk Patch

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

@cla-bot cla-bot Bot added the cla-signed label Jun 23, 2026
@cursor

cursor Bot commented Jun 23, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Changes command stream lifecycle and when exit codes are visible; behavior is covered by new tests but affects long-running background commands.

Overview
In the JS SDK, CommandHandle.disconnect() could leave onStdout / onStderr / onPty firing after it returned when the transport abort did not stop the stream quickly. Disconnect now sets a flag that is checked before each callback dispatch so late output is not delivered, and it still returns without waiting on an idle or in-flight handler.

In the JS, async Python, and sync Python SDKs, handling the command end event used to yield flushed trailing decoder bytes before storing CommandResult. If iteration stopped on the first flushed chunk (including after disconnect), wait() could fail even though the process had exited. The result is now recorded before those trailing chunks are yielded.

Reviewed by Cursor Bugbot for commit f686b3f. Bugbot is set up for automated code reviews on this repo. Configure here.

@github-actions

github-actions Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Package Artifacts

Built from 3685c65. Download artifacts from this workflow run.

JS SDK (e2b@2.30.6-mishushakov-verify-bug-repro.0):

npm install ./e2b-2.30.6-mishushakov-verify-bug-repro.0.tgz

CLI (@e2b/cli@2.12.3-mishushakov-verify-bug-repro.0):

npm install ./e2b-cli-2.12.3-mishushakov-verify-bug-repro.0.tgz

Python SDK (e2b==2.29.5+mishushakov-verify-bug-repro):

pip install ./e2b-2.29.5+mishushakov.verify.bug.repro-py3-none-any.whl

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread packages/js-sdk/src/sandbox/commands/commandHandle.ts Outdated
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>
Comment thread packages/js-sdk/src/sandbox/commands/commandHandle.ts Outdated
…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>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

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

Comment thread packages/js-sdk/src/sandbox/commands/commandHandle.ts
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>
claude[bot]

This comment was marked as outdated.

@mishushakov mishushakov requested a review from huv1k June 23, 2026 12:55

@matthewlouisbrockman matthewlouisbrockman left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should update changeset for the python before merging

…g fix

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@mishushakov mishushakov enabled auto-merge (squash) June 24, 2026 18:55
@mishushakov mishushakov merged commit 2a98cce into main Jun 24, 2026
26 of 29 checks passed
@mishushakov mishushakov deleted the mishushakov/verify-bug-repro branch June 24, 2026 19:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants