paperclipai/paperclip · error · PhotonError
delivery_unknown
delivery_unknown
Error message
Photon delivery is unknown; resolve this publication before retrying
What it means
A PhotonError (code 'delivery_unknown') thrown in sendPart when the SendRecord for this publication part is in phase 'sending' or 'uploading' — meaning a previous attempt started but never recorded an outcome — and the caller did not pass retryUnknown: true. The adapter refuses to re-send a possibly-already-delivered message because iMessage has no read-back: a blind retry could deliver a duplicate.
Solutions
- Resolve delivery out-of-band (check Photon/iMessage for the clientMessageId or messageGuid) before retrying
- Re-call publish with options.retryUnknown: true if you accept the duplicate-delivery risk and Photon's clientMessageId dedup is relied upon
- Only retry after confirming the original never appeared in the chat
- Treat the publication as requiring human/operator resolution rather than automatic retry
Example fix
// before
await adapter.publish(id, pubId, msg, opts); // throws after crash mid-send
// after
await adapter.publish(id, pubId, msg, { ...opts, retryUnknown: true }); // after verifying no duplicate was delivered Defensive patterns
Strategy: retry
Validate before calling
const record = await state.get(`send:${publicationId}:text-0`);
if (record && (record.phase === 'sending' || record.phase === 'uploading')) {
// must resolve delivery out-of-band or pass retryUnknown
} Type guard
function isResolvable(record) {
return !record || ['prepared', 'uploaded'].includes(record.phase);
} Try / catch
try {
await adapter.publish(id, pubId, msg, opts);
} catch (e) {
if (e instanceof PhotonError && e.code === 'delivery_unknown') {
const delivered = await checkPhotonForMessage(pubId); // out-of-band check
return adapter.publish(id, pubId, msg, { ...opts, retryUnknown: !delivered });
}
throw e;
} Prevention
- Persist send state durably so phase transitions survive restarts
- Always do an out-of-band delivery check before retryUnknown: true
- Alert operators on publications stuck in sending/uploading phase
- Rely on Photon's clientMessageId dedup for at-most-once semantics
When it happens
Trigger: Retrying publish/sendPart for a part whose previous attempt crashed or lost connection after save({phase:'sending'}) or save({phase:'uploading'}) but before save({phase:'sent'}), without options.retryUnknown set.
Common situations: Process restart mid-send; network drop between sendText/sendAttachment call and receipt persistence; a timeout wrapped the send but the message actually went out; re-running an interrupted publication after a deploy.
Understand the failure class
Background: "API request failed": what wrapped HTTP errors from external APIs mean and how to find the real cause — this error's family across 29 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/72f85b3de634a5a2.
Report an issue: GitHub.
Appendix: source
Thrown at server/src/services/photon/adapter.ts:494
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 };
});
};
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,
});View on GitHub (pinned to 3f1d897a7c)