Repository navigation
Description
Summary
After an agent-server image bump, a conversation can no longer be started in a sandbox that was created on the previous image. The start task ends in ERROR with an empty detail. The sandbox itself is healthy and resumes fine. Only conversation start fails.
Details
LiveStatusAppConversationService._start_app_conversation looks up the sandbox's spec by image ID and asserts it exists:
sandbox_spec = await self.sandbox_spec_service.get_sandbox_spec(sandbox.sandbox_spec_id)
assert sandbox_spec is not NoneWith DynamicRemoteSandboxSpecService, specs come from runtime-api /api/warm-runtime-configs, which lists only the current image. Once the image is bumped, get_sandbox_spec(<old image>) returns None, the bare assert raises AssertionError with no message, and _exception_detail turns that into detail="".
The start path reads one field from the spec, working_dir (/workspace/project for both the old and the current spec). Resume doesn't use the spec at all, which is why the old sandbox wakes up normally.
How it was found
An OHE Beta automation that starts a conversation in a long-lived sandbox began failing with conversation start failed: right after Beta went 0.81.1 -> 0.82.0. That release included OpenHands-Cloud#1345 (agent-server 1.50.1-python -> 1.55.1-python) and #866 (SDK pins to 1.55.1). The sandbox was still on agent-server:1.50.1-python.
Evidence
GET /api/v1/sandbox-specs?id=<...agent-server:1.50.1-python>returns[null]. The same call for:1.55.1-pythonreturns a full spec.- Control test: the same
POST /api/v1/app-conversations(same user, trivial message,run: false) against two sandboxes:
| Sandbox image | Start task result |
|---|---|
| 1.50.1 (old) | ERROR, detail="" |
| 1.55.1 (current) | READY |
- I could not read app-server logs, so the
AssertionErroritself is inferred from the code path and the empty detail, not seen in a traceback. - Schema-level compatibility looks fine for the 1.50.1 -> 1.55.1 gap: a request built by 1.55.1 validates on the 1.50.1 server model, and real conversations from a live 1.50.1 agent-server parse with the 1.55.1
ConversationInfo. This is not an end-to-end test.
Related code that makes the same assumption
get_sandbox_spec returning None for an existing sandbox is treated differently in each caller:
| Caller | Behavior on a missing spec |
|---|---|
_start_app_conversation |
bare assert -> start fails (this issue) |
app_conversation_router._get_agent_server_context |
falls back to the default spec (has a TODO about not storing previous spec versions) |
app_conversation_router.read_conversation_file |
returns '' |
sandbox_router._restore_git_user_settings_after_resume |
raises, then the error is swallowed with a warning, so the git identity is not restored |
remote_sandbox_service._resolve_archive_path |
raises SandboxError (only when the conversation has no pinned workspace path) |
Impact
- Any caller that starts a conversation in an existing sandbox on an older image: explicit
sandbox_id(automations, SDK users, sub-conversations). - Users on a sandbox grouping strategy other than the default
NO_GROUPING:_find_running_sandbox_for_userdoes not filter by spec, so an old running sandbox can be selected and then fail. - Default users (each conversation gets a fresh sandbox on the current image) are not affected.
- It recurs on every agent-server image bump. The set of affected sandboxes grows over time, and the next bump (SDK 1.56.0, chore(deps): bump SDK to 1.56.0 #876) will strand sandboxes on 1.55.1 the same way.
Proposed fix: three PRs
- Fix conversation start (the outage). In
_start_app_conversation, fall back toget_default_sandbox_spec()when the sandbox's spec is not advertised, as_get_agent_server_contextalready does. Log a warning naming the image and sandbox, and replace the bareassertwith an explicit error where there is no default either. Add tests: a start against a sandbox whose image is not advertised, and the spec service returningNonefor an unadvertised image without mocks. Small and easy to backport. - One helper, and the other callers. Add a single helper for "the spec of an existing sandbox" (default fallback plus a warning) next to
resolve_sandbox_spec, and use it in_get_agent_server_context,read_conversation_file,_restore_git_user_settings_after_resume,_resolve_archive_pathand the PR 1 call site. While there, decide whether to persistworking_diron the stored sandbox record at creation, which would remove the "default spec has the sameworking_dir" assumption. - Grouping prefers current-image sandboxes (optional follow-up). Have
_find_running_sandbox_for_user/_select_sandbox_by_strategyprefer sandboxes whose image is still advertised, so new conversations are not routed into old sandboxes when a current one exists. Intentionally kept out of PR 1.
Not PRs, but worth tracking
- After PR 1 ships, rerun the control test on Beta (start a conversation in an old-image sandbox) with the PR's image.
- Before the agent-server 1.56.0 bump, repeat the cross-version compatibility check. 1.56.0 changes agent settings and sub-agent handling, so the gap is larger than 1.50.1 -> 1.55.1.
- Open question: how long should old-image sandboxes be supported, and should the bump process say so in the release notes?
This issue was written by an AI agent (OpenHands) on behalf of @jpshackelford.
Activity
fix(app-conversation): allow starts in sandboxes on an older image
fix(app-conversation): allow starts in sandboxes on an older image (#903
fix(app-conversation): prefer sandboxes on the advertised image when …
fix(sandbox): resolve an existing sandbox's spec with a default fallb…