affaan-m/ECC · error · Error
${label} must be an ISO-8601 timestamp.
Error message
${label} must be an ISO-8601 timestamp. What it means
Thrown by validateTimestamp() when a timestamp field (createdAt or updatedAt) does not match the strict ISO-8601 pattern ^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$ AND round-trip exactly through new Date(x).toISOString(). The library requires millisecond precision and a trailing 'Z' (UTC) so timestamps are unambiguous and sort lexically. Any deviation is rejected rather than silently coerced.
Source
Thrown at scripts/lib/memory-vault-format.js:139
}
return values.reduce((result, value) => {
const normalized = validator(value);
if (result.includes(normalized)) {
throw new Error(`${label} must not contain duplicate values.`);
}
return [...result, normalized];
}, []);
}
function validateTimestamp(value, label) {
const normalized = asNonEmptyString(value, label, 64);
const parsed = new Date(normalized);
if (
!ISO_TIMESTAMP_PATTERN.test(normalized)
|| Number.isNaN(parsed.getTime())
|| parsed.toISOString() !== normalized
) {
throw new Error(`${label} must be an ISO-8601 timestamp.`);
}
return normalized;
}
function normalizeBody(value) {
if (typeof value !== 'string') {
throw new Error('memory body must be a string.');
}
if (hasUnsafeControlCharacters(value, true)) {
throw new Error('memory body must not contain unsafe control or bidirectional formatting characters.');
}
const normalized = value.trim();
if (normalized.length === 0) {
throw new Error('memory body must contain non-whitespace context.');
}
if (Buffer.byteLength(normalized, 'utf8') > MAX_BODY_BYTES) {
throw new Error(`memory body is too large (maximum ${MAX_BODY_BYTES} bytes).`);
}View on GitHub (pinned to 01e15490f0)
Solutions
- Pass new Date().toISOString() verbatim — it always emits the exact format required.
- If you have an epoch number, wrap it: new Date(epochMs).toISOString().
- If you have an offset timestamp, normalize first: new Date('2024-01-01T00:00:00.000+00:00').toISOString().
- Stop stripping the .SSS portion; the round-trip check fails without it.
- When backfilling, run each candidate through new Date(s).toISOString() === s before passing it in.
Example fix
// before
saveMemory({
title: 'handoff',
createdAt: '2024-08-12T14:30:00Z', // missing .SSS
updatedAt: Date.now(), // number, not string
body: '...'
});
// after
const now = new Date().toISOString();
saveMemory({
title: 'handoff',
createdAt: now, // 2024-08-12T14:30:00.123Z
updatedAt: now,
body: '...'
}); Defensive patterns
Strategy: validation
Validate before calling
const ISO = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
function isValidVaultTimestamp(value) {
return typeof value === 'string'
&& ISO.test(value)
&& !Number.isNaN(new Date(value).getTime())
&& new Date(value).toISOString() === value;
}
if (!isValidVaultTimestamp(input.createdAt)) {
input.createdAt = new Date().toISOString();
} Type guard
function isIsoTimestamp(value): value is string {
return typeof value === 'string'
&& /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(value)
&& new Date(value).toISOString() === value;
} Try / catch
try {
saveMemory(input);
} catch (err) {
if (/must be an ISO-8601 timestamp/.test(err.message)) {
input.createdAt = new Date().toISOString();
input.updatedAt = new Date().toISOString();
saveMemory(input);
} else throw err;
} Prevention
- Always derive timestamps from new Date().toISOString() at the call site, never from string concatenation.
- Type the input field as { createdAt: string; updatedAt: string } and document the exact format in a comment.
- Add a unit test that asserts saveMemory() works with new Date().toISOString() output.
When it happens
Trigger: saveMemory({createdAt: '2024-01-01T00:00:00Z'}) (no milliseconds), saveMemory({createdAt: '2024-01-01T00:00:00.000+00:00'}) (offset instead of Z), saveMemory({createdAt: Date.now()}) (number not string), saveMemory({createdAt: '2024-13-45T99:99:99.999Z'}) (invalid date that still matches regex but fails Date parse), or saveMemory({updatedAt: someDate.toISOString().replace('.000Z','Z')}) which strips the round-trip requirement.
Common situations: Migrating from a system that stored epoch millis or SQL DATETIME strings. Using moment/date-fns default format() output. Copy-pasting a timestamp from a log file that uses second precision. Manually constructing timestamps via string concatenation. Frontend code that runs toISOString().replace(...) to trim what looked like redundant precision.
Related errors
- ${label} must be a non-empty string.
- ${label} is too long (maximum ${maxChars} characters).
- ${label} must not contain control or bidirectional formattin
- ${label} must be one of: ${allowed.join(', ')}.
- ${label} must be a lowercase letters/numbers slug.
AI-assisted analysis of affaan-m/ECC@01e15490f0 (2026-08-13).
Data as JSON: /api/errors/d23ae71244204e7e.
Report an issue: GitHub.