{"record":{"id":"901406a10c2cf65f","repo":"Tencent/WeKnora","slug":"release-stale-sandbox-binding-w","errorCode":null,"errorMessage":"release stale sandbox binding: %w","messagePattern":"release stale sandbox binding: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/sandbox/session_lifecycle.go","lineNumber":192,"sourceCode":"\t\tif !deleted {\n\t\t\treturn nil, errors.New(\"mismatched provider binding changed during replacement\")\n\t\t}\n\t\tbinding = nil\n\t}\n\n\t// A stale binding is one whose sandbox boots an image the config has since\n\t// replaced. Rebuild waits for a turn boundary: the first resolve of a new\n\t// chat turn may destroy and recreate, later resolves of that same turn\n\t// keep the sandbox so /workspace scratch and in-flight execs survive an\n\t// install that landed mid-turn. A resolve with no turn lease (no AgentQA\n\t// in flight) still rebuilds immediately.\n\t//\n\t// Destroying before rebuilding is not optional: the recovery pass below\n\t// adopts any live sandbox carrying this session's metadata, so a surviving\n\t// one would simply be picked up again.\n\tif binding != nil && binding.StaleAt != nil && l.shouldRebuildStaleBinding(ctx, key) {\n\t\tif err := l.destroyBindingLocked(ctx, key, *binding); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"release stale sandbox binding: %w\", err)\n\t\t}\n\t\tbinding = nil\n\t}\n\t// First resolve of a turn spends rebuildOnce even when nothing was stale,\n\t// so a later install in the same turn cannot tear the sandbox down.\n\tl.consumeTurnRebuild(ctx, key)\n\n\tif binding != nil {\n\t\thandle, replace, err := l.connectBinding(ctx, *binding)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif !replace {\n\t\t\treturn handle, nil\n\t\t}\n\t\tdeleted, err := l.bindings.DeleteIfMatch(\n\t\t\tctx,\n\t\t\tkey,","sourceCodeStart":174,"sourceCodeEnd":210,"githubUrl":"https://github.com/Tencent/WeKnora/blob/988cbb03305e055d8ebb7d46d9ac6cc0803cd074/internal/sandbox/session_lifecycle.go#L174-L210","documentation":"When a binding is stale (its sandbox boots an image the config has since replaced) and a rebuild is permitted at this turn boundary, resolveLocked destroys the old sandbox via destroyBindingLocked before the recovery pass re-creates/adopts one. This error wraps a failure of that destroy (provider Destroy API error, binding store error, or context cancellation). Destroying before rebuilding is required — a surviving stale sandbox would simply be re-adopted — so the lifecycle refuses to continue on failure.","triggerScenarios":"A sandbox config/template change marked existing bindings stale; the first resolve of a new turn (or a resolve with no turn lease) triggers shouldRebuildStaleBinding; the destroyBindingLocked call fails because the provider API errors or the context is cancelled mid-destroy.","commonSituations":"Rotating the sandbox image/template while sessions are live; provider API outage or rate limiting exactly at rebuild time; cleanupTimeout too short so the provider destroy times out; user cancels the request mid-rebuild.","solutions":["Retry Resolve with backoff — the rebuild will be re-attempted at the next eligible resolve.","Check the provider destroy error in the %w chain; verify the cleanupTimeout is generous enough for the provider's delete latency.","If the provider API is degraded, wait for recovery; the old stale sandbox may keep running (and billing) until destroy succeeds — check the provider console for orphans.","Avoid cancelling requests during turn-start resolves so rebuilds complete."],"exampleFix":"// before\ncleanupTimeout := 5 * time.Second // too short for provider deletes\n// after\ncleanupTimeout := 60 * time.Second // tolerate provider delete latency during stale rebuild\n// plus retry on resolve:\nfor i := 0; i < 3; i++ {\n    handle, err = lifecycle.Resolve(ctx, key)\n    if err == nil || errors.Is(err, sandbox.ErrSandboxSessionDeleted) { break }\n    time.Sleep(time.Duration(1<<i) * time.Second)\n}","handlingStrategy":"retry","validationCode":"open, canRebuild, err := store.TurnState(ctx, key)\nif err == nil && !canRebuild {\n    // stale rebuild will be deferred to next turn boundary; no destroy imminent\n}","typeGuard":null,"tryCatchPattern":"handle, err := lifecycle.Resolve(ctx, key)\nif err != nil && strings.Contains(err.Error(), \"release stale sandbox binding\") {\n    // provider destroy failed during rebuild; retry next turn boundary\n    time.Sleep(backoff)\n    handle, err = lifecycle.Resolve(ctx, key)\n}","preventionTips":["Rotate sandbox templates/images outside peak traffic so stale rebuilds have API headroom.","Set cleanupTimeout above the provider's p99 delete latency.","Don't cancel turn-start requests mid-resolve; use detached cleanup contexts.","Watch for orphaned stale sandboxes in the provider console after rebuild failures."],"tags":["sandbox","stale-binding","rebuild","provider-api"],"backgroundTag":"stale-sandbox-rebuild-failed","analyzedSha":"988cbb03305e055d8ebb7d46d9ac6cc0803cd074","analyzedAt":"2026-09-02T14:41:08.344Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}