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

  1. Resolve delivery out-of-band (check Photon/iMessage for the clientMessageId or messageGuid) before retrying
  2. Re-call publish with options.retryUnknown: true if you accept the duplicate-delivery risk and Photon's clientMessageId dedup is relied upon
  3. Only retry after confirming the original never appeared in the chat
  4. 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

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


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)