{"record":{"id":"af5c2a0f293ba8ce","repo":"paperclipai/paperclip","slug":"photon-send-identity-changed","errorCode":null,"errorMessage":"Photon send identity changed","messagePattern":"Photon send identity changed","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"server/src/services/photon/adapter.ts","lineNumber":501,"sourceCode":"    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      });\n    };\n    if (file && !record.attachmentGuid) {\n      await options.assertCurrent();\n      await save({ phase: \"uploading\" });\n      try {\n        const uploaded = await this.client.attachments.upload({\n          fileName: file.filename,\n          data: file.data as Buffer,\n        });\n        if (!uploaded.attachment.guid)\n          throw new PhotonError(\n            \"delivery_unknown\",\n            \"Photon upload receipt is missing\",\n          );\n        await save({\n          phase: \"uploaded\",","sourceCodeStart":483,"sourceCodeEnd":519,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/server/src/services/photon/adapter.ts#L483-L519","documentation":"Thrown inside the save() helper in sendPart when state.update finds either no existing SendRecord for the key or one whose digest differs from the digest captured when this attempt started. It protects the read-modify-write cycle of the send state machine: if the record disappeared or was replaced mid-flight (payload identity changed under us), persisting a phase patch would be unsafe.","triggerScenarios":"Concurrent sendPart calls for the same `send:{publicationId}:{part}` key where one overwrote the record; another process cleared or re-keyed the state store between the initial update and a later save(); the payload was mutated so its digest no longer matches the stored record while the send is in progress.","commonSituations":"Two workers racing to publish the same publicationId; an admin reset wiped state mid-send; shared state store (e.g. DB-backed) with another instance publishing different content under the same id; retry loop that mutates message text between attempts.","solutions":["Ensure only one publisher owns a publicationId at a time (lock or single-flight per publication)","Never mutate message content between sendPart attempts — mint a new publicationId for new content","Restore or re-seed the state store if the record was deleted by accident, then restart the publication from scratch","Check for multiple server instances sharing state with divergent payloads"],"exampleFix":"// before\n// two calls racing on the same pubId\npublish(id, pubId, msgA, opts); publish(id, pubId, msgB, opts);\n// after\nawait withPublicationLock(pubId, () => publish(id, pubId, msgA, opts)); // serialize; use pubId' for msgB","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try {\n  await adapter.publish(id, pubId, msg, opts);\n} catch (e) {\n  if (e.message === 'Photon send identity changed') {\n    log.error('concurrent publisher or state reset on ' + pubId);\n    abortPublication(pubId); // do not retry blindly\n  }\n  throw e;\n}","preventionTips":["Single-flight/lock publishing per publicationId","Run only one server instance owning a given publication's state","Never clear the state store while sends are in flight","Never mutate payload content between sendPart attempts"],"tags":["concurrency","state-conflict","race-condition"],"backgroundTag":"internal-invariant-violation","analyzedSha":"3f1d897a7c018d76563a21c6e39c3c9b03933622","analyzedAt":"2026-09-18T08:03:59.046Z","contentChangedAt":"2026-09-18T08:03:59.046Z","schemaVersion":2},"datasetVersion":"2026-09-22T11:17:16.035Z"}