{"record":{"id":"d2121a798e33672f","repo":"bytedance/deer-flow","slug":"expected-fact-normalized-id-r-revision-revis","errorCode":null,"errorMessage":"Expected fact {normalized['id']!r} revision {revision}, found {existing_revision}","messagePattern":"Expected fact (.+?) revision (.+?), found (.+?)","errorType":"exception","errorClass":"MemoryFactRevisionConflict","httpStatus":null,"severity":"error","filePath":"backend/packages/harness/deerflow/agents/memory/backends/deermem/deermem/core/storage.py","lineNumber":238,"sourceCode":"        normalized[\"source\"] = {\"type\": \"unknown\", \"threadId\": None}\n    else:\n        normalized[\"source\"].setdefault(\"type\", \"unknown\")\n        if not isinstance(normalized[\"source\"].get(\"type\"), str):\n            raise ValueError(\"fact.source.type must be a string\")\n        if normalized[\"source\"].get(\"threadId\") is not None and not isinstance(normalized[\"source\"].get(\"threadId\"), str):\n            raise ValueError(\"fact.source.threadId must be a string or null\")\n    normalized[\"title\"] = _fact_title(normalized)\n    now = utc_now_iso_z()\n    if existing is None:\n        normalized.setdefault(\"createdAt\", now)\n        normalized.setdefault(\"updatedAt\", normalized[\"createdAt\"])\n        normalized[\"revision\"] = revision\n    else:\n        existing_revision = existing.get(\"revision\")\n        if not isinstance(existing_revision, int) or existing_revision < 1:\n            raise MemoryStorageCorruption(f\"Stored fact {normalized['id']!r} has an invalid revision\")\n        if revision != existing_revision:\n            raise MemoryFactRevisionConflict(f\"Expected fact {normalized['id']!r} revision {revision}, found {existing_revision}\")\n        normalized[\"createdAt\"] = existing.get(\"createdAt\") or normalized.get(\"createdAt\") or now\n        comparison_keys = {\"revision\", \"updatedAt\"}\n        incoming_material = {key: value for key, value in normalized.items() if key not in comparison_keys}\n        existing_material = {key: value for key, value in existing.items() if key not in comparison_keys}\n        if incoming_material == existing_material:\n            normalized[\"revision\"] = existing_revision\n            normalized[\"updatedAt\"] = existing.get(\"updatedAt\") or normalized[\"createdAt\"]\n        else:\n            normalized[\"revision\"] = existing_revision + 1\n            normalized[\"updatedAt\"] = now\n    if not isinstance(normalized.get(\"createdAt\"), str) or not isinstance(normalized.get(\"updatedAt\"), str):\n        raise ValueError(\"fact.createdAt and fact.updatedAt must be strings\")\n    if normalized[\"consolidatedFrom\"]:\n        normalized.setdefault(\"consolidatedAt\", normalized[\"updatedAt\"])\n    return normalized\n\n\ndef _safe_relative_path(root: Path, relative: str, *, label: str) -> Path:","sourceCodeStart":220,"sourceCodeEnd":256,"githubUrl":"https://github.com/bytedance/deer-flow/blob/1dd6ba1acb03700589994b0366c5d1c7d05e2eff/backend/packages/harness/deerflow/agents/memory/backends/deermem/deermem/core/storage.py#L220-L256","documentation":"The classic optimistic-concurrency failure: the incoming fact carries revision R, but the stored fact is already at a different revision (another writer updated it first). Raised as MemoryFactRevisionConflict - the stored state is valid, your copy is just stale. The backend deliberately refuses blind last-writer-wins for facts.","triggerScenarios":"Two agents/processes read the same fact (both see revision 3), both modify it, both save: the second save sends revision 3 but the store is now at 4 and fails. Long-lived in-memory copies of a fact get saved after another session touched it.","commonSituations":"Concurrent sessions or subagents updating the same memory; retry logic that re-sends the original payload after an intervening successful write; caching fact objects across requests.","solutions":["Catch the conflict, re-read the fresh fact, re-apply your intended change onto it, and save with the new revision (read-modify-write retry).","If the change is idempotent and the stored version already includes it (the backend no-ops when material is equal), treat the conflict as done.","Serialize memory writes per fact (single writer or queue) if contention is hot."],"exampleFix":"# before\nfact = memory.get_fact(fid); fact[\"content\"] = new_text; memory.save_fact(fact)  # raises on stale revision\n# after\nfor _ in range(3):\n    fact = memory.get_fact(fid)          # fresh copy, fresh revision\n    fact[\"content\"] = new_text\n    try:\n        memory.save_fact(fact); break\n    except MemoryFactRevisionConflict:\n        continue","handlingStrategy":"retry","validationCode":"# before saving an update, confirm your copy is current\nfresh = store.get(fact[\"id\"])\nif fresh is not None and fresh.get(\"revision\") != fact.get(\"revision\"):\n    fact = {**fresh, **your_changes, \"revision\": fresh[\"revision\"]}  # rebase before save","typeGuard":null,"tryCatchPattern":"for attempt in range(3):\n    try:\n        store.save(fact)\n        break\n    except MemoryFactRevisionConflict:\n        fresh = store.get(fact[\"id\"])\n        if fresh is None:\n            break  # deleted elsewhere; nothing to update\n        fact = {**fresh, **changes, \"revision\": fresh[\"revision\"]}","preventionTips":["Keep fact copies short-lived: read, modify, save in one pass.","Re-read before save instead of caching facts across requests.","Serialize concurrent writers per fact id (queue/lock) when contention is hot."],"tags":["deermem","memory","optimistic-concurrency","conflict"],"backgroundTag":null,"analyzedSha":"1dd6ba1acb03700589994b0366c5d1c7d05e2eff","analyzedAt":"2026-08-14T21:20:34.804Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}