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

  1. Re-serialize the checkpoint from a fresh PetSim run instead of restoring the corrupt one
  2. Validate the checkpoint against the same in-range/length/match rules before calling restore
  3. If CHANNELS changed, re-map old checkpoints' channel/arch fields to the new table
  4. 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

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


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)