Skip to content

Commit 0a2d5eb

Browse files
committed
fix: only treat PENDING as transient; STOPPING cannot reach RUNNING
1 parent 8c3526a commit 0a2d5eb

2 files changed

Lines changed: 16 additions & 15 deletions

File tree

src/agents/extensions/sandbox/vercel/sandbox.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -79,10 +79,11 @@
7979
httpx.ProtocolError,
8080
)
8181

82-
# Sandbox status values that can still transition to RUNNING (non-terminal).
83-
# Terminal states (e.g. "stopped", "failed") are not included because a sandbox
84-
# in those states can never become RUNNING, so waiting is futile.
85-
_VERCEL_TRANSIENT_SANDBOX_STATUSES: frozenset[str] = frozenset({"pending", "stopping"})
82+
# Sandbox status values from which the sandbox can still transition to RUNNING.
83+
# Only "pending" qualifies: a freshly created sandbox transitions PENDING -> RUNNING.
84+
# Other non-RUNNING states ("stopping", "stopped", "failed", "aborted",
85+
# "snapshotting") cannot reach RUNNING, so waiting is futile.
86+
_VERCEL_TRANSIENT_SANDBOX_STATUSES: frozenset[str] = frozenset({"pending"})
8687

8788

8889
def _is_transient_create_error(exc: BaseException) -> bool:
@@ -764,14 +765,15 @@ async def resume(self, state: SandboxSessionState) -> SandboxSession:
764765
# Already running; skip the wait entirely.
765766
reconnected = True
766767
elif current_status in _VERCEL_TRANSIENT_SANDBOX_STATUSES:
767-
# Still transitioning toward RUNNING; wait normally.
768+
# Still transitioning toward RUNNING (e.g. PENDING); wait normally.
768769
await sandbox.wait_for_status(
769770
SandboxStatus.RUNNING,
770771
timeout=DEFAULT_VERCEL_WAIT_FOR_RUNNING_TIMEOUT_S,
771772
)
772773
reconnected = True
773774
else:
774-
# Terminal state (e.g. "stopped", "failed"): cannot reach RUNNING.
775+
# Cannot reach RUNNING from here (STOPPING, STOPPED, FAILED,
776+
# ABORTED, SNAPSHOTTING). Drop the handle and recreate below.
775777
await sandbox.client.aclose()
776778
sandbox = None
777779
except TimeoutError:

tests/extensions/sandbox/test_vercel.py

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -802,13 +802,11 @@ async def test_vercel_resume_reconnects_existing_running_sandbox(
802802

803803

804804
@pytest.mark.asyncio
805-
@pytest.mark.parametrize("transient_status", ["pending", "stopping"])
806-
async def test_vercel_resume_waits_when_sandbox_in_transient_state(
805+
async def test_vercel_resume_waits_when_sandbox_pending(
807806
monkeypatch: pytest.MonkeyPatch,
808-
transient_status: str,
809807
) -> None:
810808
vercel_module = _load_vercel_module(monkeypatch)
811-
existing = _FakeAsyncSandbox(sandbox_id="sandbox-existing", status=transient_status)
809+
existing = _FakeAsyncSandbox(sandbox_id="sandbox-existing", status="pending")
812810
_FakeAsyncSandbox.sandboxes[existing.sandbox_id] = existing
813811

814812
state = vercel_module.VercelSandboxSessionState(
@@ -830,11 +828,15 @@ async def test_vercel_resume_waits_when_sandbox_in_transient_state(
830828

831829

832830
@pytest.mark.asyncio
833-
@pytest.mark.parametrize("terminal_status", ["stopped", "failed"])
834-
async def test_vercel_resume_recreates_sandbox_when_in_terminal_state(
831+
@pytest.mark.parametrize(
832+
"terminal_status", ["stopping", "stopped", "failed", "aborted", "snapshotting"]
833+
)
834+
async def test_vercel_resume_recreates_sandbox_when_cannot_reach_running(
835835
monkeypatch: pytest.MonkeyPatch,
836836
terminal_status: str,
837837
) -> None:
838+
"""A sandbox in any state that cannot transition to RUNNING must be recreated
839+
immediately, without waiting for the wait_for_status timeout."""
838840
vercel_module = _load_vercel_module(monkeypatch)
839841
existing = _FakeAsyncSandbox(sandbox_id="sandbox-terminal", status=terminal_status)
840842
_FakeAsyncSandbox.sandboxes[existing.sandbox_id] = existing
@@ -849,11 +851,8 @@ async def test_vercel_resume_recreates_sandbox_when_in_terminal_state(
849851
client = vercel_module.VercelSandboxClient()
850852
resumed = await client.resume(state)
851853

852-
# Should NOT have waited for status — the sandbox is already terminal.
853854
assert existing.wait_for_status_calls == []
854-
# Client must be closed before abandoning the sandbox.
855855
assert existing.client.closed is True
856-
# A new sandbox must have been created to replace the terminal one.
857856
assert len(_FakeAsyncSandbox.create_calls) == 1
858857
assert resumed._inner.state.sandbox_id != "sandbox-terminal"
859858
assert resumed._inner.state.workspace_root_ready is False

0 commit comments

Comments
 (0)