bytedance/deer-flow · error · ValueError
fact.{field} must be a list of strings
Error message
fact.{field} must be a list of strings What it means
The fields 'topics' and 'consolidatedFrom' on a fact must be lists whose elements are all strings (a missing field defaults to []). Any other shape - a bare string, a dict, or a list containing numbers/objects - raises ValueError. These fields feed rendering and consolidation logic that iterates strings.
Source
Thrown at backend/packages/harness/deerflow/agents/memory/backends/deermem/deermem/core/storage.py:169
raise
except OSError as exc:
raise OSError(f"Failed to create durable migration backup {backup_path}: {exc}") from exc
def _normalize_category(fact: dict[str, Any]) -> None:
raw_category = fact.get("category", "context")
if not isinstance(raw_category, str):
raise ValueError("fact.category must be a string")
category = raw_category or "context"
if category not in CORE_CATEGORIES:
fact.setdefault("categoryExtension", category)
fact["category"] = "other"
def _require_string_list(fact: dict[str, Any], field: str) -> None:
value = fact.get(field, [])
if not isinstance(value, list) or any(not isinstance(item, str) for item in value):
raise ValueError(f"fact.{field} must be a list of strings")
fact[field] = value
def _normalize_fact(
fact: dict[str, Any],
*,
scope: dict[str, str | None],
existing: dict[str, Any] | None = None,
) -> dict[str, Any]:
"""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)View on GitHub (pinned to 1dd6ba1acb)
Solutions
- Wrap scalars: fact['topics'] = [fact['topics']] if it is a str, and stringify or drop non-string elements before saving.
- Validate the two fields with a type guard before the save call and log-and-drop malformed facts during bulk import.
- Update the producing agent/tool prompt or schema to require an array of strings.
Example fix
# before
memory.save_fact({"content": "likes rust", "topics": "rust, systems"})
# after
memory.save_fact({"content": "likes rust", "topics": ["rust", "systems"]}) Defensive patterns
Strategy: type-guard
Validate before calling
for field in ("topics", "consolidatedFrom"):
v = fact.get(field, [])
if isinstance(v, str):
fact[field] = [v] if v else []
elif isinstance(v, list):
fact[field] = [str(x) for x in v]
else:
fact[field] = [] Type guard
def has_string_list_fields(fact: dict) -> bool:
return all(
isinstance(fact.get(f, []), list) and all(isinstance(x, str) for x in fact.get(f, []))
for f in ("topics", "consolidatedFrom")
) Try / catch
try:
store.save(fact)
except ValueError as exc:
if "list of strings" in str(exc):
v = fact.get("topics", [])
fact["topics"] = [v] if isinstance(v, str) else [str(x) for x in v] if isinstance(v, list) else []
fact.setdefault("consolidatedFrom", [])
store.save(fact)
else:
raise Prevention
- Normalize topics at the LLM-tool boundary: split comma-joined strings into arrays.
- Validate imported facts field-by-field and quarantine malformed rows.
When it happens
Trigger: Saving a fact with {'topics': 'python'} (single string instead of list), {'topics': ['python', 42]}, or {'consolidatedFrom': {'fact_a': True}}; importing JSON where the producer serialized topics as a comma-joined string.
Common situations: LLM tool output emitting a scalar where the schema says array; producers joining tags into one string; version changes in the writer.
Related errors
- fact.category must be a string
- fact.content must be a string
- fact.source.type must be a string
- fact.source.threadId must be a string or null
- retrieval fact.id must be a non-empty string
AI-assisted analysis of bytedance/deer-flow@1dd6ba1acb (2026-08-14).
Data as JSON: /api/errors/8a96ae5b358e2bb9.
Report an issue: GitHub.