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
- 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.
- 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.
- Replace undefined/NaN fields with valid defaults before restoring (e.g. color: [r,g,b], hollow: false).
- 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
- Validate checkpoints at load time with the same CHANNELS table the renderer uses.
- Migrate saved checkpoints when CHANNELS keys or arch values change.
- Treat NaN/undefined as invalid during deserialization, before restore.
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
- Invalid pet particle checkpoint.
- 1
- Invalid pet checkpoint.
- Invalid pet checkpoint.
- Invalid pet particle checkpoint.
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)