{"record":{"id":"006c3442cae69202","repo":"Hmbown/CodeWhale","slug":"only-a-live-recording-can-resume-observation-pet-native","errorCode":null,"errorMessage":"Only a live recording can resume observation.","messagePattern":"Only a live recording can resume observation\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"pet/ios/Resources/pet-native.js","lineNumber":419,"sourceCode":"        world.flip = c.flip;\n        world.lit = c.lit;\n        world.lastActivity = c.lastActivity;\n        world.addressedAt = c.addressedAt ?? -Infinity;\n        world.waitSince = c.waitSince;\n        world.food = structuredClone(c.food);\n        world.members = new Map(c.members.map(m => [m.id, { ...m }]));\n        world.lastStill = c.lastStill;\n        // JSON omits undefined properties; keep the same frame shape as makeFrame.\n        world.frame = { ...structuredClone(c.frame), telemetry: telemetry ? structuredClone(telemetry) : undefined };\n        world.voices = structuredClone(c.voices);\n        return world;\n    }\n    /** Resume observation beyond all already accepted live packets, without\n     * replaying their sound or exposing a stale request as current work. */\n    resumeObservation() {\n        const last = this.tapeLog.at(-1), end = last ? (last.sequence + 1) * 12 : 0;\n        if (!end || end - this.tick > 24)\n            throw new Error('Only a live recording can resume observation.');\n        while (this.tick < end)\n            this.step(1 / 30, { motion: false, sensitivity: 1 });\n        this.voices = [];\n    }\n    /** Branch at the current playhead; input is journalled for the next fixed tick.\n     * A live touch never needs to re-simulate the creature's entire lifetime. */\n    interact(kind, x, y) {\n        if (!['attention', 'food'].includes(kind) || !Number.isFinite(x) || !Number.isFinite(y) || Math.abs(x) > 1 || Math.abs(y) > 1)\n            throw new Error('Invalid pet interaction.');\n        if (this.branchTick !== this.tick)\n            this.interactionLog.splice(this.interactionIndex);\n        this.branchTick = this.tick;\n        this.interactionLog.push({ timeMs: (this.tick + 1) * 1000 / HZ, kind, x, y });\n    }\n    /** Accept a live source packet at the next 400ms boundary. The accepted tape,\n     * including any missing intervals, is the exact replay authority for this host. */\n    acceptTelemetry(input) {\n        (0, pet_telemetry_js_1.validatePetBucket)(input);","sourceCodeStart":401,"sourceCodeEnd":437,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/433685b2024e7bc4c99e1e2e326bcad39b4d9d65/pet/ios/Resources/pet-native.js#L401-L437","documentation":"resumeObservation() fast-forwards the simulation to the end of the last accepted live packet, then resumes observing. It only works when the recording is genuinely live: the last tape bucket's sequence must end within 24 ticks (~800ms at 1/30s steps) of the current tick. If there is no tape at all, or the gap between the current playhead and the end of the recording exceeds 24 ticks, the recording is stale or finished and resuming would skip or re-simulate history, so the world throws.","triggerScenarios":"Calling resumeObservation() on a PetWorld with an empty tapeLog (end === 0); calling it after playback/observation has drifted more than 24 ticks past the last accepted telemetry packet (end - tick > 24); calling it after the live source stopped sending packets long ago.","commonSituations":"UI 'resume watching' button pressed after the pet recording ended; live stream dropped and packets stopped, then resume is attempted; calling resumeObservation before any acceptTelemetry call.","solutions":["Call acceptTelemetry() with fresh packets so the tape's last sequence is within 24 ticks of the current tick before resuming","Check that this.hasTelemetry / tapeLog is non-empty before calling resumeObservation","If the gap is larger, replay or re-create the PetWorld from the archive rather than resuming","Guard the call: only enable resume in the UI while the live stream is actively delivering packets"],"exampleFix":"// before\nworld.resumeObservation(); // may be stale\n// after\nconst last = world.tapeLog.at(-1);\nconst end = last ? (last.sequence + 1) * 12 : 0;\nif (end && end - world.tick <= 24) world.resumeObservation();\nelse world.acceptTelemetry(freshBucket); // then resume","handlingStrategy":"validation","validationCode":"function canResume(world) {\n  const last = world.tapeLog.at(-1);\n  if (!last) return false;\n  const end = (last.sequence + 1) * 12;\n  return end - world.tick <= 24;\n}","typeGuard":null,"tryCatchPattern":"try {\n  world.resumeObservation();\n} catch (e) {\n  if (e.message.includes('live recording')) {\n    // fall back: replay from archive or wait for fresh packets\n  } else throw e;\n}","preventionTips":["Only offer resume in the UI while the live stream is actively delivering packets","Accept fresh telemetry before resuming after a pause","Never call resumeObservation on a world with an empty tapeLog","Treat resume as a live-only path; use replay/archives for finished recordings"],"tags":["state-machine","replay","stale-state"],"backgroundTag":"invalid-state-transition","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"}