{"record":{"id":"174c74192fd8666e","repo":"Hmbown/CodeWhale","slug":"invalid-pet-export-cursor-world","errorCode":null,"errorMessage":"Invalid pet export cursor.","messagePattern":"Invalid pet export cursor\\.","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"pet/src/core/pet-world.ts","lineNumber":179,"sourceCode":"    c.petCheckpointVersion = 2; c.historyStart = this.tick; c.hasTelemetry = this.hasTelemetry;\n    c.bucketIndex -= dropTape; c.interactionIndex = 0;\n    c.history = new PetWorld(points, tape, interactions, this.sim.expressionVersion, true).historyDigest();\n    const next = PetWorld.restore(points, tape, interactions, c); next.origin = next.checkpoint();\n    let committed = false;\n    return { recording: next.recording(), archive: this.recording(true, true), commit: () => {\n      if (committed || this.origin !== previous || this.tick < c.tick) throw new Error('The pet recording segment has changed.');\n      this.tapeLog.splice(0, dropTape); this.interactionLog.splice(0, dropInputs);\n      this.bucketIndex -= dropTape; this.interactionIndex -= dropInputs;\n      this.tapeHashes = []; this.hashTape(0); this.segmented = true; this.origin = next.origin;\n      committed = true;\n    } };\n  }\n\n  /** Lossless version 1 export, including the current pose and score. Drivers\n   * consume all chunks synchronously on the world's owner before another tick.\n   * Only a small slice is serialized inside an embedded runtime at a time. */\n  recordingChunk(index: number, completed = false): string | null {\n    if (!Number.isSafeInteger(index) || index < 0) throw new Error('Invalid pet export cursor.');\n    const size = 16, tapeEnd = completed ? this.bucketIndex + 1 : this.tapeLog.length;\n    const inputEnd = completed ? this.interactionIndex : this.interactionLog.length;\n    const tapes = Math.ceil(tapeEnd / size), inputs = Math.ceil(inputEnd / size);\n    if (index === 0) return `{\"petReplayVersion\":${this.segmented ? 2 : 1},\"expressionVersion\":${this.sim.expressionVersion},\"tape\":[`;\n    if (index <= tapes) return (index === 1 ? '' : ',') + JSON.stringify(this.tapeLog.slice((index - 1) * size, Math.min(tapeEnd, index * size))).slice(1, -1);\n    if (index === tapes + 1) return '],\"interactions\":[';\n    const part = index - tapes - 2;\n    if (part < inputs) return (part === 0 ? '' : ',') + JSON.stringify(this.interactionLog.slice(part * size, Math.min(inputEnd, (part + 1) * size))).slice(1, -1);\n    if (part === inputs) {\n      const history = this.historyDigest(tapeEnd, inputEnd);\n      return `]${this.origin ? ',\"start\":' + JSON.stringify({ ...this.origin, hasTelemetry: this.hasTelemetry, history }) : ''},\"checkpoint\":${JSON.stringify({ ...this.checkpoint(), history })}}`;\n    }\n    return null;\n  }\n\n  /** Prefix states preserve the original FNV checksum byte for byte. Live\n   * appends and replacements hash only the changed suffix, so checkpointing\n   * does not rescan hours of accepted telemetry on the world worker. */","sourceCodeStart":161,"sourceCodeEnd":197,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/433685b2024e7bc4c99e1e2e326bcad39b4d9d65/pet/src/core/pet-world.ts#L161-L197","documentation":"recordingChunk(index) paginates the lossless recording export into fixed-size chunks indexed from 0. A negative or non-safe-integer cursor cannot address any chunk, so the library rejects it up front rather than returning misleading data.","triggerScenarios":"Calling world.recordingChunk(-1) or recordingChunk(1.5), or passing a non-numeric index (e.g. a parsed string) as the cursor.","commonSituations":"Parsing the next-chunk pointer out of JSON and forgetting Number(); decrementing a cursor that reached 0; arithmetic on chunk counters producing floats.","solutions":["Pass a non-negative safe integer as the cursor, starting at 0 and incrementing by 1 until recordingChunk returns null.","Coerce/validate the index with Number.isSafeInteger(i) && i >= 0 before calling.","Stop iterating when recordingChunk returns null instead of guessing the last index."],"exampleFix":"// before\nlet i = chunks.length - 1; // could be -1 on empty\nworld.recordingChunk(i); // throws when i === -1\n// after\nfor (let i = 0; ; i++) { const c = world.recordingChunk(i); if (c === null) break; chunks.push(c); }","handlingStrategy":"validation","validationCode":"if (!Number.isSafeInteger(index) || index < 0) throw new Error('export cursor must be a non-negative safe integer');\nconst chunk = world.recordingChunk(index);","typeGuard":"function isValidExportCursor(i: unknown): i is number {\n  return typeof i === 'number' && Number.isSafeInteger(i) && i >= 0;\n}","tryCatchPattern":"try {\n  const chunk = world.recordingChunk(Number(cursor));\n} catch (e) {\n  if (e.message === 'Invalid pet export cursor.') {\n    cursor = 0; // restart pagination from a known-good cursor\n  } else throw e;\n}","preventionTips":["Always start pagination at index 0 and stop at null.","Coerce cursor with Number() when it comes from JSON or storage.","Loop rather than computing the last chunk index yourself."],"tags":["export","pagination","invalid-argument"],"backgroundTag":"invalid-argument-value","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"}