fix(sandbox/v2): oneshot containers never cleaned up by watcher

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
This commit is contained in:
Max 2026-03-29 23:01:21 +08:00
parent 8fdd1a3a6c
commit c26d764857
4 changed files with 15 additions and 0 deletions

View file

@ -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:

View file

@ -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:

View file

@ -73,6 +73,7 @@ const (
const (
DefaultStopTimeout = 2 * time.Second
DefaultOneShotIdleTimeout = 30 * time.Minute
DefaultSessionIdleTimeout = 30 * time.Minute
DefaultLongRunningIdleTimeout = 2 * time.Hour
)

View file

@ -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,