ruvnet/ruflo · error · Error

canonical JSON does not support cycles

Error message

canonical JSON does not support cycles

What it means

canonicalJson tracks a set of ancestor objects while recursing; re-entering an object already on the stack means a reference cycle and throws immediately. Without this guard, deep recursion would overflow the stack, and a digest over a cyclic structure is not meaningful. The ancestor entry is removed after encoding, so DAGs (shared, non-circular references) are fine.

Source

Thrown at v3/@claude-flow/codex/src/harness/repository-state.ts:131

    if (typeof item === 'string') {
      assertUnicodeScalarString(item);
      return JSON.stringify(item);
    }
    if (typeof item === 'number') {
      if (
        !Number.isFinite(item)
        || Object.is(item, -0)
        || (Number.isInteger(item) && !Number.isSafeInteger(item))
      ) {
        throw new Error('canonical JSON requires finite, safe, non-negative-zero numbers');
      }
      return JSON.stringify(item);
    }
    if (item === undefined) throw new Error('canonical JSON does not support undefined');
    if (typeof item !== 'object') {
      throw new Error(`canonical JSON does not support ${typeof item}`);
    }
    if (ancestors.has(item)) throw new Error('canonical JSON does not support cycles');
    ancestors.add(item);
    try {
      if (Array.isArray(item)) return `[${item.map(encode).join(',')}]`;
      const prototype = Object.getPrototypeOf(item);
      if (prototype !== Object.prototype && prototype !== null) {
        throw new Error('canonical JSON supports only arrays and plain objects');
      }
      const entries = Object.entries(item as Record<string, unknown>)
        .sort(([left], [right]) => codeUnitCompare(left, right));
      return `{${entries.map(([key, child]) => {
        assertUnicodeScalarString(key);
        return `${JSON.stringify(key)}:${encode(child)}`;
      }).join(',')}}`;
    } finally {
      ancestors.delete(item);
    }
  };
  return encode(value);

View on GitHub (pinned to fa13ee4ad6)

Solutions

  1. Project the structure into a plain acyclic DTO tree before canonicalization (drop parent pointers)
  2. Run a sanitizer with a WeakSet seen-set that replaces repeated references with a stable marker or omits them
  3. Keep evidence payloads acyclic by construction (flat records, ID references instead of object links)

Example fix

// before
canonicalJson(taskGraph); // task.child.parent === task

// after: project to acyclic DTOs linked by ids
canonicalJson(taskGraph.nodes.map(({ id, childId }) => ({ id, childId: childId ?? null })));
Defensive patterns

Strategy: validation

Validate before calling

function assertAcyclic(value: unknown, seen = new Set<object>()): void {
  if (Array.isArray(value)) {
    if (seen.has(value)) throw new TypeError('cycle detected');
    seen.add(value);
    value.forEach((item) => assertAcyclic(item, seen));
    seen.delete(value);
  } else if (typeof value === 'object' && value !== null) {
    if (seen.has(value)) throw new TypeError('cycle detected');
    seen.add(value);
    Object.values(value).forEach((item) => assertAcyclic(item, seen));
    seen.delete(value);
  }
}

Try / catch

try {
  return canonicalJson(payload);
} catch (error) {
  if (error instanceof Error && error.message === 'canonical JSON does not support cycles') {
    throw new Error('payload contains a reference cycle; project it to an acyclic DTO (ids instead of links)');
  }
  throw error;
}

Prevention

When it happens

Trigger: Structures like node.parent = node, a.children = [a], bidirectional relations, memo caches that store their own wrapper, or ORM entities with back-references passed straight into evidence.

Common situations: Serializing ASTs, graphs, or domain models with parent pointers; objects enriched with a reference to their container; test fixtures wired with circular links.

Related errors


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