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
- Pass the checkpoint together with the exact 'to' value that was current when the checkpoint was saved (store them as a pair).
- Re-save the checkpoint at the desired playback position before restoring.
- 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.
- 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
- Save checkpoint and its tick/seek position atomically as one record.
- Never reuse a checkpoint with a different 'to' target.
- Version checkpoint payloads so format/time-cap changes invalidate stale saves explicitly.
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
- 1
- Pet checkpoint does not match its recording.
- Pet checkpoint does not match its recording.
- Pet checkpoint telemetry does not match its clock.
- Pet checkpoint telemetry does not match its clock.
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)