docs(design): plan warm and resumable Daytona sessions (F-020)#5214
docs(design): plan warm and resumable Daytona sessions (F-020)#5214mmabrouk wants to merge 6 commits into
Conversation
Plan-feature workspace for the two-tier warm-session design on top of PR #5197: Tier 1 parks the sandbox to stopped behind a default-off flag, Tier 2 defers a true running-warm pool. Includes the load-bearing finding that the vendored Daytona provider lacks pause/reconnect hooks, an ask-codex xhigh review round folded into the plan, and the open billing decisions. Claude-Session: https://claude.ai/code/session_018MaXPNpvzN22kngHno3VMj
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Feedback needed on three decisions before implementation starts: (1) the crash-orphan compute budget and the DAYTONA_AUTOSTOP value (5 min can stop a sandbox under a live long tool call; the old default was 15) — needs the billing owner; (2) where the sandbox-pointer fence lives (session_states generation column vs the existing turn counter vs the Redis owner claim) — see open-questions.md #3; (3) whether Tier 2 should ship at all before Tier 1's real second-turn latency is measured on E3. Everything else in plan.md is ready to implement once these are settled. |
There was a problem hiding this comment.
Actionable comments posted: 5
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 62da98bc-f563-4773-a4f5-c26e6426585e
📒 Files selected for processing (6)
docs/design/agent-workflows/projects/warm-daytona-sessions/README.mddocs/design/agent-workflows/projects/warm-daytona-sessions/context.mddocs/design/agent-workflows/projects/warm-daytona-sessions/open-questions.mddocs/design/agent-workflows/projects/warm-daytona-sessions/plan.mddocs/design/agent-workflows/projects/warm-daytona-sessions/research.mddocs/design/agent-workflows/projects/warm-daytona-sessions/status.md
| 6. The reaper cascade is the sweeper, and the orphan budget is a decision. Once a sandbox | ||
| survives turn end, Daytona's own timers reap abandonment: autoStop (5 min) stops a warm | ||
| sandbox, autoArchive (15) colds it, autoDelete (30) deletes it. No runner cron is needed. | ||
| Two open points for a billing owner: | ||
| - Crash orphans. `ephemeral: true` auto-deleted on stop, so a crashed runner leaked little. | ||
| Now a SIGKILL'd runner leaves a running sandbox billing compute for up to 5 idle minutes, | ||
| then storage until autoDelete. Note the API-side `orphan_sweep.py` does NOT help here: it | ||
| only cleans Postgres rows and Redis locks and never contacts Daytona. If the 5-minute | ||
| compute budget is unacceptable, the options are a lower `DAYTONA_AUTOSTOP` or a new | ||
| provider-side sweeper; do not lean on the existing sweep task. | ||
| - Auto-stop can fire mid-turn. Daytona's inactivity clock resets on external API | ||
| interactions, not on processes running inside the sandbox. A long silent tool call or an | ||
| unanswered approval gate can outlast a 5-minute autoStop and the sandbox stops under a live | ||
| turn. The old default was 15. Compare the autoStop value against the runner's maximum | ||
| silent interval (the 300s run-limits guard is one bound) before keeping 5. |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift
Make the auto-stop decision a Tier 1 enablement blocker.
With a 5-minute auto-stop and a 300-second maximum silent interval, Daytona can stop a still-live tool call or approval wait. Before enabling Tier 1, either set auto-stop above the maximum silent interval with margin or add a reliable keepalive mechanism and test this boundary explicitly.
| 7. Stale stored-id hygiene. When autoDelete reaps a sandbox, `session_states.sandbox_id` goes | ||
| stale. The ladder degrades gracefully (failed get, fresh create), but the doomed reconnect | ||
| repeats every turn. Clear the stored id on a failed reconnect, conditionally (see item 3). |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
Do not clear the stored ID for every reconnect failure.
Only confirmed terminal cases such as not-found, deleted, or unrecoverable error states should clear the pointer. Transient API failures and transitional-state timeouts should retain it for retry; otherwise one temporary failure forces a cold create and can overwrite a still-recoverable sandbox.
| 4. Eviction wired to the lifecycle, with awaited teardown. On TTL expiry or LRU eviction, a | ||
| Daytona entry stops the sandbox (drop to Tier 1 warm) rather than deletes it, so the session | ||
| can still reconnect after the live window closes. And unlike the local pool's fire-and-forget | ||
| LRU destroy (fine for a soft RAM cache), the Daytona pool must await the stop before | ||
| admitting a replacement, or the cap does not actually bound running sandboxes: the old one | ||
| still bills while the new one starts, and a failed stop exceeds the cap indefinitely. A | ||
| failed stop escalates to delete. |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift
Bound and verify stop-before-replacement.
Awaiting stop() is insufficient if the provider call hangs or returns before the sandbox reaches stopped. Add a deadline, poll/reconcile the actual state, and admit a replacement only after the old sandbox is stopped or deleted; otherwise MAX_RUNNING cannot reliably cap billed sandboxes.
| | State | How reached | Disk | Billing | Resume cost | | ||
| |---|---|---|---|---| | ||
| | Running (hot) | create, or start a stopped one | live | full compute | none, already up | | ||
| | Stopped (warm) | `pauseSandbox()` / autoStop after 5 min idle | retained | storage only, no compute | fast start (about 1s) + remount + session load | | ||
| | Archived (cold) | autoArchive after 15 min idle | moved to cold storage | cheaper cold storage | slower restore + remount + load | | ||
| | Deleted (dead) | autoDelete after 30 min idle, or `destroySandbox()` | gone | none | full cold create + mount + replay | | ||
|
|
||
| Tier 1 parks to Stopped: storage cost only, and the autoArchive then autoDelete cascade bounds | ||
| even that. Tier 2 parks to Running for a short TTL: the parked cost is live compute, so the TTL | ||
| is a direct billing knob, and the pool cap bounds the concurrent spend. |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Show the relevant section with line numbers.
sed -n '170,220p' docs/design/agent-workflows/projects/warm-daytona-sessions/research.md
printf '\n---\n'
# Search for measurement / latency claims in the document.
rg -n "measure|measured|latenc|resume cost|about 1s|session/load|mount" docs/design/agent-workflows/projects/warm-daytona-sessions/research.mdRepository: Agenta-AI/agenta
Length of output: 5295
🏁 Script executed:
#!/bin/bash
set -euo pipefail
sed -n '130,160p' docs/design/agent-workflows/projects/warm-daytona-sessions/research.mdRepository: Agenta-AI/agenta
Length of output: 2006
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Look for any explicit "Phase 3" measurement requirement or latency estimate caveats nearby.
rg -n "Phase 3|measure|measured|estimate|about 1s|resume|restart path|latency" docs/design/agent-workflows/projects/warm-daytona-sessions/research.mdRepository: Agenta-AI/agenta
Length of output: 1374
Qualify the “about 1s” resume estimate
The Stopped (warm) row presents a concrete resume latency, but the surrounding text doesn’t show an end-to-end measurement for stop/start + remount + session/load. Mark it as an estimate or remove the number until that path is measured.
| ## Open questions | ||
|
|
||
| See `open-questions.md`. The three that gate shipping: the orphan compute budget and autoStop | ||
| value (billing owner), the pointer-fencing design (reviewer), and the Tier 2 TTL and running | ||
| cap (billing owner). |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Separate Tier 1 blockers from Tier 2 prerequisites.
The status currently lists the Tier 2 TTL and running cap as a shipping gate, while plan.md explicitly defers Tier 2 and recommends enabling Tier 1 first. Split this into “Tier 1 enablement blockers” and “Tier 2 prerequisites” so approval of the first rollout is unambiguous.
…cabulary, navigation)
mmabrouk
left a comment
There was a problem hiding this comment.
Navigation aids for the rewrite. These inline notes mark where to start, what each key section answers, and the one or two decisions that need a human. The design and every technical claim are unchanged from the previous version; only the structure, vocabulary, and ordering moved.
| - **Park a sandbox:** stop it but keep its disk, so the next turn can restart the same one instead | ||
| of rebuilding it. This is the whole idea behind the project. | ||
|
|
||
| ## Read the files in this order |
There was a problem hiding this comment.
Start reading here. This lists the five files in order and says what each one answers, so you can stop at the first that gives you what you need.
| @@ -0,0 +1,111 @@ | |||
| # Context: warm and resumable Daytona sessions | |||
|
|
|||
| ## What a user sees today | |||
There was a problem hiding this comment.
The story starts here: what a user actually experiences today (every Daytona turn waits about twenty seconds), before any mechanism or jargon.
| The net effect is a full rebuild on every turn, exactly what F-020 reported. `research.md` | ||
| walks the code that proves each step. | ||
|
|
||
| ## The key finding |
There was a problem hiding this comment.
This section answers the 'so what is actually wrong' question in one paragraph: the warm-reuse code is already written, and only two missing Daytona functions plus a few gaps block it.
| reconnect that starts the old instance and then fails a later step (daemon, URL, or client | ||
| setup) leaves it running while the runner builds a second one. | ||
|
|
||
| ### The gap that defeats it all: the Daytona provider has no pause or reconnect |
There was a problem hiding this comment.
This is the code-level evidence for that key finding. It names the exact three consequences of the two missing provider functions.
| needs a compatibility check and a guard against racing writes. Those are the must-fix items under | ||
| park-to-stopped below. | ||
|
|
||
| ## The two levels of reuse |
There was a problem hiding this comment.
The proposal in one screen: park-to-stopped (cheaper, ships first) and park-to-running (pricier, deferred). Read just this if you only want the shape.
| `session_states.sandbox_id`, and the reconnect ladder at the start of a run. | ||
| - `patches/sandbox-agent@0.4.2.patch`: the native session reload and the local process-group kill. | ||
|
|
||
| ### Must-fix before it can be turned on |
There was a problem hiding this comment.
This section answers why the recommendation is 'flag off, then turn on after a live test' and not 'on by default'. These are the correctness gaps the design review found.
| - Fidelity: highest. It is the only level that can hold an open approval gate for byte-exact | ||
| resume, though holding a gate open is a separate concern (F-018) and is not needed for chat. | ||
|
|
||
| ## Recommendation |
There was a problem hiding this comment.
This is the decision to weigh in on: ship park-to-stopped behind a default-off flag now, and defer park-to-running until a billing owner sets its cost limits.
| owner owns the cost ones. Each names the phase it gates. The three that gate shipping at all are | ||
| the abandonment budget, the pointer write guard, and the park-to-running cost limits. | ||
|
|
||
| 1. **Abandonment compute budget** (Phase 2, billing owner). With `ephemeral: false` and a real |
There was a problem hiding this comment.
This is the decision that most needs you or a billing owner. With the sandbox no longer auto-deleted on stop, a crashed runner can bill compute for up to five minutes. Acceptable, or should the stop timer drop?
…slices, park-to-running in scope Claude-Session: https://claude.ai/code/session_018MaXPNpvzN22kngHno3VMj
Reading guide for this revisionReading order: What changed since the version you last saw:
Open for you (inline comments below mark the exact lines): the pointer-write guard location, the shutdown split, saturation behavior of the capacity gate (queue or reject), and two defaults to confirm (15-minute stop timer; 60-second window with cap 4). |
| measured restore from archive takes 33 to 66 seconds, slower than the 1.2 to 1.7 second | ||
| fresh create, and the disk it frees costs under a tenth of a cent per hour. The ladder is | ||
| stop, then delete. | ||
| - Set the stop timer to 15 minutes, not 5. Daytona's idle clock resets on external API calls, |
There was a problem hiding this comment.
Decision: stop timer 15 minutes (was 5). Cost of the longer timer is about 4 cents per crash orphan versus 1.4; the gain is never stopping a sandbox under a live silent turn. Confirm 15, or pick another value.
| - `AGENTA_RUNNER_DAYTONA_SESSION_KEEPALIVE_ENABLED` (default off; flipped on after | ||
| verification). Independent of the park-to-stopped flag: disabling the live pool must not | ||
| disable stopped reuse. | ||
| - `AGENTA_RUNNER_DAYTONA_SESSION_IDLE_TTL_MS` (default 60000; at $0.0028 per parked minute this |
There was a problem hiding this comment.
Decision: the park-to-running defaults. Window 60 seconds (about a third of a cent per parked turn), cap 4 (worst case about $0.67/hour). Both env-tunable later, so this only sets the shipping default.
|
|
||
| ### The work | ||
|
|
||
| 1. **Capacity admission before anything starts a Daytona sandbox.** The pool alone cannot enforce |
There was a problem hiding this comment.
Decision: when the cap is saturated by active turns, queue briefly or reject with a clear error? Queue is friendlier, reject is simpler and easier to observe. open-questions.md item 3.
| decisions: the defaults above are conservative enough to ship, and an operator can tighten or | ||
| widen them without a code change. | ||
|
|
||
| 4. **Eviction wired to the lifecycle, awaited.** When the window expires or the admission gate |
There was a problem hiding this comment.
Note the disposition here: pool eviction stops the sandbox (drops it to park-to-stopped), never deletes it, and is awaited so the cap holds. Object here if you want evictions to delete instead.
| calls, custom-tool authorization at the execution relay, and a two-lifetime timeout. Its resume | ||
| model is the contract this plan follows, not a thing this plan redefines: | ||
|
|
||
| - **Cold path (its default, and ours below park-to-running):** when a turn pauses on a pending |
There was a problem hiding this comment.
The approval rule bound to the F-018 gate plan: below park-to-running, a pending approval always ends the turn and re-asks the model next turn (the stored decision answers it). Byte-exact gate holding waits for park-to-running plus the gate plan's file-transport parked-gate variant. Confirm this sequencing is acceptable.
|
|
||
| ## Still open (correctness, reviewer) | ||
|
|
||
| 1. **How to guard the sandbox pointer write** (Slice 1). `writeSandboxId` is a fire-and-forget, |
| for local sessions? Related: PR #5197's own risk list flags the same missing concurrency guard | ||
| on its durable-continuity write, so one guard mechanism should probably cover both writes. | ||
|
|
||
| 2. **Shutdown policy** (Slice 2). The plan splits shutdown (SIGTERM) by state: delete when a turn |
There was a problem hiding this comment.
Reviewer decision 2: shutdown split. Delete when a turn is in flight (partial transcript), stop when idle; /kill stays a hard delete. Confirm.
| It is a per-process map from a pool key to a live session, with a size cap and idle reaping. The | ||
| facts that bear on extending it to Daytona: | ||
|
|
||
| - The pool key is `<projectId>:<sessionId>`. Without a project scope there is no parking, which is |
There was a problem hiding this comment.
Slight question: where does this Project 3D come from? I had a previous version where it came with a mount, but I wanted to refactor this. I just wanted to know whether it's still the case.
There was a problem hiding this comment.
It is still both sources, in order. The preferred source is the project id the services layer stamps on the run context; the mount's owning project is only the fallback when that is missing. I clarified the sentence in research.md so the source is stated where the key is introduced.
Update: the full where-does-the-time-go measurement is now in the PRThe earlier revision had only the sandbox-lifecycle numbers (create ~1.5 s, start from stopped ~0.8 s), which told you the sandbox is cheap but not where the fifteen seconds actually goes. This push adds the full stage split from three real E3 chat runs plus micro-measurements inside the runner container, and recosts both park levels from it. What changed:
Inline comments on this commit mark the stage table and the two savings statements so you can react line by line. |
| The cold Daytona turn took about 15 seconds of client wall time (16.6, 14.9, and 15.5 seconds | ||
| across the three runs; F-020's earlier QA runs saw about 20). The stage split: | ||
|
|
||
| | Stage of a cold Daytona Pi turn | Measured | |
There was a problem hiding this comment.
The measured stage split for a cold Daytona Pi turn (three real E3 runs, 2026-07-11). The two big rows are the redundant Pi install (skip already live on the dev sidecar) and the harness spawn (~2 s of it is pi-acp probes, removal in PR #5221).
| EU sandbox: two `pi --version` calls at ~750 ms each and one `npm view` at ~305 ms. Their | ||
| removal is planned in PR #5221. The remaining ~3,150 ms is estimated (not separately logged): | ||
| Pi process start, the ACP handshake, and workspace and probe round-trips. | ||
| - **Net Daytona-specific pipeline: ~12.3 seconds.** About 7.2 seconds of it (install skip plus |
There was a problem hiding this comment.
The headline: ~12.3 s of the ~15 s turn is our pipeline, and ~7.2 s of that falls to the two config/patch fixes independent of this plan. The lifecycle work in this plan competes with the ~5 s remainder.
| Daytona is currently blocked before harness start (auth), so no Daytona Claude timing exists | ||
| yet. | ||
|
|
||
| What this means for the two park levels, stated once here and reflected in `plan.md`: |
There was a problem hiding this comment.
The recosted savings, from measurement: park-to-stopped saves about 1 second (a stopped restart still pays the ~5.15 s harness respawn plus mounts); park-to-running takes the turn to an estimated 2 to 3 seconds. This is the strongest evidence for your call to not defer park-to-running.
| Goal: at a clean turn end, stop the Daytona sandbox instead of deleting it; on the next turn, | ||
| start the same instance and reload the harness session. The parked cost is disk storage only. | ||
|
|
||
| The measured stage split (research.md, "Where the time goes") bounds its latency win honestly: |
There was a problem hiding this comment.
Park-to-stopped's win, restated from measurement: about 1 second out of a ~10 s pipeline once the install skip is live. Its value is as the eviction target and correctness base, not as a latency fix. React here if that changes how much of slices 1-2 you want to fund.
| Goal: keep the Daytona sandbox running with its live session between turns for a short window, so | ||
| a second turn is near-instant (no start, no remount, no daemon or harness startup, no reload). | ||
| This is the only level that removes the pipeline rather than shaving the sandbox-create second | ||
| off it: a resumed turn is the model time plus small overheads, roughly 2 to 3 seconds against |
There was a problem hiding this comment.
Park-to-running's target number: a resumed turn at roughly 2 to 3 seconds against today's ~15 s, estimated from the stage split and confirmed in Slice 5.
| `DAYTONA_TARGET`, which also invalidate a stopped sandbox. On a mismatch, delete the old | ||
| sandbox and build fresh. | ||
|
|
||
| 3. **A write guard on the sandbox pointer.** `writeSandboxId` is a fire-and-forget, |
There was a problem hiding this comment.
I honestly don't understand this point: this fire-and-forget, last-writer-wins, and go. I really don't have the context to make a decision here. When does this happen? What are the trade-offs of building this fix, etc., etc.? I remember from the F-018 PR comment that this is supposed to be quite complex and requires re-architecting, so I really want to understand its requirements before jumping to an implementation.
There was a problem hiding this comment.
Rewrote this item so it stands alone (plan.md must-fix item 3). Short version: the runner records which sandbox belongs to a conversation in a database row. It bites when two turns race on one conversation (a second message before the first turn finishes, or cancel and immediately re-send): the older write can land last and point the row at a dying sandbox. The cost is one doomed reconnect next turn plus an orphaned sandbox that bills cents until the timers reap it. The fix is small: wait for the write and add a compare-and-set so an older turn cannot overwrite a newer one. It is plumbing in one function plus one API check, not the re-architecture the F-018 gate needed. The only open choice is which counter to compare against; open-questions item 1 now lists the three candidates with trade-offs.
|
|
||
| 6. **The idle timers are the sweeper, with measured defaults.** Once a sandbox survives turn end, | ||
| Daytona's own timers reap abandonment. Two settings change from the current working-tree values: | ||
| - Configure the archive step out by setting `DAYTONA_AUTOARCHIVE` strictly greater than |
There was a problem hiding this comment.
From the research above, we will never ever use AutoArchive. It doesn't make sense to have the AutoArchive, so we should remove this whole logic.
There was a problem hiding this comment.
Done. The plan now removes the auto-archive logic entirely: the create call drops the autoArchiveInterval field and the DAYTONA_AUTOARCHIVE override with it. Daytona's own archive default is 7 days, far past our 30-minute delete, so archive can never fire.
| measured restore from archive takes 33 to 66 seconds, slower than the 1.2 to 1.7 second | ||
| fresh create, and the disk it frees costs under a tenth of a cent per hour. The ladder is | ||
| stop, then delete. | ||
| - Set the stop timer to 15 minutes, not 5. Daytona's idle clock resets on external API calls, |
There was a problem hiding this comment.
I think it makes sense. We just need to make sure to have these editable as environment variables at the end of the day. Even if we don't do the plumbing right now end to end, it needs to be easy later to expose these and modify these as you wish.
There was a problem hiding this comment.
Recorded as confirmed: 15 minutes. It is already an env override (DAYTONA_AUTOSTOP, whole minutes) and stays one; the delete timer (DAYTONA_AUTODELETE) too. The plan text now says all timer values stay env-overridable.
| Goal: keep the Daytona sandbox running with its live session between turns for a short window, so | ||
| a second turn is near-instant (no start, no remount, no daemon or harness startup, no reload). | ||
| This is the only level that removes the pipeline rather than shaving the sandbox-create second | ||
| off it: a resumed turn is the model time plus small overheads, roughly 2 to 3 seconds against |
There was a problem hiding this comment.
What is the smell overhead? Why do we have small overhead if it's already running?
There was a problem hiding this comment.
Named them in the text now. Even with the sandbox up, a turn still pays the hop through the services layer (measured 100 to 220 ms locally), the pool checkout with its validation (config and history fingerprints, credential expiry), and forwarding the prompt over the open session. That plus the model's own generation time is the 2 to 3 seconds.
…ained decisions Claude-Session: https://claude.ai/code/session_018MaXPNpvzN22kngHno3VMj
Update: review comments addressedAll fifteen of your comments are answered in their threads, and the documents changed accordingly. The substance:
Also applied your two writing corrections to the passages touched in this pass: provenance and status narration moved out of the document bodies (they live in these comments), and changes are just made in place with an inline comment at the spot. |
| - A new generation column on `session_states`. Cleanest semantics, needs a schema migration. | ||
| - The Redis session-owner claim PR #5197 added. No schema change and already per-session, | ||
| but Redis contents can be flushed, and a flushed key silently drops the guard. | ||
| Related: PR #5197's own durable-continuity write has the same unguarded pattern, so one |
There was a problem hiding this comment.
5197 has been already merged. and yes it makes sense let\s go with the simple solution that does not require any schema migration
| save the next turn about a second but carries that risk. | ||
| - Idle: stop, or delete? Stop means a rolling deploy keeps the parked disks, and the next | ||
| turn restarts one instead of rebuilding. Delete is simpler and forces the next turn cold. | ||
| The plan proposes delete for mid-flight, stop for idle, and `/kill` keeps its hard-delete |
| meaning. The cost difference is about a second per affected conversation either way; the | ||
| proposal favors correctness for mid-flight and warmth for idle. Confirm or override. | ||
|
|
||
| 3. **Does the session reload behave identically on a reattached same-instance sandbox?** With the |
…ovider pools, park-to-running, verified live Completes the warm-daytona-sessions plan (PR #5214) on top of the slice-1 correctness base. - Slice 2 (park-to-stopped active): clean resumable Daytona turns and idle shutdown stop the sandbox instead of deleting it; stop timer default 15 minutes; auto-archive removed entirely (create field, env override, compose and helm forwarding); terminal-vs-transient reconnect classification (only not-found/error/destroyed/unknown clear the stored pointer, guarded); clean remount before every remote mount so a reattached sandbox never serves a stale FUSE mount. - Slice 3 (provider-aware pools): one SessionPool per provider; local env vars and behavior unchanged; Daytona configured by AGENTA_RUNNER_DAYTONA_SESSION_IDLE_TTL_MS (0 disables, no separate flag) and AGENTA_RUNNER_DAYTONA_SESSION_MAX_WARM (default 20); pool evictions carry typed teardown reasons; unknown providers fail closed to cold. - Slice 4 (park-to-running warm slots): strict awaited capacity for the Daytona pool (a stopping sandbox keeps its warm slot until the stop confirms, so the billed count cannot overshoot); overflow parks to stopped; one-time activity refresh at live park, no recurring pings. - Slice 5: per-stage [timing] log lines in acquire; Daytona live-window default 120000 ms after the E3 gate passed; the TTL env parse accepts 0 as a valid off switch (a positive-only parse would silently re-enable the default). - Docs and config: the two new env vars forwarded in compose, env examples, and helm; self-host pages updated to the three-state lifecycle. - Live E3 verification: cold 12.5 s, live-warm 1.39 s, stopped-restart 7.7 s; one instance served all three turns; expiry observed as stop; SIGTERM drain observed as stop; zero sandbox leaks. Requires alembic migration core_oss oss000000011 (nullable session_states.sandbox_fingerprint). Carries shared-file hunks from the unmerged lanes feat/keepalive-project-scope (protocol.ts runContext scope and pool key preference) and fix/sessions-continuity-review (models.py replica_id validation, mdx edits) per the coordination board's shared-file rule. Claude-Session: https://claude.ai/code/session_018MaXPNpvzN22kngHno3VMj
…every turn (#5225) * feat(runner): warm daytona sessions slice 1 - provider pause/reconnect, pointer guard, typed teardown Slice 1 of the warm-daytona-sessions plan (PR #5214): the correctness base, with parking still inert (every teardown reason still deletes). - daytona provider wrapper adds pause/reconnect with a bounded state machine over Daytona transitional states; deleteSandbox helper; create-spec fingerprint (snapshot/image/target/env NAMES/network policy). - sandbox-agent package patch: a failed pause keeps the provider handles so the delete fallback works; a reconnected sandbox that fails later attach steps is paused, not leaked. - sessions API: nullable session_states.sandbox_fingerprint column; the sandbox pointer write is guarded by a compare-and-set on data.latest_turn_index (sandbox_turn_index token; tokenless writes keep today's behavior). - runner: pointer write is awaited, carries the fingerprint and guard token, and moved after continuity hydrate so a cold restart sends the right token; reconnect only on fingerprint match, best-effort delete on mismatch. - typed teardown reasons replace the keepWarm boolean; reason-to-disposition mapping with PARK_CLEAN_RESUMABLE_TURNS=false (Slice 2 flips it). Claude-Session: https://claude.ai/code/session_018MaXPNpvzN22kngHno3VMj * feat(runner): warm daytona sessions, slices 2-5 - park-to-stopped, provider pools, park-to-running, verified live Completes the warm-daytona-sessions plan (PR #5214) on top of the slice-1 correctness base. - Slice 2 (park-to-stopped active): clean resumable Daytona turns and idle shutdown stop the sandbox instead of deleting it; stop timer default 15 minutes; auto-archive removed entirely (create field, env override, compose and helm forwarding); terminal-vs-transient reconnect classification (only not-found/error/destroyed/unknown clear the stored pointer, guarded); clean remount before every remote mount so a reattached sandbox never serves a stale FUSE mount. - Slice 3 (provider-aware pools): one SessionPool per provider; local env vars and behavior unchanged; Daytona configured by AGENTA_RUNNER_DAYTONA_SESSION_IDLE_TTL_MS (0 disables, no separate flag) and AGENTA_RUNNER_DAYTONA_SESSION_MAX_WARM (default 20); pool evictions carry typed teardown reasons; unknown providers fail closed to cold. - Slice 4 (park-to-running warm slots): strict awaited capacity for the Daytona pool (a stopping sandbox keeps its warm slot until the stop confirms, so the billed count cannot overshoot); overflow parks to stopped; one-time activity refresh at live park, no recurring pings. - Slice 5: per-stage [timing] log lines in acquire; Daytona live-window default 120000 ms after the E3 gate passed; the TTL env parse accepts 0 as a valid off switch (a positive-only parse would silently re-enable the default). - Docs and config: the two new env vars forwarded in compose, env examples, and helm; self-host pages updated to the three-state lifecycle. - Live E3 verification: cold 12.5 s, live-warm 1.39 s, stopped-restart 7.7 s; one instance served all three turns; expiry observed as stop; SIGTERM drain observed as stop; zero sandbox leaks. Requires alembic migration core_oss oss000000011 (nullable session_states.sandbox_fingerprint). Carries shared-file hunks from the unmerged lanes feat/keepalive-project-scope (protocol.ts runContext scope and pool key preference) and fix/sessions-continuity-review (models.py replica_id validation, mdx edits) per the coordination board's shared-file rule. Claude-Session: https://claude.ai/code/session_018MaXPNpvzN22kngHno3VMj * feat(runner): drop sandbox fingerprint, converge network policy at reconnect Trust the stored sandbox pointer instead of comparing a create-spec fingerprint. A resumed Daytona turn reconnects the parked instance by id. Snapshot, image, and target drift is accepted as per-conversation version pinning. Network policy is converged at reconnect: read the live networkBlockAll/networkAllowList and call updateNetworkSettings only when they differ from the run's plan. Per-turn env sync is deferred to the daytona-secret-delivery direction (#5223). Removes the session_states.sandbox_fingerprint column and migration oss000000011, the createSpecFingerprint/buildResolvedDaytonaCreate helpers, and the fingerprint fields on the pointer read/write. Keeps the turn-index compare-and-set guard on sandbox_id, which uses only pre-existing columns. Claude-Session: https://claude.ai/code/session_018MaXPNpvzN22kngHno3VMj * fix(runner): service CodeRabbit review on warm daytona sessions - Gate the sandbox pointer write on the Daytona plan so a local run cannot overwrite a conversation's remote pointer. - Hydrate the continuity store before the guarded pointer clear so a post-restart clear carries the durable turn index. - pause() throws for the error sandbox state so teardown falls back to delete instead of reporting a parked sandbox with a stale pointer. - Catch live-park activity-refresh failures; the hook is best-effort. - Dedupe writeSandboxPointer/clearSandboxPointer into one guarded PUT. - Assert status_code in the tokenless pointer acceptance test. - ruff format on the warm_daytona_probe QA script (CI red). Claude-Session: https://claude.ai/code/session_018MaXPNpvzN22kngHno3VMj * fix(hosting): restore the MCP gate default to off The branch flipped AGENTA_AGENT_MCPS_ENABLED from false to true across the compose files, env examples, and the helm comment. The flip is unrelated to warm Daytona sessions and contradicts the SDK gate and the helm template, which both default off. Restore false everywhere. Claude-Session: https://claude.ai/code/session_018MaXPNpvzN22kngHno3VMj --------- Co-authored-by: Mahmoud Mabrouk <team@agenta.ai>
What a user sees today
When you chat with an agent that runs on Daytona, every message waits about twenty seconds before the agent starts to answer. The answer itself is correct, because durable continuity replays the conversation into a fresh sandbox, but every turn pays the full setup time. This is QA finding F-020.
What we measured (new in this revision)
A credit-controlled measurement against the Daytona API (2026-07-11, two repetitions, all sandboxes deleted afterwards) changed the plan's shape:
Full numbers and prices are in
research.md.What this PR proposes
One progressive sequence of five slices, each shippable on its own, ending with warm running sandboxes:
session-pool.ts) gets per-provider configuration and typed teardown reasons instead of the local-only gate. Local behavior is preserved; no second pooling mechanism.Pending approvals follow the F-018 gate plan's resume model (
daytona-gate-delivery/): below park-to-running a pending approval always takes the cold path, because a stopped sandbox kills the waiting process.What the design reviews changed
Two adversarial review rounds ran. Round 1 found the teardown leaks and races now in the must-fix list. Round 2 reviewed the five-slice reshape and found two design properties that were false as drafted, both fixed: the pool alone cannot enforce the running cap (fresh misses create sandboxes before the pool sees them; the cap now lives in an admission gate taken before creation), and a single eviction disposition cannot implement the teardown matrix (teardown reasons are now typed).
status.mdrecords both rounds in full.Decisions that still need you
open-questions.mditem 1).How to read it
Start with
README.md(reading order), thencontext.mdfor the story. The inline comments on this PR mark the exact decision points.https://claude.ai/code/session_018MaXPNpvzN22kngHno3VMj