{"record":{"id":"66d6881e7af1c461","repo":"paperclipai/paperclip","slug":"createos-lease-cleanup-is-already-in-progress","errorCode":null,"errorMessage":"CreateOS lease cleanup is already in progress.","messagePattern":"CreateOS lease cleanup is already in progress\\.","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"packages/plugins/sandbox-providers/createos/src/plugin.ts","lineNumber":109,"sourceCode":"    active.set(scope, calls);\n    try {\n      return await work(new CreateosClient(config), AbortSignal.any([controller.signal, AbortSignal.timeout(timeoutMs)]));\n    } catch (error) {\n      if (error instanceof CreateosCleanupError) unconfirmedCleanup.add(scope);\n      throw error;\n    } finally {\n      calls.delete(entry);\n      if (calls.size === 0) active.delete(scope);\n      finish();\n    }\n  }\n\n  async function release(params: PluginEnvironmentReleaseLeaseParams, destroy: boolean) {\n    const id = params.providerLeaseId;\n    if (!id) return;\n    if (!metadataMatches(params, params.leaseMetadata)) throw new Error(\"CreateOS lease does not belong to this environment.\");\n    const scope = key(params, id);\n    if (closing.has(scope)) throw new Error(\"CreateOS lease cleanup is already in progress.\");\n    closing.add(scope);\n    try {\n      await stopActive(scope);\n      const config = parseConfig(params.config);\n      const client = new CreateosClient(config);\n      if (destroy || !config.reuseLease) {\n        await client.destroySandbox(id);\n        unconfirmedCleanup.delete(scope);\n      } else {\n        if (unconfirmedCleanup.has(scope)) throw new Error(\"CreateOS process cleanup is unconfirmed; destroy this lease before reusing it.\");\n        try { await client.transition(id, \"paused\", AbortSignal.timeout(config.timeoutMs)); }\n        catch (error) { if (!(error instanceof CreateosApiError && error.status === 404)) throw error; }\n      }\n    } finally {\n      closing.delete(scope);\n    }\n  }\n","sourceCodeStart":91,"sourceCodeEnd":127,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/packages/plugins/sandbox-providers/createos/src/plugin.ts#L91-L127","documentation":"This error is thrown by the CreateOS sandbox provider's release() function when a release/cleanup of the same lease scope is already running. The provider tracks in-flight cleanups in a 'closing' set keyed by environment+lease; a second concurrent release for the same lease is rejected instead of interleaving two destroys/pauses of the same sandbox. It is a concurrency guard, not a resource failure.","triggerScenarios":"Calling release (environment teardown or plugin release-lease) twice concurrently for the same providerLeaseId — e.g. the environment stop path and a manual destroy both firing, or retrying release while the first call is still awaiting client.destroySandbox/transition.","commonSituations":"Double-teardown on shutdown (host stop + lease reaper both releasing), UI-triggered destroy racing automatic cleanup, retry logic with too-aggressive timeouts calling release again while the first request is still in flight.","solutions":["Wait for the first release to complete instead of retrying; the error is transient — once the in-flight cleanup finishes, 'closing' no longer contains the scope.","Serialize release calls per lease: queue or memoize the in-flight promise and await it rather than calling release again.","Check whether two code paths (environment shutdown and lease cleanup) can both release the same lease and ensure only one owns cleanup.","If a release appears permanently stuck, inspect network/connectivity to the CreateOS API — a hung stopActive/destroySandbox keeps the scope in 'closing'."],"exampleFix":"// before: retry immediately on failure\nawait provider.release(params).catch(() => provider.release(params));\n// after: wait for in-flight cleanup to settle before retrying\ntry { await provider.release(params); }\ncatch (e) {\n  if (String(e.message).includes('already in progress')) {\n    await new Promise(r => setTimeout(r, 1000));\n    await provider.release(params);\n  } else throw e;\n}","handlingStrategy":"retry","validationCode":"// Track in-flight releases per lease in caller code\nconst inFlight = new Map<string, Promise<void>>();\nfunction canRelease(leaseId: string): boolean { return !inFlight.has(leaseId); }","typeGuard":"function isCleanupInProgressError(e: unknown): boolean {\n  return e instanceof Error && e.message.includes('lease cleanup is already in progress');\n}","tryCatchPattern":"try {\n  await plugin.release(params);\n} catch (e) {\n  if (isCleanupInProgressError(e)) {\n    await inFlightRelease; // await the already-running cleanup\n  } else throw e;\n}","preventionTips":["Memoize the release promise per lease and reuse it for concurrent callers","Ensure only one code path owns lease teardown for an environment","Add backoff before retrying release after this error","Monitor for releases that hang (scope stuck in closing) via CreateOS API latency"],"tags":["concurrency","sandbox","cleanup","race-condition"],"backgroundTag":"invalid-state-transition","analyzedSha":"3f1d897a7c018d76563a21c6e39c3c9b03933622","analyzedAt":"2026-09-18T08:03:59.046Z","contentChangedAt":"2026-09-18T08:03:59.046Z","schemaVersion":2},"datasetVersion":"2026-09-22T11:17:16.035Z"}