Conversation
When a command output stream ends on the client side without an end event,
the SDK left no trace of why. The server (envd) only records that the stream
was cancelled, not the client-side reason, so a cancelled commands.run
(consumer stopped iterating, request timeout, sibling task cancel, caller
exit, explicit disconnect(), or an RPC error) was invisible from the SDK
side — and note the process itself is NOT killed, it keeps running, so the
result is silently uncollected.
CommandHandle (sync + async) now takes the logger the sandbox was already
constructed with (ConnectionConfig.logger) and records, at INFO, why the
stream ended, tagged with the pid:
- explicit disconnect() — command left running
- consumer stopped iterating (break / cancel / caller exit): the sync
handle catches GeneratorExit; the async handle logs in disconnect() and
the error path of its reader task
- stream error before the end event (RPC failure): the error type/message
No-op when no logger is configured (the default), so nothing changes for
callers that did not opt into logging. This is the client-side, first
increment of e2b-dev#1877 (correlate SDK cancellations with envd logs);
it does not yet depend on a shared request-id — logs correlate by sandbox id
+ pid + timestamp with the server-side change e2b-dev/runtime#3647.
🦋 Changeset detectedLatest commit: 628298c The changes in this PR will be included in the next version bump. This PR includes changesets to release 2 packages
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 |
There was a problem hiding this comment.
TASTE.md review
Checked the changed code against the parity rules (T-1, T-2), API shape (T-3/T-3a, T-26), streaming (T-27–T-30), configuration (T-49–T-51), errors/messages (T-62) and docstring rules (T-69–T-71). The sync and async Python handles mirror each other correctly (T-2), the logger is read from ConnectionConfig rather than a new knob (T-49), and it is a no-op by default.
3 findings (1 body-only, 2 inline):
- T-1 / T-2 — JS parity missing (not on a changed line). The three surfaces mirror each other 1:1.
packages/js-sdk/src/connectionConfig.tsalready carrieslogger, butpackages/js-sdk/src/sandbox/commands/commandHandle.tsgets no equivalent "stream ended: " logging indisconnect()/ the stream error path. Either add the JS half in this PR or say explicitly in the description that it is a follow-up (AGENTS.md also asks for equivalent JS changes when touching the SDK). - T-3a — the new
loggerparameter is a defaulted positional in bothCommandHandle.__init__signatures (inline, low severity since the constructors are internal per T-26). - T-62 — the sync handle can emit two contradictory "stream ended" lines for a single
disconnect()(inline).
Not TASTE-related but worth noting: no test covers the new log lines and no changeset was added for the python-sdk behavior change (both requested by AGENTS.md).
…hangeset Follow-up to the review on this PR: - T-62: `_log_stream_ended` now records at most once per handle (guarded by `_stream_end_logged`), so `disconnect()` and the generator-close / reader-task path can no longer emit two contradictory causes for the same command. - T-3a: the new `logger` parameter is keyword-only in both CommandHandle constructors. - T-1/T-2: JS parity — `commandHandle.ts` gets the equivalent stream-ended logging in `disconnect()` and the stream-error path, reading `connectionConfig.logger`, wired through all four construction sites (commands + pty). - Tests: `tests/test_command_handle.py` covers the log lines for sync and async — stream error, explicit disconnect (asserting a single line), and the no-logger no-op. - Adds a changeset (patch) for e2b and @e2b/python-sdk.
Problem
When a command's output stream ends on the client side without an end event, the SDK leaves no trace of why. The server (envd) only records that the stream was cancelled — not the client-side reason — so a cancelled
commands.runis invisible from the SDK logs.This matters because envd runs the guest process on a context decoupled from the streaming RPC: cancelling the stream does not kill the process, it runs to completion. So the common failure mode is "the command exited 0 but its result was never collected", with the only evidence being an ambiguous server-side
context canceledline.Cancellation reaches this state through several client-side paths, none of which currently log:
break, an outer timeout/wait_for, a sibling task cancel, the caller exiting)disconnect()(which deliberately leaves the command running)Change
CommandHandle(sync) andAsyncCommandHandlenow take the logger the sandbox was already constructed with (ConnectionConfig.logger) and record, at INFO, why the stream ended, tagged with the pid:disconnect()— "command left running"GeneratorExit; the async handle logs indisconnect()and in the error path of its reader taskIt is a no-op when no logger is configured (the default), so nothing changes for callers who did not opt into logging.
Scope
This is the client-side, first increment of #1877 (correlate SDK cancellations with envd logs). It intentionally does not yet introduce a shared request-id — the logs correlate by sandbox id + pid + timestamp with the server-side change e2b-dev/runtime#3647, which downgrades the client-cancel line from ERROR→INFO, fixes its status code, and annotates the cancel phase. The shared-id closing of the loop is tracked in #1877.
Testing
ruff format --checkpasses; the addedOptional[logging.Logger]parameter matches the existingOptional[...]convention in these files.None).Related: #1877, e2b-dev/runtime#3647.