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
- Pass a plain finite number for the field (e.g. duration_ms: 1200 instead of '1200ms').
- Parse/ununit the value before calling: Number(String(v).replace(/[^0-9.]/g, '')) and verify Number.isFinite.
- Omit the field (or pass null) if the measurement is genuinely unknown.
- 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
- Store metrics as raw numbers (milliseconds, counts), never formatted strings.
- Sanitize values from logs/CSV/JSON imports with Number() + Number.isFinite before recording.
- Pass null explicitly for unknown measurements instead of placeholder strings.
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
- outcome must be one of success, failure, or partial
- recorded_at must be an ISO timestamp
- skill execution payload must be an object
- skill_id is required
- skill_version is required
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)