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
- Remove or map unknown keys so the event only contains the allowed fields before calling observe().
- Truncate string values to 4096 characters before passing them in.
- Fix `event` to a supported string and `channel` to exactly 'text' or 'reasoning' (or omit it).
- Send `failed` as a real boolean and `index` as a non-negative safe integer (coerce from strings with Number() and validate).
- 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
- Whitelist-map upstream payloads to the engine's allowed keys; strip anything else before observe().
- Truncate every string field to 4096 chars as a hard rule in your adapter layer.
- Keep booleans as booleans and indexes as numbers through serialization (avoid JSON-stringified booleans).
- Pin the library version and re-check the allowed-fields list after upgrades.
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
- Duplicate .
- Unknown field: .
- Codewhale terminal receipt contained a non-scalar field
- Duplicate policy identity.
- Facts must be scalar metadata, not content objects.
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)