Skip to content

fix(sandbox): keep Claude Code dispatches out of tenant warm pools - #5835

Merged
pedrofrxncx merged 2 commits into
mainfrom
fix/tenant-pool-dispatches
Aug 7, 2026
Merged

fix(sandbox): keep Claude Code dispatches out of tenant warm pools#5835
pedrofrxncx merged 2 commits into
mainfrom
fix/tenant-pool-dispatches

Conversation

@pedrofrxncx

@pedrofrxncx pedrofrxncx commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Two bugs found while running the Electrolux pool in prod (follow-up to #5830).

1. Dispatches were eating — and de-warming — the tenant pool

The Claude Code dispatch path already asks for cloneOnly: true
(harnesses/sandbox-dispatch-client.ts): it wants a checkout, no install, no
dev server. But that claim could still resolve a tenant pool, and binding one is
worse than wasteful:

  1. the claim takes a pod that cost minutes of clone + install to warm;
  2. Studio then posts cloneOnly: true + the thread branch;
  3. the daemon classifies branch-change → clone step → stopDevTask().

So every dispatch consumed a warm slot and killed the dev server on the pod
it took. On a size: 2 pool a couple of dispatches empty it out.

resolveTenantPool now refuses cloneOnly claims, which fall back to
warmpool: "default" — the generic pool of empty pods, which is exactly what a
dispatch wants and gets there fastest. The rule lives in the pure resolver so
it's covered by a unit test rather than implied by the call site.

2. The daemon would spawn a second dev server

POST /_sandbox/exec/<script> spawned unconditionally, while the orchestrator's
own dev task spawns with ReplaceByLogName. So an agent running dev on a pod
that is already running it — the normal state of a warmed sandbox, and exactly
what this feature creates more of — got a second Vite/Next build on the same
pod's memory limit and OOMed the pod out from under itself.

Now a well-known starter (dev/start) with a live task returns that task
({ taskId, status, alreadyRunning: true }) instead of spawning. Idempotent, so
an agent that asks twice keeps pointing at the same server rather than
restarting it — a restart on a big repo is minutes.

Ceiling worth naming: this guards the /exec route, which is the path Studio's
tooling uses. An agent shelling out to npm run dev in a terminal still
bypasses it.

Tests

  • unit: a cloneOnly claim resolves no pool; cloneOnly: false still does.
  • daemon e2e (black-box, real binary): with a live dev task, POST /exec/dev
    answers alreadyRunning: true and twice in a row returns the same taskId.
    The fixture helper gained an optional scripts map so a repo can declare a
    long-running dev.
  • full daemon-e2e suite green.

Summary by cubic

Prevented Claude Code clone-only dispatches from using tenant warm pool pods, and made dev/start execution idempotent to avoid duplicate dev servers. This keeps tenant pools warm and reduces OOMs during dispatch-heavy periods.

  • Bug Fixes

    • Pool routing: resolveTenantPool now takes a claim object and rejects cloneOnly claims, falling back to warmpool: "default". runner.ts passes cloneOnly. Added unit tests.
    • Daemon exec: POST /_sandbox/exec/<starter> returns the running task (alreadyRunning: true) instead of starting a second dev server. Adds RunningByLogName in the Go task manager and an e2e test.
    • Test helpers: setupBareRepo accepts scripts to define a long-running dev.
  • Refactors

    • Removed sandbox spec docs from this PR.

Written for commit 9a89e4f. Summary will update on new commits.

Review in cubic

Pedro França added 2 commits August 6, 2026 21:14
A dispatch claim is `cloneOnly` — it wants a checkout, not a dev server. It
could still resolve a tenant pool, and binding one is actively destructive:
Studio posts `cloneOnly` + the thread branch, the daemon classifies
`branch-change`, and its clone step stops the dev task. So every dispatch
consumed a warm slot AND de-warmed the pod it took. They fall back to the
generic pool, which is what an empty pod is for.

Also stop the daemon spawning a second dev server: POST /exec/<starter> now
returns the running task (`alreadyRunning: true`) instead of starting another.
An agent that runs `dev` on a pod already running it — the normal state of a
warmed sandbox — otherwise puts two builds on one memory limit and OOMs the pod
out from under itself.
@pedrofrxncx
pedrofrxncx enabled auto-merge (squash) August 7, 2026 00:21
@pedrofrxncx
pedrofrxncx merged commit c4e38e7 into main Aug 7, 2026
24 checks passed
@pedrofrxncx
pedrofrxncx deleted the fix/tenant-pool-dispatches branch August 7, 2026 00:24
decocms Bot pushed a commit that referenced this pull request Aug 7, 2026
PR: #5835 fix(sandbox): keep Claude Code dispatches out of tenant warm pools
Bump type: patch

- @decocms/sandbox (packages/sandbox/package.json): 1.43.0 -> 1.43.1
- deploy/helm/sandbox-env (chart 0.12.2) (deploy/helm/sandbox-env/values.yaml deploy/helm/sandbox-env/Chart.yaml): image.tag/appVersion -> 1.43.1

Deploy-Scope: both
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant