paperclipai/paperclip · error · Error

--api-key and --api-key-env are mutually exclusive.

Error message

--api-key and --api-key-env are mutually exclusive.

What it means

After a warm transition, the transport polls the runner store until the recovered identity matches the desired identity and the warm transition completes. If the identity has not converged within the grace period (`runnerReconnectGraceMs`, default 5s), or the runner process exited while waiting, it throws this error indicating recovery never finished in time.

Source

Thrown at cli/src/commands/test-drive.ts:228

        "DATABASE_MIGRATION_URL from the selected data directory's .env, or choose a fresh --data-dir.",
    );
  }

  const config = readConfigFile(configPath);
  if (config?.database.mode === "postgres") {
    throw new Error(
      "test-drive cannot reuse a data directory configured for an external PostgreSQL database. " +
        "Choose a fresh data directory or change database.mode to embedded-postgres.",
    );
  }
}

export function resolveTestDriveBootstrap(
  options: TestDriveOptions,
  env: NodeJS.ProcessEnv = process.env,
): ResolvedTestDriveBootstrap {
  if (options.apiKey !== undefined && options.apiKeyEnv !== undefined) {
    throw new Error("--api-key and --api-key-env are mutually exclusive.");
  }

  const harness = options.harness ?? "claude";
  const definition = HARNESS_DEFINITIONS[harness];
  if (!definition) {
    throw new Error(`Unsupported test-drive harness: ${String(harness)}.`);
  }

  const companyName = (options.companyName ?? "Test Company").trim();
  const agentName = (options.agentName ?? "CEO").trim();
  if (!companyName) throw new Error("--company-name cannot be empty.");
  if (!agentName) throw new Error("--agent-name cannot be empty.");

  const model = options.model;
  if (model !== undefined && (!model || model.trim() !== model)) {
    throw new Error("--model cannot be empty or have surrounding whitespace.");
  }
  if (

View on GitHub (pinned to 01ad858492)

Solutions

  1. Increase `runnerReconnectGraceMs` to allow slower warm recovery to finish (e.g. 30_000).
  2. Inspect runnerd logs for a crash during the transition and fix the underlying exit (memory, binary path, config).
  3. Retry the warm transition once the runner is healthy, or fall back to a cold start.

Example fix

// before
new RunnerdCodexTransport({ runnerReconnectGraceMs: 5_000 });
// after
new RunnerdCodexTransport({ runnerReconnectGraceMs: 30_000 });
Defensive patterns

Strategy: retry

Validate before calling

const graceMs = options.runnerReconnectGraceMs ?? 5_000;
if (graceMs < 10_000) console.warn("warm transition grace period may be too short on slow hosts");

Type guard

null

Try / catch

try {
  await transport.startTurn(input);
} catch (e) {
  if (e.message === "native_runner_warm_transition_recovery_pending" && runnerAlive()) {
    await retryWithBackoff(() => transport.startTurn(input), { retries: 2 });
  }
}

Prevention

When it happens

Trigger: The wait loop at the transition point times out (`Date.now() >= deadline`) or `#runnerHasExited()` observes the runnerd process died before `recoveryIdentityMatches` became true and `warmTransition` cleared.

Common situations: Slow or overloaded machine where the runner cannot recover within the default 5s grace; runner crash mid-transition (OOM, bad binary); setting `runnerReconnectGraceMs` too low for large session state restores.

Understand the failure class

Background: Request timed out: what client-side request timeouts mean across libraries (Request timed out, TIMED_OUT, APITimeoutError) — this error's family across 39 libraries.

Related errors


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