From a8910a49f727966b2a4ef319a9f66c1a30b6f7cf Mon Sep 17 00:00:00 2001 From: majiayu000 <1835304752@qq.com> Date: Mon, 23 Mar 2026 08:40:59 +0800 Subject: [PATCH 1/2] fix: disable stream timeout for background commands to allow reconnect by PID When running background commands, the default 60s stream timeout caused the RPC connection to terminate with DeadlineExceeded. This made envd clean up process tracking, so subsequent connect(pid) calls would fail. For background commands where timeout is not explicitly set, override to 0 (no timeout) so the process remains reachable for reconnection. Fixes #1074 Signed-off-by: majiayu000 <1835304752@qq.com> --- packages/js-sdk/src/sandbox/commands/index.ts | 6 +++++- packages/python-sdk/e2b/sandbox_async/commands/command.py | 6 +++++- packages/python-sdk/e2b/sandbox_sync/commands/command.py | 6 +++++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/packages/js-sdk/src/sandbox/commands/index.ts b/packages/js-sdk/src/sandbox/commands/index.ts index 61cb9e283c..9e2457c37a 100644 --- a/packages/js-sdk/src/sandbox/commands/index.ts +++ b/packages/js-sdk/src/sandbox/commands/index.ts @@ -393,7 +393,11 @@ export class Commands { cmd: string, opts?: CommandStartOpts & { background?: boolean } ): Promise { - const proc = await this.start(cmd, opts) + const startOpts = + opts?.background && opts?.timeoutMs === undefined + ? { ...opts, timeoutMs: 0 } + : opts + const proc = await this.start(cmd, startOpts) return opts?.background ? proc : proc.wait() } diff --git a/packages/python-sdk/e2b/sandbox_async/commands/command.py b/packages/python-sdk/e2b/sandbox_async/commands/command.py index 32b75fd26b..208fd37e82 100644 --- a/packages/python-sdk/e2b/sandbox_async/commands/command.py +++ b/packages/python-sdk/e2b/sandbox_async/commands/command.py @@ -220,12 +220,16 @@ async def run( # Default to `False` stdin = stdin or False + # For background commands, disable the stream timeout by default + # so the process remains reachable for connect(pid) + effective_timeout = 0 if background and timeout == 60 else timeout + proc = await self._start( cmd, envs, user, cwd, - timeout, + effective_timeout, request_timeout, stdin, on_stdout=on_stdout, diff --git a/packages/python-sdk/e2b/sandbox_sync/commands/command.py b/packages/python-sdk/e2b/sandbox_sync/commands/command.py index 512b7d9923..5ea70606db 100644 --- a/packages/python-sdk/e2b/sandbox_sync/commands/command.py +++ b/packages/python-sdk/e2b/sandbox_sync/commands/command.py @@ -217,13 +217,17 @@ def run( # Default to `False` stdin = stdin or False + # For background commands, disable the stream timeout by default + # so the process remains reachable for connect(pid) + effective_timeout = 0 if background and timeout == 60 else timeout + proc = self._start( cmd, envs, user, cwd, stdin, - timeout, + effective_timeout, request_timeout, ) From bca54bd6257d0c4da589885ebfd5fd3acb33cda4 Mon Sep 17 00:00:00 2001 From: majiayu000 <1835304752@qq.com> Date: Mon, 23 Mar 2026 08:44:51 +0800 Subject: [PATCH 2/2] fix(python-sdk): use sentinel pattern for background command timeout default Replace `timeout == 60` check with `timeout: Optional[float] = None` sentinel to correctly distinguish 'user omitted timeout' from 'user explicitly set timeout=60'. This preserves the user's explicit timeout choice for background commands while defaulting to unlimited (0) when no timeout is specified. Signed-off-by: majiayu000 <1835304752@qq.com> --- .../e2b/sandbox_async/commands/command.py | 20 +++++++++++-------- .../e2b/sandbox_sync/commands/command.py | 20 +++++++++++-------- 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/packages/python-sdk/e2b/sandbox_async/commands/command.py b/packages/python-sdk/e2b/sandbox_async/commands/command.py index 208fd37e82..715c8ae2c4 100644 --- a/packages/python-sdk/e2b/sandbox_async/commands/command.py +++ b/packages/python-sdk/e2b/sandbox_async/commands/command.py @@ -144,7 +144,7 @@ async def run( on_stdout: Optional[OutputHandler[Stdout]] = None, on_stderr: Optional[OutputHandler[Stderr]] = None, stdin: Optional[bool] = None, - timeout: Optional[float] = 60, + timeout: Optional[float] = None, request_timeout: Optional[float] = None, ) -> CommandResult: """ @@ -158,7 +158,7 @@ async def run( :param on_stdout: Callback for command stdout output :param on_stderr: Callback for command stderr output :param stdin: If `True`, the command will have a stdin stream that you can send data to using `sandbox.commands.send_stdin()` - :param timeout: Timeout for the command connection in **seconds**. Using `0` will not limit the command connection time + :param timeout: Timeout for the command connection in **seconds**. Using `0` will not limit the command connection time. Default is `60` seconds for foreground commands, `0` (no limit) for background commands :param request_timeout: Timeout for the request in **seconds** :return: `CommandResult` result of the command execution @@ -176,7 +176,7 @@ async def run( on_stdout: Optional[OutputHandler[Stdout]] = None, on_stderr: Optional[OutputHandler[Stderr]] = None, stdin: Optional[bool] = None, - timeout: Optional[float] = 60, + timeout: Optional[float] = None, request_timeout: Optional[float] = None, ) -> AsyncCommandHandle: """ @@ -190,7 +190,7 @@ async def run( :param on_stdout: Callback for command stdout output :param on_stderr: Callback for command stderr output :param stdin: If `True`, the command will have a stdin stream that you can send data to using `sandbox.commands.send_stdin()` - :param timeout: Timeout for the command connection in **seconds**. Using `0` will not limit the command connection time + :param timeout: Timeout for the command connection in **seconds**. Using `0` will not limit the command connection time. Default is `0` (no limit) for background commands :param request_timeout: Timeout for the request in **seconds** :return: `AsyncCommandHandle` handle to interact with the running command @@ -207,7 +207,7 @@ async def run( on_stdout: Optional[OutputHandler[Stdout]] = None, on_stderr: Optional[OutputHandler[Stderr]] = None, stdin: Optional[bool] = None, - timeout: Optional[float] = 60, + timeout: Optional[float] = None, request_timeout: Optional[float] = None, ): # Check version for stdin support @@ -220,9 +220,13 @@ async def run( # Default to `False` stdin = stdin or False - # For background commands, disable the stream timeout by default - # so the process remains reachable for connect(pid) - effective_timeout = 0 if background and timeout == 60 else timeout + # When timeout is not explicitly provided, default to 60s for foreground + # commands, or 0 (unlimited) for background commands so the process + # remains reachable for connect(pid) + if timeout is None: + effective_timeout = 0 if background else 60 + else: + effective_timeout = timeout proc = await self._start( cmd, diff --git a/packages/python-sdk/e2b/sandbox_sync/commands/command.py b/packages/python-sdk/e2b/sandbox_sync/commands/command.py index 5ea70606db..bb0c87e44b 100644 --- a/packages/python-sdk/e2b/sandbox_sync/commands/command.py +++ b/packages/python-sdk/e2b/sandbox_sync/commands/command.py @@ -143,7 +143,7 @@ def run( on_stdout: Optional[Callable[[str], None]] = None, on_stderr: Optional[Callable[[str], None]] = None, stdin: Optional[bool] = None, - timeout: Optional[float] = 60, + timeout: Optional[float] = None, request_timeout: Optional[float] = None, ) -> CommandResult: """ @@ -157,7 +157,7 @@ def run( :param on_stdout: Callback for command stdout output :param on_stderr: Callback for command stderr output :param stdin: If `True`, the command will have a stdin stream that you can send data to using `sandbox.commands.send_stdin()` - :param timeout: Timeout for the command connection in **seconds**. Using `0` will not limit the command connection time + :param timeout: Timeout for the command connection in **seconds**. Using `0` will not limit the command connection time. Default is `60` seconds for foreground commands, `0` (no limit) for background commands :param request_timeout: Timeout for the request in **seconds** :return: `CommandResult` result of the command execution @@ -175,7 +175,7 @@ def run( on_stdout: None = None, on_stderr: None = None, stdin: Optional[bool] = None, - timeout: Optional[float] = 60, + timeout: Optional[float] = None, request_timeout: Optional[float] = None, ) -> CommandHandle: """ @@ -187,7 +187,7 @@ def run( :param user: User to run the command as :param cwd: Working directory to run the command :param stdin: If `True`, the command will have a stdin stream that you can send data to using `sandbox.commands.send_stdin()` - :param timeout: Timeout for the command connection in **seconds**. Using `0` will not limit the command connection time + :param timeout: Timeout for the command connection in **seconds**. Using `0` will not limit the command connection time. Default is `0` (no limit) for background commands :param request_timeout: Timeout for the request in **seconds** :return: `CommandHandle` handle to interact with the running command @@ -204,7 +204,7 @@ def run( on_stdout: Optional[Callable[[str], None]] = None, on_stderr: Optional[Callable[[str], None]] = None, stdin: Optional[bool] = None, - timeout: Optional[float] = 60, + timeout: Optional[float] = None, request_timeout: Optional[float] = None, ): # Check version for stdin support @@ -217,9 +217,13 @@ def run( # Default to `False` stdin = stdin or False - # For background commands, disable the stream timeout by default - # so the process remains reachable for connect(pid) - effective_timeout = 0 if background and timeout == 60 else timeout + # When timeout is not explicitly provided, default to 60s for foreground + # commands, or 0 (unlimited) for background commands so the process + # remains reachable for connect(pid) + if timeout is None: + effective_timeout = 0 if background else 60 + else: + effective_timeout = timeout proc = self._start( cmd,