bytedance/deer-flow · error · ValueError

fact.source.threadId must be a string or null

Error message

fact.source.threadId must be a string or null

What it means

When fact['source'] is a dict, its 'threadId' may be null or a str; any other type raises ValueError. threadId links a fact to the conversation that produced it and is used as a string key in lookups and rendering.

Source

Thrown at backend/packages/harness/deerflow/agents/memory/backends/deermem/deermem/core/storage.py:226

    _require_string_list(normalized, "topics")
    _require_string_list(normalized, "consolidatedFrom")
    revision = normalized.get("revision", 1)
    if isinstance(revision, bool) or not isinstance(revision, int) or revision < 1:
        raise ValueError("fact.revision must be an integer >= 1")
    source = normalized.get("source")
    if isinstance(source, str):
        if source in {"manual", "consolidation", "import", "unknown"}:
            normalized["source"] = {"type": source, "threadId": None}
        else:
            normalized["source"] = {"type": "conversation", "threadId": source}
    elif not isinstance(source, dict):
        normalized["source"] = {"type": "unknown", "threadId": None}
    else:
        normalized["source"].setdefault("type", "unknown")
        if not isinstance(normalized["source"].get("type"), str):
            raise ValueError("fact.source.type must be a string")
        if normalized["source"].get("threadId") is not None and not isinstance(normalized["source"].get("threadId"), str):
            raise ValueError("fact.source.threadId must be a string or null")
    normalized["title"] = _fact_title(normalized)
    now = utc_now_iso_z()
    if existing is None:
        normalized.setdefault("createdAt", now)
        normalized.setdefault("updatedAt", normalized["createdAt"])
        normalized["revision"] = revision
    else:
        existing_revision = existing.get("revision")
        if not isinstance(existing_revision, int) or existing_revision < 1:
            raise MemoryStorageCorruption(f"Stored fact {normalized['id']!r} has an invalid revision")
        if revision != existing_revision:
            raise MemoryFactRevisionConflict(f"Expected fact {normalized['id']!r} revision {revision}, found {existing_revision}")
        normalized["createdAt"] = existing.get("createdAt") or normalized.get("createdAt") or now
        comparison_keys = {"revision", "updatedAt"}
        incoming_material = {key: value for key, value in normalized.items() if key not in comparison_keys}
        existing_material = {key: value for key, value in existing.items() if key not in comparison_keys}
        if incoming_material == existing_material:
            normalized["revision"] = existing_revision

View on GitHub (pinned to 1dd6ba1acb)

Solutions

  1. Stringify ids: source['threadId'] = str(tid) if tid is not None else None.
  2. Use None explicitly for 'no thread' rather than 0 or ''.
  3. Validate with a guard: tid is None or isinstance(tid, str).

Example fix

# before
memory.save_fact({"content": "...", "source": {"type": "conversation", "threadId": 9001}})
# after
memory.save_fact({"content": "...", "source": {"type": "conversation", "threadId": "9001"}})
Defensive patterns

Strategy: type-guard

Validate before calling

src = fact.get("source")
if isinstance(src, dict) and src.get("threadId") is not None and not isinstance(src["threadId"], str):
    src["threadId"] = str(src["threadId"])

Type guard

def has_valid_thread_id(fact: dict) -> bool:
    src = fact.get("source")
    return not isinstance(src, dict) or src.get("threadId") is None or isinstance(src["threadId"], str)

Prevention

When it happens

Trigger: Saving {'source': {'type': 'conversation', 'threadId': 12345}} (numeric id), threadId as a list, or a dict copied from a record whose id field is an int.

Common situations: External systems with numeric conversation ids; JSON imports preserving numbers; serializers writing 0 instead of null for 'no thread'.

Related errors


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