bytedance/deer-flow · error · ValueError
memory_data.facts must contain only fact objects
Error message
memory_data.facts must contain only fact objects
What it means
Raised by FileMemoryStorage.save() when memory_data['facts'] is a list but at least one element is not a dict. The storage layer validates the shape of a full-save payload before taking the scope/file lock, because every later step (id extraction, deep-copy upserts, delete-diffing) assumes each fact is a JSON object. Any non-dict entry (string, number, list, null) triggers this ValueError.
Source
Thrown at backend/packages/harness/deerflow/agents/memory/backends/deermem/deermem/core/storage.py:1085
This API must scan the selected agent to determine which omitted facts
are deletions, but the commit writes only new/changed/deleted facts.
Repository callers should prefer ``apply_changes`` to avoid even that
full comparison scan.
"""
path = self._get_memory_file_path(agent_name, user_id=user_id)
key = self._cache_key(agent_name, user_id=user_id)
lock_path = path.parent / ".memory.lock"
notifications: list[RetrievalNotification] = []
try:
if not isinstance(memory_data, dict):
raise ValueError("memory_data must be an object")
if agent_name is not None and "facts" not in memory_data:
raise ValueError("memory_data.facts is required for an agent full save")
facts_raw = memory_data.get("facts", [])
if not isinstance(facts_raw, list):
raise ValueError("memory_data.facts must be a list")
if any(not isinstance(fact, dict) for fact in facts_raw):
raise ValueError("memory_data.facts must contain only fact objects")
if agent_name is None and facts_raw:
raise ValueError("agent_name is required to persist facts")
with self._scope_lock(key), _process_file_lock(lock_path, float(getattr(self._config, "file_lock_timeout_seconds", 10))):
self._recover_if_needed(path)
ids = [str(fact.get("id") or "") for fact in facts_raw]
if len(ids) != len(set(ids)):
raise ValueError("Duplicate fact ids are not allowed")
old_ids = set(self._agent_entries(path, agent_name, user_id=user_id)) if agent_name is not None else set()
summaries = None
if agent_name is None:
summaries = {"user": memory_data.get("user", {}), "history": memory_data.get("history", {})}
_, notifications = self._commit_changes_locked(
path,
user_id=user_id,
agent_name=agent_name,
upserts=copy.deepcopy(facts_raw),
deletes=sorted(old_ids - set(ids)),
summaries=summaries,View on GitHub (pinned to 1dd6ba1acb)
Solutions
- Inspect memory_data['facts'] before saving and fix each entry to be a dict (minimally {'id': ..., 'content': ...}).
- If facts were produced by an LLM/tool call, add a post-processing step that wraps or rejects non-object entries before they reach save().
- Write a failing test reproducing the payload to confirm the fix and prevent regression.
- If the data is corrupt on disk, locate the memory file via _get_memory_file_path and repair or clear it.
Example fix
# before
facts = ["user likes coffee", "user lives in Berlin"]
storage.save({"facts": facts}, agent_name="researcher")
# after
facts = [
{"id": "fact_1", "content": "user likes coffee"},
{"id": "fact_2", "content": "user lives in Berlin"},
]
storage.save({"facts": facts}, agent_name="researcher") Defensive patterns
Strategy: type-guard
Validate before calling
def all_facts_are_objects(memory_data: dict) -> bool:
facts = memory_data.get("facts", [])
return isinstance(facts, list) and all(isinstance(f, dict) for f in facts)
if not all_facts_are_objects(memory_data):
raise HTTPException(400, "facts must be a list of objects") Type guard
def is_fact_list(value: object) -> TypeGuard[list[dict[str, Any]]]:
return isinstance(value, list) and all(isinstance(item, dict) for item in value) Prevention
- Validate LLM/tool-produced fact batches with a schema before persisting.
- Keep a pydantic model (facts: list[FactModel]) at every boundary that builds memory payloads.
- Never hand-edit memory JSON without re-validating its shape.
When it happens
Trigger: Calling storage.save(memory_data, agent_name=...) or MemoryUpdater.import_memory_data-equivalent full-save paths where memory_data['facts'] contains e.g. a plain string ('user likes coffee'), a null, or a nested list instead of {"id": ..., "content": ...} objects.
Common situations: Hand-edited memory JSON files; an LLM generating the facts array emitting raw strings instead of objects; a migration script passing a list of contents rather than fact dicts; deserialized JSON where facts was serialized as a map of id->string.
Related errors
- retrieval fact.id must be a non-empty string
- retrieval fact.content must be a non-empty string
- unsupported FTS5 retrieval mode: {mode}
- retrieval category filter must be a string
- fact.category must be a string
AI-assisted analysis of bytedance/deer-flow@1dd6ba1acb (2026-08-14).
Data as JSON: /api/errors/f5c472e1dedf869c.
Report an issue: GitHub.