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

  1. Convert the category to a string before save if it is a scalar label, or drop the key to get the 'context' default.
  2. Validate imported facts with a small schema check (category must be str) and quarantine failing rows.
  3. 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

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


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