Hmbown/CodeWhale · error · Error
Pet expression version does not match its checkpoint.
Error message
Pet expression version does not match its checkpoint.
What it means
PetWorld.fromRecording validates that any start/checkpoint embedded in a recording carries the same expressionVersion as the recording's own declared expression version. If a checkpoint's sim.expressionVersion (defaulting to 1) differs from the resolved top-level version, the recording's segments would replay under divergent behavior models, so the library refuses to build a world. This guards deterministic replay across expression engine upgrades.
Solutions
- Re-capture the recording with a single consistent build so the top-level expressionVersion and each checkpoint's sim.expressionVersion agree.
- Align the recording's top-level expressionVersion with the nested checkpoint's sim.expressionVersion (or vice versa) before calling fromRecording.
- If intentionally upgrading, run the checkpoint through the proper migration path that rewrites both version markers together.
Example fix
// before (mismatched versions)
const rec = { petReplayVersion: 2, expressionVersion: 2, tape: [...], interactions: [...], checkpoint: { petCheckpointVersion: 2, sim: { expressionVersion: 1 } } };
PetWorld.fromRecording(points, rec); // throws
// after
rec.checkpoint.sim.expressionVersion = 2;
PetWorld.fromRecording(points, rec); // ok Defensive patterns
Strategy: validation
Validate before calling
const version = r.expressionVersion ?? 1;
for (const c of [r.start, r.checkpoint]) {
if (c && (c.sim?.expressionVersion ?? 1) !== version)
throw new Error('checkpoint expressionVersion mismatch before fromRecording');
} Type guard
function hasMatchingExpressionVersion(r: unknown): r is PetRecording & { expressionVersion: 1 | 2 } {
const rec = r as PetRecording;
const v = rec?.expressionVersion ?? 1;
return [1, 2].includes(v) && [rec.start, rec.checkpoint].every(c => !c || (c.sim?.expressionVersion ?? 1) === v);
} Try / catch
try {
const world = PetWorld.fromRecording(points, rec);
} catch (e) {
if (e.message.includes('does not match its checkpoint')) {
// re-capture or migrate the recording to a single expression version
} else throw e;
} Prevention
- Never edit a recording's top-level expressionVersion without updating nested checkpoints.
- Store recordings and their checkpoints as one atomic blob.
- Version the whole recording format, not fields individually.
When it happens
Trigger: Calling PetWorld.fromRecording(points, value) with a recording whose expressionVersion is 1 or 2 while r.start.sim.expressionVersion or r.checkpoint.sim.expressionVersion is a different value (e.g. a checkpoint saved by an older pet build embedded into a newer-versioned recording).
Common situations: Mixing recordings and checkpoints from different app versions; hand-editing a recording's top-level expressionVersion without updating the nested checkpoint sim blob; migrating archived segments by changing only one of the two version fields.
Understand the failure class
Background: "is not a compatible type" / "cannot merge" errors: when a value's type doesn't match what the library requires — this error's family across 65 libraries.
Related errors
- Invalid legacy pet recording.
- Invalid pet checkpoint.
- Invalid pet interaction.
- Invalid pet recording.
- Invalid pet recording.
AI-assisted analysis of Hmbown/CodeWhale@433685b202 (2026-09-15).
Data as JSON: /api/errors/b787848444d51530.
Report an issue: GitHub.
Appendix: source
Thrown at pet/src/core/pet-world.ts:142
}
recording(withCheckpoint = true, completed = false): PetRecording {
const tapeEnd = completed ? this.bucketIndex + 1 : this.tapeLog.length;
const inputEnd = completed ? this.interactionIndex : this.interactionLog.length;
const history = this.historyDigest(tapeEnd, inputEnd);
return { petReplayVersion: this.segmented ? 2 : 1, expressionVersion: this.sim.expressionVersion,
tape: this.tapeLog.slice(0, tapeEnd), interactions: this.interactionLog.slice(0, inputEnd).map(e => ({ ...e })),
...(this.origin ? { start: { ...structuredClone(this.origin), hasTelemetry: this.hasTelemetry, history } } : {}),
...(withCheckpoint ? { checkpoint: { ...this.checkpoint(), history } } : {}) };
}
static fromRecording(points: [number, number][], value: unknown): PetWorld {
const r = value as PetRecording;
if (!r || ![1, 2].includes(r.petReplayVersion) || !Array.isArray(r.tape) || !Array.isArray(r.interactions)) throw new Error('Invalid pet recording.');
const version = r.expressionVersion === undefined ? 1 : r.expressionVersion;
if (![1, 2].includes(version)) throw new Error('Unsupported pet expression version.');
if (r.checkpoint !== undefined && !r.checkpoint || r.start !== undefined && !r.start) throw new Error('Invalid pet checkpoint.');
for (const c of [r.start, r.checkpoint]) if (c && (c.sim?.expressionVersion ?? 1) !== version) throw new Error('Pet expression version does not match its checkpoint.');
if (r.petReplayVersion === 1 && (r.start || r.checkpoint && r.checkpoint.petCheckpointVersion !== 1)) throw new Error('Invalid legacy pet recording.');
if (r.petReplayVersion === 2 && (!r.start || r.start.petCheckpointVersion !== 2 || r.start.historyStart !== r.start.tick)) throw new Error('The recording segment is missing its starting checkpoint.');
const first = r.start && PetWorld.restore(points, r.tape, r.interactions, r.start);
const world = r.checkpoint ? PetWorld.restore(points, r.tape, r.interactions, r.checkpoint) : first ?? new PetWorld(points, r.tape, r.interactions, version);
if (first) {
if (!world.segmented || world.tick < first.tick || r.checkpoint && r.checkpoint.historyStart !== first.tick) throw new Error('Pet segment checkpoints do not agree.');
world.origin = first.checkpoint();
}
return world;
}
/** Retire only consumed input. The exact origin makes each archived segment
* independently replayable; no particle, random stream or score is reset. */
prepareSegment(): PetSegment {
const dropTape = Math.max(0, this.bucketIndex), dropInputs = this.interactionIndex, previous = this.origin;
const tape = this.tapeLog.slice(dropTape), interactions = this.interactionLog.slice(dropInputs);
const points = this.sim.p.map(p => [p.hx, p.hy] as [number, number]);
const c = this.checkpoint();View on GitHub (pinned to 433685b202)