Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions apps/server/src/sandbox/SandboxProvisioner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -954,7 +954,11 @@ describe("celestoDriver", () => {
Effect.gen(function* () {
const cases = [
{ state: "running", expected: "running" },
...["creating", "starting", "restoring"].map((state) => ({ state, expected: "starting" })),
// `restorable` and `restoring` are Celesto moving a sandbox after a spot interruption.
...["creating", "pending", "starting", "restoring", "restorable"].map((state) => ({
state,
expected: "starting",
})),
{ state: "stopping", expected: "stopped" },
{ state: "stopped", expected: "stopped" },
{ state: "stopped", disk_delete_at: "2026-12-01T00:00:00Z", expected: "stopped" },
Expand All @@ -965,7 +969,7 @@ describe("celestoDriver", () => {
{ state: "deleting", expected: "deleted" },
{ state: "deleted", expected: "deleted" },
{ status: 404, expected: "deleted" },
{ state: "restorable", expected: null },
{ state: "hibernating", expected: null },
] as const;
for (const testCase of cases) {
const client = HttpClient.make((request) =>
Expand Down
10 changes: 8 additions & 2 deletions apps/server/src/sandbox/providers/celesto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,14 @@ const CelestoUsage = Schema.Struct({
running_limit: Schema.Number,
});

// `restorable` is a spot move Celesto finishes on its own, so it leaves the record alone.
// `restorable` then `restoring` is a spot move Celesto finishes on its own, so like any `starting`
// it leaves the record alone.
const CELESTO_STATUS: Record<string, SandboxProviderStatus> = {
running: "running",
creating: "starting",
pending: "starting",
starting: "starting",
restorable: "starting",
restoring: "starting",
stopping: "stopped",
stopped: "stopped",
Expand Down Expand Up @@ -104,6 +107,8 @@ const info = {
{ id: "celesto-large", label: "Large", detail: "4 vCPU · 16 GB · 60 GB" },
{ id: "celesto-xl", label: "Extra large", detail: "8 vCPU · 32 GB · 100 GB" },
],
// https://celesto.ai/pricing: "Stopped root disks are retained free for 30 days."
diskRetentionDays: 30,
} satisfies SandboxProviderDriver["info"];

const computerUrl = (sandboxId: string, path = "") =>
Expand Down Expand Up @@ -382,7 +387,8 @@ export const celestoDriver: SandboxProviderDriver = {
authorize(HttpClientRequest.post(`${CELESTO_API_BASE}/computers`), input.config.apiKey),
{ "Idempotency-Key": input.idempotencyKey, "X-Celesto-Client": "sdk" },
),
// Without a size the template's default is Standard, not Micro.
// Without a size the template's default is Standard, not Micro. A persistent home
// (`external_volume_enabled`) answers 422 here: `coding-agent` is a pool template.
{ template_id: "coding-agent", size_id: SIZES[input.size] ?? "micro" },
),
});
Expand Down
12 changes: 9 additions & 3 deletions docs/internals/sandbox-environments.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,9 @@ Drivers must keep two invariants:
- **Tags.** `create` receives `tags: { host, instance }`, the host's environment id and the
record's instance id. A driver that can label sandboxes stores both, and `listSandboxes` returns
the sandboxes carrying this host's tag. Boat stores them as `t3.code/host` and `t3.code/instance`
labels; the other drivers do not tag yet, so they report no orphans.
labels; the other drivers do not tag yet, so they report no orphans. Celesto cannot: its create
takes no name, label, or project, and its list filters only by status, template, project, and
origin.

## Adding a provider

Expand Down Expand Up @@ -564,8 +566,12 @@ sandbox, disk included, even when stopped, so it is set 24 hours past the runnin
is the session timeout, capped by the plan (45 minutes on Hobby, a day on Pro), so a Hobby sandbox
stops early and the sweep records it as stopped. Its stopped disk is kept for 30 days. Upstash and
Celesto have no lifetime of their own, so the sweep's stop is the only limit; a Celesto sandbox
bills until T3 stops it. Upstash also pauses an idle box and wakes it on any visit to its URL,
restarting T3 through the box's init command; the sweep records both.
bills until T3 stops it. Celesto keeps the root disk, home included, across stop and start for 30
days. Its persistent home (`external_volume_enabled`) would also keep `/home/ohm` across a spot move
(`restorable`, then `restoring`, which the sweep treats as starting), but the `coding-agent`
template refuses it, so a moved sandbox may lose its T3 state. Upstash also pauses an idle box
and wakes it on any visit to its URL, restarting T3 through the box's init command; the sweep
records both.

The same sweep makes the record match the provider, because T3 usually runs where webhooks can't
reach it. Each record gets at most one provider call per sweep, with a few in flight at a time. An
Expand Down