{"record":{"id":"39c1b7f498f8a0ae","repo":"Hmbown/CodeWhale","slug":"invalid-engine-pet-metadata-fields","errorCode":null,"errorMessage":"Invalid Engine pet metadata fields.","messagePattern":"Invalid Engine pet metadata fields\\.","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"crates/tui/src/tui/pet_watch/pet-native.js","lineNumber":1740,"sourceCode":"        next.waiting = this.waiting ? span(this.waiting) : undefined;\n        next.sequence = this.sequence;\n        next.lastTime = this.lastTime;\n        return next;\n    }\n    observe(value, at) {\n        if (!Number.isFinite(at) || at < this.lastTime || at > pet_sim_js_1.PET_MAX_SECONDS * 1000)\n            throw new Error('Invalid Engine pet clock.');\n        if (!value || typeof value !== 'object' || Array.isArray(value))\n            throw new Error('Invalid Engine pet metadata.');\n        const e = value;\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)\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 < 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) => { const s = e[field]; if (typeof s !== 'string' || !s)\n            throw new Error(`Missing Engine ${field}.`); return s; };\n        const index = () => { if (!Number.isSafeInteger(e.index))\n            throw new Error('Missing Engine index.'); return String(e.index); };\n        const start = (key, name, category, agentId) => {\n            if (this.active.size >= 256 && !this.active.has(key))\n                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) => { this.pulse(key, at); this.active.delete(key); };\n        switch (e.event) {\n            case 'turn_started':\n                this.active.clear();\n                this.waiting = undefined;\n                break;\n            case 'message_started':","sourceCodeStart":1722,"sourceCodeEnd":1758,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/src/tui/pet_watch/pet-native.js#L1722-L1758","documentation":"After basic shape checks, observe validates each metadata field of the Engine event. It throws 'Invalid Engine pet metadata fields.' when any key is outside the allowed set ['event','index','channel','tool_call_id','tool_name','id','worker_status','failed'], event is not a string, any string value exceeds 4096 chars, channel is present but not 'text'/'reasoning', the id-ish fields are present but not strings, failed is present but not boolean, or index is present but not a non-negative safe integer.","triggerScenarios":"Forwarding raw EventMsg JSON that includes extra fields (e.g. 'text', 'result', nested objects); index as a float or negative; channel misspelled; failed as 0/1 instead of boolean; tool_name longer than 4096 characters; event missing or not a string.","commonSituations":"A protocol version change adds a new EventMsg field not yet in the allowlist; a caller passes the full deserialized message including payloads instead of the projected metadata; JSON round-tripping turns an integer index into a float; snake_case vs camelCase mixing.","solutions":["Project the event to only the allowed keys before calling observe","Whitelist any legitimately new protocol field in the allowed array (with the protocol change)","Coerce index to Math.trunc and validate it is a non-negative safe integer; convert failed/channel types at the bridge boundary","Log the offending event to identify which field violates the schema"],"exampleFix":"// before\ntelemetry.observe(rawEventMsg, at); // carries extra fields like 'text'\n// after\nconst e = (({ event, index, channel, tool_call_id, tool_name, id, worker_status, failed }) => ({ event, index, channel, tool_call_id, tool_name, id, worker_status, failed }))(rawEventMsg);\ntelemetry.observe(e, at);","handlingStrategy":"validation","validationCode":"const ALLOWED = ['event','index','channel','tool_call_id','tool_name','id','worker_status','failed'];\nfunction projectEngineMeta(raw) {\n  const e = {};\n  for (const k of ALLOWED) if (raw[k] !== undefined) e[k] = raw[k];\n  e.event = String(raw.event);\n  return e;\n}\ntelemetry.observe(projectEngineMeta(rawMsg), at);","typeGuard":"const hasValidFields = (e) => Object.keys(e).every(k => ALLOWED.includes(k)) && typeof e.event === 'string' && (e.index === undefined || (Number.isSafeInteger(e.index) && e.index >= 0));","tryCatchPattern":"try {\n  telemetry.observe(meta, at);\n} catch (err) {\n  if (err.message === 'Invalid Engine pet metadata fields.') {\n    console.warn('dropping malformed engine event', meta);\n    return;\n  }\n  throw err;\n}","preventionTips":["Project raw protocol messages to the allowlisted keys before observing","Coerce index with Math.trunc and failed with Boolean at the bridge","Keep the allowlist in sync when the protocol adds fields","Cap string field lengths to 4096 at ingestion"],"tags":["telemetry","schema","validation"],"backgroundTag":"schema-validation-failed","analyzedSha":"73e0f67d83c59909b571efdfc88c4bc28c309cb1","analyzedAt":"2026-09-22T01:30:00.501Z","contentChangedAt":"2026-09-22T01:30:00.501Z","schemaVersion":2},"datasetVersion":"2026-09-22T11:17:16.035Z"}