{"record":{"id":"813ce8e9e8bd5b1e","repo":"bytedance/deer-flow","slug":"artifact-changed-since-it-was-opened","errorCode":null,"errorMessage":"Artifact changed since it was opened","messagePattern":"Artifact changed since it was opened","errorType":"http","errorClass":"HTTPException","httpStatus":412,"severity":"error","filePath":"backend/app/gateway/routers/artifacts.py","lineNumber":101,"sourceCode":"        raise HTTPException(status_code=415, detail=\"Symlinked artifacts cannot be edited\")\n    if not stat.S_ISREG(file_stat.st_mode):\n        raise HTTPException(status_code=400, detail=f\"Path is not a file: {path}\")\n    if file_stat.st_size > MAX_EDITABLE_ARTIFACT_BYTES:\n        raise HTTPException(status_code=413, detail=\"Artifact is too large to edit\")\n\n    current = actual_path.read_bytes()\n    if len(current) > MAX_EDITABLE_ARTIFACT_BYTES:\n        raise HTTPException(status_code=413, detail=\"Artifact is too large to edit\")\n    if b\"\\x00\" in current:\n        raise HTTPException(status_code=415, detail=\"Binary artifacts cannot be edited\")\n    try:\n        current.decode(\"utf-8\")\n    except UnicodeDecodeError:\n        raise HTTPException(status_code=415, detail=\"Only UTF-8 text artifacts can be edited\") from None\n\n    current_sha256 = hashlib.sha256(current).hexdigest()\n    if current_sha256 != expected_sha256:\n        raise HTTPException(status_code=412, detail=\"Artifact changed since it was opened\")\n    return current, file_stat\n\n\ndef _encode_artifact_update(content: str) -> bytes:\n    encoded = content.encode(\"utf-8\")\n    if len(encoded) > MAX_EDITABLE_ARTIFACT_BYTES:\n        raise HTTPException(status_code=413, detail=\"Artifact is too large to edit\")\n    if b\"\\x00\" in encoded:\n        raise HTTPException(status_code=415, detail=\"Binary content cannot be saved as an artifact\")\n    return encoded\n\n\ndef _replace_artifact_atomically(actual_path: Path, content: bytes, file_stat: os.stat_result) -> None:\n    temp_fd, temp_path_str = tempfile.mkstemp(prefix=_ARTIFACT_EDIT_TEMP_PREFIX, dir=actual_path.parent)\n    temp_path = Path(temp_path_str)\n    try:\n        # Preserve ownership where possible and keep replacement permissions\n        # scoped to the owner/group. The shared outputs directory allows a","sourceCodeStart":83,"sourceCodeEnd":119,"githubUrl":"https://github.com/bytedance/deer-flow/blob/1dd6ba1acb03700589994b0366c5d1c7d05e2eff/backend/app/gateway/routers/artifacts.py#L83-L119","documentation":"HTTP 412 from _load_editable_artifact: sha256 of the current file bytes does not match the expected_sha256 the client sent with the edit. This is optimistic-concurrency control: the artifact changed on disk since the client opened it, and saving would silently clobber someone else's (or another run's) writes.","triggerScenarios":"PUT edit-artifact where two sessions/tabs edit the same file, the agent rewrites the artifact between open and save, or a run re-executed and regenerated outputs while an editor was open.","commonSituations":"Multi-tab editing, long-held editor sessions across run retries, mobile+desktop editing the same artifact, or automated clients caching stale sha256 from an old listing.","solutions":["Re-open the artifact to get fresh content and the new sha256, merge your changes, then save again","Avoid concurrent editors on the same thread's artifacts — coordinate through one session","Clients should always send the sha256 obtained from the most recent GET, not a cached one"],"exampleFix":"// before: sha256 cached from an old listing → 412\npayload = {\"path\": p, \"content\": new_text, \"sha256\": cached_sha}\n\n// after: fetch fresh content+sha right before saving\nfresh = await client.get(artifact_url(p))\npayload = {\"path\": p, \"content\": merge(fresh.text, local_edits), \"sha256\": sha256(fresh_bytes)}\nawait client.put(EDIT_URL, json=payload)","handlingStrategy":"validation","validationCode":"import hashlib\n\ndef sha_matches(actual_path: str, expected_sha256: str) -> bool:\n    h = hashlib.sha256()\n    with open(actual_path, \"rb\") as f:\n        for chunk in iter(lambda: f.read(1 << 16), b\"\"):\n            h.update(chunk)\n    return h.hexdigest() == expected_sha256","typeGuard":null,"tryCatchPattern":"for attempt in range(3):\n    resp = await client.put(EDIT_URL, json=payload)\n    if resp.status_code != 412:\n        break\n    fresh = await client.get(artifact_url(path))           # reload\n    payload[\"content\"] = merge(fresh.text, payload[\"content\"])\n    payload[\"sha256\"] = sha256_of(fresh)                     # then retry\nelse:\n    raise ConflictError(\"artifact kept changing\")","preventionTips":["Always fetch fresh content+sha immediately before save","On 412, reload-and-merge; never blind-retry the same payload","Lock the UI editor while a run that writes the same artifact is active"],"tags":["api","artifacts","optimistic-concurrency","http-412","etag"],"backgroundTag":null,"analyzedSha":"1dd6ba1acb03700589994b0366c5d1c7d05e2eff","analyzedAt":"2026-08-14T21:20:34.804Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}