Skip to content

fix(python-sdk): log the local cause when a command stream is cancelled - #1878

Open
AdaAibaby wants to merge 2 commits into
e2b-dev:mainfrom
AdaAibaby:fix/sdk-command-stream-cancel-logging
Open

AdaAibaby wants to merge 2 commits into
e2b-dev:mainfrom
AdaAibaby:fix/sdk-command-stream-cancel-logging

Conversation

@AdaAibaby

Copy link
Copy Markdown

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.run is 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 canceled line.

Cancellation reaches this state through several client-side paths, none of which currently log:

  • the consumer stops iterating early (break, an outer timeout/wait_for, a sibling task cancel, the caller exiting)
  • explicit disconnect() (which deliberately leaves the command running)
  • an RPC/transport error before the end event

Change

CommandHandle (sync) and AsyncCommandHandle now take the logger the sandbox was already constructed with (ConnectionConfig.logger) and record, at INFO, why the stream ended, tagged with the pid:

  • explicit disconnect() — "command left running"
  • consumer stopped iterating — the sync handle catches GeneratorExit; the async handle logs in disconnect() and in the error path of its reader task
  • stream error before the end event — the error type and message

It 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

  • All six changed files parse; ruff format --check passes; the added Optional[logging.Logger] parameter matches the existing Optional[...] convention in these files.
  • No behaviour change for existing callers (logger defaults to None).

Related: #1877, e2b-dev/runtime#3647.

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

changeset-bot Bot commented Sep 17, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 628298c

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

@devin-ai-integration devin-ai-integration Bot 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.

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):

  1. 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.ts already carries logger, but packages/js-sdk/src/sandbox/commands/commandHandle.ts gets no equivalent "stream ended: " logging in disconnect() / 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).
  2. T-3a — the new logger parameter is a defaulted positional in both CommandHandle.__init__ signatures (inline, low severity since the constructors are internal per T-26).
  3. 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).

Comment thread packages/python-sdk/e2b/sandbox_sync/commands/command_handle.py
Comment thread packages/python-sdk/e2b/sandbox_async/commands/command_handle.py
Comment thread packages/python-sdk/e2b/sandbox_sync/commands/command_handle.py
…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.
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