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)

    @staticmethod

View on GitHub (pinned to 1dd6ba1acb)

Solutions

  1. Convert ids to str when building the scope: {"userId": str(user_id)}.
  2. Normalize at the boundary: one helper that renders scope dicts with stringified ids and use it everywhere.
  3. 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

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


AI-assisted analysis of bytedance/deer-flow@1dd6ba1acb (2026-08-14). Data as JSON: /api/errors/aa9b798d19c94006. Report an issue: GitHub.