bytedance/deer-flow · error · ValueError
fact.category must be a string
Error message
fact.category must be a string
What it means
Fact normalization requires fact['category'] (or its default 'context') to be a str. A non-string category (int, list, dict) fails with ValueError before the fact is stored. Categories drive FTS5 filtering and bucketing into CORE_CATEGORIES, so the type is fixed at string.
Source
Thrown at backend/packages/harness/deerflow/agents/memory/backends/deermem/deermem/core/storage.py:159
backup_path = source_path.with_name(f"{source_path.name}.v1.bak")
try:
source_bytes = source_path.read_bytes()
if backup_path.exists():
if backup_path.read_bytes() != source_bytes:
raise MemoryStorageCorruption(f"Existing migration backup {backup_path} differs from source {source_path}; the original backup was kept and migration was stopped")
return backup_path
_atomic_write(backup_path, source_bytes)
return backup_path
except MemoryStorageCorruption:
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,View on GitHub (pinned to 1dd6ba1acb)
Solutions
- Convert the category to a string before save if it is a scalar label, or drop the key to get the 'context' default.
- Validate imported facts with a small schema check (category must be str) and quarantine failing rows.
- If the producer genuinely has structured categories, flatten to a single string label and put the structure in a custom field.
Example fix
# before
memory.save_fact({"content": "prefers dark mode", "category": 7})
# after
memory.save_fact({"content": "prefers dark mode", "category": "preference"}) Defensive patterns
Strategy: type-guard
Validate before calling
raw = fact.get("category", "context")
if raw is not None and not isinstance(raw, str):
fact["category"] = str(raw) # or reject Type guard
def has_string_category(fact: dict) -> bool:
c = fact.get("category", "context")
return c is None or isinstance(c, str) Try / catch
try:
store.save(fact)
except ValueError as exc:
if "fact.category" in str(exc):
fact["category"] = "context"
store.save(fact)
else:
raise Prevention
- Define a Fact TypedDict with category: str and validate at deserialization.
- Reject or coerce non-string categories in import tooling before they reach storage.
When it happens
Trigger: Saving a fact with {'category': 3}, {'category': ['skill']}, or a None set explicitly; JSON-imported facts where the producer used a numeric or composite category field.
Common situations: LLM-generated fact JSON using wrong types; import scripts mapping an enum id instead of its label; schema drift after a producer refactor.
Related errors
- fact.{field} must be a list of strings
- 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/c26092ccd0326438.
Report an issue: GitHub.