Hmbown/CodeWhale · error · Error

Only a live recording can resume observation.

Error message

Only a live recording can resume observation.

What it means

resumeObservation() fast-forwards the simulation to just past the last accepted live telemetry packet so observation continues without replaying old audio or resurrecting stale requests. It requires the recording to be genuinely live: there must be at least one packet, and the playhead may lag the end by at most 24 ticks (800 ms). Anything else — an empty recording or one that has fallen too far behind — is not live, so resuming is refused.

Solutions

  1. Ensure acceptTelemetry has delivered at least one packet and the stream is still live before calling resumeObservation.
  2. If the recording is old or archived, replay/seek through step() instead of resumeObservation().
  3. Keep the playback loop ticking (call step() regularly) so tick stays within 24 ticks of the last packet.
  4. Check tick vs. last packet sequence before calling resume and reconnect/refresh telemetry if the lag is too large.

Example fix

// before
world.resumeObservation(); // throws if recording is stale or empty
// after
const last = world.tapeLog.at(-1), end = last ? (last.sequence + 1) * 12 : 0;
if (end && end - world.tick <= 24) world.resumeObservation();
else world.step((end - world.tick) / 30, { motion: false, sensitivity: 1 });
Defensive patterns

Strategy: type-guard

Validate before calling

function canResumeObservation(world) {
  const last = world.tapeLog.at(-1);
  const end = last ? (last.sequence + 1) * 12 : 0;
  return end > 0 && end - world.tick <= 24;
}

Type guard

const isLiveRecording = (world) => Array.isArray(world?.tapeLog) && world.tapeLog.length > 0 && (() => { const l = world.tapeLog.at(-1); return (l.sequence + 1) * 12 - world.tick <= 24; })();

Try / catch

try {
  world.resumeObservation();
} catch (e) {
  if (e.message === 'Only a live recording can resume observation.') {
    // stale or empty recording: fast-forward manually or restart stream
    world.step(remainingTicks / 30, { motion: false, sensitivity: 1 });
  } else throw e;
}

Prevention

When it happens

Trigger: Calling world.resumeObservation() when tapeLog is empty (end === 0), or when end - this.tick > 24, i.e. the sim tick is more than 24 ticks behind the last packet's end (sequence+1)*12.

Common situations: Calling resume after loading an archived/finished recording instead of a live one; a paused or suspended host that stopped accepting telemetry for more than ~0.8 s; calling resume before the first acceptTelemetry packet arrives; clock drift after a long block of the event loop.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22). Data as JSON: /api/errors/986e4d666f8823ad. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/src/tui/pet_watch/pet-native.js:419

        world.flip = c.flip;
        world.lit = c.lit;
        world.lastActivity = c.lastActivity;
        world.addressedAt = c.addressedAt ?? -Infinity;
        world.waitSince = c.waitSince;
        world.food = structuredClone(c.food);
        world.members = new Map(c.members.map(m => [m.id, { ...m }]));
        world.lastStill = c.lastStill;
        // JSON omits undefined properties; keep the same frame shape as makeFrame.
        world.frame = { ...structuredClone(c.frame), telemetry: telemetry ? structuredClone(telemetry) : undefined };
        world.voices = structuredClone(c.voices);
        return world;
    }
    /** Resume observation beyond all already accepted live packets, without
     * replaying their sound or exposing a stale request as current work. */
    resumeObservation() {
        const last = this.tapeLog.at(-1), end = last ? (last.sequence + 1) * 12 : 0;
        if (!end || end - this.tick > 24)
            throw new Error('Only a live recording can resume observation.');
        while (this.tick < end)
            this.step(1 / 30, { motion: false, sensitivity: 1 });
        this.voices = [];
    }
    /** Branch at the current playhead; input is journalled for the next fixed tick.
     * A live touch never needs to re-simulate the creature's entire lifetime. */
    interact(kind, x, y) {
        if (!['attention', 'food'].includes(kind) || !Number.isFinite(x) || !Number.isFinite(y) || Math.abs(x) > 1 || Math.abs(y) > 1)
            throw new Error('Invalid pet interaction.');
        if (this.branchTick !== this.tick)
            this.interactionLog.splice(this.interactionIndex);
        this.branchTick = this.tick;
        this.interactionLog.push({ timeMs: (this.tick + 1) * 1000 / HZ, kind, x, y });
    }
    /** Accept a live source packet at the next 400ms boundary. The accepted tape,
     * including any missing intervals, is the exact replay authority for this host. */
    acceptTelemetry(input) {
        (0, pet_telemetry_js_1.validatePetBucket)(input);

View on GitHub (pinned to 73e0f67d83)