From 0845c331cb38c25db5265d2e1056dc3eb0fee6ec Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Thu, 20 Aug 2026 17:26:13 -0400 Subject: [PATCH] Default Daytona auto-stop to 120 minutes Omitting autoStopInterval from the create-sandbox request inherits Daytona's server-side default of 15 idle minutes. Daytona counts inactivity from the last sandbox interaction, and LLM inference never touches the sandbox, so a single long inference call is enough for the sandbox to auto-stop mid-run: a workflow failed exactly this way, with the sandbox entering its stop transition 15 minutes after the last command while the agent was still thinking. Send an explicit 120-minute default when lifecycle.auto_stop is unset. That clears any realistic inference call while still reclaiming sandboxes leaked by a dead worker. An explicit auto_stop = "0s" still disables auto-stop entirely. Co-Authored-By: Claude Fable 5 --- docs/public/execution/run-configuration.mdx | 2 +- docs/public/integrations/daytona.mdx | 4 +++ .../fabro-sandbox/src/daytona/mod.rs | 36 ++++++++++++++++++- 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/docs/public/execution/run-configuration.mdx b/docs/public/execution/run-configuration.mdx index a8f1bc927..d2ce9bd5e 100644 --- a/docs/public/execution/run-configuration.mdx +++ b/docs/public/execution/run-configuration.mdx @@ -321,7 +321,7 @@ memory = "8GB" | `network.allow` | CIDRs for `cidr_allow_list`; entries are validated as CIDRs. | | `lifecycle.preserve` | Keep the created sandbox after the run finishes. | | `lifecycle.stop_on_terminal` | Stop the sandbox when the run reaches a terminal state. | -| `lifecycle.auto_stop` | Daytona auto-stop duration, such as `"30m"`. | +| `lifecycle.auto_stop` | Daytona auto-stop duration, such as `"30m"`. Defaults to `"120m"`; `"0s"` disables auto-stop. | | `labels` | Provider labels. Merge by key across layers. | | `env` | Environment variables passed to command and agent execution. Merge by key across layers. | diff --git a/docs/public/integrations/daytona.mdx b/docs/public/integrations/daytona.mdx index 6ad19b9b3..1a2f25790 100644 --- a/docs/public/integrations/daytona.mdx +++ b/docs/public/integrations/daytona.mdx @@ -198,6 +198,10 @@ The `lifecycle.auto_stop` setting tells Daytona to stop the sandbox after a peri auto_stop = "30m" ``` +When `auto_stop` is unset, Fabro applies a default of 120 minutes so a sandbox leaked by an interrupted run is still reclaimed. Set `auto_stop = "0s"` to disable auto-stop and let the sandbox run indefinitely. + +Daytona counts inactivity from the last sandbox interaction (a command, file operation, or other API call). Time an agent spends on LLM inference does not touch the sandbox, so intervals shorter than your longest inference call risk stopping the sandbox mid-run. + ## Server defaults When running via `fabro server start`, the server config at `~/.fabro/settings.toml` can set default Daytona settings for all runs. Run config TOML values override server defaults. Labels are **merged** — run config labels win on key collisions. The `network` setting uses simple override (run config replaces the server default entirely). diff --git a/lib/components/fabro-sandbox/src/daytona/mod.rs b/lib/components/fabro-sandbox/src/daytona/mod.rs index 6371f23d2..91a0cc797 100644 --- a/lib/components/fabro-sandbox/src/daytona/mod.rs +++ b/lib/components/fabro-sandbox/src/daytona/mod.rs @@ -68,6 +68,12 @@ const DAYTONA_START_TIMEOUT: Duration = Duration::from_mins(1); /// deletion, temporary stdin files) so a stalled REST call cannot block /// cancellation/timeout paths indefinitely. const DAYTONA_CLEANUP_TIMEOUT: Duration = Duration::from_secs(10); +/// Auto-stop applied when `lifecycle.auto_stop` is unset. Omitting the field +/// would inherit Daytona's server-side default of 15 idle minutes, which is +/// shorter than a single long inference call and stops the sandbox mid-run; +/// 120 minutes clears any realistic call while still reclaiming sandboxes +/// leaked by a dead worker. An explicit `0` disables auto-stop entirely. +const DEFAULT_AUTO_STOP_INTERVAL_MINUTES: i32 = 120; /// Permissions a Daytona API key needs for Fabro's snapshot and sandbox flow. pub const REQUIRED_DAYTONA_PERMISSIONS: &[Permissions] = &[ @@ -727,7 +733,10 @@ impl DaytonaSandbox { daytona_sdk::SandboxBaseParams { name: Some(name), env_vars: Some(clean_bash_env(None)), - auto_stop_interval: self.config.auto_stop_interval, + auto_stop_interval: self + .config + .auto_stop_interval + .or(Some(DEFAULT_AUTO_STOP_INTERVAL_MINUTES)), labels: Some(managed_labels::merge_for_run( self.config.labels.as_ref(), self.run_id.as_ref(), @@ -2950,6 +2959,10 @@ mod tests { assert_eq!(params.ephemeral, Some(false)); assert_eq!(params.auto_delete_interval, Some(-1)); + assert_eq!( + params.auto_stop_interval, + Some(DEFAULT_AUTO_STOP_INTERVAL_MINUTES) + ); assert_eq!( params.env_vars, Some(HashMap::from([(BASH_ENV_VAR.to_string(), String::new())])) @@ -2963,6 +2976,27 @@ mod tests { ); } + #[tokio::test] + async fn base_params_passes_explicit_auto_stop_through() { + for interval in [0, 45] { + let sandbox = DaytonaSandbox::new( + DaytonaConfig { + auto_stop_interval: Some(interval), + ..DaytonaConfig::default() + }, + None, + None, + None, + None, + Some("dtn_test".to_string()), + ) + .await + .expect("sandbox config should be valid"); + + assert_eq!(sandbox.base_params().auto_stop_interval, Some(interval)); + } + } + #[tokio::test] async fn activate_skips_start_when_daytona_reports_started() { let server = MockServer::start_async().await;