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
- Log the offending at value and compare with engine.lastTime (from checkpoint/snapshot); ensure at >= lastTime and at <= PET_MAX_SECONDS*1000.
- Convert absolute time to session-elapsed milliseconds: at = Date.now() - sessionStart, clamped to PET_MAX_SECONDS*1000.
- Before calling, clamp: at = Math.min(Math.max(at, lastKnownTime), PET_MAX_SECONDS * 1000) and skip or drop events that arrive out of order.
- If events can arrive out of order, buffer and sort by timestamp, or drop ones older than lastTime, before observing.
- 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
- Pass session-elapsed milliseconds, never Date.now() epoch values.
- Compute time from performance.now() to avoid wall-clock jumps and invalid-Date NaN.
- Track lastObserved yourself and drop/skip events older than it (monotonic delivery).
- Clamp every timestamp to [lastTime, PET_MAX_SECONDS * 1000] before calling observe().
- When replaying recorded streams, re-base timestamps to a fresh session clock first.
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
- Invalid Engine pet clock.
- 1
- A pinned task provider requires an explicit model
- A positive pull request number is required
- A provider and page loader are required.
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)