Hmbown/CodeWhale · error · Error

Invalid Engine pet clock.

Error message

Invalid Engine pet clock.

What it means

observe() validates the wall-clock timestamp before accepting an observation: at must be a finite number that is monotonically non-decreasing (>= this.lastTime) and within the session horizon (<= PET_MAX_SECONDS*1000, i.e. PET_MAX_SECONDS in milliseconds). Non-monotonic or out-of-horizon clocks are rejected so event ordering and window math stay consistent.

Solutions

  1. Log the offending at value and compare with engine.lastTime (from checkpoint/snapshot); ensure at >= lastTime and at <= PET_MAX_SECONDS*1000.
  2. Convert absolute time to session-elapsed milliseconds: at = Date.now() - sessionStart, clamped to PET_MAX_SECONDS*1000.
  3. Before calling, clamp: at = Math.min(Math.max(at, lastKnownTime), PET_MAX_SECONDS * 1000) and skip or drop events that arrive out of order.
  4. If events can arrive out of order, buffer and sort by timestamp, or drop ones older than lastTime, before observing.
  5. Guard Date arithmetic: replace NaN-producing Date.now() paths (invalid Date objects) with performance.now()-based elapsed time.

Example fix

// before
engine.observe(evt, Date.now());

// after
const elapsed = Math.min(Math.round(performance.now()), PET_MAX_SECONDS * 1000);
if (Number.isFinite(elapsed) && elapsed >= lastObserved) {
  engine.observe(evt, elapsed);
  lastObserved = elapsed;
}
Defensive patterns

Strategy: validation

Validate before calling

function isClockValid(at: number, lastTime: number): boolean {
  return Number.isFinite(at) && at >= lastTime && at <= PET_MAX_SECONDS * 1000;
}
if (isClockValid(at, lastObserved)) engine.observe(evt, at);

Type guard

null

Try / catch

try {
  engine.observe(evt, at);
} catch (err) {
  if (err instanceof Error && err.message === 'Invalid Engine pet clock.') {
    console.warn('dropping out-of-order/overflow clock', at);
    // or: re-base and clamp
    engine.observe(evt, Math.min(Math.max(at, lastObserved), PET_MAX_SECONDS * 1000));
  } else throw err;
}

Prevention

When it happens

Trigger: Calling engine.observe(value, at) with: a non-finite at (NaN, Infinity from Date arithmetic on an invalid date), a timestamp earlier than a previously observed one (clock rollback, out-of-order event delivery, replay of old events), or a timestamp beyond PET_MAX_SECONDS*1000 (e.g. passing Date.now() instead of session-elapsed ms).

Common situations: Passing Date.now() (epoch ms, huge) instead of elapsed session milliseconds; feeding events from two sources whose timestamps interleave backwards; system clock adjustment/NTP correction mid-session; replaying a recorded event stream without re-basing timestamps; computing at from a performance.now()/Date.now() mix.

Understand the failure class

Background: "value must be between 0 and 1" / "out of range" / "must not be negative" errors: fixing range-validation failures across open-source libraries — this error's family across 42 libraries.

Related errors


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

Appendix: source

Thrown at pet/src/core/pet-engine.ts:73

  }

  /** Transactional batch copy; failed validation cannot accept half a packet. */
  clone(): PetEngineTelemetry {
    const next = new PetEngineTelemetry();
    const copy = <T>(value: T): T => JSON.parse(JSON.stringify(value)) as T;
    next.events = copy(this.events);
    const spans = new Map(next.events.map(event => [event.id, event]));
    // Active and waiting spans must still reference their journal entry so a
    // later heartbeat extends the coverage consumed by bucket().
    const span = (event: WhaleEvent) => spans.get(event.id) ?? copy(event);
    next.active = new Map(Array.from(this.active, ([key, event]) => [key, span(event)]));
    next.waiting = this.waiting ? span(this.waiting) : undefined;
    next.sequence = this.sequence; next.lastTime = this.lastTime;
    return next;
  }

  observe(value: unknown, at: number): void {
    if (!Number.isFinite(at) || at < this.lastTime || at > PET_MAX_SECONDS * 1000) throw new Error('Invalid Engine pet clock.');
    if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('Invalid Engine pet metadata.');
    const e = value as Record<string, unknown>;
    const allowed = ['event', 'index', 'channel', 'tool_call_id', 'tool_name', 'id', 'worker_status', 'failed'];
    if (Object.keys(e).some(k => !allowed.includes(k)) || typeof e.event !== 'string'
      || Object.values(e).some(v => typeof v === 'string' && v.length > 4096)
      || e.channel !== undefined && !['text', 'reasoning'].includes(e.channel as string)
      || ['tool_call_id', 'tool_name', 'id', 'worker_status'].some(k => e[k] !== undefined && typeof e[k] !== 'string')
      || e.failed !== undefined && typeof e.failed !== 'boolean'
      || e.index !== undefined && (!Number.isSafeInteger(e.index) || (e.index as number) < 0))
      throw new Error('Invalid Engine pet metadata fields.');
    this.lastTime = at;
    this.events = this.events.filter(span => span.endTime >= at - 12_800);
    const id = (field: string) => { const s = e[field]; if (typeof s !== 'string' || !s) throw new Error(`Missing Engine ${field}.`); return s; };
    const index = () => { if (!Number.isSafeInteger(e.index)) throw new Error('Missing Engine index.'); return String(e.index); };
    const start = (key: string, name: string, category: Category, agentId?: string) => {
      if (this.active.size >= 256 && !this.active.has(key)) throw new Error('Too many active Engine pet spans.');
      this.active.set(key, this.add(name, category, at, agentId));
    };

View on GitHub (pinned to 433685b202)