coleam00/Archon · error
workflow.compose_fan_out_shared_checkout_collision
workflow.compose_fan_out_shared_checkout_collision
Error message
composed fan-out node '${node.id}': up to ${String(plannedConcurrency)} instances of '${node.include}' would run at once in this run's checkout, and that block 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 instance is not recoverable by resume (#2180). Choose one: add `mutates_checkout: false` to '${node.include}' if it only reads the repo; or set `fan_out.max_parallel: 1` on '${node.id}' to run the instances one at a time. What it means
Refused at planning time: a composed fan-out would run multiple instances of an `include:` block that can mutate the run's shared checkout and does not declare `mutates_checkout: false`. Concurrent instances take a path-exclusive lock, so all but the first would self-cancel, and lock-cancelled instances are not recoverable by resume (#2180).
Source
Thrown at packages/workflows/src/dag-executor.ts:9060
'items' in currentItems
? buildInstanceSnapshots(currentItems.items, {}, fanOut.as)
: [...persistedSnapshots];
}
const snapshots = persistedSnapshots ?? computedSnapshots;
// Concurrent instances share the parent checkout. Use the authoritative persisted
// width on resume so a changed producer cannot bypass the original safety preflight.
const plannedConcurrency = Math.min(fanOut.max_parallel, snapshots.length);
if (plannedConcurrency > 1 && resolved.definition.mutates_checkout !== false) {
const msg =
`composed fan-out node '${node.id}': up to ${String(plannedConcurrency)} instances of ` +
`'${node.include}' would run at once in this run's checkout, and that block 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 instance is not recoverable by resume (#2180). Choose one: add ' +
`\`mutates_checkout: false\` to '${node.include}' if it only reads the repo; or set ` +
`\`fan_out.max_parallel: 1\` on '${node.id}' to run the instances one at a time.`;
getLog().warn(
{ parentRunId: parentRun.id, nodeId: node.id, include: node.include, plannedConcurrency },
'workflow.compose_fan_out_shared_checkout_collision'
);
await notify(`❌ **Composed fan-out blocked** (node \`${node.id}\`): ${msg}`);
return failResult(msg);
}
if (persistedSnapshots === undefined) {
try {
await deps.store.persistWorkflowEvent({
workflow_run_id: parentRun.id,
event_type: 'fan_out_instances',
step_name: fanOutScopeName,
data: { instances: computedSnapshots },
});
} catch (err) {
const msg =
`composed fan-out node '${node.id}' could not persist its item snapshot before ` +View on GitHub (pinned to 0773b97458)
Solutions
- Add `mutates_checkout: false` to the included block if it only reads the repo
- Set `fan_out.max_parallel: 1` on the node to run instances one at a time
Example fix
# before
- id: loop
include: transform
fan_out: { over: items }
# after
- id: loop
include: transform # block frontmatter: mutates_checkout: false (read-only)
fan_out: { over: items }
# or
fan_out: { over: items, max_parallel: 1 } Defensive patterns
Strategy: validation
Validate before calling
// Pre-flight composed fan-out against checkout mutation
const block = await composer.resolveBlock(node.include);
const parallel = node.fan_out?.max_parallel ?? Infinity;
if (parallel > 1 && block.frontmatter.mutates_checkout !== false) {
throw new Error(`${node.include} may mutate the checkout; declare mutates_checkout: false or set max_parallel: 1`);
} Type guard
function isComposedFanOutSafe(node: { fan_out?: { max_parallel?: number } }, block: { frontmatter: { mutates_checkout?: boolean } }): boolean {
return (node.fan_out?.max_parallel ?? 2) <= 1 || block.frontmatter.mutates_checkout === false;
} Prevention
- Tag read-only blocks with `mutates_checkout: false` so they are composable in parallel
- Prefer max_parallel: 1 for blocks that touch the working tree
- Review block frontmatter whenever a block gains file writes
When it happens
Trigger: Composed fan-out node where plannedConcurrency > 1, no `isolation: worktree` alternative applies, and the included block lacks `mutates_checkout: false`.
Common situations: Fan-out of a block that writes files or runs git commands; block authored as read-only later gaining writes; forgetting max_parallel: 1 for shared-checkout blocks.
Related errors
- workflow.fan_out_shared_checkout_collision
- workflow.compose_fan_out_suspension_rejected
- Failed to claim write-back apply: ${err.message}
- Workflow run ${runId} was already resolved and is awaiting r
- ❌ **Composed fan-out failed** (node `${node.id}`): ${current
AI-assisted analysis of coleam00/Archon@0773b97458 (2026-09-01).
Data as JSON: /api/errors/d595720428fa1225.
Report an issue: GitHub.