From cc0bb7e161acd752f9588adb2d0c985be44f4f16 Mon Sep 17 00:00:00 2001 From: Max Date: Sun, 29 Mar 2026 23:08:50 +0800 Subject: [PATCH] fix(sandbox/v2): LifecycleAction uses computer.BoxID instead of cfg.ID MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cfg (ast.SandboxV2) is a pointer shared across all concurrent calls to the same Assistant. GetComputer writes cfg.ID = for each oneshot request, so concurrent tasks overwrite each other's ID. When cleanup runs in defer, cfg.ID holds whichever task last wrote it — not the ID of the box this request created — so manager.Remove/Heartbeat silently fails with "sandbox: not found" and the container leaks. Fix: derive the box ID from computer.ComputerInfo().BoxID, which is the immutable b.id set at Create time and never mutated by concurrent callers. Made-with: Cursor --- agent/sandbox/v2/lifecycle.go | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/agent/sandbox/v2/lifecycle.go b/agent/sandbox/v2/lifecycle.go index 8e64d832..606352a1 100644 --- a/agent/sandbox/v2/lifecycle.go +++ b/agent/sandbox/v2/lifecycle.go @@ -284,24 +284,30 @@ func resolveBox( // LifecycleAction performs the post-request lifecycle operation based on policy. // Called in defer after executeSandboxStream completes. +// +// NOTE: cfg.ID must NOT be used here — it is a shared mutable field on the +// Assistant struct and is overwritten by concurrent requests. The authoritative +// box ID is computer.ComputerInfo().BoxID, which is set once when the Box is +// created and never changes. func LifecycleAction(ctx context.Context, cfg *types.SandboxConfig, computer infra.Computer, manager *infra.Manager) { if computer == nil || cfg == nil { return } info := computer.ComputerInfo() + boxID := info.BoxID // use the box's own immutable ID, not cfg.ID switch cfg.Lifecycle { case "oneshot": if info.Kind == "box" && manager != nil { - if err := manager.Remove(ctx, cfg.ID); err != nil { - log.Trace("[sandbox/v2] oneshot remove %s: %v", cfg.ID, err) + if err := manager.Remove(ctx, boxID); err != nil { + log.Trace("[sandbox/v2] oneshot remove %s: %v", boxID, err) } } case "session", "longrunning": if info.Kind == "box" && manager != nil { - manager.Heartbeat(cfg.ID, false, 0) // active=false: request finished, start idle timer + manager.Heartbeat(boxID, false, 0) // active=false: request finished, start idle timer } case "persistent":