bytedance/deer-flow · error · ValueError
cursor must be >= 0 and limit must be >= 1
Error message
cursor must be >= 0 and limit must be >= 1
What it means
list_facts() paginates an in-memory filtered list with Python slicing (matched[cursor:cursor+limit]); the guard rejects cursor < 0 or limit < 1 before doing anything. These are pure argument-contract errors, not data errors - the underlying facts are not inspected.
Source
Thrown at backend/packages/harness/deerflow/agents/memory/backends/deermem/deermem/core/storage.py:1220
with self._scope_lock(key), _process_file_lock(path.parent / ".memory.lock", float(getattr(self._config, "file_lock_timeout_seconds", 10))):
self._recover_if_needed(path)
if legacy_path.exists():
_, _, notifications = self._migrate_locked(path, agent_name, user_id=user_id, include_global=False)
fact, _ = self._read_fact(path, fact_id, user_id=user_id, agent_name=agent_name)
self._dispatch_retrieval_notifications(notifications, user_id=user_id, agent_name=agent_name)
return copy.deepcopy(fact)
def list_facts(
self,
*,
user_id: str | None = None,
agent_name: str | None = None,
filters: dict[str, Any] | None = None,
cursor: int = 0,
limit: int = 100,
) -> list[dict[str, Any]]:
if cursor < 0 or limit < 1:
raise ValueError("cursor must be >= 0 and limit must be >= 1")
facts = self.load(agent_name, user_id=user_id).get("facts", [])
filters = filters or {}
matched = [fact for fact in facts if all(key in fact and fact.get(key) == value for key, value in filters.items())]
return copy.deepcopy(matched[cursor : cursor + limit])
def apply_changes(
self,
change_set: dict[str, Any],
*,
user_id: str | None = None,
agent_name: str | None = None,
expected_manifest_revision: int | None = None,
allow_manifest_rebase: bool = False,
) -> dict[str, Any]:
"""Commit an incremental change set and return only the applied delta.
``complete`` is deliberately false: callers that require the historical
full document must explicitly call ``load``. This prevents a freshView on GitHub (pinned to 1dd6ba1acb)
Solutions
- Clamp inputs before calling: cursor = max(0, cursor); limit = max(1, min(limit, max_page_size)).
- Validate at the API boundary (FastAPI Query(ge=0) / Query(ge=1)) so bad values never reach storage.
- If you meant 'fetch everything', pass a large finite limit rather than 0.
Example fix
# before facts = storage.list_facts(cursor=offset - page_size, limit=0, agent_name=a) # after cursor = max(0, offset - page_size) facts = storage.list_facts(cursor=cursor, limit=max(1, page_size), agent_name=a)
Defensive patterns
Strategy: validation
Validate before calling
cursor = max(0, int(cursor or 0)) limit = max(1, min(int(limit or 100), 500)) facts = storage.list_facts(cursor=cursor, limit=limit, agent_name=agent_name)
Prevention
- Clamp pagination params at the API boundary (FastAPI Query(ge=0)/Query(ge=1)).
- Never use limit=0 to mean unlimited; pick a finite cap.
- Compute 'previous page' cursors with max(0, ...) to avoid underflow.
When it happens
Trigger: list_facts(cursor=-1), list_facts(limit=0), or values derived from request query params / arithmetic that can go negative (e.g. cursor = offset - page_size with offset < page_size).
Common situations: Exposing pagination params straight from an HTTP API without clamping; computing cursor from a 'previous page' calculation that underflows; passing limit=0 meaning 'no limit' (not supported here).
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/e88a17cf068ad1b3.
Report an issue: GitHub.