bytedance/deer-flow · error · MemoryFactRevisionConflict
Expected fact {normalized['id']!r} revision {revision}, foun
Error message
Expected fact {normalized['id']!r} revision {revision}, found {existing_revision} What it means
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.
Source
Thrown at backend/packages/harness/deerflow/agents/memory/backends/deermem/deermem/core/storage.py:238
normalized["source"] = {"type": "unknown", "threadId": None}
else:
normalized["source"].setdefault("type", "unknown")
if not isinstance(normalized["source"].get("type"), str):
raise ValueError("fact.source.type must be a string")
if normalized["source"].get("threadId") is not None and not isinstance(normalized["source"].get("threadId"), str):
raise ValueError("fact.source.threadId must be a string or null")
normalized["title"] = _fact_title(normalized)
now = utc_now_iso_z()
if existing is None:
normalized.setdefault("createdAt", now)
normalized.setdefault("updatedAt", normalized["createdAt"])
normalized["revision"] = revision
else:
existing_revision = existing.get("revision")
if not isinstance(existing_revision, int) or existing_revision < 1:
raise MemoryStorageCorruption(f"Stored fact {normalized['id']!r} has an invalid revision")
if revision != existing_revision:
raise MemoryFactRevisionConflict(f"Expected fact {normalized['id']!r} revision {revision}, found {existing_revision}")
normalized["createdAt"] = existing.get("createdAt") or normalized.get("createdAt") or now
comparison_keys = {"revision", "updatedAt"}
incoming_material = {key: value for key, value in normalized.items() if key not in comparison_keys}
existing_material = {key: value for key, value in existing.items() if key not in comparison_keys}
if incoming_material == existing_material:
normalized["revision"] = existing_revision
normalized["updatedAt"] = existing.get("updatedAt") or normalized["createdAt"]
else:
normalized["revision"] = existing_revision + 1
normalized["updatedAt"] = now
if not isinstance(normalized.get("createdAt"), str) or not isinstance(normalized.get("updatedAt"), str):
raise ValueError("fact.createdAt and fact.updatedAt must be strings")
if normalized["consolidatedFrom"]:
normalized.setdefault("consolidatedAt", normalized["updatedAt"])
return normalized
def _safe_relative_path(root: Path, relative: str, *, label: str) -> Path:View on GitHub (pinned to 1dd6ba1acb)
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.
Example fix
# before
fact = memory.get_fact(fid); fact["content"] = new_text; memory.save_fact(fact) # raises on stale revision
# after
for _ in range(3):
fact = memory.get_fact(fid) # fresh copy, fresh revision
fact["content"] = new_text
try:
memory.save_fact(fact); break
except MemoryFactRevisionConflict:
continue Defensive patterns
Strategy: retry
Validate before calling
# before saving an update, confirm your copy is current
fresh = store.get(fact["id"])
if fresh is not None and fresh.get("revision") != fact.get("revision"):
fact = {**fresh, **your_changes, "revision": fresh["revision"]} # rebase before save Try / catch
for attempt in range(3):
try:
store.save(fact)
break
except MemoryFactRevisionConflict:
fresh = store.get(fact["id"])
if fresh is None:
break # deleted elsewhere; nothing to update
fact = {**fresh, **changes, "revision": fresh["revision"]} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- fact.revision must be an integer >= 1
- Stored fact {normalized['id']!r} has an invalid revision
- change_set.deleteRevisions must be an object
- change_set.upsertRevisions must be an object
- change_set.upsertRevisions values must be null or integers >
AI-assisted analysis of bytedance/deer-flow@1dd6ba1acb (2026-08-14).
Data as JSON: /api/errors/d2121a798e33672f.
Report an issue: GitHub.