paperclipai/paperclip · error · PhotonError

invalid_response

invalid_response

Error message

Photon event timestamp is invalid

What it means

Every non-ignored Photon event must carry a parseable occurredAt timestamp, because admission gating compares occurredAt to the intakeAfter threshold. If new Date(event.occurredAt).getTime() is not a finite number (missing field, malformed string, or epoch placeholder producing NaN), the response is considered invalid and the receiver throws invalid_response instead of admitting the event with unknown timing.

Solutions

  1. Check the Photon server version for known occurredAt serialization regressions and upgrade to a fixed release
  2. Log the raw event envelope to identify which producer emitted the malformed occurredAt and fix it at the source
  3. Add upstream validation on the Photon server side so envelopes without a valid occurredAt are rejected before publication
  4. If a third-party producer cannot be fixed immediately, downgrade such events to "photon.ignored" on the server so the client skips timestamp validation

Example fix

// before: producer omits timestamp
publishEvent({ type: "photon.created", payload });
// after: always stamp a valid occurredAt
publishEvent({ type: "photon.created", occurredAt: new Date().toISOString(), payload });
Defensive patterns

Strategy: validation

Validate before calling

function hasValidOccurredAt(event: PhotonEvent): boolean {
  return event.type === "photon.ignored" || Number.isFinite(new Date(event.occurredAt).getTime());
}
// filter/validate envelopes before handing them to the receiver

Type guard

function isWellFormedEnvelope(e: unknown): e is PhotonEvent & { occurredAt: string } {
  const ev = e as PhotonEvent;
  return typeof ev?.occurredAt === "string" && !Number.isNaN(Date.parse(ev.occurredAt));
}

Try / catch

try {
  await receiver.catchUp(opts);
} catch (e) {
  if (e instanceof PhotonError && e.code === "invalid_response" && /timestamp/.test(e.message)) {
    await quarantineLastEnvelopeAndReport(); // capture raw frame for producer debugging
  } else throw e;
}

Prevention

When it happens

Trigger: catchUp() processes an event with type other than "photon.ignored" whose occurredAt is undefined, null, or a string unparseable by Date (e.g. "", "not-a-date"), so Number.isFinite(new Date(...).getTime()) fails.

Common situations: Photon server version regression that omits occurredAt from envelopes; a custom event producer emitting malformed timestamps; schema drift between the client's expected envelope shape and what the server sends; clock/serialization bugs producing null timestamps.

Related errors


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

Appendix: source

Thrown at server/src/services/photon/receiver.ts:160

            "Photon returned an invalid event sequence",
          );
        if (++batchEvents > 100_000)
          throw new PhotonError("history_gap", "Photon replay exceeds the supported recovery window");
        if (shared && sequence !== undefined && event.sequence < sequence && event.sequence > (original?.sequence ?? -1))
          throw new PhotonError("history_gap", "Photon replay arrived out of order; the saved cursor was retained");
        if (sequence !== undefined && event.sequence <= sequence) continue;
        if (!shared && sequence !== undefined && event.sequence !== sequence + 1)
          throw new PhotonError(
            "history_gap",
            "Photon event history is incomplete; operator recovery is required",
          );
        // On the first connection, the server may retain only a tail of history.
        // Establish that boundary explicitly; it is never allowed after a cursor.
        sequence ??= event.sequence - 1;
        if (event.type !== "photon.ignored") {
          const occurredAt = new Date(event.occurredAt).getTime();
          if (!Number.isFinite(occurredAt))
            throw new PhotonError(
              "invalid_response",
              "Photon event timestamp is invalid",
            );
          if (occurredAt >= intakeAfter) await admit(event);
        }
        // admission must durably store or classify even irrelevant events.
        if (!shared) await this.checkpoint(event.sequence);
        sequence = event.sequence;
      }
      if (!completed && !this.stopped)
        throw new PhotonError(
          "network",
          "Photon catch-up ended before its checkpoint barrier",
        );
    } finally {
      await stream.close();
      if (this.catchUpStream === stream) this.catchUpStream = undefined;
    }

View on GitHub (pinned to 3f1d897a7c)