From 50e02c67ea25eadcfc69d042ce670d37d7143477 Mon Sep 17 00:00:00 2001 From: aran-yogesh Date: Tue, 17 Feb 2026 15:03:20 -0800 Subject: [PATCH 1/4] fix: restore sandbox backend via thread metadata when cache misses --- apps/agent/agent/middleware/open_pr.py | 4 +- apps/agent/agent/server.py | 20 +++++++++- apps/agent/agent/tools/commit_and_open_pr.py | 13 ++----- apps/agent/agent/utils/sandbox_state.py | 39 ++++++++++++++++++++ 4 files changed, 62 insertions(+), 14 deletions(-) diff --git a/apps/agent/agent/middleware/open_pr.py b/apps/agent/agent/middleware/open_pr.py index 158b7671..af19bf70 100644 --- a/apps/agent/agent/middleware/open_pr.py +++ b/apps/agent/agent/middleware/open_pr.py @@ -32,7 +32,7 @@ from ..utils.github import ( git_push, ) from ..utils.linear import comment_on_linear_issue -from ..utils.sandbox_state import SANDBOX_BACKENDS +from ..utils.sandbox_state import get_sandbox_backend logger = logging.getLogger(__name__) @@ -147,7 +147,7 @@ I've {action} pull request to address this issue: repo_owner = repo_config.get("owner") repo_name = repo_config.get("name") - sandbox_backend = SANDBOX_BACKENDS.get(thread_id) + sandbox_backend = await get_sandbox_backend(thread_id) if thread_id else None repo_dir = f"/workspace/{repo_name}" diff --git a/apps/agent/agent/server.py b/apps/agent/agent/server.py index c44c76b4..b3d92cb7 100644 --- a/apps/agent/agent/server.py +++ b/apps/agent/agent/server.py @@ -223,13 +223,29 @@ async def get_agent(config: RunnableConfig) -> Pregel: # noqa: PLR0915 tools=[], ).with_config(config) + sandbox_backend = SANDBOX_BACKENDS.get(thread_id) sandbox_id = await _get_sandbox_id_from_metadata(thread_id) - if sandbox_id == SANDBOX_CREATING: + if sandbox_id == SANDBOX_CREATING and not sandbox_backend: logger.info("Sandbox creation in progress, waiting...") sandbox_id = await _wait_for_sandbox_id(thread_id) - if sandbox_id is None: + if sandbox_backend: + logger.info("Using cached sandbox backend for thread %s", thread_id) + thread = await client.threads.get(thread_id=thread_id) + repo_dir = thread.get("metadata", {}).get("repo_dir") + + if repo_owner and repo_name: + logger.info("Pulling latest changes for repo %s/%s", repo_owner, repo_name) + try: + repo_dir = await _clone_or_pull_repo_in_sandbox( + sandbox_backend, repo_owner, repo_name, github_token + ) + except Exception: + logger.exception("Failed to pull repo in cached sandbox") + raise + + elif sandbox_id is None: logger.info("Creating new sandbox for thread %s", thread_id) await client.threads.update(thread_id=thread_id, metadata={"sandbox_id": SANDBOX_CREATING}) diff --git a/apps/agent/agent/tools/commit_and_open_pr.py b/apps/agent/agent/tools/commit_and_open_pr.py index 6a40ac73..2afc3615 100644 --- a/apps/agent/agent/tools/commit_and_open_pr.py +++ b/apps/agent/agent/tools/commit_and_open_pr.py @@ -5,7 +5,6 @@ from typing import Any from langgraph.config import get_config from ..encryption import decrypt_token -from ..integrations.langsmith import _create_langsmith_sandbox from ..utils.github import ( create_github_pr, get_github_default_branch, @@ -19,7 +18,7 @@ from ..utils.github import ( git_has_unpushed_commits, git_push, ) -from ..utils.sandbox_state import SANDBOX_BACKENDS +from ..utils.sandbox_state import get_sandbox_backend_sync logger = logging.getLogger(__name__) @@ -128,15 +127,9 @@ def commit_and_open_pr( "pr_url": None, } - sandbox_backend = SANDBOX_BACKENDS.get(thread_id) + sandbox_backend = get_sandbox_backend_sync(thread_id) if not sandbox_backend: - sandbox_id = configurable.get("sandbox_id") - - if not sandbox_id: - return {"success": False, "error": "No sandbox found for thread", "pr_url": None} - - sandbox_backend = _create_langsmith_sandbox(sandbox_id) - SANDBOX_BACKENDS[thread_id] = sandbox_backend + return {"success": False, "error": "No sandbox found for thread", "pr_url": None} repo_dir = f"/workspace/{repo_name}" diff --git a/apps/agent/agent/utils/sandbox_state.py b/apps/agent/agent/utils/sandbox_state.py index 5d8d2b9c..46adc855 100644 --- a/apps/agent/agent/utils/sandbox_state.py +++ b/apps/agent/agent/utils/sandbox_state.py @@ -2,7 +2,46 @@ from __future__ import annotations +import asyncio +import logging from typing import Any +from langgraph_sdk import get_client + +from ..integrations.langsmith import _create_langsmith_sandbox + +logger = logging.getLogger(__name__) +client = get_client() + # Thread ID -> SandboxBackend mapping, shared between server.py and middleware SANDBOX_BACKENDS: dict[str, Any] = {} + + +async def _get_sandbox_id_from_metadata(thread_id: str) -> str | None: + """Fetch sandbox_id from thread metadata.""" + try: + thread = await client.threads.get(thread_id=thread_id) + except Exception: + logger.exception("Failed to fetch thread metadata for sandbox") + return None + return thread.get("metadata", {}).get("sandbox_id") + + +async def get_sandbox_backend(thread_id: str) -> Any | None: + """Get sandbox backend from cache, or connect using thread metadata.""" + sandbox_backend = SANDBOX_BACKENDS.get(thread_id) + if sandbox_backend: + return sandbox_backend + + sandbox_id = await _get_sandbox_id_from_metadata(thread_id) + if not sandbox_id: + return None + + sandbox_backend = await asyncio.to_thread(_create_langsmith_sandbox, sandbox_id) + SANDBOX_BACKENDS[thread_id] = sandbox_backend + return sandbox_backend + + +def get_sandbox_backend_sync(thread_id: str) -> Any | None: + """Sync wrapper for get_sandbox_backend.""" + return asyncio.run(get_sandbox_backend(thread_id)) From 616730fb1d6254c6257bdcae7907741bb62140a7 Mon Sep 17 00:00:00 2001 From: aran-yogesh Date: Tue, 17 Feb 2026 16:50:08 -0800 Subject: [PATCH 2/4] fix: use config metadata for sandbox state lookups --- apps/agent/agent/middleware/open_pr.py | 7 +------ apps/agent/agent/server.py | 21 ++++++++------------- apps/agent/agent/utils/sandbox_state.py | 15 +++++++-------- 3 files changed, 16 insertions(+), 27 deletions(-) diff --git a/apps/agent/agent/middleware/open_pr.py b/apps/agent/agent/middleware/open_pr.py index af19bf70..82388a53 100644 --- a/apps/agent/agent/middleware/open_pr.py +++ b/apps/agent/agent/middleware/open_pr.py @@ -136,12 +136,7 @@ I've {action} pull request to address this issue: commit_message = pr_payload.get("commit_message", pr_title) if not thread_id: - if linear_issue_id and last_message_content: - comment = f"""🤖 **Agent Response** - -{last_message_content}""" - await comment_on_linear_issue(linear_issue_id, comment) - return None + raise ValueError("Missing thread_id in middleware config") repo_config = configurable.get("repo", {}) repo_owner = repo_config.get("owner") diff --git a/apps/agent/agent/server.py b/apps/agent/agent/server.py index b3d92cb7..1f808984 100644 --- a/apps/agent/agent/server.py +++ b/apps/agent/agent/server.py @@ -9,6 +9,7 @@ import warnings logger = logging.getLogger(__name__) +from langgraph.config import get_config from langgraph.graph.state import RunnableConfig from langgraph.pregel import Pregel from langgraph_sdk import get_client @@ -45,7 +46,7 @@ SANDBOX_CREATING = "__creating__" SANDBOX_CREATION_TIMEOUT = 180 SANDBOX_POLL_INTERVAL = 1.0 -from .utils.sandbox_state import SANDBOX_BACKENDS +from .utils.sandbox_state import SANDBOX_BACKENDS, get_sandbox_id_from_metadata async def _clone_or_pull_repo_in_sandbox( # noqa: PLR0915 @@ -161,12 +162,6 @@ async def _clone_or_pull_repo_in_sandbox( # noqa: PLR0915 return repo_dir -async def _get_sandbox_id_from_metadata(thread_id: str) -> str | None: - """Get sandbox_id from thread metadata.""" - thread = await client.threads.get(thread_id=thread_id) - return thread.get("metadata", {}).get("sandbox_id") - - async def _wait_for_sandbox_id(thread_id: str) -> str: """Wait for sandbox_id to be set in thread metadata. @@ -178,7 +173,7 @@ async def _wait_for_sandbox_id(thread_id: str) -> str: """ elapsed = 0.0 while elapsed < SANDBOX_CREATION_TIMEOUT: - sandbox_id = await _get_sandbox_id_from_metadata(thread_id) + sandbox_id = await get_sandbox_id_from_metadata(thread_id) if sandbox_id is not None and sandbox_id != SANDBOX_CREATING: return sandbox_id await asyncio.sleep(SANDBOX_POLL_INTERVAL) @@ -224,7 +219,7 @@ async def get_agent(config: RunnableConfig) -> Pregel: # noqa: PLR0915 ).with_config(config) sandbox_backend = SANDBOX_BACKENDS.get(thread_id) - sandbox_id = await _get_sandbox_id_from_metadata(thread_id) + sandbox_id = await get_sandbox_id_from_metadata(thread_id) if sandbox_id == SANDBOX_CREATING and not sandbox_backend: logger.info("Sandbox creation in progress, waiting...") @@ -232,8 +227,8 @@ async def get_agent(config: RunnableConfig) -> Pregel: # noqa: PLR0915 if sandbox_backend: logger.info("Using cached sandbox backend for thread %s", thread_id) - thread = await client.threads.get(thread_id=thread_id) - repo_dir = thread.get("metadata", {}).get("repo_dir") + metadata = get_config().get("metadata", {}) + repo_dir = metadata.get("repo_dir") if repo_owner and repo_name: logger.info("Pulling latest changes for repo %s/%s", repo_owner, repo_name) @@ -308,8 +303,8 @@ async def get_agent(config: RunnableConfig) -> Pregel: # noqa: PLR0915 await client.threads.update(thread_id=thread_id, metadata={"sandbox_id": None}) raise - thread = await client.threads.get(thread_id=thread_id) - repo_dir = thread.get("metadata", {}).get("repo_dir") + metadata = get_config().get("metadata", {}) + repo_dir = metadata.get("repo_dir") if repo_owner and repo_name: logger.info("Pulling latest changes for repo %s/%s", repo_owner, repo_name) diff --git a/apps/agent/agent/utils/sandbox_state.py b/apps/agent/agent/utils/sandbox_state.py index 46adc855..98f97ff0 100644 --- a/apps/agent/agent/utils/sandbox_state.py +++ b/apps/agent/agent/utils/sandbox_state.py @@ -6,25 +6,24 @@ import asyncio import logging from typing import Any -from langgraph_sdk import get_client +from langgraph.config import get_config from ..integrations.langsmith import _create_langsmith_sandbox logger = logging.getLogger(__name__) -client = get_client() # Thread ID -> SandboxBackend mapping, shared between server.py and middleware SANDBOX_BACKENDS: dict[str, Any] = {} -async def _get_sandbox_id_from_metadata(thread_id: str) -> str | None: +async def get_sandbox_id_from_metadata(thread_id: str) -> str | None: """Fetch sandbox_id from thread metadata.""" try: - thread = await client.threads.get(thread_id=thread_id) + config = get_config() except Exception: - logger.exception("Failed to fetch thread metadata for sandbox") + logger.exception("Failed to read thread metadata for sandbox") return None - return thread.get("metadata", {}).get("sandbox_id") + return config.get("metadata", {}).get("sandbox_id") async def get_sandbox_backend(thread_id: str) -> Any | None: @@ -33,9 +32,9 @@ async def get_sandbox_backend(thread_id: str) -> Any | None: if sandbox_backend: return sandbox_backend - sandbox_id = await _get_sandbox_id_from_metadata(thread_id) + sandbox_id = await get_sandbox_id_from_metadata(thread_id) if not sandbox_id: - return None + raise ValueError(f"Missing sandbox_id in thread metadata for {thread_id}") sandbox_backend = await asyncio.to_thread(_create_langsmith_sandbox, sandbox_id) SANDBOX_BACKENDS[thread_id] = sandbox_backend From ae5b5b6de40064d97092a7d57567c566f1419028 Mon Sep 17 00:00:00 2001 From: aran-yogesh Date: Tue, 17 Feb 2026 17:17:42 -0800 Subject: [PATCH 3/4] thread_id error --- apps/agent/agent/middleware/open_pr.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/apps/agent/agent/middleware/open_pr.py b/apps/agent/agent/middleware/open_pr.py index 82388a53..d79ac3cd 100644 --- a/apps/agent/agent/middleware/open_pr.py +++ b/apps/agent/agent/middleware/open_pr.py @@ -136,13 +136,18 @@ I've {action} pull request to address this issue: commit_message = pr_payload.get("commit_message", pr_title) if not thread_id: - raise ValueError("Missing thread_id in middleware config") + if linear_issue_id and last_message_content: + comment = f"""🤖 **Agent Response** + +{last_message_content}""" + await comment_on_linear_issue(linear_issue_id, comment) + raise ValueError("threadId not found") repo_config = configurable.get("repo", {}) repo_owner = repo_config.get("owner") repo_name = repo_config.get("name") - sandbox_backend = await get_sandbox_backend(thread_id) if thread_id else None + sandbox_backend = await get_sandbox_backend(thread_id) repo_dir = f"/workspace/{repo_name}" From 564998c8268689618f46278bf1b19d0c00849816 Mon Sep 17 00:00:00 2001 From: Aran Yogesh Date: Tue, 17 Feb 2026 17:44:13 -0800 Subject: [PATCH 4/4] Update apps/agent/agent/middleware/open_pr.py Co-authored-by: Brace Sproul --- apps/agent/agent/middleware/open_pr.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/agent/agent/middleware/open_pr.py b/apps/agent/agent/middleware/open_pr.py index d79ac3cd..426bed6d 100644 --- a/apps/agent/agent/middleware/open_pr.py +++ b/apps/agent/agent/middleware/open_pr.py @@ -141,7 +141,7 @@ I've {action} pull request to address this issue: {last_message_content}""" await comment_on_linear_issue(linear_issue_id, comment) - raise ValueError("threadId not found") + raise ValueError("No thread_id found in config") repo_config = configurable.get("repo", {}) repo_owner = repo_config.get("owner")