Hmbown/CodeWhale · error · Error

Invalid pet particle checkpoint.

Error message

Invalid pet particle checkpoint.

What it means

Pet checkpoints restored from storage are fully validated before the engine accepts their state (phase/clock ranges, integer channel indices, RGB color bounds, alpha/work in [0,1], boolean hollow, and frame channel/arch matching the current channel). If any field of the checkpoint object is missing, out of range, or inconsistent with the channel tables, the engine throws rather than animating from corrupt state.

Solutions

  1. Re-serialize the checkpoint from a live pet instance instead of editing the stored JSON by hand.
  2. Fix the specific failing field flagged by validation (ranges are: phase/clock in [0,PET_MAX_SECONDS], color RGB 0-255, alpha/work 0-1, hollow boolean, channel indices valid integers).
  3. Ensure frame.channel and frame.arch match exports.CHANNELS[current] after any channel change.
  4. Migrate old checkpoints through the version's loader rather than feeding them directly to the new code.

Example fix

// before
new ParticleState({ phase: 12, clock: 3, tear: 0.1, previous: 0, current: 1, color: [255,0,0] })
// after
new ParticleState({ phase: 12, clock: 3, tear: 0.1, previous: 0, current: 1, color: [255,0,0],
  frame: { r: 255, g: 0, b: 0, alpha: 0.3, hollow: false, channel: CHANNELS[1].key, arch: CHANNELS[1].arch, work: 0 } })
Defensive patterns

Strategy: type-guard

Validate before calling

function isValidCheckpoint(c, CHANNELS, PET_MAX_SECONDS) {
  const inRange = (n, lo, hi) => typeof n === 'number' && Number.isFinite(n) && n >= lo && n <= hi;
  return !!c && 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.every(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;
}

Type guard

const inRange = (n, lo, hi) => typeof n === 'number' && Number.isFinite(n) && n >= lo && n <= hi;

Try / catch

try { engine.restore(checkpoint); }
catch (e) { if (e.message === 'Invalid pet particle checkpoint.') { engine.reset(); log.warn('discarded corrupt pet checkpoint'); } else throw e; }

Prevention

When it happens

Trigger: Calling the checkpoint-restore constructor/path with an object that fails any validation: missing frame, color array not length 3, frame.channel not equal to CHANNELS[c.current].key, non-boolean hollow, alpha/work outside [0,1], phase or clock outside [0, PET_MAX_SECONDS], or previous/current indices outside CHANNELS.

Common situations: Hand-editing a persisted pet JSON file; a schema change in the checkpoint format between app versions; truncation/corruption during file sync; constructing a checkpoint by hand and forgetting derived fields like frame.channel/arch.

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/05d17fa733711430. Report an issue: GitHub.

Appendix: source

Thrown at pet/ios/Resources/pet-native.js:945

            phase: this.phase, clock: this.clock, tear: this.tear, previous: this.prev, current: this.cur,
            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) {
        const c = value;
        const inRange = (n, low, high) => 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 * exports.PET_MAX_SECONDS : 8)))
            || !inRange(c.phase, 0, exports.PET_MAX_SECONDS) || !inRange(c.clock, 0, exports.PET_MAX_SECONDS) || !inRange(c.tear, 0, 1)
            || ![c.previous, c.current].every(n => Number.isInteger(n) && n >= 0 && n < exports.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 !== exports.CHANNELS[c.current].key || c.frame.arch !== exports.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, state, opts) {
        const S = (v) => 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 (exports.CHANNEL_INDEX[state.channel] !== undefined)

View on GitHub (pinned to 433685b202)