{"record":{"id":"bbcb7b8f6d4c8e94","repo":"Hmbown/CodeWhale","slug":"saved-pet-clock-does-not-match-its-checkpoint","errorCode":null,"errorMessage":"Saved pet clock does not match its checkpoint.","messagePattern":"Saved pet clock does not match its checkpoint\\.","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"pet/src/ui/pet.ts","lineNumber":53,"sourceCode":"  if (!audio || !sound || paused) return;\n  for (const voice of voices) {\n    const start = Math.floor(voice.start * audio.sampleRate), length = Math.ceil(voice.duration * audio.sampleRate) + 2;\n    const pcm = renderPetPCM([voice], start, length, audio.sampleRate);\n    const buffer = audio.createBuffer(2, length, audio.sampleRate);\n    buffer.copyToChannel(pcm.left, 0); buffer.copyToChannel(pcm.right, 1);\n    const node = audio.createBufferSource(); node.buffer = buffer; node.connect(audio.destination);\n    const at = anchor + start / audio.sampleRate;\n    if (at + buffer.duration < audio.currentTime) continue;\n    playing.add(node); node.onended = () => { playing.delete(node); node.disconnect(); };\n    node.start(Math.max(audio.currentTime, at), Math.max(0, audio.currentTime - at));\n  }\n}\nasync function rebuild(to = 0, checkpoint?: PetWorldCheckpoint, start?: PetWorldCheckpoint) {\n  const ticket = ++generation;\n  const next = start ? PetWorld.fromRecording(points, { petReplayVersion: 2, expressionVersion, tape, interactions, start, checkpoint })\n    : checkpoint ? PetWorld.restore(points, tape, interactions, checkpoint) : new PetWorld(points, tape, interactions, expressionVersion, true);\n  const ticks = Math.round(Math.max(next.startTimeMs / 1000, Math.min(PET_MAX_SECONDS, to)) * 30);\n  if (checkpoint && checkpoint.tick !== ticks) throw new Error('Saved pet clock does not match its checkpoint.');\n  silence(); restoring = true;\n  canvas.setAttribute('aria-busy', 'true');\n  for (const id of ['save', 'attention', 'feed']) get<HTMLButtonElement>(id).disabled = true;\n  for (let i = Math.round(next.frame.timeMs * 30 / 1000); i < ticks; i++) {\n    next.step(1 / 30, { motion: !motion.checked, sensitivity: 1 });\n    if (i > 0 && i % 600 === 0) { await new Promise(resolve => setTimeout(resolve, 0)); if (ticket !== generation) return; }\n  }\n  if (ticket !== generation) return;\n  adoptWorld(next);\n}\nfunction adoptWorld(next: PetWorld) {\n  world = next; worldMode = mode.value; tape = world.tape; interactions = [...world.interactions];\n  seek.min = String(next.startTimeMs / 1000); expressionVersion = next.sim.expressionVersion; restoring = false;\n  if (mode.value === 'replay') imported = { world: next, name: imported?.name ?? source.textContent ?? 'Imported replay' };\n  canvas.setAttribute('aria-busy', 'false');\n  for (const id of ['save', 'attention', 'feed']) get<HTMLButtonElement>(id).disabled = false;\n  seconds = next.frame.timeMs / 1000; accumulator = 0; last = 0;\n  if (audio) anchor = audio.currentTime - seconds + .08;","sourceCodeStart":35,"sourceCodeEnd":71,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/433685b2024e7bc4c99e1e2e326bcad39b4d9d65/pet/src/ui/pet.ts#L35-L71","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nrebuild(newSeekTime, savedCheckpoint); // savedCheckpoint.tick is from an older save\n// after\nconst pair = savedSession; // { to, checkpoint } saved together\nrebuild(pair.to, pair.checkpoint);","handlingStrategy":"try-catch","validationCode":"const expected = Math.round(Math.max(startTimeMs / 1000, Math.min(PET_MAX_SECONDS, to)) * 30);\nif (checkpoint && checkpoint.tick !== expected) throw new Error('checkpoint/clock mismatch');","typeGuard":"null","tryCatchPattern":"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; }","preventionTips":["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."],"tags":["state-restoration","checkpoint","consistency"],"backgroundTag":"checksum-mismatch","analyzedSha":"433685b2024e7bc4c99e1e2e326bcad39b4d9d65","analyzedAt":"2026-09-15T12:24:24.634Z","contentChangedAt":"2026-09-15T12:24:24.634Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}