Summary
After a sandbox auto-pauses and resumes, reattaching to an existing
long-lived background process via commands.connect(pid) produces a
stdout stream that intermittently delivers its first response and then
goes silent — the on_stdout callback stops firing for subsequent
responses even though the process is still running and still writing.
This is a regression introduced in python-sdk 2.29.1 and present in
every release since (through 2.34.0). The likely change is cb061d2
("Fix several command and PTY streaming issues"), which reworked
incremental stdout/stderr decoding and stream-connection teardown.
Bisect (strong evidence)
Identical workload, same account/templates; only the SDK version changed.
The workload is a real stdio server (@modelcontextprotocol/server-everything
over npx) driven through repeated pause → resume → commands.connect(pid)
reattach cycles. "Stall rate" = fraction of reattach cycles where the
reconnected stdout stream failed to deliver a complete response, over 6
cycles:
| Version |
Stall rate |
| 2.20.2 |
1/6 |
| 2.21.0 |
1/6 |
| 2.27.0 |
1/6 |
| 2.29.0 |
1/6 ← last good |
| 2.29.1 |
6/6 ← first bad |
| 2.30.0 |
6/6 |
| 2.34.0 |
6/6 |
The transition is sharp and clean: every version up to and including
2.29.0 stalls ~1/6; every version from 2.29.1 on stalls ~6/6. The only
command/stream change in 2.29.1 is cb061d2.
Reproduction conditions
The stall reproduces reliably only through a realistic streaming
workload — a long-lived process reattached after resume that answers
several requests with sizeable, multi-chunk stdout responses (in
our case JSON-RPC tools/list / resources/list / prompts/list from an
stdio MCP server). The pattern observed: the first response after reattach
arrives, later ones never do.
A trivial synthetic process (one small line per second, or a single large
one-shot write) does not isolate the regression — those either stream
fine on both versions, or hit an unrelated single-write truncation that is
version-independent. So the reliable repro is the real stdio-server flow
above; we can share the exact harness on request.
Expected vs actual
- Expected (≤ 2.29.0): after reattach,
on_stdout keeps firing for
every subsequent response; a full multi-request refresh completes.
- Actual (≥ 2.29.1): after reattach, the stream delivers the first
response (sometimes partially) then goes silent; subsequent responses
never arrive, while a fresh reconnect shows the process still alive.
Impact
Any workload relying on a long-lived background process surviving
auto-pause and being reattached after resume (e.g. an stdio server behind
a gateway) loses output on nearly every resume from 2.29.1 on, forcing a
full fresh-sandbox rebuild as a workaround. On 2.29.0 this was rare
(~1/6); from 2.29.1 it is effectively every resume.
Environment
- python-sdk 2.29.1 first bad, confirmed through 2.34.0; 2.29.0 last good
AsyncSandbox.create(lifecycle={"on_timeout": "pause", "auto_resume": True})
- Reattach via
AsyncSandbox.connect(sandbox_id=...) + commands.connect(pid=...)
- Suspected change:
cb061d2 (incremental stdout/stderr decode + stream teardown)
Summary
After a sandbox auto-pauses and resumes, reattaching to an existing
long-lived background process via
commands.connect(pid)produces astdout stream that intermittently delivers its first response and then
goes silent — the
on_stdoutcallback stops firing for subsequentresponses even though the process is still running and still writing.
This is a regression introduced in python-sdk 2.29.1 and present in
every release since (through 2.34.0). The likely change is
cb061d2("Fix several command and PTY streaming issues"), which reworked
incremental stdout/stderr decoding and stream-connection teardown.
Bisect (strong evidence)
Identical workload, same account/templates; only the SDK version changed.
The workload is a real stdio server (
@modelcontextprotocol/server-everythingover
npx) driven through repeated pause → resume →commands.connect(pid)reattach cycles. "Stall rate" = fraction of reattach cycles where the
reconnected stdout stream failed to deliver a complete response, over 6
cycles:
The transition is sharp and clean: every version up to and including
2.29.0 stalls ~1/6; every version from 2.29.1 on stalls ~6/6. The only
command/stream change in 2.29.1 is
cb061d2.Reproduction conditions
The stall reproduces reliably only through a realistic streaming
workload — a long-lived process reattached after resume that answers
several requests with sizeable, multi-chunk stdout responses (in
our case JSON-RPC
tools/list/resources/list/prompts/listfrom anstdio MCP server). The pattern observed: the first response after reattach
arrives, later ones never do.
A trivial synthetic process (one small line per second, or a single large
one-shot write) does not isolate the regression — those either stream
fine on both versions, or hit an unrelated single-write truncation that is
version-independent. So the reliable repro is the real stdio-server flow
above; we can share the exact harness on request.
Expected vs actual
on_stdoutkeeps firing forevery subsequent response; a full multi-request refresh completes.
response (sometimes partially) then goes silent; subsequent responses
never arrive, while a fresh reconnect shows the process still alive.
Impact
Any workload relying on a long-lived background process surviving
auto-pause and being reattached after resume (e.g. an stdio server behind
a gateway) loses output on nearly every resume from 2.29.1 on, forcing a
full fresh-sandbox rebuild as a workaround. On 2.29.0 this was rare
(~1/6); from 2.29.1 it is effectively every resume.
Environment
AsyncSandbox.create(lifecycle={"on_timeout": "pause", "auto_resume": True})AsyncSandbox.connect(sandbox_id=...)+commands.connect(pid=...)cb061d2(incremental stdout/stderr decode + stream teardown)