Hmbown/CodeWhale · error · Error

Invalid pet score checkpoint.

Error message

Invalid pet score checkpoint.

What it means

restore() validates a deserialized checkpoint tuple before assigning it back to the audio state. A valid checkpoint is exactly [lastWindow, lastSequence, lastAddress]: an array of length 3 whose first two entries are safe integers in [-1, PET_MAX_SECONDS*2.5] and whose third entry is a boolean. Anything else (wrong shape, wrong types, out-of-range values, or a corrupted/truncated payload) is rejected so bad persisted state can never poison render state.

Solutions

  1. Inspect the value passed to restore(): log or console.log it right before the call and verify it is exactly [number, number, boolean].
  2. Ensure checkpoints are only created via checkpoint() and stored verbatim — do not reshape, sort, or spread the tuple before saving.
  3. Clamp/sanitize legacy payloads before restoring: coerce 0/1 to boolean and clamp window/sequence into [-1, PET_MAX_SECONDS*2.5].
  4. If a versioned persisted format changed, migrate old checkpoints (or drop them and start from a fresh checkpoint()) rather than passing them to restore().
  5. Wrap restore() in try/catch and fall back to checkpoint() defaults on failure.

Example fix

// before
pet.restore(JSON.parse(saved));

// after
const raw = JSON.parse(saved);
const ck = Array.isArray(raw) && raw.length === 3
  ? [Number(raw[0]), Number(raw[1]), Boolean(raw[2])]
  : pet.checkpoint();
try { pet.restore(ck); } catch { pet.restore(pet.checkpoint()); }
Defensive patterns

Strategy: validation

Validate before calling

function isValidCheckpoint(v: unknown): boolean {
  return Array.isArray(v) && v.length === 3
    && v.slice(0, 2).every(n => Number.isSafeInteger(n) && n >= -1 && n <= PET_MAX_SECONDS * 2.5)
    && typeof v[2] === 'boolean';
}
if (!isValidCheckpoint(raw)) raw = pet.checkpoint();

Type guard

function isCheckpoint(v: unknown): v is [number, number, boolean] {
  return Array.isArray(v) && v.length === 3
    && Number.isSafeInteger(v[0]) && v[0] >= -1 && v[0] <= PET_MAX_SECONDS * 2.5
    && Number.isSafeInteger(v[1]) && v[1] >= -1 && v[1] <= PET_MAX_SECONDS * 2.5
    && typeof v[2] === 'boolean';
}

Try / catch

try {
  pet.restore(saved);
} catch (err) {
  if (err instanceof Error && err.message === 'Invalid pet score checkpoint.') {
    pet.restore(pet.checkpoint()); // reset to defaults
  } else throw err;
}

Prevention

When it happens

Trigger: Calling petAudio.restore() with: a non-array (null, object, string), an array of length != 3, non-integer or negative-below -1 or above PET_MAX_SECONDS*2.5 window/sequence values (e.g. NaN, Infinity, floats), or a non-boolean third element (e.g. 0/1 stored instead of true/false).

Common situations: Loading a checkpoint from localStorage/JSON that was written by an older library version with a different tuple arity; JSON round-trip coercion turning booleans into numbers or numbers into strings; a schema migration truncating or reordering the array; a hand-edited save file.

Understand the failure class

Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@433685b202 (2026-09-15). Data as JSON: /api/errors/7db10ea876e91989. Report an issue: GitHub.

Appendix: source

Thrown at pet/src/core/pet-audio.ts:21

import type { WorldFrame } from './pet-world.js';

export interface PetVoice {
  id: string; start: number; duration: number; frequency: number;
  gain: number; pan: number; kind: 'tone' | 'noise';
}

/** Core emits score events; WebAudio / AVAudioEngine only present their PCM.
 * Calling at any display cadence produces the same score when all world ticks
 * are supplied. Calling twice for a world tick cannot retrigger a voice. */
export class PetScore {
  private lastWindow = -1;
  private lastSequence = -1;
  private lastAddress = false;
  checkpoint(): [number, number, boolean] { return [this.lastWindow, this.lastSequence, this.lastAddress]; }
  restore(value: unknown): void {
    if (!Array.isArray(value) || value.length !== 3
      || !value.slice(0, 2).every(n => Number.isSafeInteger(n) && n >= -1 && n <= PET_MAX_SECONDS * 2.5) || typeof value[2] !== 'boolean')
      throw new Error('Invalid pet score checkpoint.');
    [this.lastWindow, this.lastSequence, this.lastAddress] = value as [number, number, boolean];
  }
  voices(frame: WorldFrame): PetVoice[] {
    const time = frame.timeMs / 1000, window = Math.floor((frame.timeMs + 1e-7) / 400);
    const out: PetVoice[] = [];
    const add = (id: string, frequency: number, duration: number, gain: number, pan = 0, delay = 0, kind: PetVoice['kind'] = 'tone') =>
      out.push({ id, start: time + delay, duration, frequency, gain, pan, kind });
    const t = frame.telemetry, fresh = t !== undefined && t.sequence !== this.lastSequence;
    if (fresh) {
      this.lastSequence = t.sequence;
      for (let c = 0; c < CHANNELS.length; c++) {
        const channel = CHANNELS[c], n = t.onsets[c];
        if (!n || channel.sustained || ['human', 'error'].includes(channel.key)) continue;
        add(`onset:${t.sequence}:${c}`, channel.freq, .24, .035 * Math.min(2, Math.sqrt(n)), (c / 12 - .5) * .7);
      }
      if (t.errors) add(`tear:${t.sequence}`, CHANNELS.find(c => c.key === 'error')!.freq, .22, .05, 0, 0, 'noise');
    }
    const address = frame.state.channel === 'human' && frame.state.attention > .5;

View on GitHub (pinned to 433685b202)