paperclipai/paperclip · error

PRP provider thread is already started

Error message

PRP provider thread is already started

What it means

Double-start guard on the runnerd Codex transport's #start: the provider thread is single-instance — #core must be null when starting — so a second start request while a provider thread is already running throws this sentinel instead of spawning a duplicate core. It fires on re-entrant or duplicate start RPCs (e.g. a start retried after a timeout that actually succeeded).

Solutions

  1. Check the start result/state before retrying a start that may have already succeeded (idempotent start handling).
  2. Stop the existing provider thread (close) before starting a new one if a restart is intended.
  3. Trace the duplicate start RPC to its source and deduplicate at the control-plane caller.
Defensive patterns

Strategy: validation

When it happens

Trigger: Thrown at packages/paperclip-runner/src/live/runnerd-codex-transport.ts:4285 when the library encounters an invalid state.

Common situations: See trigger scenarios.


AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18). Data as JSON: /api/errors/414af5ac6d0ae249. Report an issue: GitHub.

Appendix: source

Thrown at packages/paperclip-runner/src/live/runnerd-codex-transport.ts:4285

    } finally {
      await this.#core?.stop();
      this.#controlPlaneCheckpoint = null;
      this.#controlPlaneRelease = null;
    }
    if (suspensionRequired && !runnerSettled) {
      throw new NativeSessionCloseUnrecoverableError();
    }
    if (this.#ownsRoot && !adoptedRunner) {
      rmSync(this.#root, { recursive: true, force: true });
    }
    this.#publish();
  }

  async #start(
    params: Record<string, unknown>,
  ): Promise<Record<string, unknown>> {
    if (this.#core !== null)
      throw new Error("PRP provider thread is already started");
    if (this.options.adoptExistingRunner) {
      // A crash can precede the first driver checkpoint even though runnerd
      // already opened the provider. Exact process adoption must reuse that
      // authority instead of enqueueing another run.prepare/session.open pair.
      await this.#resume();
      return this.#openedThreadResponse(params);
    }
    const token = randomUUID().replaceAll("-", "");
    const identity = this.options.prpIdentity ?? {
      runnerInstanceId: `runner_lab_${token}`,
      environmentLeaseId: `lease_lab_${token}`,
      runId: `run_lab_${token}`,
      normalizedSessionId: `session_lab_${token}`,
      turnId: `turn_lab_${token}`,
      itemId: `item_lab_${token}`,
    };
    const runnerBinaryPath =
      this.options.runnerBinary ?? defaultCapabilityRunnerdBinary();

View on GitHub (pinned to 3f1d897a7c)