{"record":{"id":"e86bf0eeb4cfbda0","repo":"bytedance/deer-flow","slug":"fact-must-be-an-object","errorCode":null,"errorMessage":"fact must be an object","messagePattern":"fact must be an object","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"backend/packages/harness/deerflow/agents/memory/backends/deermem/deermem/core/storage.py","lineNumber":186,"sourceCode":"    if not isinstance(value, list) or any(not isinstance(item, str) for item in value):\n        raise ValueError(f\"fact.{field} must be a list of strings\")\n    fact[field] = value\n\n\ndef _normalize_fact(\n    fact: dict[str, Any],\n    *,\n    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\":","sourceCodeStart":168,"sourceCodeEnd":204,"githubUrl":"https://github.com/bytedance/deer-flow/blob/1dd6ba1acb03700589994b0366c5d1c7d05e2eff/backend/packages/harness/deerflow/agents/memory/backends/deermem/deermem/core/storage.py#L168-L204","documentation":"The fact normalization entrypoint (_normalize_fact) requires its fact argument to be a dict (JSON object). Passing a list, string, number, or None raises ValueError('fact must be an object') before any field is read. It is the outermost type gate for every fact written to memory.","triggerScenarios":"Calling save/upsert with a JSON string instead of a parsed dict (e.g. json.dumps applied twice), a list of facts where one is expected, or None from an upstream extraction step that found nothing.","commonSituations":"LLM extraction pipelines returning null on 'no memories found' and the caller forwarding it; double serialization bugs; passing the whole response envelope instead of response['fact'].","solutions":["Parse before saving: fact = json.loads(raw) if isinstance(raw, str) else raw, and check isinstance(fact, dict).","Skip or log-and-continue when the extraction step yields None or a non-object, instead of forwarding it to storage.","If you meant to save many facts, loop over the list and save each dict element individually."],"exampleFix":"# before\nmemory.save_fact(raw_fact_json)  # raw_fact_json is a str\n# after\nmemory.save_fact(json.loads(raw_fact_json))","handlingStrategy":"type-guard","validationCode":"import json\nif isinstance(fact, (bytes, str)):\n    fact = json.loads(fact)\nif not isinstance(fact, dict):\n    return  # nothing to save","typeGuard":"from typing import Any\n\ndef is_fact_object(value: Any) -> bool:\n    return isinstance(value, dict)","tryCatchPattern":"try:\n    store.save(fact)\nexcept ValueError as exc:\n    if \"must be an object\" in str(exc):\n        return  # extraction produced nothing; not an error condition\n    raise","preventionTips":["Parse JSON payloads once, at the boundary, and assert the expected envelope shape.","Treat a null extraction result as 'skip', never forward it to storage."],"tags":["deermem","memory","validation","type-error"],"backgroundTag":null,"analyzedSha":"1dd6ba1acb03700589994b0366c5d1c7d05e2eff","analyzedAt":"2026-08-14T21:20:34.804Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}