ruvnet/ruflo · error · Error

run completedAt precedes startedAt

Error message

run completedAt precedes startedAt

What it means

After both timestamps parse successfully, validateRun requires Date.parse(completedAt) >= Date.parse(startedAt); a strictly earlier completion time throws. Equal timestamps are allowed. The ordering guarantee protects receipt semantics: a run cannot finish before it starts on the recording clock.

Source

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

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
  ) {
    throw new Error('build evidence belongs to a different source state');
  }
}

/**
 * Unsigned, non-durable, content-addressed in-memory conformance ledger.
 *
 * Exact retries converge on one receipt. Reusing an execution ID with changed
 * evidence is refused rather than rewriting history. These receipts are local
 * debugging evidence and cannot authorize enforce mode or release.
 */
export class InMemoryRunReceiptReference {
  readonly referenceOnly = true;
  private readonly receipts = new Map<string, RunReceipt>();

View on GitHub (pinned to fa13ee4ad6)

Solutions

  1. Capture startedAt immediately before launching the command and completedAt immediately after it exits, on the same host
  2. If clocks may skew, reconcile offsets (NTP/PTP or a shared authority) before recording
  3. Pre-validate Date.parse(completedAt) >= Date.parse(startedAt) and fail fast locally with context

Example fix

// before: timestamps captured in different code paths
const startedAt = queueEnqueueTime;   // earlier, from another host
const completedAt = new Date().toISOString();

// after: both stamped around execution on one clock
const startedAt = new Date().toISOString();
const completedAt = new Date().toISOString();
if (Date.parse(completedAt) < Date.parse(startedAt)) throw new Error('clock moved backwards');
Defensive patterns

Strategy: validation

Validate before calling

function assertRunInterval(run: { startedAt: string; completedAt: string }): void {
  const started = Date.parse(run.startedAt);
  const completed = Date.parse(run.completedAt);
  if (!(Number.isFinite(started) && Number.isFinite(completed) && completed >= started)) {
    throw new RangeError(`completedAt ${run.completedAt} precedes startedAt ${run.startedAt}`);
  }
}

Try / catch

try {
  ledger.recordRun(run);
} catch (error) {
  if (error instanceof Error && error.message === 'run completedAt precedes startedAt') {
    // skew detected: re-stamp both from local clock preserving true duration
    const duration = measuredDurationMs;
    const end = Date.now();
    run.completedAt = new Date(end).toISOString();
    run.startedAt = new Date(end - duration).toISOString();
    ledger.recordRun(run);
  } else throw error;
}

Prevention

When it happens

Trigger: startedAt and completedAt captured on different hosts whose clocks skew; completedAt copied from a previous run's record; async code that stamps completedAt before startedAt (misordered awaits); startedAt refreshed after a queue delay while completedAt stayed stale.

Common situations: Distributed runners where each machine stamps its own time; replaying recorded events out of order; timestamps taken from file mtimes or log lines written at different pipeline stages.

Related errors


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