Hmbown/CodeWhale · error · Error

Invalid Engine pet metadata fields.

Error message

Invalid Engine pet metadata fields.

What it means

After the shape check, observe() validates the event's fields against a strict whitelist (`event`, `index`, `channel`, `tool_call_id`, `tool_name`, `id`, `worker_status`, `failed`) and per-field types. This error means the object was structurally an object but one or more fields violated the contract: an unknown key, a non-string `event`, a string longer than 4096 chars, an invalid `channel`, wrong types on the string-typed fields, a non-boolean `failed`, or a non-safe-integer/negative `index`.

Solutions

  1. Remove or map unknown keys so the event only contains the allowed fields before calling observe().
  2. Truncate string values to 4096 characters before passing them in.
  3. Fix `event` to a supported string and `channel` to exactly 'text' or 'reasoning' (or omit it).
  4. Send `failed` as a real boolean and `index` as a non-negative safe integer (coerce from strings with Number() and validate).
  5. Wrap observe() in try/catch and log `Object.keys(e)` on failure to identify the offending key quickly.

Example fix

// before
engine.observe({ event: 'message_started', index: 1.5, extra: true, channel: 'system' }, now);

// after
engine.observe({ event: 'message_started', index: 1, channel: 'text' }, now);
Defensive patterns

Strategy: validation

Validate before calling

const ALLOWED = ['event','index','channel','tool_call_id','tool_name','id','worker_status','failed'];
function validateEvent(e: Record<string, unknown>): string | null {
  if (Object.keys(e).some(k => !ALLOWED.includes(k))) return 'unknown key';
  if (typeof e.event !== 'string') return 'event must be string';
  if (Object.values(e).some(v => typeof v === 'string' && v.length > 4096)) return 'string too long';
  if (e.channel !== undefined && !['text','reasoning'].includes(e.channel as string)) return 'bad channel';
  if (e.failed !== undefined && typeof e.failed !== 'boolean') return 'failed must be boolean';
  if (e.index !== undefined && (!Number.isSafeInteger(e.index) || (e.index as number) < 0)) return 'bad index';
  return null;
}

Type guard

const isPetEvent = (v: unknown): v is { event: string; index?: number; channel?: 'text'|'reasoning'; failed?: boolean } & Record<string, unknown> =>
  typeof v === 'object' && v !== null && !Array.isArray(v) && typeof (v as any).event === 'string';

Try / catch

try {
  engine.observe(event, at);
} catch (err) {
  if (err instanceof Error && err.message === 'Invalid Engine pet metadata fields.') {
    console.error('event rejected:', JSON.stringify(Object.keys(event)));
    return;
  }
  throw err;
}

Prevention

When it happens

Trigger: Sending an event with extra/renamed keys (e.g. `kind` instead of `event`, or leftover fields like `timestamp`); `event` not a string; any string field exceeding 4096 chars (e.g. an untruncated tool output in `tool_name`); `channel: 'system'` instead of 'text'|'reasoning'; `index: 1.5` or `index: -1`; `failed: 'true'` (string) instead of boolean.

Common situations: Schema drift after a library version bump added/renamed event fields; forwarding raw provider payloads that carry extra metadata keys; stringifying booleans when serializing through a queue; giant tool outputs blowing the 4096-char cap.

Understand the failure class

Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.

Related errors


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

Appendix: source

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

    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); };
    switch (e.event) {
      case 'turn_started': this.active.clear(); this.waiting = undefined; break;
      case 'message_started': start(`message:${index()}`, 'assistant_message', 'communication'); this.waiting = undefined; break;
      case 'thinking_started': start(`thinking:${index()}`, 'thinking', 'reasoning'); this.waiting = undefined; break;
      case 'response_delta': {
        const reasoning = e.channel === 'reasoning';
        const key = `${reasoning ? 'thinking' : 'message'}:${index()}`;
        if (!this.active.has(key)) start(key, reasoning ? 'thinking' : 'assistant_message', reasoning ? 'reasoning' : 'communication');
        else this.pulse(key, at);

View on GitHub (pinned to 433685b202)