ruvnet/ruflo · error · Error

executionId ${run.executionId} already records different evi

Error message

executionId ${run.executionId} already records different evidence

What it means

The ledger is content-addressed: receiptId = sha256(canonicalJson(run)), and one executionId may map to exactly one receipt. recordRun throws when the same executionId arrives with a different canonical form — even a one-millisecond difference in startedAt yields a new receiptId and trips the guard. Exact retries converge on the existing receipt; changed evidence is refused rather than rewriting history.

Source

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

 *
 * 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>();
  private readonly executionReceipts = new Map<string, string>();

  constructor(private readonly now: () => number = Date.now) {}

  recordRun(run: RunEvidence): RunReceipt {
    validateRun(run);
    const canonical = canonicalJson(run);
    const receiptId = sha256(canonical);
    const existingExecution = this.executionReceipts.get(run.executionId);
    if (existingExecution !== undefined && existingExecution !== receiptId) {
      throw new Error(`executionId ${run.executionId} already records different evidence`);
    }
    const existing = this.receipts.get(receiptId);
    if (existing) return copy(existing);

    const receipt: RunReceipt = {
      ...copy(run),
      receiptId,
      recordedAt: new Date(this.now()).toISOString(),
    };
    this.receipts.set(receiptId, receipt);
    this.executionReceipts.set(run.executionId, receiptId);
    return copy(receipt);
  }

  get(receiptId: string): RunReceipt | undefined {
    const receipt = this.receipts.get(receiptId);
    return receipt ? copy(receipt) : undefined;
  }

View on GitHub (pinned to fa13ee4ad6)

Solutions

  1. On retry/redelivery, replay the byte-identical RunEvidence — cache the first attempt's object and resend it
  2. For a genuinely new attempt or re-run, mint a fresh executionId (crypto.randomUUID())
  3. Treat the thrown conflict as 'already recorded' only after comparing the prior receipt's digests to confirm divergence vs. duplication

Example fix

// before: evidence rebuilt on every retry (new startedAt each time)
async function record(retry) {
  const run = buildRunEvidence(); // fresh timestamps => different digest
  await ledger.recordRun(run);
}

// after: build once, replay the identical object on retry
const run = buildRunEvidence();
await withRetry(() => ledger.recordRun(run));
Defensive patterns

Strategy: try-catch

Validate before calling

const firstAttempt = new Map<string, RunEvidence>();
function replayStableEvidence(run: RunEvidence): RunEvidence {
  const prior = firstAttempt.get(run.executionId);
  if (prior) return prior; // byte-identical retry keeps one receipt
  firstAttempt.set(run.executionId, run);
  return run;
}

Try / catch

try {
  ledger.recordRun(run);
} catch (error) {
  if (error instanceof Error && error.message.includes('already records different evidence')) {
    // same executionId with different evidence: either a duplicate re-run or a bug
    const existing = ledger.getReceipt?.(run.executionId);
    if (existing && existing.commandDigest === run.commandDigest) return existing; // benign duplicate
    throw new Error(`executionId ${run.executionId} diverged between attempts; mint a new executionId`);
  }
  throw error;
}

Prevention

When it happens

Trigger: Retrying recordRun after a transient failure while rebuilding the RunEvidence object (fresh timestamps each attempt); changing exitCode or digests for the 'same' execution; two workers recording different results under one executionId.

Common situations: Retry wrappers that regenerate timestamps instead of replaying the original payload; duplicate message delivery causing a real re-execution that reuses the executionId; executionIds seeded from counters that reset across process restarts.

Related errors


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