From 3066b3fc846a1cbd36bf1ef584c6285b379cbb13 Mon Sep 17 00:00:00 2001 From: aran-yogesh Date: Wed, 25 Feb 2026 18:27:22 -0800 Subject: [PATCH 1/4] fix: recover from sandbox connection failures by recreating sandbox and re-cloning repo --- apps/agent/agent/server.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/apps/agent/agent/server.py b/apps/agent/agent/server.py index bbad1fae..888f8849 100644 --- a/apps/agent/agent/server.py +++ b/apps/agent/agent/server.py @@ -26,6 +26,8 @@ from deepagents.backends.protocol import SandboxBackendProtocol from langchain_openai import ChatOpenAI from .encryption import decrypt_token +from langsmith.sandbox import SandboxClientError + from .integrations.langsmith import create_langsmith_sandbox from .middleware import ( ToolErrorMiddleware, @@ -242,6 +244,29 @@ async def get_agent(config: RunnableConfig) -> Pregel: # noqa: PLR0915 repo_dir = await _clone_or_pull_repo_in_sandbox( sandbox_backend, repo_owner, repo_name, github_token ) + except SandboxClientError: + logger.warning( + "Cached sandbox is no longer reachable for thread %s, recreating sandbox", + thread_id, + ) + SANDBOX_BACKENDS.pop(thread_id, None) + await client.threads.update( + thread_id=thread_id, + metadata={"sandbox_id": SANDBOX_CREATING}, + ) + try: + sandbox_backend = await asyncio.to_thread(create_langsmith_sandbox) + repo_dir = await _clone_or_pull_repo_in_sandbox( + sandbox_backend, repo_owner, repo_name, github_token + ) + await client.threads.update( + thread_id=thread_id, + metadata={"repo_dir": repo_dir}, + ) + except Exception: + logger.exception("Failed to recreate sandbox after connection failure") + await client.threads.update(thread_id=thread_id, metadata={"sandbox_id": None}) + raise except Exception: logger.exception("Failed to pull repo in cached sandbox") raise From 18f1801005283250510d564d2f7501e06d163f0d Mon Sep 17 00:00:00 2001 From: aran-yogesh Date: Wed, 25 Feb 2026 18:30:00 -0800 Subject: [PATCH 2/4] linting --- apps/agent/agent/server.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/agent/agent/server.py b/apps/agent/agent/server.py index 888f8849..31d6279d 100644 --- a/apps/agent/agent/server.py +++ b/apps/agent/agent/server.py @@ -24,10 +24,9 @@ warnings.filterwarnings("ignore", message=".*Pydantic V1.*", category=UserWarnin from deepagents import create_deep_agent from deepagents.backends.protocol import SandboxBackendProtocol from langchain_openai import ChatOpenAI - -from .encryption import decrypt_token from langsmith.sandbox import SandboxClientError +from .encryption import decrypt_token from .integrations.langsmith import create_langsmith_sandbox from .middleware import ( ToolErrorMiddleware, From bec922b3b1aa4fae487acc4cf8097f60a8a69955 Mon Sep 17 00:00:00 2001 From: aran-yogesh Date: Thu, 26 Feb 2026 09:52:27 -0800 Subject: [PATCH 3/4] refactor: extract sandbox recovery logic into _recreate_sandbox util function --- apps/agent/agent/server.py | 51 +++++++++++++++++++++++++------------- 1 file changed, 34 insertions(+), 17 deletions(-) diff --git a/apps/agent/agent/server.py b/apps/agent/agent/server.py index 31d6279d..86965df4 100644 --- a/apps/agent/agent/server.py +++ b/apps/agent/agent/server.py @@ -170,6 +170,38 @@ async def _clone_or_pull_repo_in_sandbox( # noqa: PLR0915 return repo_dir +async def _recreate_sandbox( + thread_id: str, + repo_owner: str, + repo_name: str, + github_token: str | None, +) -> tuple[SandboxBackendProtocol, str]: + """Recreate a sandbox and clone the repo after a connection failure. + + Clears the stale cache entry, sets the SANDBOX_CREATING sentinel, + creates a fresh sandbox, clones the repo, and updates repo_dir metadata. + """ + SANDBOX_BACKENDS.pop(thread_id, None) + await client.threads.update( + thread_id=thread_id, + metadata={"sandbox_id": SANDBOX_CREATING}, + ) + try: + sandbox_backend = await asyncio.to_thread(create_langsmith_sandbox) + repo_dir = await _clone_or_pull_repo_in_sandbox( + sandbox_backend, repo_owner, repo_name, github_token + ) + await client.threads.update( + thread_id=thread_id, + metadata={"repo_dir": repo_dir}, + ) + except Exception: + logger.exception("Failed to recreate sandbox after connection failure") + await client.threads.update(thread_id=thread_id, metadata={"sandbox_id": None}) + raise + return sandbox_backend, repo_dir + + async def _wait_for_sandbox_id(thread_id: str) -> str: """Wait for sandbox_id to be set in thread metadata. @@ -248,24 +280,9 @@ async def get_agent(config: RunnableConfig) -> Pregel: # noqa: PLR0915 "Cached sandbox is no longer reachable for thread %s, recreating sandbox", thread_id, ) - SANDBOX_BACKENDS.pop(thread_id, None) - await client.threads.update( - thread_id=thread_id, - metadata={"sandbox_id": SANDBOX_CREATING}, + sandbox_backend, repo_dir = await _recreate_sandbox( + thread_id, repo_owner, repo_name, github_token ) - try: - sandbox_backend = await asyncio.to_thread(create_langsmith_sandbox) - repo_dir = await _clone_or_pull_repo_in_sandbox( - sandbox_backend, repo_owner, repo_name, github_token - ) - await client.threads.update( - thread_id=thread_id, - metadata={"repo_dir": repo_dir}, - ) - except Exception: - logger.exception("Failed to recreate sandbox after connection failure") - await client.threads.update(thread_id=thread_id, metadata={"sandbox_id": None}) - raise except Exception: logger.exception("Failed to pull repo in cached sandbox") raise From 9066b36063ea0c3c1fd33f43d185f7257ad489ef Mon Sep 17 00:00:00 2001 From: aran-yogesh Date: Thu, 26 Feb 2026 10:50:13 -0800 Subject: [PATCH 4/4] refactor: make github_token keyword-only in _recreate_sandbox and drop redundant repo_dir metadata update --- apps/agent/agent/server.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/apps/agent/agent/server.py b/apps/agent/agent/server.py index 86965df4..0de0029d 100644 --- a/apps/agent/agent/server.py +++ b/apps/agent/agent/server.py @@ -174,12 +174,13 @@ async def _recreate_sandbox( thread_id: str, repo_owner: str, repo_name: str, + *, github_token: str | None, ) -> tuple[SandboxBackendProtocol, str]: """Recreate a sandbox and clone the repo after a connection failure. Clears the stale cache entry, sets the SANDBOX_CREATING sentinel, - creates a fresh sandbox, clones the repo, and updates repo_dir metadata. + creates a fresh sandbox, and clones the repo. """ SANDBOX_BACKENDS.pop(thread_id, None) await client.threads.update( @@ -191,10 +192,6 @@ async def _recreate_sandbox( repo_dir = await _clone_or_pull_repo_in_sandbox( sandbox_backend, repo_owner, repo_name, github_token ) - await client.threads.update( - thread_id=thread_id, - metadata={"repo_dir": repo_dir}, - ) except Exception: logger.exception("Failed to recreate sandbox after connection failure") await client.threads.update(thread_id=thread_id, metadata={"sandbox_id": None}) @@ -281,7 +278,7 @@ async def get_agent(config: RunnableConfig) -> Pregel: # noqa: PLR0915 thread_id, ) sandbox_backend, repo_dir = await _recreate_sandbox( - thread_id, repo_owner, repo_name, github_token + thread_id, repo_owner, repo_name, github_token=github_token ) except Exception: logger.exception("Failed to pull repo in cached sandbox")