coleam00/Archon · error

workflow.fan_out_shared_checkout_collision

workflow.fan_out_shared_checkout_collision

Error message

fan_out node '${node.id}': up to ${String(concurrency)} children of '${node.workflow}' would run at once in the parent checkout, and that workflow does not declare `mutates_checkout: false`. Concurrent runs on one checkout take a path-exclusive lock, so all but the first would cancel themselves — and a lock-cancelled child is not recoverable by resume (#2180). Choose one: add `mutates_checkout: false` to '${node.workflow}' if it only reads the repo; set `isolation: worktree` on '${node.id}' if the children write to it; or set `fan_out.max_parallel: 1` to run them one at a time.

What it means

Refused at planning time: a fan-out would run more than one child of a workflow that can mutate the shared parent checkout. Concurrent runs on one checkout take a path-exclusive lock, so all but the first child would self-cancel, and lock-cancelled children are not recoverable by resume (#2180). The executor blocks the node before creating children.

Source

Thrown at packages/workflows/src/dag-executor.ts:8504

        `fan_out node '${node.id}': target workflow '${node.workflow}' is interactive-class ` +
        "('interactive: true') and may pause for human input — a fan-out has a single " +
        'approval-gate slot for N children, so this is refused before any child is created. ' +
        `Remove the pause capability from '${node.workflow}', or invoke it as a single ` +
        "(non-fan-out) 'workflow:' node instead.";
      getLog().warn(
        { parentRunId: parentRun.id, nodeId: node.id, childWorkflow: node.workflow },
        'workflow.fan_out_interactive_target'
      );
      await notify(`❌ **Fan-out blocked** (node \`${node.id}\`): ${msg}`);
      return failResult(msg);
    }
    if (
      node.isolation !== 'worktree' &&
      plannedConcurrency > 1 &&
      resolved.definition.mutates_checkout !== false
    ) {
      const msg = fanOutSharedCheckoutMessage(node, plannedConcurrency);
      getLog().warn(
        {
          parentRunId: parentRun.id,
          nodeId: node.id,
          childWorkflow: node.workflow,
          plannedConcurrency,
        },
        'workflow.fan_out_shared_checkout_collision'
      );
      await notify(`❌ **Fan-out blocked** (node \`${node.id}\`): ${msg}`);
      return failResult(msg);
    }
  }

  // 6. Execute EVERY index through a bounded sliding window. Classification per index: an
  //    existing completed child threads its recorded outcome (resume skip); an existing
  //    failed OR fan-out-cancelled (recoverable) child is re-driven; a user-cancelled child
  //    stays terminal; a missing index spawns fresh.
  //

View on GitHub (pinned to 0773b97458)

Solutions

  1. Add `mutates_checkout: false` to the target workflow if it only reads the repo
  2. Set `isolation: worktree` on the fan-out node so each child gets its own checkout
  3. Set `fan_out.max_parallel: 1` to run children sequentially under the lock

Example fix

# before
- id: batch
  workflow: repo-scan
  fan_out: { over: repos }
# after (read-only target)
workflow: repo-scan   # frontmatter now: mutates_checkout: false
  fan_out: { over: repos }
Defensive patterns

Strategy: validation

Validate before calling

// Pre-flight: fan-out into a shared checkout is only safe if read-only or serialized
const def = await workflows.resolve(node.workflow);
const parallel = node.fan_out?.max_parallel ?? Infinity;
if (node.isolation !== 'worktree' && parallel > 1 && def.frontmatter.mutates_checkout !== false) {
  throw new Error(`${node.workflow} mutates the checkout; use isolation: worktree, max_parallel: 1, or mutates_checkout: false`);
}

Type guard

function isCheckoutSafeFanOut(node: { isolation?: string; fan_out?: { max_parallel?: number } }, def: { frontmatter: { mutates_checkout?: boolean } }): boolean {
  return node.isolation === 'worktree' || (node.fan_out?.max_parallel ?? 2) <= 1 || def.frontmatter.mutates_checkout === false;
}

Prevention

When it happens

Trigger: Executing a `fan_out` node where `node.isolation !== 'worktree'`, plannedConcurrency > 1, and the target workflow definition does not set `mutates_checkout: false`.

Common situations: Fanning out a workflow that runs `git commit`/build steps; forgetting `mutates_checkout: false` on a read-only workflow; forgetting `isolation: worktree` on write-heavy fan-outs.

Related errors


AI-assisted analysis of coleam00/Archon@0773b97458 (2026-09-01). Data as JSON: /api/errors/b2dfe8891af97452. Report an issue: GitHub.