{"record":{"id":"6456c914eed75f40","repo":"bytedance/deer-flow","slug":"fact-content-must-be-a-string","errorCode":null,"errorMessage":"fact.content must be a string","messagePattern":"fact\\.content 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":194,"sourceCode":"    scope: dict[str, str | None],\n    existing: dict[str, Any] | None = None,\n) -> dict[str, Any]:\n    \"\"\"Validate one fact and derive its per-item revision.\n\n    The shared JSON revision protects the multi-file transaction.  The fact's\n    own revision protects one Markdown object when a disjoint transaction is\n    safely rebased after that shared revision changed.\n    \"\"\"\n    if not isinstance(fact, dict):\n        raise ValueError(\"fact must be an object\")\n    normalized = copy.deepcopy(fact)\n    normalized[\"id\"] = str(normalized.get(\"id\") or f\"fact_{uuid.uuid4().hex}\")\n    # Validate the id through the canonical path builder's public contract.\n    if not normalized[\"id\"] or any(character not in \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-\" for character in normalized[\"id\"]):\n        raise ValueError(\"fact.id may contain only letters, numbers, '_' and '-'\")\n    normalized[\"schemaVersion\"] = 2\n    if not isinstance(normalized.get(\"content\"), str):\n        raise ValueError(\"fact.content must be a string\")\n    normalized[\"content\"] = normalized[\"content\"].strip()\n    if not normalized[\"content\"]:\n        raise ValueError(\"fact.content must not be empty\")\n    _normalize_category(normalized)\n    confidence = normalized.get(\"confidence\", 0.5)\n    if isinstance(confidence, bool) or not isinstance(confidence, (int, float)) or not 0 <= confidence <= 1:\n        raise ValueError(\"fact.confidence must be a number between 0 and 1\")\n    normalized[\"confidence\"] = float(confidence)\n    status = normalized.get(\"status\", \"active\")\n    if status != \"active\":\n        raise ValueError(\"fact.status must be 'active'; deletion is physical\")\n    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\")","sourceCodeStart":176,"sourceCodeEnd":212,"githubUrl":"https://github.com/bytedance/deer-flow/blob/1dd6ba1acb03700589994b0366c5d1c7d05e2eff/backend/packages/harness/deerflow/agents/memory/backends/deermem/deermem/core/storage.py#L176-L212","documentation":"fact['content'] must be a str before storage; None, numbers, lists, or dicts raise ValueError. Content is the payload rendered into both JSON and Markdown objects, so its type is non-negotiable. There is no coercion: absent content also fails (None is not str).","triggerScenarios":"Saving {'content': None}, {'content': 42}, or {'content': ['a','b']}; forwarding an LLM message object instead of its text field.","commonSituations":"Extraction steps returning structured data where text was expected; optional fields left as None by serializers instead of omitted; upstream schema change from text to rich-content objects.","solutions":["Extract the text explicitly: use the text member of structured payloads, or str(value) for scalars.","Drop the key only if you also skip the save - absent content still fails validation.","Add a boundary check that rejects non-str content with a clear error naming the offending fact."],"exampleFix":"# before\nmemory.save_fact({\"content\": message})  # message is a dict\n# after\nmemory.save_fact({\"content\": message[\"text\"]})","handlingStrategy":"type-guard","validationCode":"content = fact.get(\"content\")\nif not isinstance(content, str):\n    fact[\"content\"] = content[\"text\"] if isinstance(content, dict) and \"text\" in content else str(content or \"\")","typeGuard":"def has_string_content(fact: dict) -> bool:\n    return isinstance(fact.get(\"content\"), str)","tryCatchPattern":null,"preventionTips":["Pull the text member explicitly from structured message objects before saving.","Assert isinstance(content, str) in extraction code with an error naming the source record."],"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"}