From c26d76485771a0ef0aa0600071be275e83c70659 Mon Sep 17 00:00:00 2001 From: Max Date: Sun, 29 Mar 2026 23:01:21 +0800 Subject: [PATCH] fix(sandbox/v2): oneshot containers never cleaned up by watcher MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three related bugs caused oneshot containers to run indefinitely: 1. watcher.go: switch b.policy had no case OneShot, so even when the idle timeout fired, no remove action was emitted. 2. watcher.go + manager.go/recoverBoxes: idleTimeoutD was only set for Session and LongRunning on recovery; OneShot defaulted to 0, which caused the watcher to hit the `timeout <= 0` early-return and skip all checks entirely. 3. agent/sandbox/v2/options.go: same gap — opts.IdleTimeout == 0 guard only filled defaults for Session and LongRunning. Fix: add DefaultOneShotIdleTimeout (30 min), wire it in recoverBoxes and options.go, and add case OneShot → Remove in watcher.go. Made-with: Cursor --- agent/sandbox/v2/options.go | 2 ++ sandbox/v2/manager.go | 2 ++ sandbox/v2/types.go | 1 + sandbox/v2/watcher.go | 10 ++++++++++ 4 files changed, 15 insertions(+) diff --git a/agent/sandbox/v2/options.go b/agent/sandbox/v2/options.go index 2e51f55f..801a093c 100644 --- a/agent/sandbox/v2/options.go +++ b/agent/sandbox/v2/options.go @@ -64,6 +64,8 @@ func BuildCreateOptions(cfg *types.SandboxConfig, identifier, ownerID, workspace } if opts.IdleTimeout == 0 { switch opts.Policy { + case infra.OneShot: + opts.IdleTimeout = infra.DefaultOneShotIdleTimeout case infra.Session: opts.IdleTimeout = infra.DefaultSessionIdleTimeout case infra.LongRunning: diff --git a/sandbox/v2/manager.go b/sandbox/v2/manager.go index 70ce5ebd..4d60a14c 100644 --- a/sandbox/v2/manager.go +++ b/sandbox/v2/manager.go @@ -478,6 +478,8 @@ func (m *Manager) recoverBoxes(ctx context.Context, nodeID string, res *tai.Conn manager: m, } switch policy { + case OneShot: + box.idleTimeoutD = DefaultOneShotIdleTimeout case Session: box.idleTimeoutD = DefaultSessionIdleTimeout case LongRunning: diff --git a/sandbox/v2/types.go b/sandbox/v2/types.go index 312aa54b..1974e04a 100644 --- a/sandbox/v2/types.go +++ b/sandbox/v2/types.go @@ -73,6 +73,7 @@ const ( const ( DefaultStopTimeout = 2 * time.Second + DefaultOneShotIdleTimeout = 30 * time.Minute DefaultSessionIdleTimeout = 30 * time.Minute DefaultLongRunningIdleTimeout = 2 * time.Hour ) diff --git a/sandbox/v2/watcher.go b/sandbox/v2/watcher.go index 32c89af3..2862edd2 100644 --- a/sandbox/v2/watcher.go +++ b/sandbox/v2/watcher.go @@ -86,6 +86,16 @@ func (w *sandboxWatcher) Check(ctx context.Context) []monitor.Alert { } switch b.policy { + case OneShot: + alerts = append(alerts, monitor.Alert{ + Level: monitor.Warn, + Target: "box:" + b.id, + Message: fmt.Sprintf("oneshot idle expired (idle=%s, timeout=%s), removing", idle.Round(time.Second), timeout), + Action: func(ctx context.Context) { + mgr.Remove(ctx, b.id) + }, + }) + case Session: alerts = append(alerts, monitor.Alert{ Level: monitor.Warn,