Hmbown/CodeWhale · error · Error

Invalid Engine pet metadata.

Error message

Invalid Engine pet metadata.

What it means

PetEngineTelemetry.observe() validates every incoming event payload before converting it into spans. This error means the `value` argument passed to observe was not a plain non-null object: it was null, undefined, a primitive, or an Array. The engine throws early so malformed telemetry never corrupts the span timeline.

Solutions

  1. Ensure you call observe() once per event object, not with an array of events — iterate and call observe per element.
  2. Check the variable passed as `value` is actually defined and an object at the call site (log it before calling).
  3. If events arrive as JSON strings, JSON.parse them first and confirm the result is a non-array object.
  4. Wrap observe() in try/catch and drop (or report) malformed events instead of letting one bad record crash the telemetry loop.

Example fix

// before
events.forEach(e => engine.observe(e, now)); // e may be null / an array

// after
if (e && typeof e === 'object' && !Array.isArray(e)) {
  engine.observe(e, now);
}
Defensive patterns

Strategy: type-guard

Validate before calling

function canObserve(v: unknown): boolean {
  return !!v && typeof v === 'object' && !Array.isArray(v);
}
if (!canObserve(event)) throw new TypeError('observe expects a plain object event');
engine.observe(event, timestamp);

Type guard

const isPlainObject = (v: unknown): v is Record<string, unknown> =>
  typeof v === 'object' && v !== null && !Array.isArray(v);

Try / catch

try {
  engine.observe(event, at);
} catch (err) {
  if (err instanceof Error && err.message === 'Invalid Engine pet metadata.') {
    console.warn('dropping malformed telemetry event', event);
    return; // skip, don't crash the loop
  }
  throw err;
}

Prevention

When it happens

Trigger: Calling engine.observe(null, t), observe(undefined, t), observe(42, t), observe('event', t), or observe([..], t) — any first argument that fails `value && typeof value === 'object' && !Array.isArray(value)`.

Common situations: Passing a parsed-JSON array of events instead of a single event; passing undefined because an upstream variable was never assigned; passing a class instance after JSON round-trip when it became a string; forwarding a Kafka/stream record whose payload field was never deserialized.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


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

Appendix: source

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

  /** 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));
    };
    const finish = (key: string) => { this.pulse(key, at); this.active.delete(key); };

View on GitHub (pinned to 433685b202)