Hmbown/CodeWhale · error · Error

Unknown category " ". Use "other" plus subtype for…

Error message

Unknown category "${a.category}". Use "other" plus subtype for extensions.

What it means

normalizedEvent only accepts category values listed in the CATEGORIES allowlist. An unknown category string is rejected so the event taxonomy stays closed; extension is done via category 'other' plus a free-form subtype, as the message says.

Solutions

  1. Change the category to one of the accepted CATEGORIES values.
  2. Use category 'other' and move the descriptive value into the subtype field.
  3. Check CATEGORIES in pet/src/core/ingest.ts for the exact allowed strings and match casing exactly.

Example fix

// before
{ "id": "a1", "traceId": "t1", "name": "db.query", "category": "database" }
// after
{ "id": "a1", "traceId": "t1", "name": "db.query", "category": "other", "subtype": "database" }
Defensive patterns

Strategy: validation

Validate before calling

const CATEGORIES = ['other' /* + the rest from pet/src/core/ingest.ts */];
if (record.category !== undefined && !CATEGORIES.includes(record.category))
  throw new Error(`category must be one of ${CATEGORIES.join(', ')} or 'other'+subtype`);

Type guard

type Category = typeof CATEGORIES[number];
const isCategory = (c: unknown): c is Category =>
  typeof c === 'string' && (CATEGORIES as readonly string[]).includes(c);

Try / catch

try {
  traces = importTrace(text, file);
} catch (e) {
  if (e instanceof Error && e.message.startsWith('Unknown category')) {
    const bad = /category "([^"]+)"/.exec(e.message)?.[1];
    console.error(`Remap category '${bad}' to the allowed set or 'other'+subtype`);
  } else throw e;
}

Prevention

When it happens

Trigger: Passing a record with a.category set to any string not in CATEGORIES, e.g. category:'database', category:'llm', or a misspelled value like 'catergory' typo entries. Any ingest call path that builds WhaleEvent records with custom categories.

Common situations: Mappings from other taxonomy systems (OTel span kinds, Datadog resource types) that don't use this library's closed set; typos; older exports predating a category rename; inventing a new category instead of using 'other'+subtype.

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


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

Appendix: source

Thrown at pet/src/core/ingest.ts:131

}
function ns(v: unknown, field: string): bigint {
  if (typeof v === 'number' && !Number.isSafeInteger(v)) throw new Error(`${field} lost precision: encode OTLP nanoseconds as a decimal string.`);
  const s = String(v ?? '');
  if (!/^\d{1,20}$/.test(s) || BigInt(s) > 18446744073709551615n) throw new Error(`Invalid ${field}: expected an OTLP nanosecond integer string.`);
  return BigInt(s);
}
function otelStatus(v: unknown): Status {
  const c = obj(v).code;
  return c === 2 || c === 'STATUS_CODE_ERROR' ? 'error' : c === 1 || c === 'STATUS_CODE_OK' ? 'success' : 'unknown';
}
function normalizedEvent(v: unknown, index: number): WhaleEvent {
  const a = obj(v), id = str(a.id), traceId = str(a.traceId), name = str(a.name);
  if (!id || !traceId || !name) throw new Error(`Record ${index + 1} requires nonempty id, traceId, and name.`);
  if (a.schemaVersion !== undefined && a.schemaVersion !== 1) throw new Error(`Record ${index + 1}: unsupported schemaVersion ${a.schemaVersion}.`);
  const startTime = number(a.startTime, 'startTime', false)!;
  const endTime = number(a.endTime, 'endTime') ?? startTime;
  if (endTime < startTime) throw new Error(`Record ${index + 1}: endTime precedes startTime.`);
  if (a.category !== undefined && !CATEGORIES.includes(a.category)) throw new Error(`Unknown category "${a.category}". Use "other" plus subtype for extensions.`);
  const allowed: Status[] = ['pending', 'running', 'success', 'error', 'unknown'];
  if (a.status !== undefined && !allowed.includes(a.status)) throw new Error(`Record ${index + 1}: invalid status.`);
  const at = obj(a.attributes);
  return {
    schemaVersion: 1, id, traceId, name, parentId: str(a.parentId), startTime, endTime,
    openEnded: a.endTime === undefined || a.openEnded === true,
    agentId: str(a.agentId) ?? 'unattributed', agentType: str(a.agentType), parentAgentId: str(a.parentAgentId),
    category: a.category ?? categoryFor(name, at), subtype: str(a.subtype),
    model: str(a.model), provider: str(a.provider), tool: str(a.tool),
    inputTokens: nonnegative(a.inputTokens, 'inputTokens'), outputTokens: nonnegative(a.outputTokens, 'outputTokens'),
    cachedTokens: nonnegative(a.cachedTokens, 'cachedTokens'), cost: nonnegative(a.cost, 'cost'),
    costCurrency: str(a.costCurrency), latency: nonnegative(a.latency, 'latency'),
    contextTokens: nonnegative(a.contextTokens, 'contextTokens'), contextLimit: nonnegative(a.contextLimit, 'contextLimit'),
    retry: nonnegative(a.retry, 'retry'), status: a.status ?? 'unknown',
    sourceId: str(a.sourceId), targetId: str(a.targetId), targetType: str(a.targetType),
    links: list(a.links).filter(x => typeof x?.traceId === 'string' && typeof x?.spanId === 'string'),
    attributes: at, payload: a.payload, raw: a.raw ?? v,
    observation: a.observation === undefined ? undefined : validateObservation(a.observation),

View on GitHub (pinned to 433685b202)