Hmbown/CodeWhale · error · Error

Invalid pet particle checkpoint.

Error message

Invalid pet particle checkpoint.

What it means

A particle checkpoint is validated field-by-field before being applied: phase/clock/tear ranges, previous/current channel indices, 3-channel color, frame RGB/alpha/work ranges, boolean hollow, and frame channel/arch matching the current CHANNELS entry. Any violation throws 'Invalid pet particle checkpoint.' rather than rendering a corrupt pet.

Solutions

  1. Log the rejected checkpoint and compare each field against the constraints: phase/clock in [0, PET_MAX_SECONDS], tear in [0,1], integer channel indices < CHANNELS.length, color length 3 with 0-255 values, frame.channel/frame.arch equal to CHANNELS[current].key/.arch.
  2. If CHANNELS was reordered or renamed since the checkpoint was saved, re-save the checkpoint with the current build or migrate the stored channel key/arch and index.
  3. Replace undefined/NaN fields with valid defaults before restoring (e.g. color: [r,g,b], hollow: false).
  4. If the checkpoint is from an incompatible version, start a fresh pet instead of restoring.

Example fix

// before
pet.restoreCheckpoint(JSON.parse(savedJson)); // frame.arch is 'old-arch', CHANNELS now uses 'gyre'
// after
const c = JSON.parse(savedJson);
const ch = CHANNELS[c.current];
if (ch && c.frame && c.frame.channel === ch.key && c.frame.arch === ch.arch) {
  pet.restoreCheckpoint(c);
} else {
  c.frame = { r: 0, g: 0, b: 0, alpha: 0.3, hollow: false, channel: ch.key, arch: ch.arch, work: 0 };
  pet.restoreCheckpoint(c);
}
Defensive patterns

Strategy: validation

Validate before calling

const ok = (c, CH, MAX) => c && typeof c.phase === 'number' && typeof c.clock === 'number' &&
  c.phase >= 0 && c.phase <= MAX && c.clock >= 0 && c.clock <= MAX && c.tear >= 0 && c.tear <= 1 &&
  Number.isInteger(c.previous) && Number.isInteger(c.current) &&
  c.previous >= 0 && c.current >= 0 && c.current < CH.length &&
  Array.isArray(c.color) && c.color.length === 3 && c.color.every(n => n >= 0 && n <= 255) &&
  !!c.frame && c.frame.channel === CH[c.current].key && c.frame.arch === CH[c.current].arch;

Type guard

const isParticleCheckpoint = (c, CH) => !!c && typeof c.phase === 'number' && typeof c.tear === 'number' &&
  Number.isInteger(c.current) && c.current >= 0 && c.current < CH.length &&
  Array.isArray(c.color) && c.color.length === 3 &&
  !!c.frame && c.frame.channel === CH[c.current].key && c.frame.arch === CH[c.current].arch;

Try / catch

try {
  pet.restoreCheckpoint(c);
} catch (e) {
  if (e.message === 'Invalid pet particle checkpoint.') resetPetToDefault();
  else throw e;
}

Prevention

When it happens

Trigger: Calling the checkpoint-restore path (e.g. GyrePet.restoreCheckpoint(c) / constructor with checkpoint) where c is missing fields, has out-of-range numbers (NaN included — inRange rejects non-finite), an invalid channel index, a 2- or 4-length color array, or a frame whose channel/arch do not match CHANNELS[c.current].

Common situations: Restoring a checkpoint serialized by a different app version whose CHANNELS list changed (so channel/arch strings no longer match); hand-edited or truncated JSON checkpoints; NaN/undefined leaking in from a partial deserialization.

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@73e0f67d83 (2026-09-22). Data as JSON: /api/errors/615444720aa14edc. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/src/tui/pet_watch/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 73e0f67d83)