fix(preview): fall back to the serving sandbox when the pinned provider kind has no entry - #5239
Merged
Merged
Conversation
…as no entry
Sending the first message on a fresh thread tore down a working preview
("Starting your preview") and booted a second, competing sandbox that then
failed. Two independently-reasonable changes interact to cause it.
#4025 made preview entry selection an exact-key lookup, `branchMap[kind]`,
replacing `selectVmEntry`'s heuristic — deliberately, so a codex-local thread
keeps showing its desktop sandbox even when a cluster sibling exists. That
pinning is right; what it didn't cover is the pinned kind having NO entry at
all, which collapses to null rather than degrading to whatever is serving.
The same commit also threads sandboxProviderKind into SANDBOX_START and drops
the `!!branch` gate from shouldAutoStart, so a lookup miss no longer just
blanks the preview — it also boots a rival VM.
#4558 then made that reachable. It mirrors the first-message runtime pin into
the thread store so findReusableNewChat stops reusing a thread whose first
message already ran (harness_id never flows back through /watch) — correct and
needed. But it also mirrors `sandbox_provider_kind`, taken from the client's
picker. The server resolves that field from state the client doesn't own,
notably whether a desktop link is live: it pins `user-desktop` while the picker
says `agent-sandbox`. Mirroring the guess flips pendingSandboxProviderKind
mid-session, the exact-key lookup misses the running desktop sandbox, and the
preview drops out until the authoritative row lands.
Fixes:
- `resolveVmEntry(branchMap, kind)` in @decocms/shared — exact match still wins,
but a missing entry falls back to the serving sandbox instead of null. Both
call sites (SandboxLifecycleProvider, VmEventsBridge) now share it, so the
"must always agree" duplication is gone.
- Stop mirroring `sandbox_provider_kind` at send time. harness_id is all the
reuse gate needs; the provider kind stays null until the server's row arrives.
Verified against a linked desktop on a live branch. Before: preview flips
IFRAME -> STARTING and one SANDBOX_START returns 500 ("SandboxClaim did not
record an adopted Sandbox within 60s"). After: preview stays on IFRAME, zero
SANDBOX_START calls, message dispatches normally and the server pins
sandbox_provider_kind=user-desktop as it always did.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
pedrofrxncx
approved these changes
Jul 25, 2026
decocms Bot
pushed a commit
that referenced
this pull request
Jul 25, 2026
PR: #5239 fix(preview): fall back to the serving sandbox when the pinned provider kind has no entry Bump type: patch - decocms (apps/api/package.json): 4.127.0 -> 4.127.1 - @decocms/shared (packages/shared/package.json): 0.1.0 -> 0.1.1 Deploy-Scope: web
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Sending the first message on a fresh thread tears down a working preview into "Starting your preview" and boots a second, competing sandbox that then fails. It self-heals a moment later when the authoritative thread row lands, which is why it reads as a flicker rather than a hard break.
It takes two individually-reasonable commits to produce, ~4 weeks apart.
The latent half — #4025 (
ad6bcfd10)Preview entry selection became an exact-key lookup:
That was deliberate and correct — from the commit message, "so a codex-local thread always shows the desktop sandbox even when a cluster sibling exists." Pinning beats the heuristic.
What it doesn't cover is the pinned kind having no entry at all. That collapses to
nullinstead of degrading to whatever is actually serving the branch. The same commit also threadssandboxProviderKindintoSANDBOX_STARTand drops the!!branchgate fromshouldAutoStart— so a lookup miss no longer just blanks the preview, it also boots a rival VM.What made it reachable — #4558 (
73b15a685)That PR mirrors the first-message runtime pin into the thread store, so
findReusableNewChatstops reusing a thread whose first message already ran (harness_idis pinned server-side on dispatch and never flows back through/watch). Correct, and needed.It also mirrors
sandbox_provider_kind— sourced from the client's picker:The server resolves that field from state the client doesn't own — notably whether a desktop link is live. With a linked desktop it pins
user-desktopwhile the picker saysagent-sandbox. Mirroring the guess flipspendingSandboxProviderKindmid-session, the exact-key lookup misses the running desktop sandbox, and the preview drops out from under the user.Neither commit is wrong on its own. The bug is the interaction: one made a miss fatal, the other made misses happen.
Reproduction
Linked desktop, agent whose branch map holds exactly one entry:
New chat → send first message. The client fires a start for a provider that isn't there:
{"status":"failed","previewUrl":null, "failureReason":"SandboxClaim gimenes-xiwzhzrm-4993c564eb23f9e7 did not record an adopted Sandbox (status.sandbox.name) within 60s"}Note the handle differs from the one serving the preview (
d7600347fc5a12ae) — it's a second sandbox racing the healthy one. Meanwhile the server had pinned the threadsandbox_provider_kind: "user-desktop"all along.Fix
1.
resolveVmEntry(branchMap, kind)in@decocms/shared. An exact match still wins, so #4025's pinning is preserved intact; a missing entry falls back to the serving sandbox instead ofnull. Both call sites —SandboxLifecycleProviderandVmEventsBridge— now share it, retiring the duplicated logic whose comment already said the two "must always agree."2. Stop mirroring
sandbox_provider_kindat send time.harness_idis all the reuse gate needs. Never optimistically write a field the server resolves from state the client can't see; it stays null until the real row arrives.(1) is the load-bearing one — it makes a transient wrong pin harmless rather than destructive. (2) removes the trigger.
Testing
4 unit tests on
resolveVmEntry, including one encoding this exact failure shape (desktop serving, thread pinned toagent-sandbox).Verified in a live dev app against a linked desktop:
IFRAME → STARTINGIFRAMEonlySANDBOX_STARTcallsMessage dispatches normally; thread ends at
harness_id: decopilot,sandbox_provider_kind: user-desktop,branch: gimenes-xiwzhzrm.bun run checkclean across all 14 workspaces ·bun run lint0 errors ·bun run fmtapplied · sandbox + chat unit suites 853/853.Notes for reviewers
@pedrofrxncx — tagging you since #4558 is the half that surfaces this, and the mirror there is doing real work for
findReusableNewChatthat I don't want to regress. Please sanity-check that dropping onlysandbox_provider_kind(keepingharness_id) still fully covers the reuse gate you were fixing.Split out of #5231 — unrelated to that PR's new-chat branch-inheritance change, so it ships on its own.
🤖 Generated with Claude Code
Summary by cubic
Fixes preview flicker on the first message by falling back to the serving sandbox when the pinned
sandbox_provider_kindhas no entry, preventing a second, failing VM from starting. Also stops writing the client’s provider-kind guess to avoid mismatches mid-session.resolveVmEntry(branchMap, kind)in@decocms/shared; exact match wins, otherwise falls back toselectVmEntry.resolveVmEntryin SandboxLifecycleProvider and VmEventsBridge to keep selection consistent.sandbox_provider_kindat send time (still mirrorharness_id); the server remains the source of truth.resolveVmEntry, including the desktop-serving/pinned-to-agent regression case.Written for commit c833a7e. Summary will update on new commits.