{"record":{"id":"6965fcfd760391c0","repo":"coleam00/Archon","slug":"node-artifact-id-collision-distinct-producers-bot","errorCode":null,"errorMessage":"node artifact id collision: distinct producers both map to filename segment '${stem}'","messagePattern":"node artifact id collision: distinct producers both map to filename segment '(.+?)'","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/workflows/src/artifacts-index.ts","lineNumber":129,"sourceCode":"  const parsedParams = nodeArtifactWriteParamsSchema.parse(params);\n  const nodesDir = join(artifactsDir, NODES_SUBDIR);\n  await mkdir(nodesDir, { recursive: true });\n  const owner: ArtifactOwner = {\n    nodeId: parsedParams.nodeId,\n    ...(parsedParams.loopGroupPath !== undefined\n      ? { loopGroupPath: parsedParams.loopGroupPath }\n      : {}),\n  };\n  const stem = artifactStem(owner);\n  const metaPath = join(nodesDir, `${stem}.meta.json`);\n\n  // Collision guard: top-level safeSegment() can collapse distinct node ids (for\n  // example `a.b` and `a_b`), and loop digests retain an ownership check rather\n  // than assuming their hash alone is authoritative. Compare the complete\n  // producer identity and fail loudly instead of overwriting another artifact.\n  const priorOwner = await readArtifactOwner(metaPath);\n  if (priorOwner !== undefined && !sameArtifactOwner(priorOwner, owner)) {\n    throw new Error(\n      `node artifact id collision: distinct producers both map to filename segment '${stem}'`\n    );\n  }\n\n  const relPath = join(NODES_SUBDIR, `${stem}.md`);\n  await writeFile(join(artifactsDir, relPath), outputText, 'utf8');\n  const meta: NodeArtifact = {\n    nodeId: parsedParams.nodeId,\n    outputType: parsedParams.outputType,\n    ...(parsedParams.loopGroupPath !== undefined\n      ? { loopGroupPath: parsedParams.loopGroupPath }\n      : {}),\n    path: relPath,\n    runId: parsedParams.runId,\n    producedAt: parsedParams.producedAt,\n    size: Buffer.byteLength(outputText, 'utf8'),\n    ...(parsedParams.sessionId !== undefined ? { sessionId: parsedParams.sessionId } : {}),\n  };","sourceCodeStart":111,"sourceCodeEnd":147,"githubUrl":"https://github.com/coleam00/Archon/blob/0773b9745896ef0612e709c80845a0f7db315b19/packages/workflows/src/artifacts-index.ts#L111-L147","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["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).","Delete stale artifacts for the run's output directory if the collision comes from leftovers of a previous, differently-shaped run.","If it occurs on resume after renaming a node, clear the run's artifacts directory or start a fresh run so owner records reset.","Reproduce with the workflow's node-id list through safeSegment locally to confirm which pair collapses, then adjust ids accordingly."],"exampleFix":"# before: two nodes in one workflow\n- id: step.one\n- id: step_one\n# after: distinct sanitized segments\n- id: step-one\n- id: step-summary","handlingStrategy":"validation","validationCode":"const segment = (id: string) => id.toLowerCase().replace(/[^a-z0-9]+/g, '_');\nconst seen = new Set<string>();\nfor (const node of workflow.nodes) {\n  const s = segment(node.id);\n  if (seen.has(s)) throw new Error(`Node ids ${[...seen].find(x => segment(x) === s)} and ${node.id} collide in artifact filenames`);\n  seen.add(s);\n}","typeGuard":null,"tryCatchPattern":"try {\n  await writeNodeArtifact(artifactsDir, owner, stem, outputText);\n} catch (e) {\n  if (/node artifact id collision/i.test(e?.message ?? '')) {\n    logError(`Artifact filename segment '${stem}' is claimed by two producers; rename a node id.`);\n  } else throw e;\n}","preventionTips":["Choose node ids that sanitize to unique strings (avoid `a.b` vs `a_b` pairs).","Lint workflow YAML for post-sanitization id uniqueness before running.","Clear a run's artifacts directory when resuming after renaming node ids.","Never hand-edit or delete artifact sidecar owner files in a live run directory."],"tags":["workflows","artifacts","id-collision","persistence"],"backgroundTag":"artifact-id-collision","analyzedSha":"0773b9745896ef0612e709c80845a0f7db315b19","analyzedAt":"2026-09-01T02:28:07.064Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T05:18:18.240Z"}