{"record":{"id":"dc1b3edea5ab281f","repo":"paperclipai/paperclip","slug":"durable-prp-run-attachment-template-conflicts","errorCode":null,"errorMessage":"Durable PRP run attachment template conflicts.","messagePattern":"Durable PRP run attachment template conflicts\\.","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/paperclip-runner/src/control-plane/durable-prp-control-plane.ts","lineNumber":1721,"sourceCode":"\n  /**\n   * Retain the connection-free provider preparation payload before the first\n   * runner bootstrap. Completed command history is bounded and may be\n   * compacted before a warm continuation arrives, so it cannot be the sole\n   * source for a later run.attach. Repeating the same write is idempotent;\n   * changing an established seed fails closed.\n   */\n  persistRunAttachTemplate(runAttachTemplate: Record<string, unknown>): void {\n    if (!isRecord(runAttachTemplate.provider)) {\n      throw new Error(\"Durable PRP run attachment template is invalid.\");\n    }\n    const existing = this.#store.state.runAttachTemplate;\n    if (\n      existing !== undefined &&\n      existing !== null &&\n      canonicalJson(existing) !== canonicalJson(runAttachTemplate)\n    ) {\n      throw new Error(\"Durable PRP run attachment template conflicts.\");\n    }\n    if (existing !== undefined && existing !== null) return;\n    this.#store.state.runAttachTemplate = structuredClone(runAttachTemplate);\n    this.#store.save();\n  }\n\n  issueBootstrapTicket(ttlMs = 5_000): string {\n    this.#store.assertWritable();\n    if (this.#store.state.warmTransition) {\n      throw new Error(\n        \"Warm transition recovery requires its explicit one-use bootstrap capability.\",\n      );\n    }\n    if (!Number.isInteger(ttlMs) || ttlMs < 1_000 || ttlMs > 60_000) {\n      throw new Error(\"Durable PRP bootstrap TTL is invalid.\");\n    }\n    this.#pruneCredentials();\n    const ticket = `bootstrap_${randomUUID()}`;","sourceCodeStart":1703,"sourceCodeEnd":1739,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/packages/paperclip-runner/src/control-plane/durable-prp-control-plane.ts#L1703-L1739","documentation":"persistRunAttachTemplate() is idempotent for identical templates but fails closed when a different template is presented after one is already established in the durable store. Because the seed may be the only source for a later run.attach (command history can be compacted), silently overwriting it would be unsafe, so any canonical-JSON mismatch is rejected.","triggerScenarios":"Calling persistRunAttachTemplate() a second time (after a successful seed) with a template whose canonical JSON differs — any added, removed, or changed key/value, including key content in the provider object.","commonSituations":"Provider config (model, version, options) changed between process restarts; the caller recomputes the template with volatile fields (timestamps, request ids) embedded; two code paths seed the template with slightly different payloads; environment variable drift between runs.","solutions":["Reuse the exact already-persisted template (read store.state.runAttachTemplate) instead of rebuilding it.","Remove volatile fields (timestamps, nonces, request ids) from the template so repeated construction is byte-stable.","If the template legitimately must change, follow the defined reset/re-rotation procedure rather than re-seeding.","Diff canonicalJson(existing) vs canonicalJson(incoming) to find the drifted field before retrying."],"exampleFix":"// before\ncontrolPlane.persistRunAttachTemplate({ provider: { name: \"anthropic\" }, requestedAt: new Date().toISOString() }); // volatile field\n// after\ncontrolPlane.persistRunAttachTemplate({ provider: { name: \"anthropic\" } }); // stable payload","handlingStrategy":"validation","validationCode":"const existing = store.state.runAttachTemplate;\nif (existing != null && canonicalJson(existing) !== canonicalJson(template)) {\n  template = structuredClone(existing); // reuse established seed instead of failing\n}","typeGuard":null,"tryCatchPattern":"try {\n  controlPlane.persistRunAttachTemplate(template);\n} catch (err) {\n  if (err instanceof Error && err.message.includes(\"run attachment template conflicts\")) {\n    // already seeded with a different template — adopt the durable one\n    template = structuredClone(store.state.runAttachTemplate);\n  } else throw err;\n}","preventionTips":["Remove volatile fields (timestamps, ids, nonces) from templates so rebuilds are byte-stable.","Load the persisted template on startup and reuse it rather than re-deriving from config.","Version provider config deliberately; changing it should go through a defined reset flow, not re-seeding."],"tags":["idempotency","durability","fail-closed","config-drift"],"backgroundTag":"conflicting-config-options","analyzedSha":"01ad8584922b5d85292b1723cae71fa0d9b07a19","analyzedAt":"2026-09-10T03:14:50.855Z","contentChangedAt":"2026-09-10T03:14:50.855Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}