Hmbown/CodeWhale · error · Error

Invalid legacy pet recording.

Error message

Invalid legacy pet recording.

What it means

Version 1 (legacy) recordings must never carry a start checkpoint, and any checkpoint they do carry must have petCheckpointVersion === 1. fromRecording throws when a legacy recording violates either rule, because v1 has no segmented replay support and mixing checkpoint versions would make deterministic restore impossible.

Solutions

  1. Remove the stray start/checkpoint from the legacy recording, or set checkpoint.petCheckpointVersion to 1 if it truly is a legacy checkpoint.
  2. Re-export the recording as petReplayVersion 2 with a valid v2 start checkpoint if segmentation is intended.
  3. Regenerate the recording with the tooling version that matches its declared petReplayVersion.

Example fix

// before
const rec = { petReplayVersion: 1, tape, interactions, start: someV2Checkpoint }; // throws
// after
const rec2 = { petReplayVersion: 2, expressionVersion: 1, tape, interactions, start: someV2Checkpoint };
Defensive patterns

Strategy: validation

Validate before calling

if (rec.petReplayVersion === 1 && (rec.start || (rec.checkpoint && rec.checkpoint.petCheckpointVersion !== 1)))
  throw new Error('recording is not a valid legacy v1 blob');

Type guard

function isLegacyRecording(r: unknown): r is PetRecording {
  const rec = r as PetRecording;
  return rec?.petReplayVersion === 1 && !rec.start && (!rec.checkpoint || rec.checkpoint.petCheckpointVersion === 1);
}

Try / catch

try {
  return PetWorld.fromRecording(points, rec);
} catch (e) {
  if (e.message === 'Invalid legacy pet recording.') {
    // upgrade to v2 or strip the stray checkpoint
  } else throw e;
}

Prevention

When it happens

Trigger: Calling PetWorld.fromRecording(points, value) with petReplayVersion === 1 and either r.start set, or r.checkpoint whose petCheckpointVersion is not 1.

Common situations: Upgrading a v1 recording to include a modern v2 checkpoint; a serialization bug that injects a start field into old recordings; importing recordings exported by a newer app into an older runtime that still tags them version 1.

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


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

Appendix: source

Thrown at pet/src/core/pet-world.ts:143

  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();
    c.petCheckpointVersion = 2; c.historyStart = this.tick; c.hasTelemetry = this.hasTelemetry;

View on GitHub (pinned to 433685b202)