bytedance/deer-flow · error · ValueError
fact.content must not be empty
Error message
fact.content must not be empty
What it means
After stripping whitespace, fact['content'] must be non-empty; a whitespace-only or empty string raises ValueError. Empty facts would corrupt dedup, titles (which derive from the first content line), and retrieval scoring, so they are rejected rather than stored as no-ops.
Source
Thrown at backend/packages/harness/deerflow/agents/memory/backends/deermem/deermem/core/storage.py:197
"""Validate one fact and derive its per-item revision.
The shared JSON revision protects the multi-file transaction. The fact's
own revision protects one Markdown object when a disjoint transaction is
safely rebased after that shared revision changed.
"""
if not isinstance(fact, dict):
raise ValueError("fact must be an object")
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"}:View on GitHub (pinned to 1dd6ba1acb)
Solutions
- Guard at the call site: if not content.strip(), skip the save (this is a no-op case, not an error to surface).
- Fix the extractor to omit empty extraction results instead of materializing them as facts.
- Trim user input earlier and validate non-empty at the form/API boundary.
Example fix
# before
memory.save_fact({"content": extracted_text}) # extracted_text == ""
# after
if extracted_text and extracted_text.strip():
memory.save_fact({"content": extracted_text}) Defensive patterns
Strategy: validation
Validate before calling
content = fact.get("content")
if not isinstance(content, str) or not content.strip():
return # nothing memorable - skip the save entirely Type guard
def has_nonempty_content(fact: dict) -> bool:
return isinstance(fact.get("content"), str) and bool(fact["content"].strip()) Prevention
- Skip empty extraction results instead of materializing empty facts.
- Validate non-empty trimmed input at the form/API boundary.
When it happens
Trigger: Saving {'content': ''}, {'content': ' \n '}, or content assembled by joining an empty list of extracted sentences.
Common situations: Extraction pipelines that emit empty strings when nothing memorable was found; template rendering producing only whitespace; users submitting blank form fields.
Related errors
- content
- 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
AI-assisted analysis of bytedance/deer-flow@1dd6ba1acb (2026-08-14).
Data as JSON: /api/errors/076f210622c6a69a.
Report an issue: GitHub.