{"record":{"id":"3c9147512ca137ec","repo":"paperclipai/paperclip","slug":"acpx-provider-ownership-admission-is-closed","errorCode":null,"errorMessage":"ACPX provider ownership admission is closed","messagePattern":"ACPX provider ownership admission is closed","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/paperclip-runner/src/drivers/acpx/codex-runtime-adapter.ts","lineNumber":1501,"sourceCode":"        const ownership = this.#lifetimeOwnership.splice(0);\n        if (ownership.length === 0) {\n          // This check and seal are synchronous. Any spawn added while an\n          // earlier batch was pending is observed by the next loop iteration;\n          // no later provider can race admission after the stable-empty point.\n          this.#lifetimeOwnershipSealed = true;\n          return;\n        }\n        await Promise.all(ownership);\n      }\n    } catch (error) {\n      this.#lifetimeOwnershipSealed = true;\n      throw error;\n    }\n  }\n\n  beginLifetimeOwnershipAdmission(): () => Promise<void> {\n    if (this.#sealed) {\n      throw new Error(\"ACPX provider ownership admission is closed\");\n    }\n    if (!this.#lifetimeOwnershipSealed) {\n      throw new Error(\"ACPX provider ownership admission is already active\");\n    }\n    this.#lifetimeOwnershipSealed = false;\n    let finished = false;\n    return async () => {\n      if (finished) return;\n      finished = true;\n      await this.verifyLifetimeOwnership();\n    };\n  }\n\n  #track(child: ChildProcess, providerExit: ProviderExitObservation): void {\n    this.#children.add(child);\n    const onError = (error: unknown) => this.#errors.add(error);\n    let guardianExited = !running(child);\n    let providerExited = false;","sourceCodeStart":1483,"sourceCodeEnd":1519,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/packages/paperclip-runner/src/drivers/acpx/codex-runtime-adapter.ts#L1483-L1519","documentation":"SpawnedChildSet.beginLifetimeOwnershipAdmission() opens a window in which newly spawned ACPX provider children can be tracked and verified. The seal flags enforce a strict lifecycle: admission may only begin after a previous admission round was sealed via verifyLifetimeOwnership(), and never after the set was fully sealed for cleanup. Calling it while the set is #sealed (post-cleanup) throws 'ACPX provider ownership admission is closed'. It is a lifecycle invariant protecting against spawning provider work after teardown has begun.","triggerScenarios":"startTurn() invokes children.beginLifetimeOwnershipAdmission() after the child set was sealed for cleanup — i.e. a turn is started on a runtime whose cleanup/seal already ran.","commonSituations":"A queued or retried turn racing with runtime shutdown/cleanup; calling startTurn after the runtime was closed or the runner was torn down; a double-shutdown path sealing the set while work is still being submitted.","solutions":["Do not call startTurn after runtime cleanup has begun; check the runtime's closed/sealed state before submitting turns.","Serialize turn submission against shutdown so startTurn cannot race the seal (queue or reject turns before cleanup starts).","Re-create the runtime/child set if work must continue after a teardown; the sealed set cannot be reopened.","Audit error/retry paths that may re-submit turns after the runtime's close() was invoked."],"exampleFix":"// before\nawait runtime.close();\nawait port.startTurn({ text: \"one more\" }); // throws: admission closed\n// after\nif (runtime.isClosed()) {\n  throw new Error(\"runtime already torn down; spawn a new session\");\n}\nawait port.startTurn({ text: \"one more\" });","handlingStrategy":"try-catch","validationCode":"if (runtime.isClosed || runtime.isSealed) {\n  throw new Error(\"runtime teardown already started; create a new session before starting turns\");\n}","typeGuard":null,"tryCatchPattern":"try {\n  const finishAdmission = children.beginLifetimeOwnershipAdmission();\n  // ... spawn/turn work ...\n  await finishAdmission();\n} catch (e) {\n  if (e.message === \"ACPX provider ownership admission is closed\" ||\n      e.message === \"ACPX provider ownership admission is already active\") {\n    throw new Error(\"cannot start turns during/after runtime cleanup\");\n  }\n  throw e;\n}","preventionTips":["Serialize startTurn calls against runtime.close()/cleanup with a mutex or state flag.","Reject new turns as soon as shutdown begins instead of letting them race the seal.","Never start turns on a runtime after observing any cleanup-sealed error; respawn instead.","In retries, always obtain a fresh runtime/session rather than reusing a torn-down one."],"tags":["lifecycle","invalid-state-transition","acpx","concurrency"],"backgroundTag":"invalid-state-transition","analyzedSha":"01ad8584922b5d85292b1723cae71fa0d9b07a19","analyzedAt":"2026-09-10T03:14:50.855Z","contentChangedAt":"2026-09-10T03:14:50.855Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}