fix(sandbox): create shell session before retrying on a fresh id (#3577)

* fix(sandbox): create shell session before retrying on a fresh id

The AIO sandbox recovery path generated a UUID and passed it straight to
exec_command(id=...). The sandbox image only auto-creates a session when
exec_command is called with *no* id; an exec carrying an unknown id returns
HTTP 404 "Session not found". So every ErrorObservation recovery itself
404'd, turning a transient session lapse into an unrecoverable tool error
that looped the run up to the LangGraph recursion limit.

Explicitly create_session(id=fresh_id) before targeting that id on retry.
create_session is idempotent (returns the existing session if the id already
exists), so this is safe under the serializing lock.

Updated the regression test to assert the retry targets exactly the
created session id rather than a fabricated, uncreated one.

* fix(sandbox): release the one-shot recovery session after retry

The fresh session created on the ErrorObservation recovery path is used for
exactly one command -- the next execute_command runs with no id and returns
to the default session. Under persistent session corruption every command
would create another session that is never reused or released, accumulating
sessions on the container.

Release it best-effort with cleanup_session() in a finally, swallowing any
cleanup error so it never masks a successful retry.

Addresses review feedback on #3577.

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
This commit is contained in:
stphtt
2026-06-17 09:21:27 +07:00
committed by GitHub
parent c361fa3e49
commit f212da9f89
2 changed files with 74 additions and 14 deletions
@@ -132,10 +132,22 @@ class AioSandbox(Sandbox):
output = result.data.output if result.data else ""
if output and _ERROR_OBSERVATION_SIGNATURE in output:
logger.warning("ErrorObservation detected in sandbox output, retrying with a fresh session")
logger.warning("ErrorObservation detected in sandbox output, retrying on a fresh session")
# exec_command only auto-creates a session when called with
# no id, so the recovery session must be created explicitly
# before we target it on retry.
fresh_id = str(uuid.uuid4())
result = self._client.shell.exec_command(command=command, id=fresh_id, no_change_timeout=self._DEFAULT_NO_CHANGE_TIMEOUT)
output = result.data.output if result.data else ""
self._client.shell.create_session(id=fresh_id)
try:
result = self._client.shell.exec_command(command=command, id=fresh_id, no_change_timeout=self._DEFAULT_NO_CHANGE_TIMEOUT)
output = result.data.output if result.data else ""
finally:
# Release the one-shot recovery session, best-effort, so
# repeated corruption can't accumulate sessions.
try:
self._client.shell.cleanup_session(fresh_id)
except Exception as cleanup_error:
logger.warning(f"Failed to release recovery session {fresh_id}: {cleanup_error}")
return output if output else "(no output)"
except Exception as e: