{"record":{"id":"0c3a5fad750535a2","repo":"paperclipai/paperclip","slug":"createos-lease-is-closing-or-requires-cleanup","errorCode":null,"errorMessage":"CreateOS lease is closing or requires cleanup.","messagePattern":"CreateOS lease is closing or requires cleanup\\.","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/plugins/sandbox-providers/createos/src/plugin.ts","lineNumber":82,"sourceCode":"    const config = parseConfig(params.config);\n    const account = createHash(\"sha256\").update(resolveApiKey(config)).digest(\"hex\");\n    return JSON.stringify([params.companyId, params.environmentId, config.apiUrl, account, id]);\n  }\n\n  async function stopActive(scope: string) {\n    const calls = [...(active.get(scope) ?? [])];\n    for (const call of calls) call.controller.abort();\n    await Promise.all(calls.map((call) => call.done));\n  }\n\n  async function track<T>(\n    params: PluginEnvironmentDriverBaseParams & { lease: PluginEnvironmentLease },\n    work: (client: CreateosClient, signal: AbortSignal) => Promise<T>,\n    timeoutOverride?: number,\n  ): Promise<T> {\n    if (!params.lease.providerLeaseId || !metadataMatches(params, params.lease.metadata)) throw new Error(\"CreateOS execution requires a lease from this environment.\");\n    const scope = key(params, params.lease.providerLeaseId);\n    if (shuttingDown || closing.has(scope) || unconfirmedCleanup.has(scope)) throw new Error(\"CreateOS lease is closing or requires cleanup.\");\n    const config = parseConfig(params.config);\n    const timeoutMs = timeoutOverride ?? config.timeoutMs;\n    if (!Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 86_400_000) throw new Error(\"Invalid CreateOS command timeout.\");\n    const controller = new AbortController();\n    let finish!: () => void;\n    const entry: Active = { controller, done: new Promise<void>((resolve) => { finish = resolve; }) };\n    const calls = active.get(scope) ?? new Set<Active>();\n    calls.add(entry);\n    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();","sourceCodeStart":64,"sourceCodeEnd":100,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/packages/plugins/sandbox-providers/createos/src/plugin.ts#L64-L100","documentation":"track() refuses to start new work on a lease whose scope is shutting down, already closing, or flagged as having unconfirmed cleanup. The plugin tracks per-lease lifecycle state (closing set, unconfirmedCleanup set, shutdown flag) so operations are never issued against a sandbox being destroyed or one whose earlier cleanup failed. This error means the lease is in (or heading into) a terminal state.","triggerScenarios":"Executing or syncing while: the plugin is shutting down (onShutdown ran); release() marked the scope closing and is still stopping active calls; a prior operation threw CreateosCleanupError, adding the scope to unconfirmedCleanup; concurrent release and execute race on the same lease.","commonSituations":"Host timeout/auto-pause triggering lease release while a command is still being queued; an earlier execute failed with unconfirmed cleanup and the host retries on the same lease; process shutdown overlapping in-flight agent commands; a reuseLease=false config causing immediate destroy paths to close scopes aggressively.","solutions":["Acquire a fresh lease — the old one is closing or flagged; it cannot accept new work.","If unconfirmed cleanup was flagged, call onEnvironmentDestroyLease (destroy=true) to force sandbox destruction and clear the flag, then re-acquire.","Wait for the in-flight release/destroy to finish before retrying, or serialize release and execute calls per lease in the host.","Avoid issuing execute calls after shutdown has been initiated; check the plugin lifecycle state first."],"exampleFix":"// before: blind retry against a closing lease\nawait driver.execute({ ...params, lease });\n// after: destroy the flagged lease and re-acquire\ntry {\n  return await driver.execute({ ...params, lease });\n} catch (e) {\n  if (e.message.includes(\"closing or requires cleanup\")) {\n    await driver.destroyLease({ ...params, providerLeaseId: lease.providerLeaseId, leaseMetadata: lease.metadata });\n    lease = await driver.acquireLease(params);\n    return await driver.execute({ ...params, lease });\n  }\n  throw e;\n}","handlingStrategy":"retry","validationCode":"// track a local set of lease ids you have released or that failed cleanup\nconst unusable = new Set<string>();\nif (unusable.has(lease.providerLeaseId)) {\n  lease = await acquireLease(params); // skip known-closing leases\n}","typeGuard":null,"tryCatchPattern":"try {\n  return await driver.execute({ ...params, lease });\n} catch (e) {\n  if (e.message.includes(\"closing or requires cleanup\")) {\n    await driver.destroyLease({ ...params, providerLeaseId: lease.providerLeaseId, leaseMetadata: lease.metadata });\n    const fresh = await driver.acquireLease(params);\n    return await driver.execute({ ...params, lease: fresh });\n  }\n  throw e;\n}","preventionTips":["Serialize release/destroy and execute calls per lease in the host.","After a cleanup-unconfirmed failure, destroy (not reuse) the lease.","Do not queue new commands after initiating shutdown.","Treat this error as terminal for the lease: always re-acquire rather than retry in place."],"tags":["lease","lifecycle","race-condition","sandbox"],"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"}