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

  1. 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.
  2. Check for duplicate/concurrent call sites that invoke beginLifetimeOwnershipAdmission() on the same adapter; gate behind a mutex or single owner.
  3. If a prior admission was abandoned after a crash, create a fresh runtime adapter instance instead of reusing the old one.
  4. 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

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


AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10). Data as JSON: /api/errors/aa99a98c310d0e16. Report an issue: GitHub.