coleam00/Archon · error

node artifact id collision: distinct producers both map to…

Error message

node artifact id collision: distinct producers both map to filename segment '${stem}'

What it means

writeNodeArtifact persists each workflow node's output as a markdown file whose filename comes from safeSegment(nodeId) (plus a loop digest for loop iterations). safeSegment can collapse distinct node ids onto the same filename segment (e.g. `a.b` and `a_b`), so before writing, the function reads the artifact owner recorded in the sidecar meta file; if a different producer already owns that path it throws instead of overwriting the other node's artifact.

Solutions

  1. Rename one of the colliding nodes in the workflow YAML so their ids sanitize to distinct filename segments (avoid mixing '.' and '_' variants of the same name).
  2. Delete stale artifacts for the run's output directory if the collision comes from leftovers of a previous, differently-shaped run.
  3. If it occurs on resume after renaming a node, clear the run's artifacts directory or start a fresh run so owner records reset.
  4. Reproduce with the workflow's node-id list through safeSegment locally to confirm which pair collapses, then adjust ids accordingly.

Example fix

# before: two nodes in one workflow
- id: step.one
- id: step_one
# after: distinct sanitized segments
- id: step-one
- id: step-summary
Defensive patterns

Strategy: validation

Validate before calling

const segment = (id: string) => id.toLowerCase().replace(/[^a-z0-9]+/g, '_');
const seen = new Set<string>();
for (const node of workflow.nodes) {
  const s = segment(node.id);
  if (seen.has(s)) throw new Error(`Node ids ${[...seen].find(x => segment(x) === s)} and ${node.id} collide in artifact filenames`);
  seen.add(s);
}

Try / catch

try {
  await writeNodeArtifact(artifactsDir, owner, stem, outputText);
} catch (e) {
  if (/node artifact id collision/i.test(e?.message ?? '')) {
    logError(`Artifact filename segment '${stem}' is claimed by two producers; rename a node id.`);
  } else throw e;
}

Prevention

When it happens

Trigger: Two distinct node ids in the same run normalize to the same filename segment under safeSegment (characters like '.' vs '_' collapsed, or case/whitespace folding), and the second node writes its artifact to the already-claimed metaPath while a different owner is recorded.

Common situations: Workflow authors naming nodes `step.one` and `step_one` (or similar near-identical ids) in one workflow; a loop node whose digest coincides with another node's id segment after sanitization; a renamed node in a resumed run colliding with artifacts written by the old id; hand-edited artifacts leaving a stale owner file.

Related errors


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

Appendix: source

Thrown at packages/workflows/src/artifacts-index.ts:129

  const parsedParams = nodeArtifactWriteParamsSchema.parse(params);
  const nodesDir = join(artifactsDir, NODES_SUBDIR);
  await mkdir(nodesDir, { recursive: true });
  const owner: ArtifactOwner = {
    nodeId: parsedParams.nodeId,
    ...(parsedParams.loopGroupPath !== undefined
      ? { loopGroupPath: parsedParams.loopGroupPath }
      : {}),
  };
  const stem = artifactStem(owner);
  const metaPath = join(nodesDir, `${stem}.meta.json`);

  // Collision guard: top-level safeSegment() can collapse distinct node ids (for
  // example `a.b` and `a_b`), and loop digests retain an ownership check rather
  // than assuming their hash alone is authoritative. Compare the complete
  // producer identity and fail loudly instead of overwriting another artifact.
  const priorOwner = await readArtifactOwner(metaPath);
  if (priorOwner !== undefined && !sameArtifactOwner(priorOwner, owner)) {
    throw new Error(
      `node artifact id collision: distinct producers both map to filename segment '${stem}'`
    );
  }

  const relPath = join(NODES_SUBDIR, `${stem}.md`);
  await writeFile(join(artifactsDir, relPath), outputText, 'utf8');
  const meta: NodeArtifact = {
    nodeId: parsedParams.nodeId,
    outputType: parsedParams.outputType,
    ...(parsedParams.loopGroupPath !== undefined
      ? { loopGroupPath: parsedParams.loopGroupPath }
      : {}),
    path: relPath,
    runId: parsedParams.runId,
    producedAt: parsedParams.producedAt,
    size: Buffer.byteLength(outputText, 'utf8'),
    ...(parsedParams.sessionId !== undefined ? { sessionId: parsedParams.sessionId } : {}),
  };

View on GitHub (pinned to 0773b97458)