paperclipai/paperclip · error

Photon publication payload changed after preparation

Error message

Photon publication payload changed after preparation

What it means

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.

Solutions

  1. Use a fresh, unique publicationId when the content changes — the publication identity must be immutable per payload
  2. Retry the exact same payload that was originally prepared (byte-identical text, files, replyTo)
  3. Clear/reset the stored send state for that publicationId only if you are certain the old record is stale garbage
  4. Investigate part-index drift: adding a file or text part shifts text-N/file-N keys onto digests of other content

Example fix

// before
pubId = runId; // reused across runs with different content
await adapter.publish(id, pubId, message, opts);
// after
pubId = `${runId}:${sha256(JSON.stringify(payload)).slice(0, 16)}`;
await adapter.publish(id, pubId, message, opts);
Defensive patterns

Strategy: validation

Validate before calling

const digest = createHash('sha256').update(JSON.stringify(payload)).digest('hex');
const existing = await state.get(`send:${publicationId}:text-0`);
if (existing && existing.digest !== digest)
  throw new Error(`publicationId ${publicationId} already prepared with different content`);

Type guard

function matchesPrepared(record, payload) {
  return !record || record.digest === createHash('sha256').update(JSON.stringify(payload)).digest('hex');
}

Try / catch

try {
  await adapter.publish(id, pubId, msg, opts);
} catch (e) {
  if (e.message === 'Photon publication payload changed after preparation') {
    pubId = `${pubId}:${Date.now()}`; // mint new identity
    return adapter.publish(id, pubId, msg, opts);
  }
  throw e;
}

Prevention

When it happens

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

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

Understand the failure class

Background: Checksum mismatch errors: "checksum verification failed", "digest mismatch", "expected vs actual checksum" — what they mean and how to fix them — this error's family across 41 libraries.

Related errors


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

Appendix: source

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

    part: string,
    payload: {
      chatGuid: string;
      text?: string;
      filename?: string;
      sha256?: string;
      replyTo?: string;
      replaceMessageId?: string;
    },
    file: FileUpload | undefined,
    options: { retryUnknown?: boolean; assertCurrent(): Promise<void> },
  ): Promise<string> {
    const key = `send:${publicationId}:${part}`;
    const digest = createHash("sha256")
      .update(JSON.stringify(payload))
      .digest("hex");
    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 };
      });

View on GitHub (pinned to 3f1d897a7c)