paperclipai/paperclip · error

Photon send identity changed

Error message

Photon send identity changed

What it means

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.

Solutions

  1. Ensure only one publisher owns a publicationId at a time (lock or single-flight per publication)
  2. Never mutate message content between sendPart attempts — mint a new publicationId for new content
  3. Restore or re-seed the state store if the record was deleted by accident, then restart the publication from scratch
  4. Check for multiple server instances sharing state with divergent payloads

Example fix

// before
// two calls racing on the same pubId
publish(id, pubId, msgA, opts); publish(id, pubId, msgB, opts);
// after
await withPublicationLock(pubId, () => publish(id, pubId, msgA, opts)); // serialize; use pubId' for msgB
Defensive patterns

Strategy: try-catch

Try / catch

try {
  await adapter.publish(id, pubId, msg, opts);
} catch (e) {
  if (e.message === 'Photon send identity changed') {
    log.error('concurrent publisher or state reset on ' + pubId);
    abortPublication(pubId); // do not retry blindly
  }
  throw e;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Understand the failure class

Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.

Related errors


AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18). Data as JSON: /api/errors/af5c2a0f293ba8ce. Report an issue: GitHub.

Appendix: source

Thrown at server/src/services/photon/adapter.ts:501

    let record = await this.state.update<SendRecord>(key, (current) => {
      if (current && current.digest !== digest)
        throw new Error("Photon publication payload changed after preparation");
      return current ?? { schema: 1, digest, phase: "prepared" };
    });
    if (record.phase === "sent" && record.messageGuid)
      return record.messageGuid;
    if (
      (record.phase === "sending" || record.phase === "uploading") &&
      !options.retryUnknown
    )
      throw new PhotonError(
        "delivery_unknown",
        "Photon delivery is unknown; resolve this publication before retrying",
      );
    const save = async (patch: Partial<SendRecord>) => {
      record = await this.state.update<SendRecord>(key, (current) => {
        if (!current || current.digest !== digest)
          throw new Error("Photon send identity changed");
        return { ...current, ...patch };
      });
    };
    if (file && !record.attachmentGuid) {
      await options.assertCurrent();
      await save({ phase: "uploading" });
      try {
        const uploaded = await this.client.attachments.upload({
          fileName: file.filename,
          data: file.data as Buffer,
        });
        if (!uploaded.attachment.guid)
          throw new PhotonError(
            "delivery_unknown",
            "Photon upload receipt is missing",
          );
        await save({
          phase: "uploaded",

View on GitHub (pinned to 3f1d897a7c)