{"record":{"id":"c53803194d1ef425","repo":"Hmbown/CodeWhale","slug":"invalid-engine-pet-metadata-fields-c53803","errorCode":null,"errorMessage":"Invalid Engine pet metadata fields.","messagePattern":"Invalid Engine pet metadata fields\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"pet/src/core/pet-engine.ts","lineNumber":83,"sourceCode":"    const span = (event: WhaleEvent) => spans.get(event.id) ?? copy(event);\n    next.active = new Map(Array.from(this.active, ([key, event]) => [key, span(event)]));\n    next.waiting = this.waiting ? span(this.waiting) : undefined;\n    next.sequence = this.sequence; next.lastTime = this.lastTime;\n    return next;\n  }\n\n  observe(value: unknown, at: number): void {\n    if (!Number.isFinite(at) || at < this.lastTime || at > PET_MAX_SECONDS * 1000) throw new Error('Invalid Engine pet clock.');\n    if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('Invalid Engine pet metadata.');\n    const e = value as Record<string, unknown>;\n    const allowed = ['event', 'index', 'channel', 'tool_call_id', 'tool_name', 'id', 'worker_status', 'failed'];\n    if (Object.keys(e).some(k => !allowed.includes(k)) || typeof e.event !== 'string'\n      || Object.values(e).some(v => typeof v === 'string' && v.length > 4096)\n      || e.channel !== undefined && !['text', 'reasoning'].includes(e.channel as string)\n      || ['tool_call_id', 'tool_name', 'id', 'worker_status'].some(k => e[k] !== undefined && typeof e[k] !== 'string')\n      || e.failed !== undefined && typeof e.failed !== 'boolean'\n      || e.index !== undefined && (!Number.isSafeInteger(e.index) || (e.index as number) < 0))\n      throw new Error('Invalid Engine pet metadata fields.');\n    this.lastTime = at;\n    this.events = this.events.filter(span => span.endTime >= at - 12_800);\n    const id = (field: string) => { const s = e[field]; if (typeof s !== 'string' || !s) throw new Error(`Missing Engine ${field}.`); return s; };\n    const index = () => { if (!Number.isSafeInteger(e.index)) throw new Error('Missing Engine index.'); return String(e.index); };\n    const start = (key: string, name: string, category: Category, agentId?: string) => {\n      if (this.active.size >= 256 && !this.active.has(key)) throw new Error('Too many active Engine pet spans.');\n      this.active.set(key, this.add(name, category, at, agentId));\n    };\n    const finish = (key: string) => { this.pulse(key, at); this.active.delete(key); };\n    switch (e.event) {\n      case 'turn_started': this.active.clear(); this.waiting = undefined; break;\n      case 'message_started': start(`message:${index()}`, 'assistant_message', 'communication'); this.waiting = undefined; break;\n      case 'thinking_started': start(`thinking:${index()}`, 'thinking', 'reasoning'); this.waiting = undefined; break;\n      case 'response_delta': {\n        const reasoning = e.channel === 'reasoning';\n        const key = `${reasoning ? 'thinking' : 'message'}:${index()}`;\n        if (!this.active.has(key)) start(key, reasoning ? 'thinking' : 'assistant_message', reasoning ? 'reasoning' : 'communication');\n        else this.pulse(key, at);","sourceCodeStart":65,"sourceCodeEnd":101,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/433685b2024e7bc4c99e1e2e326bcad39b4d9d65/pet/src/core/pet-engine.ts#L65-L101","documentation":"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`.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nengine.observe({ event: 'message_started', index: 1.5, extra: true, channel: 'system' }, now);\n\n// after\nengine.observe({ event: 'message_started', index: 1, channel: 'text' }, now);","handlingStrategy":"validation","validationCode":"const ALLOWED = ['event','index','channel','tool_call_id','tool_name','id','worker_status','failed'];\nfunction validateEvent(e: Record<string, unknown>): string | null {\n  if (Object.keys(e).some(k => !ALLOWED.includes(k))) return 'unknown key';\n  if (typeof e.event !== 'string') return 'event must be string';\n  if (Object.values(e).some(v => typeof v === 'string' && v.length > 4096)) return 'string too long';\n  if (e.channel !== undefined && !['text','reasoning'].includes(e.channel as string)) return 'bad channel';\n  if (e.failed !== undefined && typeof e.failed !== 'boolean') return 'failed must be boolean';\n  if (e.index !== undefined && (!Number.isSafeInteger(e.index) || (e.index as number) < 0)) return 'bad index';\n  return null;\n}","typeGuard":"const isPetEvent = (v: unknown): v is { event: string; index?: number; channel?: 'text'|'reasoning'; failed?: boolean } & Record<string, unknown> =>\n  typeof v === 'object' && v !== null && !Array.isArray(v) && typeof (v as any).event === 'string';","tryCatchPattern":"try {\n  engine.observe(event, at);\n} catch (err) {\n  if (err instanceof Error && err.message === 'Invalid Engine pet metadata fields.') {\n    console.error('event rejected:', JSON.stringify(Object.keys(event)));\n    return;\n  }\n  throw err;\n}","preventionTips":["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."],"tags":["validation","schema","typescript"],"backgroundTag":"schema-validation-failed","analyzedSha":"433685b2024e7bc4c99e1e2e326bcad39b4d9d65","analyzedAt":"2026-09-15T12:24:24.634Z","contentChangedAt":"2026-09-15T12:24:24.634Z","schemaVersion":2},"datasetVersion":"2026-09-22T11:17:16.035Z"}