{"record":{"id":"8734a0cc46bc32a4","repo":"paperclipai/paperclip","slug":"photon-publication-payload-changed-after-preparation","errorCode":null,"errorMessage":"Photon publication payload changed after preparation","messagePattern":"Photon publication payload changed after preparation","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"server/src/services/photon/adapter.ts","lineNumber":485,"sourceCode":"    part: string,\n    payload: {\n      chatGuid: string;\n      text?: string;\n      filename?: string;\n      sha256?: string;\n      replyTo?: string;\n      replaceMessageId?: string;\n    },\n    file: FileUpload | undefined,\n    options: { retryUnknown?: boolean; assertCurrent(): Promise<void> },\n  ): Promise<string> {\n    const key = `send:${publicationId}:${part}`;\n    const digest = createHash(\"sha256\")\n      .update(JSON.stringify(payload))\n      .digest(\"hex\");\n    let record = await this.state.update<SendRecord>(key, (current) => {\n      if (current && current.digest !== digest)\n        throw new Error(\"Photon publication payload changed after preparation\");\n      return current ?? { schema: 1, digest, phase: \"prepared\" };\n    });\n    if (record.phase === \"sent\" && record.messageGuid)\n      return record.messageGuid;\n    if (\n      (record.phase === \"sending\" || record.phase === \"uploading\") &&\n      !options.retryUnknown\n    )\n      throw new PhotonError(\n        \"delivery_unknown\",\n        \"Photon delivery is unknown; resolve this publication before retrying\",\n      );\n    const save = async (patch: Partial<SendRecord>) => {\n      record = await this.state.update<SendRecord>(key, (current) => {\n        if (!current || current.digest !== digest)\n          throw new Error(\"Photon send identity changed\");\n        return { ...current, ...patch };\n      });","sourceCodeStart":467,"sourceCodeEnd":503,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/server/src/services/photon/adapter.ts#L467-L503","documentation":"Thrown inside state.update in PhotonAdapter.sendPart when a SendRecord already exists for `send:{publicationId}:{part}` but its stored digest (sha256 of the JSON payload) does not match the digest of the payload being sent now. The adapter keys idempotency state by publicationId+part, so reusing the same publication identity for different content is treated as a corruption of the immutable publication contract and is refused.","triggerScenarios":"Re-invoking publish/sendPart with the same publicationId and part key (`text-N`/`file-N`) but different payload content: changed text, different chatGuid, different replyTo, different filename/sha256, or an insertion/removal that shifts part indices.","commonSituations":"Retrying a failed publish after mutating the message content in memory; re-running a job with a recycled publicationId but regenerated output; part count changed (a file added) causing text-1 to map to different text; server restart with persisted state and changed message.","solutions":["Use a fresh, unique publicationId when the content changes — the publication identity must be immutable per payload","Retry the exact same payload that was originally prepared (byte-identical text, files, replyTo)","Clear/reset the stored send state for that publicationId only if you are certain the old record is stale garbage","Investigate part-index drift: adding a file or text part shifts text-N/file-N keys onto digests of other content"],"exampleFix":"// before\npubId = runId; // reused across runs with different content\nawait adapter.publish(id, pubId, message, opts);\n// after\npubId = `${runId}:${sha256(JSON.stringify(payload)).slice(0, 16)}`;\nawait adapter.publish(id, pubId, message, opts);","handlingStrategy":"validation","validationCode":"const digest = createHash('sha256').update(JSON.stringify(payload)).digest('hex');\nconst existing = await state.get(`send:${publicationId}:text-0`);\nif (existing && existing.digest !== digest)\n  throw new Error(`publicationId ${publicationId} already prepared with different content`);","typeGuard":"function matchesPrepared(record, payload) {\n  return !record || record.digest === createHash('sha256').update(JSON.stringify(payload)).digest('hex');\n}","tryCatchPattern":"try {\n  await adapter.publish(id, pubId, msg, opts);\n} catch (e) {\n  if (e.message === 'Photon publication payload changed after preparation') {\n    pubId = `${pubId}:${Date.now()}`; // mint new identity\n    return adapter.publish(id, pubId, msg, opts);\n  }\n  throw e;\n}","preventionTips":["Treat publicationId as immutable: derive it from a content hash or never reuse it","Never regenerate message content under the same publicationId on retry","Freeze the payload (deep-clone) before the first publish attempt","Watch part-count changes: adding a file shifts text-N/file-N keys"],"tags":["idempotency","digest-mismatch","state-conflict"],"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"}