ruvnet/ruflo · error · Error

${label} must be an ISO timestamp

Error message

${label} must be an ISO timestamp

What it means

In InMemoryRunReceiptReference.recordRun, the timestamp() helper validates run.startedAt and run.completedAt with Date.parse and throws `${label} must be an ISO timestamp` when the result is not finite. The labels embedded in the message are 'startedAt' and 'completedAt'. Receipt ordering and identity depend on these values, so only parseable ISO 8601 strings are accepted.

Source

Thrown at v3/@claude-flow/codex/src/harness/in-memory-run-receipt-reference.ts:17

import { createHash } from 'node:crypto';
import type { RunEvidence, RunReceipt } from './contract.js';
import { canonicalJson } from './repository-state.js';

const DIGEST = /^sha256:[0-9a-f]{64}$/;

function sha256(value: string): string {
  return `sha256:${createHash('sha256').update(value).digest('hex')}`;
}

function copy<T>(value: T): T {
  return structuredClone(value);
}

function timestamp(value: string, label: string): number {
  const parsed = Date.parse(value);
  if (!Number.isFinite(parsed)) throw new Error(`${label} must be an ISO timestamp`);
  return parsed;
}

function validateRun(run: RunEvidence): void {
  if (!run.executionId.trim() || !run.sessionId.trim() || !run.workloadId.trim()) {
    throw new Error('run execution, session, and workload identity are required');
  }
  if (!DIGEST.test(run.sourceState.sourceStateId)) throw new Error('run sourceStateId is invalid');
  if (!DIGEST.test(run.commandDigest) || !DIGEST.test(run.evidenceDigest)) {
    throw new Error('run command and evidence digests must be canonical sha256 values');
  }
  if (!Number.isSafeInteger(run.exitCode)) throw new Error('run exitCode must be a safe integer');
  const started = timestamp(run.startedAt, 'startedAt');
  const completed = timestamp(run.completedAt, 'completedAt');
  if (completed < started) throw new Error('run completedAt precedes startedAt');
  if (
    run.buildEvidence !== undefined
    && run.buildEvidence.sourceStateId !== run.sourceState.sourceStateId

View on GitHub (pinned to fa13ee4ad6)

Solutions

  1. Generate both timestamps with new Date().toISOString() at capture time
  2. Pre-check Number.isFinite(Date.parse(value)) for both fields before recordRun
  3. When integrating epoch-millisecond sources, convert first: new Date(ms).toISOString()

Example fix

// before
receipt.recordRun({ ..., startedAt: String(Date.now() - 60000), completedAt: String(Date.now()) });

// after
receipt.recordRun({ ..., startedAt: new Date(Date.now() - 60000).toISOString(), completedAt: new Date().toISOString() });
Defensive patterns

Strategy: validation

Validate before calling

function assertIsoTimestamp(value: string, label: string): void {
  if (!Number.isFinite(Date.parse(value))) {
    throw new TypeError(`${label} is not an ISO timestamp: ${JSON.stringify(value)}`);
  }
}
assertIsoTimestamp(run.startedAt, 'startedAt');
assertIsoTimestamp(run.completedAt, 'completedAt');

Type guard

function isIsoTimestamp(value: unknown): value is string {
  return typeof value === 'string' && Number.isFinite(Date.parse(value));
}

Try / catch

try {
  ledger.recordRun(run);
} catch (error) {
  if (error instanceof Error && error.message.endsWith('must be an ISO timestamp')) {
    // re-stamp both timestamps from one clock and retry once
    run.startedAt = new Date(startedMs).toISOString();
    run.completedAt = new Date(completedMs).toISOString();
    ledger.recordRun(run);
  } else throw error;
}

Prevention

When it happens

Trigger: recordRun with startedAt: 'yesterday', completedAt: '12/31/2026 10:00', '', '2026-31-12T00:00:00Z', a locale-formatted log timestamp, a raw epoch number (1767225600000 instead of an ISO string), or a Date object serialized by a non-ISO toString path.

Common situations: Reusing timestamps captured by other tooling (MM/DD/YYYY or 'Jan 5, 2026' forms); passing Date.now() output or Date objects directly; strings assembled from log lines whose format drifts across locales/timezones.

Related errors


AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18). Data as JSON: /api/errors/72845cc1a8e5e5c3. Report an issue: GitHub.