bytedance/deer-flow · error · ValueError
retrieval fact.content must be a non-empty string
Error message
retrieval fact.content must be a non-empty string
What it means
ValueError from FTS5RetrievalAdapter._document: a fact's 'content' is missing, not a string, empty, or whitespace-only. Content is what FTS5 indexes, so a contentless fact is unindexable and rejected rather than stored as a dead row.
Source
Thrown at backend/packages/harness/deerflow/agents/memory/backends/deermem/deermem/core/retrieval.py:576
document id prevents equal fact ids in two user/agent scopes from
overwriting each other.
"""
def __init__(self, db_path: str | Path = ":memory:") -> None:
self._engine = FTS5Retrieval(db_path)
@staticmethod
def _document_id(fact_id: str, scope: dict[str, str | None]) -> str:
scope_user, scope_agent = _scope_key(scope)
return json.dumps([scope_user, scope_agent, fact_id], ensure_ascii=False, separators=(",", ":"))
def _document(self, fact: dict[str, Any], scope: dict[str, str | None]) -> dict[str, Any]:
fact_id = fact.get("id")
content = fact.get("content")
if not isinstance(fact_id, str) or not fact_id:
raise ValueError("retrieval fact.id must be a non-empty string")
if not isinstance(content, str) or not content.strip():
raise ValueError("retrieval fact.content must be a non-empty string")
scope_user, scope_agent = _scope_key(scope)
payload = dict(fact)
payload["scope"] = {"userId": scope.get("userId"), "agentName": scope.get("agentName")}
source = payload.get("source")
return {
"fact_id": self._document_id(fact_id, scope),
"content": content,
"category": str(payload.get("category") or "context"),
"confidence": float(payload.get("confidence") or 0.5),
"created_at": payload.get("createdAt") if isinstance(payload.get("createdAt"), str) else None,
"scope_user": scope_user,
"scope_agent": scope_agent,
"source": source if isinstance(source, str) else json.dumps(source, ensure_ascii=False, default=str),
"fact_data": payload,
}
def upsert(self, fact: dict[str, Any], *, scope: dict[str, str | None], path: str) -> None:
del path # Canonical location belongs to storage; the index is rebuildable.View on GitHub (pinned to 1dd6ba1acb)
Solutions
- Skip facts with blank content before enqueueing for indexing (they carry nothing searchable).
- If content is structured, serialize it to a meaningful text representation first.
- Fix the extractor so it never emits empty-content facts (guard at the source).
Example fix
# before
for fact in facts:
adapter.upsert(fact, scope) # raises on blank content
# after
for fact in facts:
if isinstance(fact.get("content"), str) and fact["content"].strip():
adapter.upsert(fact, scope) Defensive patterns
Strategy: validation
Validate before calling
def has_searchable_content(fact) -> bool:
c = fact.get('content') if isinstance(fact, dict) else None
return isinstance(c, str) and bool(c.strip()) Type guard
def has_searchable_content(fact: dict) -> bool:
c = fact.get('content')
return isinstance(c, str) and c.strip() != '' Try / catch
try:
adapter.upsert(fact, scope)
except ValueError as e:
if 'fact.content' in str(e):
logger.warning('skipping contentless fact id=%s', fact.get('id'))
continue
raise Prevention
- Skip blank-content facts at the pipeline stage, before the retrieval adapter sees them.
- Serialize structured fact payloads into text rather than storing raw objects under content.
- Log skip counts — a spike means the extractor is degrading.
When it happens
Trigger: Indexing a fact dict whose content is None, '', ' ', or a non-str value (e.g. a dict/list payload put under 'content').
Common situations: Fact extraction emitting empty content for structured events; content field holding structured data instead of text; upstream summarization produced an empty string; whitespace-only content from a template render that substituted nothing.
Related errors
- retrieval fact.id must be a non-empty string
- retrieval scope userId must be a string or null
- retrieval scope agentName must be a string or null
- unsupported FTS5 retrieval mode: {mode}
- guaranteed_categories must be an iterable of strings, not a
AI-assisted analysis of bytedance/deer-flow@1dd6ba1acb (2026-08-14).
Data as JSON: /api/errors/3d45a473341d1a75.
Report an issue: GitHub.