{"record":{"id":"72c260eb7f20fb08","repo":"paperclipai/paperclip","slug":"provider-configuration-mismatch","errorCode":"provider_configuration_mismatch","errorMessage":"The clean-room path refused configuration that differed from its immutable session.","messagePattern":"The clean-room path refused configuration that differed from its immutable session\\.","errorType":"http","errorClass":"RouteError","httpStatus":500,"severity":"error","filePath":"packages/paperclip-runner/scripts/capability-issue-thread-server.mjs","lineNumber":603,"sourceCode":"        maxSessionListCostUsd: snapshot.config.managedProfile.maxSessionListCostUsd,\n      }),\n      ...(snapshot.config.agentCoreProfile === undefined ? {} : {\n        agentCoreProfileId: snapshot.config.agentCoreProfile.profileId,\n        maxEstimatedSessionCostUsd: snapshot.config.agentCoreProfile.maxEstimatedSessionCostUsd,\n      }),\n      lifecyclePolicy: snapshot.config.lifecyclePolicy ?? { mode: \"per_turn\", idleTimeoutMs: null },\n    };\n    if (\n      entry.configuration !== undefined\n      && (entry.configuration.provider !== configuration.provider\n        || entry.configuration.model !== configuration.model\n        || entry.configuration.managedProfileId !== configuration.managedProfileId\n        || entry.configuration.maxSessionListCostUsd !== configuration.maxSessionListCostUsd\n        || entry.configuration.agentCoreProfileId !== configuration.agentCoreProfileId\n        || entry.configuration.maxEstimatedSessionCostUsd !== configuration.maxEstimatedSessionCostUsd\n        || JSON.stringify(entry.configuration.lifecyclePolicy) !== JSON.stringify(configuration.lifecyclePolicy))\n    ) {\n      throw new RouteError(\n        500,\n        \"provider_configuration_mismatch\",\n        \"The clean-room path refused configuration that differed from its immutable session.\",\n      );\n    }\n    return {\n      sessionId: entry.session.id,\n      surface: \"cleanroom\",\n      identity: entry.identity,\n      limits: { maxTurns: MAX_TURNS_PER_SESSION, maxMessageBytes: MAX_MESSAGE_BYTES },\n      turns: entry.turns,\n      configuration,\n      runtime: {\n        providerSessionId: snapshot.providerSessionId ?? null,\n        driverSessionId: snapshot.providerThreadId ?? null,\n        runnerPid: snapshot.process?.runnerPid ?? null,\n        providerPid: configuration.provider === \"claude_managed\" || configuration.provider === \"aws_agentcore\"\n          ? null","sourceCodeStart":585,"sourceCodeEnd":621,"githubUrl":"https://github.com/paperclipai/paperclip/blob/5716fe907e596ce73501408fc6efdb19fb61edf2/packages/paperclip-runner/scripts/capability-issue-thread-server.mjs#L585-L621","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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."],"exampleFix":"// before\nentry.configuration.maxEstimatedSessionCostUsd = 5.0; // mutate live entry\npayload = cleanRoomPayload(runner, entry); // throws provider_configuration_mismatch\n// after\nconst newSession = registerCleanRoomSession({ ...configuration, maxEstimatedSessionCostUsd: 5.0 });\npayload = cleanRoomPayload(runner, newSession); // fresh entry matches immutable config","handlingStrategy":"validation","validationCode":"function assertCleanRoomConfigStable(entry, snapshotConfig) {\n  const next = {\n    provider: snapshotConfig.provider ?? \"codex\",\n    model: snapshotConfig.requestedModel ?? null,\n    ...(snapshotConfig.managedProfile === undefined ? {} : { managedProfileId: snapshotConfig.managedProfile.profileId, maxSessionListCostUsd: snapshotConfig.managedProfile.maxSessionListCostUsd }),\n    ...(snapshotConfig.agentCoreProfile === undefined ? {} : { agentCoreProfileId: snapshotConfig.agentCoreProfile.profileId, maxEstimatedSessionCostUsd: snapshotConfig.agentCoreProfile.maxEstimatedSessionCostUsd }),\n    lifecyclePolicy: snapshotConfig.lifecyclePolicy ?? { mode: \"per_turn\", idleTimeoutMs: null },\n  };\n  const differs = Object.keys(next).some((k) =>\n    k === \"lifecyclePolicy\"\n      ? JSON.stringify(entry.configuration?.lifecyclePolicy) !== JSON.stringify(next.lifecyclePolicy)\n      : entry.configuration?.[k] !== next[k]);\n  if (differs) throw new Error(\"clean-room session configuration would change; create a new session instead\");\n}","typeGuard":"function isSameConfiguration(a, b) {\n  if (!a || !b) return a === b;\n  return a.provider === b.provider && a.model === b.model\n    && a.managedProfileId === b.managedProfileId\n    && a.maxSessionListCostUsd === b.maxSessionListCostUsd\n    && a.agentCoreProfileId === b.agentCoreProfileId\n    && a.maxEstimatedSessionCostUsd === b.maxEstimatedSessionCostUsd\n    && JSON.stringify(a.lifecyclePolicy) === JSON.stringify(b.lifecyclePolicy);\n}","tryCatchPattern":"try {\n  const payload = cleanRoomPayload(runner, entry);\n} catch (err) {\n  if (err.code === \"provider_configuration_mismatch\") {\n    // recreate the session with the desired configuration\n    const entry2 = registerCleanRoomSession(desiredConfiguration);\n    return cleanRoomPayload(runner, entry2);\n  }\n  throw err;\n}","preventionTips":["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."],"tags":["clean-room","session-immutability","configuration-mismatch","http-500"],"backgroundTag":"session-configuration-mismatch","analyzedSha":"5716fe907e596ce73501408fc6efdb19fb61edf2","analyzedAt":"2026-09-02T18:44:00.616Z","contentChangedAt":"2026-09-02T18:44:00.616Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}