paperclipai/paperclip · error · RouteError

provider_configuration_mismatch

provider_configuration_mismatch

Error message

The clean-room path refused configuration that differed from its immutable session.

What it means

cleanRoomPayload rebuilds a configuration snapshot from the live session and compares it field-by-field against the configuration recorded when the clean-room session entry was created. Clean-room sessions are treated as immutable: provider, model, managed/agentcore profile ids, cost ceilings, and lifecycle policy must never change after creation. If any differ, the middleware throws a 500 RouteError with code provider_configuration_mismatch rather than serving a payload that diverges from the pinned session identity.

Source

Thrown at packages/paperclip-runner/scripts/capability-issue-thread-server.mjs:603

        maxSessionListCostUsd: snapshot.config.managedProfile.maxSessionListCostUsd,
      }),
      ...(snapshot.config.agentCoreProfile === undefined ? {} : {
        agentCoreProfileId: snapshot.config.agentCoreProfile.profileId,
        maxEstimatedSessionCostUsd: snapshot.config.agentCoreProfile.maxEstimatedSessionCostUsd,
      }),
      lifecyclePolicy: snapshot.config.lifecyclePolicy ?? { mode: "per_turn", idleTimeoutMs: null },
    };
    if (
      entry.configuration !== undefined
      && (entry.configuration.provider !== configuration.provider
        || entry.configuration.model !== configuration.model
        || entry.configuration.managedProfileId !== configuration.managedProfileId
        || entry.configuration.maxSessionListCostUsd !== configuration.maxSessionListCostUsd
        || entry.configuration.agentCoreProfileId !== configuration.agentCoreProfileId
        || entry.configuration.maxEstimatedSessionCostUsd !== configuration.maxEstimatedSessionCostUsd
        || JSON.stringify(entry.configuration.lifecyclePolicy) !== JSON.stringify(configuration.lifecyclePolicy))
    ) {
      throw new RouteError(
        500,
        "provider_configuration_mismatch",
        "The clean-room path refused configuration that differed from its immutable session.",
      );
    }
    return {
      sessionId: entry.session.id,
      surface: "cleanroom",
      identity: entry.identity,
      limits: { maxTurns: MAX_TURNS_PER_SESSION, maxMessageBytes: MAX_MESSAGE_BYTES },
      turns: entry.turns,
      configuration,
      runtime: {
        providerSessionId: snapshot.providerSessionId ?? null,
        driverSessionId: snapshot.providerThreadId ?? null,
        runnerPid: snapshot.process?.runnerPid ?? null,
        providerPid: configuration.provider === "claude_managed" || configuration.provider === "aws_agentcore"
          ? null

View on GitHub (pinned to 5716fe907e)

Solutions

  1. Treat clean-room session configuration as immutable: create a new session instead of mutating provider, model, profiles, cost ceilings, or lifecyclePolicy on the existing one.
  2. If a legitimate budget/profile change is needed, delete or retire the old clean-room session entry and register a fresh entry whose entry.configuration matches the new snapshot.
  3. Ensure any code path that raises a budget (e.g. increaseManagedSessionBudget) also updates entry.configuration.maxEstimatedSessionCostUsd (or the agentCoreProfile field) so the two stay in sync before the next payload read.
  4. Check for accidental mutation of snapshot.config by shared references; deep-clone configuration at entry creation to avoid aliasing-induced mismatches.

Example fix

// before
entry.configuration.maxEstimatedSessionCostUsd = 5.0; // mutate live entry
payload = cleanRoomPayload(runner, entry); // throws provider_configuration_mismatch
// after
const newSession = registerCleanRoomSession({ ...configuration, maxEstimatedSessionCostUsd: 5.0 });
payload = cleanRoomPayload(runner, newSession); // fresh entry matches immutable config
Defensive patterns

Strategy: validation

Validate before calling

function assertCleanRoomConfigStable(entry, snapshotConfig) {
  const next = {
    provider: snapshotConfig.provider ?? "codex",
    model: snapshotConfig.requestedModel ?? null,
    ...(snapshotConfig.managedProfile === undefined ? {} : { managedProfileId: snapshotConfig.managedProfile.profileId, maxSessionListCostUsd: snapshotConfig.managedProfile.maxSessionListCostUsd }),
    ...(snapshotConfig.agentCoreProfile === undefined ? {} : { agentCoreProfileId: snapshotConfig.agentCoreProfile.profileId, maxEstimatedSessionCostUsd: snapshotConfig.agentCoreProfile.maxEstimatedSessionCostUsd }),
    lifecyclePolicy: snapshotConfig.lifecyclePolicy ?? { mode: "per_turn", idleTimeoutMs: null },
  };
  const differs = Object.keys(next).some((k) =>
    k === "lifecyclePolicy"
      ? JSON.stringify(entry.configuration?.lifecyclePolicy) !== JSON.stringify(next.lifecyclePolicy)
      : entry.configuration?.[k] !== next[k]);
  if (differs) throw new Error("clean-room session configuration would change; create a new session instead");
}

Type guard

function isSameConfiguration(a, b) {
  if (!a || !b) return a === b;
  return a.provider === b.provider && a.model === b.model
    && a.managedProfileId === b.managedProfileId
    && a.maxSessionListCostUsd === b.maxSessionListCostUsd
    && a.agentCoreProfileId === b.agentCoreProfileId
    && a.maxEstimatedSessionCostUsd === b.maxEstimatedSessionCostUsd
    && JSON.stringify(a.lifecyclePolicy) === JSON.stringify(b.lifecyclePolicy);
}

Try / catch

try {
  const payload = cleanRoomPayload(runner, entry);
} catch (err) {
  if (err.code === "provider_configuration_mismatch") {
    // recreate the session with the desired configuration
    const entry2 = registerCleanRoomSession(desiredConfiguration);
    return cleanRoomPayload(runner, entry2);
  }
  throw err;
}

Prevention

When it happens

Trigger: A GET/payload request routed through cleanRoomPayload whose live session snapshot.config now differs from entry.configuration — e.g. the underlying session was re-created or rebound with a different provider/model, a managed or agentcore profile was swapped, maxSessionListCostUsd/maxEstimatedSessionCostUsd changed (including via a budget-increase that mutated snapshot config), or lifecyclePolicy was altered (JSON.stringify comparison, so even key-order/shape differences count).

Common situations: Calling the payload endpoint after a mid-session budget increase updated snapshot.config.agentCoreProfile.maxEstimatedSessionCostUsd; a test harness reusing a session entry across a config change; switching provider or model on an existing clean-room session; mutating lifecyclePolicy from per_turn to idle-based after creation; undefined-vs-value mismatches when a profile was absent at entry creation but present in the snapshot.

Related errors


AI-assisted analysis of paperclipai/paperclip@5716fe907e (2026-09-02). Data as JSON: /api/errors/72c260eb7f20fb08. Report an issue: GitHub.