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
- 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
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
- 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
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
- A different semantic result was already committed
- Cannot build API path with an empty path segment.
- Challenge secret is required. Pass --token or --token-env.
- Completed warm transition template conflicts with its exact…
- delivery_unknown
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)