affaan-m/ECC · error · Error

must be a number

Error message

${fieldName} must be a number

What it means

toNullableNumber() coerces a value to a finite number for fields in a skill execution record (e.g. duration, token counts). null/undefined pass through as null, but any other value that Number() cannot turn into a finite number (NaN, Infinity, non-numeric strings) throws '${fieldName} must be a number'. It is a strict numeric-field validator used by normalizeExecutionRecord.

Solutions

  1. Pass a plain finite number for the field (e.g. duration_ms: 1200 instead of '1200ms').
  2. Parse/ununit the value before calling: Number(String(v).replace(/[^0-9.]/g, '')) and verify Number.isFinite.
  3. Omit the field (or pass null) if the measurement is genuinely unknown.
  4. Fix upstream serialization so numbers are not stringified with units.

Example fix

// before
record({ skill_id: 'linting', duration_ms: '1.2s' });

// after
record({ skill_id: 'linting', duration_ms: 1200 });
Defensive patterns

Strategy: type-guard

Validate before calling

function assertFiniteNumber(fieldName, value) {
  if (value === null || value === undefined) return null;
  const n = Number(value);
  if (!Number.isFinite(n)) throw new TypeError(`${fieldName} must be a number, got: ${JSON.stringify(value)}`);
  return n;
}

const durationMs = assertFiniteNumber('duration_ms', raw.duration_ms);

Type guard

function isFiniteNumber(value) {
  return typeof value === 'number' && Number.isFinite(value);
}

Try / catch

try {
  tracker.record(executionPayload);
} catch (err) {
  if (err.message.endsWith('must be a number')) {
    const field = err.message.split(' ')[0];
    console.error(`Numeric field ${field} is malformed in execution payload`);
    return;
  }
  throw err;
}

Prevention

When it happens

Trigger: Passing a non-numeric value for a numeric field of an execution record, e.g. record({ duration_ms: 'fast' }) or { tokens: Infinity } — any value where Number.isFinite(Number(value)) is false and the value is not null/undefined.

Common situations: Storing durations as formatted strings ('1.2s', '120 ms') from logs; JSON payloads where a number was serialized as a string with units; spreadsheet/CSV imports producing text cells; accidentally passing the field object instead of its numeric value.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


AI-assisted analysis of affaan-m/ECC@8321021c54 (2026-09-16). Data as JSON: /api/errors/cfdb238a90af6722. Report an issue: GitHub.

Appendix: source

Thrown at scripts/lib/skill-evolution/tracker.js:39

  return homeDir ? path.resolve(homeDir) : os.homedir();
}

function getRunsFilePath(options = {}) {
  if (options.runsFilePath) {
    return path.resolve(options.runsFilePath);
  }

  return path.join(resolveHomeDir(options.homeDir), '.claude', 'state', 'skill-runs.jsonl');
}

function toNullableNumber(value, fieldName) {
  if (value === null || typeof value === 'undefined') {
    return null;
  }

  const numericValue = Number(value);
  if (!Number.isFinite(numericValue)) {
    throw new Error(`${fieldName} must be a number`);
  }

  return numericValue;
}

function normalizeExecutionRecord(input, options = {}) {
  if (!input || typeof input !== 'object' || Array.isArray(input)) {
    throw new Error('skill execution payload must be an object');
  }

  const skillId = input.skill_id || input.skillId;
  const skillVersion = input.skill_version || input.skillVersion;
  const taskDescription = input.task_description || input.task_attempted || input.taskAttempted;
  const outcome = input.outcome;
  const recordedAt = input.recorded_at || options.now || new Date().toISOString();
  const userFeedback = input.user_feedback || input.userFeedback || null;

  if (typeof skillId !== 'string' || skillId.trim().length === 0) {

View on GitHub (pinned to 8321021c54)