Hmbown/CodeWhale · error · Error

Invalid pet world checkpoint.

Error message

Invalid pet world checkpoint.

What it means

PetWorld's checkpoint deserializer validates every field of a restored snapshot (frame needs mode, surface/caustic ranges, food coordinates, pod membership, voices array and per-voice id) before rebuilding the world. If any field fails these structural or range checks the constructor throws so a corrupt or hand-edited checkpoint is never loaded. It is a load-time integrity gate for persisted pet-world state.

Solutions

  1. Compare the failing checkpoint's frame against the validator field-by-field (needs, surface, caustic, food, pod, voices) and fix or regenerate the offending field.
  2. Re-create the checkpoint by recording a fresh session instead of restoring the corrupt one.
  3. Confirm the checkpoint and code versions match (petCheckpointVersion) and migrate old-format checkpoints before constructing PetWorld.
  4. Validate the checkpoint with the same range/shape predicates before passing it to the constructor to get a precise field-level error.

Example fix

// before: frame.pod drifted from members after roster change
new PetWorld(points, tape, interactions, c.sim?.expressionVersion ?? 1, true) // throws
// after: rebuild checkpoint frame.pod to match current members before restore
c.frame.pod = structuredClone(members);
new PetWorld(points, tape, interactions, c.sim?.expressionVersion ?? 1, true);
Defensive patterns

Strategy: validation

Validate before calling

function isValidCheckpoint(c, members) {
  const range = (v, lo, hi) => typeof v === 'number' && v >= lo && v <= hi;
  const f = c?.frame;
  if (!f || !['none','orient','approach','call'].includes(f.needs)) return false;
  if (!range(f.surface, -0.9, -0.8) || !range(f.caustic, 0, 1)) return false;
  if (f.food !== null && (!f.food || !range(f.food.x, -1, 1) || !range(f.food.y, -1, 1.21) || !range(f.food.life, 0, 1))) return false;
  if (JSON.stringify(f.pod) !== JSON.stringify(members)) return false;
  if (!Array.isArray(c.voices) || c.voices.length > 1024) return false;
  return c.voices.every(v => v && typeof v.id === 'string' && v.id.length <= 256);
}

Type guard

const isCheckpoint = (c) => !!c && typeof c === 'object' && c.frame !== null && typeof c.frame === 'object' && Array.isArray(c.voices);

Try / catch

try {
  const world = new PetWorld(points, tape, interactions, ver, v2);
} catch (e) {
  if (e.message === 'Invalid pet world checkpoint.') {
    console.error('checkpoint failed validation, re-recording');
    world = startFreshWorld();
  } else throw e;
}

Prevention

When it happens

Trigger: Constructing a PetWorld from a checkpoint object `c` whose frame.needs is not one of none/orient/approach/call, whose frame.surface or frame.caustic is outside [-0.9,-0.8] / [0,1], whose food entry exists but has missing/out-of-range x,y or life, whose frame.pod JSON differs from the provided members list, or whose voices array exceeds 1024 entries or contains entries without a string id of length <= 256.

Common situations: Restoring checkpoints saved by an older pet-native version whose frame schema changed; hand-editing or truncating a checkpoint JSON; a storage layer that partially wrote the checkpoint; loading a checkpoint from a different pet whose pod roster does not match the members argument.

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

Appendix: source

Thrown at crates/tui/src/tui/pet_watch/pet-native.js:365

            || !integer(c.branchTick, -1, c.tick) || !integer(c.random, 0, 0xffffffff)
            || !['swim', 'dive', 'roll', 'breathe', 'drift', 'doze', 'wake'].includes(c.behaviour)
            || !range(c.until, 0, pet_sim_js_1.PET_MAX_SECONDS + 10) || ![c.targetX, c.targetY, c.x, c.y, c.flip].every(n => range(n, -1, 1))
            || !range(c.lit, 0, 1) || !range(c.lastActivity, 0, c.tick / HZ)
            || c.addressedAt !== null && !range(c.addressedAt, 0, c.tick / HZ)
            || !range(c.waitSince, -1, c.tick / HZ) || typeof c.lastStill !== 'string' || c.lastStill.length > 2048
            || !Array.isArray(c.members) || c.members.length > 6
            || c.members.some(m => !m || typeof m.id !== 'string' || !m.id || m.id.length > 4096
                || !integer(m.slot, 0, 5) || !range(m.phase, 0, Math.PI * 2) || typeof m.present !== 'boolean')
            || new Set(c.members.map(m => m.id)).size !== c.members.length || new Set(c.members.map(m => m.slot)).size !== c.members.length
            || c.food !== null && (!c.food || !range(c.food.time, 0, c.tick / HZ) || ![c.food.x, c.food.y].every(n => range(n, -1, 1)))
            || !c.frame || c.frame.timeMs !== c.tick * 1000 / HZ || c.frame.behaviour !== c.behaviour
            || !['none', 'orient', 'approach', 'call'].includes(c.frame.needs)
            || !range(c.frame.surface, -.9, -.8) || !range(c.frame.caustic, 0, 1)
            || c.frame.food !== null && (!c.frame.food || !range(c.frame.food.x, -1, 1) || !range(c.frame.food.y, -1, 1.21) || !range(c.frame.food.life, 0, 1))
            || JSON.stringify(c.frame.pod) !== JSON.stringify(c.members)
            || !Array.isArray(c.voices) || c.voices.length > 1024
            || c.voices.some(v => !v || typeof v.id !== 'string' || v.id.length > 256))
            throw new Error('Invalid pet world checkpoint.');
        (0, pet_sim_js_1.validatePetState)(c.frame.state);
        (0, pet_audio_js_1.renderPetPCM)(c.voices, 0, 0);
        const world = new PetWorld(points, tape, interactions, c.sim?.expressionVersion ?? 1, c.petCheckpointVersion === 2);
        if (c.petCheckpointVersion === 2) {
            world.hasTelemetry = c.hasTelemetry;
            world.origin = structuredClone(c);
        }
        if (c.history !== world.historyDigest()
            || c.bucketIndex >= 0 && world.tape[c.bucketIndex].simTimeMs > c.frame.timeMs + 1e-7
            // acceptTelemetry may fill past gaps after the most recent fixed tick.
            // Preserve that pending cursor exactly; it may lag only over empty gaps.
            || world.tape.some((b, i) => i > c.bucketIndex && b.simTimeMs <= c.frame.timeMs + 1e-7
                && (b.observed !== 0 || b.channel !== 'other' || b.errors || b.waiting || b.agentIds.length
                    || b.onsets.some(Boolean) || b.activeMs.some(Boolean)))
            || world.interactionLog.slice(0, c.interactionIndex).some(e => e.timeMs > c.frame.timeMs + 1e-7)
            || world.interactionLog[c.interactionIndex]?.timeMs <= c.frame.timeMs + 1e-7)
            throw new Error('Pet checkpoint does not match its recording.');
        const candidate = world.tape[c.bucketIndex];

View on GitHub (pinned to 73e0f67d83)