{"record":{"id":"8a96ae5b358e2bb9","repo":"bytedance/deer-flow","slug":"fact-field-must-be-a-list-of-strings","errorCode":null,"errorMessage":"fact.{field} must be a list of strings","messagePattern":"fact\\.(.+?) must be a list of strings","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"backend/packages/harness/deerflow/agents/memory/backends/deermem/deermem/core/storage.py","lineNumber":169,"sourceCode":"        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,\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)","sourceCodeStart":151,"sourceCodeEnd":187,"githubUrl":"https://github.com/bytedance/deer-flow/blob/1dd6ba1acb03700589994b0366c5d1c7d05e2eff/backend/packages/harness/deerflow/agents/memory/backends/deermem/deermem/core/storage.py#L151-L187","documentation":"The fields 'topics' and 'consolidatedFrom' on a fact must be lists whose elements are all strings (a missing field defaults to []). Any other shape - a bare string, a dict, or a list containing numbers/objects - raises ValueError. These fields feed rendering and consolidation logic that iterates strings.","triggerScenarios":"Saving a fact with {'topics': 'python'} (single string instead of list), {'topics': ['python', 42]}, or {'consolidatedFrom': {'fact_a': True}}; importing JSON where the producer serialized topics as a comma-joined string.","commonSituations":"LLM tool output emitting a scalar where the schema says array; producers joining tags into one string; version changes in the writer.","solutions":["Wrap scalars: fact['topics'] = [fact['topics']] if it is a str, and stringify or drop non-string elements before saving.","Validate the two fields with a type guard before the save call and log-and-drop malformed facts during bulk import.","Update the producing agent/tool prompt or schema to require an array of strings."],"exampleFix":"# before\nmemory.save_fact({\"content\": \"likes rust\", \"topics\": \"rust, systems\"})\n# after\nmemory.save_fact({\"content\": \"likes rust\", \"topics\": [\"rust\", \"systems\"]})","handlingStrategy":"type-guard","validationCode":"for field in (\"topics\", \"consolidatedFrom\"):\n    v = fact.get(field, [])\n    if isinstance(v, str):\n        fact[field] = [v] if v else []\n    elif isinstance(v, list):\n        fact[field] = [str(x) for x in v]\n    else:\n        fact[field] = []","typeGuard":"def has_string_list_fields(fact: dict) -> bool:\n    return all(\n        isinstance(fact.get(f, []), list) and all(isinstance(x, str) for x in fact.get(f, []))\n        for f in (\"topics\", \"consolidatedFrom\")\n    )","tryCatchPattern":"try:\n    store.save(fact)\nexcept ValueError as exc:\n    if \"list of strings\" in str(exc):\n        v = fact.get(\"topics\", [])\n        fact[\"topics\"] = [v] if isinstance(v, str) else [str(x) for x in v] if isinstance(v, list) else []\n        fact.setdefault(\"consolidatedFrom\", [])\n        store.save(fact)\n    else:\n        raise","preventionTips":["Normalize topics at the LLM-tool boundary: split comma-joined strings into arrays.","Validate imported facts field-by-field and quarantine malformed rows."],"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"}