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"
? nullView on GitHub (pinned to 5716fe907e)
Solutions
- 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.
- 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.
- 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.
- 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
- Never mutate provider, model, profile ids, cost ceilings, or lifecyclePolicy on a live clean-room session; create a new session for changed config.
- When increasing a budget, update the session entry's recorded configuration in the same code path so the two stay in sync.
- Deep-clone configuration when registering a session entry to avoid shared-reference mutation.
- Keep lifecyclePolicy shapes canonical (same key set and order) since comparison is JSON.stringify-based.
- Add an integration test that reads the payload after any config-mutation route to assert immutability.
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.