affaan-m/ECC · error

${fieldName} must be a number

Error message

${fieldName} must be a number

What it means

Thrown by toNullableNumber (used for tokens_used and duration_ms in normalizeExecutionRecord) when the value is present (not null/undefined) but Number(value) is not finite. null and undefined are allowed and pass through as null.

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 01e15490f0)

Solutions

  1. Send tokens_used and duration_ms as finite numbers, or omit them / set null.
  2. Numeric strings are tolerated, but prefer real numbers.
  3. Sanitize Infinity/NaN to null before recording.

Example fix

// before
recordSkillRun({ ..., tokens_used: 'n/a' });

// after
recordSkillRun({ ..., tokens_used: Number.isFinite(tokens) ? tokens : null });
Defensive patterns

Strategy: validation

Validate before calling

function sanitizeNumber(v) {
  if (v == null) return null;
  const n = Number(v);
  return Number.isFinite(n) ? n : null;
}
recordSkillRun({ ..., tokens_used: sanitizeNumber(raw.tokens), duration_ms: sanitizeNumber(raw.duration) });

Type guard

function isNullableNumber(v) {
  return v == null || (typeof v === 'number' && Number.isFinite(v));
}

Try / catch

try {
  normalizeExecutionRecord(input);
} catch (err) {
  if (/must be a number/.test(err.message)) {
    input.tokens_used = null; input.duration_ms = null;
  } else throw err;
}

Prevention

When it happens

Trigger: recordSkillRun({ ..., tokens_used: 'lots' }) (Number('lots')=NaN); duration_ms: true is actually allowed (Number(true)=1); tokens_used: Infinity or 'Infinity' (the string 'Infinity' parses to Infinity which is not finite); tokens_used: NaN. Note numeric strings like '100' are accepted (Number('100')=100).

Common situations: Telemetry field sent with a descriptive string instead of a count; a division producing Infinity; NaN propagation from a failed parse upstream.

Related errors


AI-assisted analysis of affaan-m/ECC@01e15490f0 (2026-08-13). Data as JSON: /api/errors/cfdb238a90af6722. Report an issue: GitHub.