{"record":{"id":"935bfdda0bb168ac","repo":"bytedance/deer-flow","slug":"fact-source-type-must-be-a-string","errorCode":null,"errorMessage":"fact.source.type must be a string","messagePattern":"fact\\.source\\.type must be a string","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"backend/packages/harness/deerflow/agents/memory/backends/deermem/deermem/core/storage.py","lineNumber":224,"sourceCode":"    normalized[\"status\"] = \"active\"\n    normalized[\"scope\"] = copy.deepcopy(scope)\n    _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}","sourceCodeStart":206,"sourceCodeEnd":242,"githubUrl":"https://github.com/bytedance/deer-flow/blob/1dd6ba1acb03700589994b0366c5d1c7d05e2eff/backend/packages/harness/deerflow/agents/memory/backends/deermem/deermem/core/storage.py#L206-L242","documentation":"When fact['source'] is a dict, its 'type' member (defaulting to 'unknown') must be a str; a non-string type (int, list, dict) raises ValueError. String sources are auto-converted to {'type': ..., 'threadId': ...}, so this error only fires for dict sources with a bad type value.","triggerScenarios":"Saving {'source': {'type': 1}}, {'source': {'type': ['manual']}}, or a source dict built from unvalidated import JSON.","commonSituations":"Imports mapping a numeric source enum into type; producers nesting the wrong object; LLM tool output with structured source objects of mixed types.","solutions":["Set source as a plain string ('manual' | 'consolidation' | 'import' | 'unknown' or a thread id) and let the backend normalize it.","If using the dict form, ensure source['type'] is a str literal from the documented set.","Validate imported source dicts with a type guard before saving."],"exampleFix":"# before\nmemory.save_fact({\"content\": \"...\", \"source\": {\"type\": 3, \"threadId\": None}})\n# after\nmemory.save_fact({\"content\": \"...\", \"source\": {\"type\": \"manual\", \"threadId\": None}})","handlingStrategy":"type-guard","validationCode":"src = fact.get(\"source\")\nif isinstance(src, dict):\n    t = src.get(\"type\", \"unknown\")\n    src[\"type\"] = t if isinstance(t, str) else str(t)\nelif src is not None and not isinstance(src, str):\n    fact[\"source\"] = \"unknown\"","typeGuard":"def has_valid_source_type(fact: dict) -> bool:\n    src = fact.get(\"source\")\n    return not isinstance(src, dict) or isinstance(src.get(\"type\", \"unknown\"), str)","tryCatchPattern":null,"preventionTips":["Prefer the string form of source and let the backend normalize it.","Validate imported source dicts against the {'type': str, 'threadId': str|None} shape."],"tags":["deermem","memory","validation","facts"],"backgroundTag":null,"analyzedSha":"1dd6ba1acb03700589994b0366c5d1c7d05e2eff","analyzedAt":"2026-08-14T21:20:34.804Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}