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 fresh

View on GitHub (pinned to 1dd6ba1acb)

Solutions

  1. Clamp inputs before calling: cursor = max(0, cursor); limit = max(1, min(limit, max_page_size)).
  2. Validate at the API boundary (FastAPI Query(ge=0) / Query(ge=1)) so bad values never reach storage.
  3. 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

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


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