{"record":{"id":"aa99a98c310d0e16","repo":"paperclipai/paperclip","slug":"acpx-provider-ownership-admission-is-already-activ","errorCode":null,"errorMessage":"ACPX provider ownership admission is already active","messagePattern":"ACPX provider ownership admission is already active","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/paperclip-runner/src/drivers/acpx/codex-runtime-adapter.ts","lineNumber":1504,"sourceCode":"          // 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;\n    const forgetIfReleased = () => {\n      if (!guardianExited || !providerExited) return;\n      this.#children.delete(child);","sourceCodeStart":1486,"sourceCodeEnd":1522,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/packages/paperclip-runner/src/drivers/acpx/codex-runtime-adapter.ts#L1486-L1522","documentation":"beginLifetimeOwnershipAdmission() manages a one-shot window during which the ACPX runtime may claim provider lifetime ownership. This error is thrown when the admission gate is re-entered while the previous admission cycle has not yet been sealed (i.e. #lifetimeOwnershipSealed is false, meaning an admission is already open or was reopened). The library throws to prevent two overlapping ownership-admission lifecycles from racing on the same runtime adapter instance.","triggerScenarios":"Calling beginLifetimeOwnershipAdmission() a second time on the same AcpRuntimeHandle/adapter instance before the prior admission window was completed and re-sealed (the finish callback that sets #lifetimeOwnershipSealed = true was never invoked, or begin was called concurrently). Note the guard is inverted: it fires when the sealed flag is false, so any begin() while a window is open throws.","commonSituations":"Retry/restart logic in the runner that re-begins admission after a failed startup without waiting for the previous finish callback; a supervisor calling begin() from two code paths (e.g. watchdog restart plus explicit start); an earlier admission whose finish callback was skipped because an exception occurred mid-admission, leaving the flag false forever so every subsequent begin() throws.","solutions":["Ensure the promise returned by beginLifetimeOwnershipAdmission() is awaited and its finish callback always runs (call it in a finally block) so the window is sealed before beginning again.","Check for duplicate/concurrent call sites that invoke beginLifetimeOwnershipAdmission() on the same adapter; gate behind a mutex or single owner.","If a prior admission was abandoned after a crash, create a fresh runtime adapter instance instead of reusing the old one.","Log the state of #lifetimeOwnershipSealed at call time to confirm whether an earlier window leaked."],"exampleFix":"// before: finish callback may be skipped on failure\nconst finish = adapter.beginLifetimeOwnershipAdmission();\nawait startProvider();\nfinish();\n\n// after: always seal the admission window\nconst finish = adapter.beginLifetimeOwnershipAdmission();\ntry {\n  await startProvider();\n} finally {\n  finish();\n}","handlingStrategy":"try-catch","validationCode":"// Track admission state before beginning\nif (!adapter.isLifetimeOwnershipSealed?.()) {\n  throw new Error('previous lifetime ownership admission still open');\n}","typeGuard":null,"tryCatchPattern":"let finish;\ntry {\n  finish = adapter.beginLifetimeOwnershipAdmission();\n} catch (err) {\n  if (err.message.includes('already active')) {\n    // wait for/seal prior window or recreate the adapter\n    await sealOrRecreateAdapter(adapter);\n    finish = adapter.beginLifetimeOwnershipAdmission();\n  } else throw err;\n}\ntry { await startProvider(); } finally { finish?.(); }","preventionTips":["Always call the admission finish callback in a finally block so the window is sealed on every path.","Never call beginLifetimeOwnershipAdmission() from more than one code path per adapter instance.","Recreate the adapter after an aborted admission instead of retrying on the same instance."],"tags":["race-condition","state-management","lifecycle","acpx"],"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"}