{"record":{"id":"c26092ccd0326438","repo":"bytedance/deer-flow","slug":"fact-category-must-be-a-string","errorCode":null,"errorMessage":"fact.category must be a string","messagePattern":"fact\\.category 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":159,"sourceCode":"    backup_path = source_path.with_name(f\"{source_path.name}.v1.bak\")\n    try:\n        source_bytes = source_path.read_bytes()\n        if backup_path.exists():\n            if backup_path.read_bytes() != source_bytes:\n                raise MemoryStorageCorruption(f\"Existing migration backup {backup_path} differs from source {source_path}; the original backup was kept and migration was stopped\")\n            return backup_path\n        _atomic_write(backup_path, source_bytes)\n        return backup_path\n    except MemoryStorageCorruption:\n        raise\n    except OSError as exc:\n        raise OSError(f\"Failed to create durable migration backup {backup_path}: {exc}\") from exc\n\n\ndef _normalize_category(fact: dict[str, Any]) -> None:\n    raw_category = fact.get(\"category\", \"context\")\n    if not isinstance(raw_category, str):\n        raise ValueError(\"fact.category must be a string\")\n    category = raw_category or \"context\"\n    if category not in CORE_CATEGORIES:\n        fact.setdefault(\"categoryExtension\", category)\n        fact[\"category\"] = \"other\"\n\n\ndef _require_string_list(fact: dict[str, Any], field: str) -> None:\n    value = fact.get(field, [])\n    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,","sourceCodeStart":141,"sourceCodeEnd":177,"githubUrl":"https://github.com/bytedance/deer-flow/blob/1dd6ba1acb03700589994b0366c5d1c7d05e2eff/backend/packages/harness/deerflow/agents/memory/backends/deermem/deermem/core/storage.py#L141-L177","documentation":"Fact normalization requires fact['category'] (or its default 'context') to be a str. A non-string category (int, list, dict) fails with ValueError before the fact is stored. Categories drive FTS5 filtering and bucketing into CORE_CATEGORIES, so the type is fixed at string.","triggerScenarios":"Saving a fact with {'category': 3}, {'category': ['skill']}, or a None set explicitly; JSON-imported facts where the producer used a numeric or composite category field.","commonSituations":"LLM-generated fact JSON using wrong types; import scripts mapping an enum id instead of its label; schema drift after a producer refactor.","solutions":["Convert the category to a string before save if it is a scalar label, or drop the key to get the 'context' default.","Validate imported facts with a small schema check (category must be str) and quarantine failing rows.","If the producer genuinely has structured categories, flatten to a single string label and put the structure in a custom field."],"exampleFix":"# before\nmemory.save_fact({\"content\": \"prefers dark mode\", \"category\": 7})\n# after\nmemory.save_fact({\"content\": \"prefers dark mode\", \"category\": \"preference\"})","handlingStrategy":"type-guard","validationCode":"raw = fact.get(\"category\", \"context\")\nif raw is not None and not isinstance(raw, str):\n    fact[\"category\"] = str(raw)  # or reject","typeGuard":"def has_string_category(fact: dict) -> bool:\n    c = fact.get(\"category\", \"context\")\n    return c is None or isinstance(c, str)","tryCatchPattern":"try:\n    store.save(fact)\nexcept ValueError as exc:\n    if \"fact.category\" in str(exc):\n        fact[\"category\"] = \"context\"\n        store.save(fact)\n    else:\n        raise","preventionTips":["Define a Fact TypedDict with category: str and validate at deserialization.","Reject or coerce non-string categories in import tooling before they reach storage."],"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"}