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

  1. Guard at the call site: if not content.strip(), skip the save (this is a no-op case, not an error to surface).
  2. Fix the extractor to omit empty extraction results instead of materializing them as facts.
  3. 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

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


AI-assisted analysis of bytedance/deer-flow@1dd6ba1acb (2026-08-14). Data as JSON: /api/errors/076f210622c6a69a. Report an issue: GitHub.