Hmbown/CodeWhale · error · Error
Unknown category for
Error message
Unknown category for ${e.id}. What it means
buildPyramid() maps each event's category to a fixed channel via CATEGORIES.indexOf(e.category). It throws this per-event error when the category string is not one of the known channel names, because the event could not be attributed to any pyramid channel.
Solutions
- Map the event's category to one of the library's supported CATEGORIES before calling buildPyramid (the error names the event id to find it).
- Check the exported CATEGORIES list and match exact casing/spelling.
- Add a fallback in your import layer that coerces unknown categories to a default channel (e.g. 'other') instead of passing them through.
- If this appeared after a library upgrade, re-export/normalize old traces to the new category names.
Example fix
// before
buildPyramid([{ id: 'a', startTime: 0, endTime: 10, category: 'Tool' }]);
// after
buildPyramid([{ id: 'a', startTime: 0, endTime: 10, category: 'tool' }]); // matches CATEGORIES exactly Defensive patterns
Strategy: validation
Validate before calling
const KNOWN = new Set(CATEGORIES); // exported by signal module
const normalized = events.map(e => KNOWN.has(e.category) ? e : { ...e, category: 'other' }); Type guard
function hasKnownCategory(e: WhaleEvent): boolean { return CATEGORIES.includes(e.category); } Try / catch
try { buildPyramid(events, duration); } catch (e) { const m = /Unknown category for (.+)\./.exec(e.message); if (m) { remapCategory(m[1], 'other'); buildPyramid(events, duration); } else throw e; } Prevention
- Map external category vocabularies (OTLP, JSONL) to CATEGORIES at ingest.
- Keep a single mapping table; never hand-type category strings at call sites.
- Re-normalize stored traces after library upgrades that rename channels.
When it happens
Trigger: An event in the array has a category not present in the library's CATEGORIES list — a typo (e.g. 'Tool' vs 'tool'), a renamed channel in a newer library version, or a custom category invented by the caller.
Common situations: Importing traces from another tool whose category vocabulary differs (e.g. OTLP span names); a library upgrade that renamed a category while old recorded traces still use the old name; case-sensitivity mistakes ('LLM' vs 'llm').
Understand the failure class
Background: Invalid enum value errors: "Unknown type", "Invalid scope", "must be one of" — when a string is not on the library's allowed list — this error's family across 23 libraries.
Related errors
- Unknown category for
- agent profile reasoning_effort must be one of: inherit…
- Choose approve or decline
- clipboard action must be "read" or "write
- computer action must be list, switch, register, spawn or…
AI-assisted analysis of Hmbown/CodeWhale@433685b202 (2026-09-15).
Data as JSON: /api/errors/0475353f5cbf861c.
Report an issue: GitHub.
Appendix: source
Thrown at pet/src/core/signal.ts:23
return { binMs, length, onsets: new Float64Array(n), activeMs: new Float64Array(n),
outputTokens: new Float64Array(n), cost: new Float64Array(n), errors: new Float64Array(n), peak: new Float64Array(n) };
}
const FIELDS = ['onsets', 'activeMs', 'outputTokens', 'cost', 'errors'] as const;
/** O(events + channels × bins), including long intervals. No span-length inner loop. */
export function buildPyramid(events: readonly WhaleEvent[], requestedDuration?: number, maxBins = 16_384): SignalPyramid {
if (!Number.isInteger(maxBins) || maxBins < 16 || maxBins > 1_048_576) throw new Error('maxBins must be an integer in [16, 1048576].');
let duration = requestedDuration ?? 1;
for (const e of events) duration = Math.max(duration, e.endTime, e.startTime, e.status === 'error' ? errorOnsetOf(e) : 0);
if (!Number.isFinite(duration) || duration < 0) throw new Error('Signal duration must be finite and nonnegative.');
duration = Math.max(1, duration);
const binMs = 2 ** Math.ceil(Math.log2(Math.max(1, duration / (maxBins - 1))));
const length = Math.floor(duration / binMs) + 1, fine = emptyLevel(length, binMs);
const stride = length + 1;
const activeDiff = new Float64Array(CATEGORIES.length * stride), tokenDiff = new Float64Array(CATEGORIES.length * stride);
for (const e of events) {
if (!Number.isFinite(e.startTime) || !Number.isFinite(e.endTime) || e.startTime < 0 || e.endTime < e.startTime) throw new Error(`Invalid interval for ${e.id}.`);
const channel = CATEGORIES.indexOf(e.category);
if (channel < 0) throw new Error(`Unknown category for ${e.id}.`);
const a = Math.floor(e.startTime / binMs), b = Math.floor(e.endTime / binMs);
const at = channel * length, diff = channel * stride;
fine.onsets[at + a]++;
fine.cost[at + a] += e.cost ?? 0;
if (e.status === 'error') fine.errors[at + Math.floor(errorOnsetOf(e) / binMs)]++;
const d = e.endTime - e.startTime, tokens = e.outputTokens ?? 0;
if (d === 0) { fine.outputTokens[at + a] += tokens; continue; }
if (a === b) { fine.activeMs[at + a] += d; fine.outputTokens[at + a] += tokens; }
else {
const left = (a + 1) * binMs - e.startTime, right = e.endTime - b * binMs, tokenRate = tokens / d;
fine.activeMs[at + a] += left;
fine.activeMs[at + b] += right;
fine.outputTokens[at + a] += left * tokenRate;
fine.outputTokens[at + b] += right * tokenRate;
if (b > a + 1) {
activeDiff[diff + a + 1] += binMs; activeDiff[diff + b] -= binMs;
tokenDiff[diff + a + 1] += binMs * tokenRate; tokenDiff[diff + b] -= binMs * tokenRate;
}View on GitHub (pinned to 433685b202)