Hmbown/CodeWhale · error · Error

Saved pet clock does not match its checkpoint.

Error message

Saved pet clock does not match its checkpoint.

What it means

pet.ts's rebuild() fast-forwards a restored or newly constructed PetWorld to a requested playback position. When a checkpoint is supplied, it independently computes the tick count from the target time and throws if checkpoint.tick does not match, catching a checkpoint that was saved at a different playback time than the one being restored — i.e. the saved clock and the checkpointed state disagree.

Solutions

  1. Pass the checkpoint together with the exact 'to' value that was current when the checkpoint was saved (store them as a pair).
  2. Re-save the checkpoint at the desired playback position before restoring.
  3. If the mismatch is benign (seek target changed), rebuild the world without the checkpoint (or with a fresh checkpoint) instead of forcing the stale one.
  4. Verify the save path persists checkpoint.tick atomically with the world state.

Example fix

// before
rebuild(newSeekTime, savedCheckpoint); // savedCheckpoint.tick is from an older save
// after
const pair = savedSession; // { to, checkpoint } saved together
rebuild(pair.to, pair.checkpoint);
Defensive patterns

Strategy: try-catch

Validate before calling

const expected = Math.round(Math.max(startTimeMs / 1000, Math.min(PET_MAX_SECONDS, to)) * 30);
if (checkpoint && checkpoint.tick !== expected) throw new Error('checkpoint/clock mismatch');

Type guard

null

Try / catch

try { await rebuild(to, checkpoint); } catch (e) { if (/Saved pet clock does not match/.test(e.message)) { await rebuild(to); /* rebuild without stale checkpoint */ } else throw e; }

Prevention

When it happens

Trigger: Calling rebuild(to, checkpoint) (via PetWorld.restore or fromRecording) where Math.round(max(startTimeMs/1000, min(PET_MAX_SECONDS, to)) * 30) !== checkpoint.tick — i.e. the save-time and checkpoint tick were captured at different moments, or 'to' was changed after the checkpoint was taken.

Common situations: Restoring an old checkpoint with a new 'to' seek time; a save routine that wrote the checkpoint state and the clock in separate steps (torn save); imported recordings where the checkpoint tick and the recording duration disagree; a changed PET_MAX_SECONDS making a previously-valid checkpoint mismatch.

Understand the failure class

Background: Checksum mismatch errors: "checksum verification failed", "digest mismatch", "expected vs actual checksum" — what they mean and how to fix them — this error's family across 41 libraries.

Related errors


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

Appendix: source

Thrown at pet/src/ui/pet.ts:53

  if (!audio || !sound || paused) return;
  for (const voice of voices) {
    const start = Math.floor(voice.start * audio.sampleRate), length = Math.ceil(voice.duration * audio.sampleRate) + 2;
    const pcm = renderPetPCM([voice], start, length, audio.sampleRate);
    const buffer = audio.createBuffer(2, length, audio.sampleRate);
    buffer.copyToChannel(pcm.left, 0); buffer.copyToChannel(pcm.right, 1);
    const node = audio.createBufferSource(); node.buffer = buffer; node.connect(audio.destination);
    const at = anchor + start / audio.sampleRate;
    if (at + buffer.duration < audio.currentTime) continue;
    playing.add(node); node.onended = () => { playing.delete(node); node.disconnect(); };
    node.start(Math.max(audio.currentTime, at), Math.max(0, audio.currentTime - at));
  }
}
async function rebuild(to = 0, checkpoint?: PetWorldCheckpoint, start?: PetWorldCheckpoint) {
  const ticket = ++generation;
  const next = start ? PetWorld.fromRecording(points, { petReplayVersion: 2, expressionVersion, tape, interactions, start, checkpoint })
    : checkpoint ? PetWorld.restore(points, tape, interactions, checkpoint) : new PetWorld(points, tape, interactions, expressionVersion, true);
  const ticks = Math.round(Math.max(next.startTimeMs / 1000, Math.min(PET_MAX_SECONDS, to)) * 30);
  if (checkpoint && checkpoint.tick !== ticks) throw new Error('Saved pet clock does not match its checkpoint.');
  silence(); restoring = true;
  canvas.setAttribute('aria-busy', 'true');
  for (const id of ['save', 'attention', 'feed']) get<HTMLButtonElement>(id).disabled = true;
  for (let i = Math.round(next.frame.timeMs * 30 / 1000); i < ticks; i++) {
    next.step(1 / 30, { motion: !motion.checked, sensitivity: 1 });
    if (i > 0 && i % 600 === 0) { await new Promise(resolve => setTimeout(resolve, 0)); if (ticket !== generation) return; }
  }
  if (ticket !== generation) return;
  adoptWorld(next);
}
function adoptWorld(next: PetWorld) {
  world = next; worldMode = mode.value; tape = world.tape; interactions = [...world.interactions];
  seek.min = String(next.startTimeMs / 1000); expressionVersion = next.sim.expressionVersion; restoring = false;
  if (mode.value === 'replay') imported = { world: next, name: imported?.name ?? source.textContent ?? 'Imported replay' };
  canvas.setAttribute('aria-busy', 'false');
  for (const id of ['save', 'attention', 'feed']) get<HTMLButtonElement>(id).disabled = false;
  seconds = next.frame.timeMs / 1000; accumulator = 0; last = 0;
  if (audio) anchor = audio.currentTime - seconds + .08;

View on GitHub (pinned to 433685b202)