{"record":{"id":"a96b0334f1ad91e7","repo":"bytedance/deer-flow","slug":"fact-id-may-contain-only-letters-numbers-and","errorCode":null,"errorMessage":"fact.id may contain only letters, numbers, '_' and '-'","messagePattern":"fact\\.id may contain only letters, numbers, '_' and '-'","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"backend/packages/harness/deerflow/agents/memory/backends/deermem/deermem/core/storage.py","lineNumber":191,"sourceCode":"def _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\":\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\")","sourceCodeStart":173,"sourceCodeEnd":209,"githubUrl":"https://github.com/bytedance/deer-flow/blob/1dd6ba1acb03700589994b0366c5d1c7d05e2eff/backend/packages/harness/deerflow/agents/memory/backends/deermem/deermem/core/storage.py#L173-L209","documentation":"A fact id (supplied or generated) is stringified and then validated against the charset [A-Za-z0-9_-] and non-empty. Any other character - spaces, dots, slashes, unicode, ':' from URNs - raises ValueError. The id becomes part of filesystem paths for Markdown objects, so the whitelist prevents path breakouts and invalid filenames.","triggerScenarios":"Passing id='fact:123', id='my fact', id='facts/v1.json', or id='' (empty after coercion). Generated ids (fact_<uuid4hex>) always pass; only caller-supplied ids can fail.","commonSituations":"Using UUIDs with braces ('{0b6e...}'), prefixed ids from external systems ('usr_42#fact'), or copy-pasted ids containing whitespace.","solutions":["Sanitize the id: re.sub(r'[^A-Za-z0-9_-]', '_', id) or use uuid4().hex.","Omit the id entirely and let the backend generate 'fact_<uuid4hex>'.","For deterministic ids from external keys, hash them: hashlib.sha256(key.encode()).hexdigest()[:32]."],"exampleFix":"# before\nmemory.save_fact({\"id\": \"ext:user:42\", \"content\": \"...\"})\n# after\nimport hashlib\nmemory.save_fact({\"id\": hashlib.sha256(b\"ext:user:42\").hexdigest()[:32], \"content\": \"...\"})","handlingStrategy":"validation","validationCode":"import re, hashlib\nSAFE_ID = re.compile(r\"^[A-Za-z0-9_-]+$\")\nfid = fact.get(\"id\")\nif fid is not None and not SAFE_ID.match(str(fid)):\n    fact[\"id\"] = hashlib.sha256(str(fid).encode()).hexdigest()[:32]","typeGuard":"import re\n\ndef is_safe_fact_id(fid: object) -> bool:\n    return isinstance(fid, str) and re.fullmatch(r\"[A-Za-z0-9_-]+\", fid) is not None","tryCatchPattern":null,"preventionTips":["Prefer omitting the id and using the backend-generated fact_<uuid4hex>.","Never embed URIs, colons, spaces, or slashes in fact ids.","Hash external keys to derive filesystem-safe ids."],"tags":["deermem","memory","validation","ids","path-safety"],"backgroundTag":null,"analyzedSha":"1dd6ba1acb03700589994b0366c5d1c7d05e2eff","analyzedAt":"2026-08-14T21:20:34.804Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}