{"record":{"id":"cb089d192b409402","repo":"bytedance/deer-flow","slug":"fact-source-threadid-must-be-a-string-or-null","errorCode":null,"errorMessage":"fact.source.threadId must be a string or null","messagePattern":"fact\\.source\\.threadId must be a string or null","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"backend/packages/harness/deerflow/agents/memory/backends/deermem/deermem/core/storage.py","lineNumber":226,"sourceCode":"    _require_string_list(normalized, \"topics\")\n    _require_string_list(normalized, \"consolidatedFrom\")\n    revision = normalized.get(\"revision\", 1)\n    if isinstance(revision, bool) or not isinstance(revision, int) or revision < 1:\n        raise ValueError(\"fact.revision must be an integer >= 1\")\n    source = normalized.get(\"source\")\n    if isinstance(source, str):\n        if source in {\"manual\", \"consolidation\", \"import\", \"unknown\"}:\n            normalized[\"source\"] = {\"type\": source, \"threadId\": None}\n        else:\n            normalized[\"source\"] = {\"type\": \"conversation\", \"threadId\": source}\n    elif not isinstance(source, dict):\n        normalized[\"source\"] = {\"type\": \"unknown\", \"threadId\": None}\n    else:\n        normalized[\"source\"].setdefault(\"type\", \"unknown\")\n        if not isinstance(normalized[\"source\"].get(\"type\"), str):\n            raise ValueError(\"fact.source.type must be a string\")\n        if normalized[\"source\"].get(\"threadId\") is not None and not isinstance(normalized[\"source\"].get(\"threadId\"), str):\n            raise ValueError(\"fact.source.threadId must be a string or null\")\n    normalized[\"title\"] = _fact_title(normalized)\n    now = utc_now_iso_z()\n    if existing is None:\n        normalized.setdefault(\"createdAt\", now)\n        normalized.setdefault(\"updatedAt\", normalized[\"createdAt\"])\n        normalized[\"revision\"] = revision\n    else:\n        existing_revision = existing.get(\"revision\")\n        if not isinstance(existing_revision, int) or existing_revision < 1:\n            raise MemoryStorageCorruption(f\"Stored fact {normalized['id']!r} has an invalid revision\")\n        if revision != existing_revision:\n            raise MemoryFactRevisionConflict(f\"Expected fact {normalized['id']!r} revision {revision}, found {existing_revision}\")\n        normalized[\"createdAt\"] = existing.get(\"createdAt\") or normalized.get(\"createdAt\") or now\n        comparison_keys = {\"revision\", \"updatedAt\"}\n        incoming_material = {key: value for key, value in normalized.items() if key not in comparison_keys}\n        existing_material = {key: value for key, value in existing.items() if key not in comparison_keys}\n        if incoming_material == existing_material:\n            normalized[\"revision\"] = existing_revision","sourceCodeStart":208,"sourceCodeEnd":244,"githubUrl":"https://github.com/bytedance/deer-flow/blob/1dd6ba1acb03700589994b0366c5d1c7d05e2eff/backend/packages/harness/deerflow/agents/memory/backends/deermem/deermem/core/storage.py#L208-L244","documentation":"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.","triggerScenarios":"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.","commonSituations":"External systems with numeric conversation ids; JSON imports preserving numbers; serializers writing 0 instead of null for 'no thread'.","solutions":["Stringify ids: source['threadId'] = str(tid) if tid is not None else None.","Use None explicitly for 'no thread' rather than 0 or ''.","Validate with a guard: tid is None or isinstance(tid, str)."],"exampleFix":"# before\nmemory.save_fact({\"content\": \"...\", \"source\": {\"type\": \"conversation\", \"threadId\": 9001}})\n# after\nmemory.save_fact({\"content\": \"...\", \"source\": {\"type\": \"conversation\", \"threadId\": \"9001\"}})","handlingStrategy":"type-guard","validationCode":"src = fact.get(\"source\")\nif isinstance(src, dict) and src.get(\"threadId\") is not None and not isinstance(src[\"threadId\"], str):\n    src[\"threadId\"] = str(src[\"threadId\"])","typeGuard":"def has_valid_thread_id(fact: dict) -> bool:\n    src = fact.get(\"source\")\n    return not isinstance(src, dict) or src.get(\"threadId\") is None or isinstance(src[\"threadId\"], str)","tryCatchPattern":null,"preventionTips":["Stringify numeric external thread/conversation ids at the boundary.","Use explicit None for 'no thread', never 0 or a placeholder."],"tags":["deermem","memory","validation","facts"],"backgroundTag":null,"analyzedSha":"1dd6ba1acb03700589994b0366c5d1c7d05e2eff","analyzedAt":"2026-08-14T21:20:34.804Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}