paperclipai/paperclip · error
ACPX provider ownership admission is already active
Error message
ACPX provider ownership admission is already active
What it means
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.
Source
Thrown at packages/paperclip-runner/src/drivers/acpx/codex-runtime-adapter.ts:1504
// earlier batch was pending is observed by the next loop iteration;
// no later provider can race admission after the stable-empty point.
this.#lifetimeOwnershipSealed = true;
return;
}
await Promise.all(ownership);
}
} catch (error) {
this.#lifetimeOwnershipSealed = true;
throw error;
}
}
beginLifetimeOwnershipAdmission(): () => Promise<void> {
if (this.#sealed) {
throw new Error("ACPX provider ownership admission is closed");
}
if (!this.#lifetimeOwnershipSealed) {
throw new Error("ACPX provider ownership admission is already active");
}
this.#lifetimeOwnershipSealed = false;
let finished = false;
return async () => {
if (finished) return;
finished = true;
await this.verifyLifetimeOwnership();
};
}
#track(child: ChildProcess, providerExit: ProviderExitObservation): void {
this.#children.add(child);
const onError = (error: unknown) => this.#errors.add(error);
let guardianExited = !running(child);
let providerExited = false;
const forgetIfReleased = () => {
if (!guardianExited || !providerExited) return;
this.#children.delete(child);View on GitHub (pinned to 01ad858492)
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.
Example fix
// before: finish callback may be skipped on failure
const finish = adapter.beginLifetimeOwnershipAdmission();
await startProvider();
finish();
// after: always seal the admission window
const finish = adapter.beginLifetimeOwnershipAdmission();
try {
await startProvider();
} finally {
finish();
} Defensive patterns
Strategy: try-catch
Validate before calling
// Track admission state before beginning
if (!adapter.isLifetimeOwnershipSealed?.()) {
throw new Error('previous lifetime ownership admission still open');
} Try / catch
let finish;
try {
finish = adapter.beginLifetimeOwnershipAdmission();
} catch (err) {
if (err.message.includes('already active')) {
// wait for/seal prior window or recreate the adapter
await sealOrRecreateAdapter(adapter);
finish = adapter.beginLifetimeOwnershipAdmission();
} else throw err;
}
try { await startProvider(); } finally { finish?.(); } Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- ACPX provider ownership admission is closed
- ACPX ${agent} runtime executable changed while it was verifi
- ACPX executable changed during snapshot
- ACPX runtime host is closing
- capability_live_attempt_not_running
AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10).
Data as JSON: /api/errors/aa99a98c310d0e16.
Report an issue: GitHub.