{"record":{"id":"4ac95e507c17d48a","repo":"paperclipai/paperclip","slug":"rejected-photon-prompt-changed-after-preparation","errorCode":"rejected","errorMessage":"Photon prompt changed after preparation","messagePattern":"Photon prompt changed after preparation","errorType":"error_code","errorClass":"PhotonError","httpStatus":null,"severity":"error","filePath":"server/src/services/photon/interactions.ts","lineNumber":169,"sourceCode":"  taskUrl?: string | null;\n  retryUnknown?: boolean;\n  assertCurrent(): Promise<void>;\n}): Promise<PhotonPromptReceipt> {\n  const { adapter, binding, interaction, questionIndex, assertCurrent } = input;\n  const key = `prompt:${binding.reference}:${questionIndex}`;\n  const digest = nativeSha256({\n    binding,\n    threadId: input.threadId,\n    kind: interaction.kind,\n    payload: interaction.payload,\n    questionIndex,\n    taskUrl: input.taskUrl ?? null,\n  });\n  await adapter.state.update<{ digest: string }>(\n    `${key}:identity`,\n    (current) => {\n      if (current && current.digest !== digest)\n        throw new PhotonError(\n          \"rejected\",\n          \"Photon prompt changed after preparation\",\n        );\n      return current ?? { digest };\n    },\n  );\n  const existing = await adapter.state.read<PhotonPromptReceipt>(key);\n  if (existing) {\n    for (const guid of existing.promptMessageGuids ?? [\n      existing.promptMessageGuid,\n    ])\n      await adapter.state.update<PhotonPromptReceipt>(\n        `prompt-message:${guid}`,\n        (current) => current ?? existing,\n      );\n    if (existing.pollMessageGuid)\n      await adapter.state.update<PhotonPromptReceipt>(\n        `poll-message:${existing.pollMessageGuid}`,","sourceCodeStart":151,"sourceCodeEnd":187,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/server/src/services/photon/interactions.ts#L151-L187","documentation":"A PhotonError with code 'rejected' thrown from publishPhotonPrompt's identity update when a prompt digest already exists for the binding key and differs from the newly computed digest. This guarantees a prompt cannot silently change after preparation — republishing mutated content for the same identity is treated as a rejection to protect answer integrity.","triggerScenarios":"Calling publishPhotonPrompt with content that hashes to a different digest than the one stored at `${key}:identity`, i.e. the prompt text/questions for the same binding were modified after the first preparation.","commonSituations":"Editing the question text or options and republishing without a new binding key; a retry path recomputes the prompt with different formatting; concurrent publishers racing on the same binding with divergent content.","solutions":["Do not mutate the prompt after preparation; publish a new binding key/identity for changed content","If the change is intentional, clear/rotate the `${key}:identity` state or create a fresh interaction","Make prompt generation deterministic so retries produce the identical digest"],"exampleFix":"// before\nconst key = `prompt:${issueId}`;\nawait publishPhotonPrompt(adapter, key, changedInput);\n// after\nconst key = `prompt:${issueId}:${hash(JSON.stringify(changedInput))}`;\nawait publishPhotonPrompt(adapter, key, changedInput);","handlingStrategy":"try-catch","validationCode":"const digest = computeDigest(input); const current = await adapter.state.get(`${key}:identity`); if (current && current.digest !== digest) throw new Error('prompt changed after preparation');","typeGuard":null,"tryCatchPattern":"try { await publishPhotonPrompt(adapter, key, input); } catch (e) { if (e instanceof PhotonError && e.code === 'rejected' && /changed after preparation/.test(e.message)) return republishUnderNewKey(input); throw e; }","preventionTips":["Treat prepared prompts as immutable; version binding keys with a content hash","Make prompt generation deterministic for retries","Never edit question text/options in place after preparation"],"tags":["integrity","digest-mismatch","immutability","republish"],"backgroundTag":"checksum-mismatch","analyzedSha":"3f1d897a7c018d76563a21c6e39c3c9b03933622","analyzedAt":"2026-09-18T08:03:59.046Z","contentChangedAt":"2026-09-18T08:03:59.046Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}