2a98cce8c7
## 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
```js
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](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>