bytedance/deer-flow · error · ValueError
fact.status must be 'active'; deletion is physical
Error message
fact.status must be 'active'; deletion is physical
What it means
The v2 storage schema has no soft-delete: fact['status'] must be exactly 'active' (the default), and anything else raises ValueError with 'deletion is physical'. Facts are removed by deleting their records; a status field carrying 'deleted'/'archived' indicates an old or wrong producer.
Source
Thrown at backend/packages/harness/deerflow/agents/memory/backends/deermem/deermem/core/storage.py:205
normalized = copy.deepcopy(fact)
normalized["id"] = str(normalized.get("id") or f"fact_{uuid.uuid4().hex}")
# Validate the id through the canonical path builder's public contract.
if not normalized["id"] or any(character not in "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-" for character in normalized["id"]):
raise ValueError("fact.id may contain only letters, numbers, '_' and '-'")
normalized["schemaVersion"] = 2
if not isinstance(normalized.get("content"), str):
raise ValueError("fact.content must be a string")
normalized["content"] = normalized["content"].strip()
if not normalized["content"]:
raise ValueError("fact.content must not be empty")
_normalize_category(normalized)
confidence = normalized.get("confidence", 0.5)
if isinstance(confidence, bool) or not isinstance(confidence, (int, float)) or not 0 <= confidence <= 1:
raise ValueError("fact.confidence must be a number between 0 and 1")
normalized["confidence"] = float(confidence)
status = normalized.get("status", "active")
if status != "active":
raise ValueError("fact.status must be 'active'; deletion is physical")
normalized["status"] = "active"
normalized["scope"] = copy.deepcopy(scope)
_require_string_list(normalized, "topics")
_require_string_list(normalized, "consolidatedFrom")
revision = normalized.get("revision", 1)
if isinstance(revision, bool) or not isinstance(revision, int) or revision < 1:
raise ValueError("fact.revision must be an integer >= 1")
source = normalized.get("source")
if isinstance(source, str):
if source in {"manual", "consolidation", "import", "unknown"}:
normalized["source"] = {"type": source, "threadId": None}
else:
normalized["source"] = {"type": "conversation", "threadId": source}
elif not isinstance(source, dict):
normalized["source"] = {"type": "unknown", "threadId": None}
else:
normalized["source"].setdefault("type", "unknown")
if not isinstance(normalized["source"].get("type"), str):View on GitHub (pinned to 1dd6ba1acb)
Solutions
- Remove the status key (or set it to 'active') for facts you intend to store; use the backend's delete/remove API to delete, which physically removes the record.
- In migration code, filter out facts with non-active status instead of importing them.
- If you need archiving, model it with a category or custom field, not status.
Example fix
# before
memory.save_fact({"content": "...", "status": "archived"})
# after
memory.save_fact({"content": "..."}) # status defaults to 'active'; use memory.delete_fact(id) to remove Defensive patterns
Strategy: validation
Validate before calling
status = fact.get("status", "active")
if status != "active":
if status in {"deleted", "archived", "inactive"}:
return # drop soft-deleted facts on import; deletion here is physical
fact["status"] = "active" Type guard
def is_active_status(fact: dict) -> bool:
return fact.get("status", "active") == "active" Prevention
- Use the delete API to remove facts; never flip status.
- Filter non-active facts out of migration/import streams.
When it happens
Trigger: Saving {'status': 'deleted'}, {'status': 'inactive'}, or forwarding v1 facts whose status was set to soft-deleted during an export/migration that skipped the physical delete.
Common situations: Porting data from a system with soft deletes; an LLM or script 'deleting' a fact by flipping status instead of calling the delete API.
Related errors
- retrieval fact.id must be a non-empty string
- retrieval fact.content must be a non-empty string
- unsupported FTS5 retrieval mode: {mode}
- retrieval category filter must be a string
- fact.category must be a string
AI-assisted analysis of bytedance/deer-flow@1dd6ba1acb (2026-08-14).
Data as JSON: /api/errors/66a5742ec71f69ca.
Report an issue: GitHub.