bytedance/deer-flow · error · ValueError
retrieval scope userId must be a string or null
Error message
retrieval scope userId must be a string or null
What it means
ValueError from _scope_key in deermem's FTS5 retrieval layer: scope dict's 'userId' is present but not a str and not None (e.g. an int). Scope values are JSON-encoded into composite keys so None cannot collide with a real id; non-string ids would break that encoding, so they are rejected up front.
Source
Thrown at backend/packages/harness/deerflow/agents/memory/backends/deermem/deermem/core/retrieval.py:547
"jieba": _jieba_available,
"db_path": self._db_path,
}
def close(self) -> None:
with self._lock:
self._conn.close()
def _scope_value(value: str | None) -> str:
"""Encode a typed scope value so ``None`` cannot collide with a user id."""
return json.dumps(value, ensure_ascii=False, separators=(",", ":"))
def _scope_key(scope: dict[str, str | None]) -> tuple[str, str]:
user_id = scope.get("userId")
agent_name = scope.get("agentName")
if user_id is not None and not isinstance(user_id, str):
raise ValueError("retrieval scope userId must be a string or null")
if agent_name is not None and not isinstance(agent_name, str):
raise ValueError("retrieval scope agentName must be a string or null")
return _scope_value(user_id), _scope_value(agent_name)
class FTS5RetrievalAdapter:
"""Scope-aware ``RetrievalPort`` adapter backed by one SQLite FTS5 DB.
The index is derived data. Canonical facts remain in Markdown and storage
notifications update only the addressed row. A deterministic composite
document id prevents equal fact ids in two user/agent scopes from
overwriting each other.
"""
def __init__(self, db_path: str | Path = ":memory:") -> None:
self._engine = FTS5Retrieval(db_path)
@staticmethodView on GitHub (pinned to 1dd6ba1acb)
Solutions
- Convert ids to str when building the scope: {"userId": str(user_id)}.
- Normalize at the boundary: one helper that renders scope dicts with stringified ids and use it everywhere.
- Keep the storage/retrieval layer's canonical user id format consistent (always str) end-to-end.
Example fix
# before
scopes = [{"userId": user.id, "agentName": None}] # int id
# after
scopes = [{"userId": str(user.id), "agentName": None}] Defensive patterns
Strategy: type-guard
Validate before calling
def norm_scope(scope: dict) -> dict:
uid, agent = scope.get('userId'), scope.get('agentName')
if uid is not None and not isinstance(uid, str): uid = str(uid)
if agent is not None and not isinstance(agent, str): agent = str(agent)
return {'userId': uid, 'agentName': agent} Type guard
def is_valid_scope(scope) -> bool:
return (
isinstance(scope, dict)
and (scope.get('userId') is None or isinstance(scope['userId'], str))
and (scope.get('agentName') is None or isinstance(scope['agentName'], str))
) Prevention
- Stringify user ids once at the system boundary (auth layer), keep them str everywhere downstream.
- Build scopes through one helper; never hand-assemble dicts at call sites.
- Assert scope shape in tests for every scope producer.
When it happens
Trigger: Passing scopes=[{"userId": 12345, ...}] to FTS5RetrievalAdapter search/upsert paths — any retrieval call whose scope userId is an int, bool, or other non-string type.
Common situations: User ids stored as integers upstream (numeric PKs) and passed through unconverted; a dataclass/Pydantic model with int user id serialized into the scope dict; tests using fixture ints as user ids.
Related errors
- retrieval scope agentName must be a string or null
- retrieval fact.id must be a non-empty string
- retrieval fact.content must be a non-empty string
- guaranteed_categories must be an iterable of strings, not a
- unsupported FTS5 retrieval mode: {mode}
AI-assisted analysis of bytedance/deer-flow@1dd6ba1acb (2026-08-14).
Data as JSON: /api/errors/aa9b798d19c94006.
Report an issue: GitHub.