Hmbown/CodeWhale · error · Error

Pet checkpoint does not match its recording.

Error message

Pet checkpoint does not match its recording.

What it means

After rebuilding the world from a checkpoint, PetWorld cross-checks the snapshot against its recording: every tape bucket after bucketIndex must be empty of observed activity, all interaction-log entries before interactionIndex must be at or before frame.timeMs, and the next interaction entry must be after it. A mismatch means the checkpoint and its backing recording are inconsistent, so resume would replay or drop events incorrectly. The library throws rather than silently diverging from the recorded timeline.

Solutions

  1. Restore the checkpoint together with the exact tapeLog and interactionLog it was saved from — do not mix snapshots across recordings.
  2. Re-take the checkpoint at the current playhead instead of restoring the stale one.
  3. If the tape was trimmed, advance bucketIndex/interactionIndex in the checkpoint to a consistent point or drop the checkpoint.
  4. Enable atomic persistence so tape and checkpoint are written together.

Example fix

// before: pairing checkpoint c with a longer/newer tapeLog
const world = new PetWorld(points, c.tapeLog, c.interactionLog, ...); // throws
// after: use the tapeLog snapshotted inside the same checkpoint
const world = new PetWorld(points, c.tape, c.interactionLog, ...);
Defensive patterns

Strategy: validation

Validate before calling

function checkpointMatchesRecording(c, tape, interactionLog) {
  const eps = 1e-7;
  const stray = tape.some((b, i) => i > c.bucketIndex && b.simTimeMs <= c.frame.timeMs + eps &&
    (b.observed !== 0 || b.channel !== 'other' || b.errors || b.waiting || b.agentIds.length || b.onsets.some(Boolean) || b.activeMs.some(Boolean)));
  if (stray) return false;
  if (interactionLog.slice(0, c.interactionIndex).some(e => e.timeMs > c.frame.timeMs + eps)) return false;
  return !(interactionLog[c.interactionIndex]?.timeMs <= c.frame.timeMs + eps);
}

Type guard

const isConsistentCheckpoint = (c) => Number.isInteger(c?.bucketIndex) && Number.isInteger(c?.interactionIndex) && typeof c?.frame?.timeMs === 'number';

Try / catch

try {
  world = new PetWorld(points, tape, interactions, ver, v2);
  // matching checks run during restore
} catch (e) {
  if (e.message === 'Pet checkpoint does not match its recording.') {
    world = replayFromStart(tape); // fall back to full replay
  } else throw e;
}

Prevention

When it happens

Trigger: Restoring a checkpoint where a non-empty tape bucket exists after c.bucketIndex with simTimeMs <= frame.timeMs, where logged interactions before interactionIndex are later than frame.timeMs, or where the interaction at interactionIndex has timeMs <= frame.timeMs + 1e-7 — i.e. the checkpoint's playhead and interaction cursor disagree with the tape/interaction log.

Common situations: Mixing a checkpoint from one recording with the tape of another; trimming or rotating the tape log after the checkpoint was taken; checkpoint/telemetry writes that are not atomic so the tape advanced past the saved playhead; hand-splicing interaction logs.

Understand the failure class

Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22). Data as JSON: /api/errors/7def3d224bf1a539. Report an issue: GitHub.

Appendix: source

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

            || 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];
        const telemetry = candidate && c.frame.timeMs < candidate.simTimeMs + candidate.durationMs ? candidate : undefined;
        if (JSON.stringify(c.frame.telemetry) !== JSON.stringify(telemetry))
            throw new Error('Pet checkpoint telemetry does not match its clock.');
        world.sim.restore(c.sim);
        world.score.restore(c.score);
        world.random.restore(c.random);
        world.accumulator = c.accumulator;
        world.tick = c.tick;
        world.bucketIndex = c.bucketIndex;
        world.interactionIndex = c.interactionIndex;
        world.branchTick = c.branchTick;
        world.behaviour = c.behaviour;
        world.until = c.until;
        world.targetX = c.targetX;
        world.targetY = c.targetY;
        world.x = c.x;
        world.y = c.y;

View on GitHub (pinned to 73e0f67d83)