Hmbown/CodeWhale · error · Error
Invalid pet particle checkpoint.
Error message
Invalid pet particle checkpoint.
What it means
PetSim.restore (the checkpoint loader) validates every field of a serialized checkpoint: phase/clock/tear ranges, previous/current channel indices, RGB color, and frame shape (r/g/b/alpha/work/hollow/channel/arch). Any violation throws this error instead of restoring a corrupt or tampered state.
Solutions
- Re-serialize the checkpoint from a fresh PetSim run instead of restoring the corrupt one
- Validate the checkpoint against the same in-range/length/match rules before calling restore
- If CHANNELS changed, re-map old checkpoints' channel/arch fields to the new table
- Fall back to a default checkpoint and log the invalid one
Example fix
// before pet.restore(savedJson); // throws if fields drifted // after if (isValidCheckpoint(savedJson)) pet.restore(savedJson); else pet = new PetSim(points, seed); // fresh default
Defensive patterns
Strategy: validation
Validate before calling
const inRange = (n: unknown, lo: number, hi: number) => typeof n === 'number' && Number.isFinite(n) && n >= lo && n <= hi;
function looksLikeValidCheckpoint(c: any): boolean {
return !!c && inRange(c.phase, 0, PET_MAX_SECONDS) && inRange(c.clock, 0, PET_MAX_SECONDS)
&& inRange(c.tear, 0, 1)
&& Number.isInteger(c.previous) && Number.isInteger(c.current)
&& Array.isArray(c.color) && c.color.length === 3
&& !!c.frame && c.frame.channel !== undefined && c.frame.arch !== undefined
&& inRange(c.frame.alpha, 0, 1) && inRange(c.frame.work, 0, 1)
&& typeof c.frame.hollow === 'boolean';
} Type guard
function isValidCheckpoint(c: unknown): c is PetCheckpoint {
const ck = c as PetCheckpoint;
return !!ck && typeof ck.phase === 'number' && typeof ck.tear === 'number'
&& Array.isArray(ck.color) && ck.color.length === 3
&& !!ck.frame && typeof ck.frame.hollow === 'boolean'
&& Array.isArray(ck.particles);
} Try / catch
try {
pet.restore(saved);
} catch (e) {
if (e.message === 'Invalid pet particle checkpoint.') {
console.warn('Discarding corrupt pet checkpoint; starting fresh');
pet = new PetSim(points, seed);
} else throw e;
} Prevention
- Validate checkpoints on write with the same rules restore enforces
- Version-stamp checkpoint payloads so schema changes trigger migration, not corruption
- Wrap restore in try/catch wherever checkpoints come from user storage
- Never hand-edit checkpoint JSON; regenerate from a live sim
When it happens
Trigger: Calling the PetSim restore/constructor path with a checkpoint whose numeric fields are out of range, whose frame.channel or frame.arch do not match the CHANNELS entry for c.current, whose color array is not length 3 of 0–255 bytes, or whose particles array has the wrong shape.
Common situations: Hand-edited or truncated checkpoint files in localStorage/disk; checkpoints written by a different CHANNELS layout after a code change; partially written saves; schema drift between app versions.
Understand the failure class
Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.
Related errors
- Invalid pet particle checkpoint.
- 1
- Invalid pet checkpoint.
- Invalid pet checkpoint.
- Invalid pet particle checkpoint.
AI-assisted analysis of Hmbown/CodeWhale@433685b202 (2026-09-15).
Data as JSON: /api/errors/1287b8115cdb6a53.
Report an issue: GitHub.
Appendix: source
Thrown at pet/src/core/pet-sim.ts:324
color: [...this.col], frame: { ...this.frame } };
}
/** Restore into a newly constructed sim. Authored body and seeded particle
* identity must match exactly; a checkpoint cannot replace the whale. */
restore(value: unknown): void {
const c = value as PetSimCheckpoint;
const inRange = (n: number, low: number, high: number) => Number.isFinite(n) && n >= low && n <= high;
if (!c || c.version !== 1 || c.expressionVersion !== undefined && ![1, 2].includes(c.expressionVersion) || (c.expressionVersion ?? 1) !== this.expressionVersion || !Array.isArray(c.body) || c.body.length !== this.p.length
|| c.body.some((v, i) => !Array.isArray(v) || v.length !== 3 || v[0] !== this.p[i].hx || v[1] !== this.p[i].hy || v[2] !== this.p[i].s)
|| !Array.isArray(c.particles) || c.particles.length !== this.p.length
|| c.particles.some(v => !Array.isArray(v) || v.length !== 8 || v.some((n, i) => !inRange(n, i === 4 || i === 5 ? 0 : -8, i === 4 || i === 5 ? 2 * PET_MAX_SECONDS : 8)))
|| !inRange(c.phase, 0, PET_MAX_SECONDS) || !inRange(c.clock, 0, PET_MAX_SECONDS) || !inRange(c.tear, 0, 1)
|| ![c.previous, c.current].every(n => Number.isInteger(n) && n >= 0 && n < CHANNELS.length)
|| !Array.isArray(c.color) || c.color.length !== 3 || c.color.some(n => !inRange(n, 0, 255))
|| !c.frame || ![c.frame.r, c.frame.g, c.frame.b].every(n => inRange(n, 0, 255))
|| !inRange(c.frame.alpha, 0, 1) || !inRange(c.frame.work, 0, 1) || typeof c.frame.hollow !== 'boolean'
|| c.frame.channel !== CHANNELS[c.current].key || c.frame.arch !== CHANNELS[c.current].arch)
throw new Error('Invalid pet particle checkpoint.');
this.phase = c.phase; this.clock = c.clock; this.tear = c.tear; this.prev = c.previous; this.cur = c.current;
this.col = [...c.color]; this.frame = { ...c.frame };
this.p.forEach((p, i) => { [p.x, p.y, p.vx, p.vy, p.jx, p.jy, p.tx, p.ty] = c.particles[i]; });
}
/** Advance the sim by dt seconds under `state`. Identical math on every port. */
step(dt: number, state: PetState, opts: PetOpts): void {
const S = (v: number) => lerp(0.5, v, opts.sensitivity);
const act = S(state.activity), coh = S(state.coherence), att = S(state.attention);
const seen = S(state.observed === undefined ? 1 : state.observed);
const motion = opts.motion ? 1 : 0;
this.phase += dt * (0.18 + act * 0.55) * motion;
this.clock += dt * (opts.motion ? 1 : 0);
if (CHANNEL_INDEX[state.channel] !== undefined) this.cur = CHANNEL_INDEX[state.channel];
const shown = this.cur;
const ch = CHANNELS[shown];
View on GitHub (pinned to 433685b202)