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

  1. Inspect memory_data['facts'] before saving and fix each entry to be a dict (minimally {'id': ..., 'content': ...}).
  2. 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().
  3. Write a failing test reproducing the payload to confirm the fix and prevent regression.
  4. 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

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


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