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
- Check the Photon server version for known occurredAt serialization regressions and upgrade to a fixed release
- Log the raw event envelope to identify which producer emitted the malformed occurredAt and fix it at the source
- Add upstream validation on the Photon server side so envelopes without a valid occurredAt are rejected before publication
- 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
- Pin and test against the Photon envelope schema version in CI
- Add server-side rejection of envelopes lacking a valid occurredAt
- Log raw malformed envelopes for producer attribution
- Watch for Photon releases that touch event serialization
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)