bytedance/deer-flow · error · ValueError
fact.id may contain only letters, numbers, '_' and '-'
Error message
fact.id may contain only letters, numbers, '_' and '-'
What it means
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.
Source
Thrown at backend/packages/harness/deerflow/agents/memory/backends/deermem/deermem/core/storage.py:191
def _normalize_fact(
fact: dict[str, Any],
*,
scope: dict[str, str | None],
existing: dict[str, Any] | None = None,
) -> dict[str, Any]:
"""Validate one fact and derive its per-item revision.
The shared JSON revision protects the multi-file transaction. The fact's
own revision protects one Markdown object when a disjoint transaction is
safely rebased after that shared revision changed.
"""
if not isinstance(fact, dict):
raise ValueError("fact must be an object")
normalized = copy.deepcopy(fact)
normalized["id"] = str(normalized.get("id") or f"fact_{uuid.uuid4().hex}")
# Validate the id through the canonical path builder's public contract.
if not normalized["id"] or any(character not in "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-" for character in normalized["id"]):
raise ValueError("fact.id may contain only letters, numbers, '_' and '-'")
normalized["schemaVersion"] = 2
if not isinstance(normalized.get("content"), str):
raise ValueError("fact.content must be a string")
normalized["content"] = normalized["content"].strip()
if not normalized["content"]:
raise ValueError("fact.content must not be empty")
_normalize_category(normalized)
confidence = normalized.get("confidence", 0.5)
if isinstance(confidence, bool) or not isinstance(confidence, (int, float)) or not 0 <= confidence <= 1:
raise ValueError("fact.confidence must be a number between 0 and 1")
normalized["confidence"] = float(confidence)
status = normalized.get("status", "active")
if status != "active":
raise ValueError("fact.status must be 'active'; deletion is physical")
normalized["status"] = "active"
normalized["scope"] = copy.deepcopy(scope)
_require_string_list(normalized, "topics")
_require_string_list(normalized, "consolidatedFrom")View on GitHub (pinned to 1dd6ba1acb)
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].
Example fix
# before
memory.save_fact({"id": "ext:user:42", "content": "..."})
# after
import hashlib
memory.save_fact({"id": hashlib.sha256(b"ext:user:42").hexdigest()[:32], "content": "..."}) Defensive patterns
Strategy: validation
Validate before calling
import re, hashlib
SAFE_ID = re.compile(r"^[A-Za-z0-9_-]+$")
fid = fact.get("id")
if fid is not None and not SAFE_ID.match(str(fid)):
fact["id"] = hashlib.sha256(str(fid).encode()).hexdigest()[:32] Type guard
import re
def is_safe_fact_id(fid: object) -> bool:
return isinstance(fid, str) and re.fullmatch(r"[A-Za-z0-9_-]+", fid) is not None Prevention
- 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.
When it happens
Trigger: 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.
Common situations: Using UUIDs with braces ('{0b6e...}'), prefixed ids from external systems ('usr_42#fact'), or copy-pasted ids containing whitespace.
Related errors
- retrieval fact.id must be a non-empty string
- retrieval fact.content must be a non-empty string
- unsupported FTS5 retrieval mode: {mode}
- retrieval category filter must be a string
- fact.category must be a string
AI-assisted analysis of bytedance/deer-flow@1dd6ba1acb (2026-08-14).
Data as JSON: /api/errors/a96b0334f1ad91e7.
Report an issue: GitHub.