bytedance/deer-flow · error · MemoryManagerError

OpenViking capture cursor is unreadable; refusing unsafe rep

Error message

OpenViking capture cursor is unreadable; refusing unsafe replay (session={session_id})

What it means

_load_cursor() raises MemoryManagerError when the per-session capture cursor file ({storage_path}/openviking/sessions/{session_id}.json) exists but cannot be read or parsed (OSError from reading, ValueError from json.loads). The cursor tracks the full-transcript suffix position and seen message IDs; rather than risk replaying already-captured messages into OpenViking (duplicates), the backend refuses to continue for that session.

Source

Thrown at backend/packages/harness/deerflow/agents/memory/backends/openviking/openviking_manager.py:507

    def _close_resources(self) -> None:
        with self._resource_lock:
            if self._resources_closed:
                return
            self._recorder.close()
            self._resources_closed = True

    def _state_path(self, session_id: str) -> Path:
        root = Path(self._config.storage_path or ".") / "openviking" / "sessions"
        return root / f"{session_id}.json"

    def _load_cursor(self, session_id: str) -> dict[str, Any]:
        path = self._state_path(session_id)
        try:
            value = json.loads(path.read_text(encoding="utf-8"))
        except FileNotFoundError:
            return {}
        except (OSError, ValueError) as exc:
            raise MemoryManagerError(f"OpenViking capture cursor is unreadable; refusing unsafe replay (session={session_id})") from exc
        if not isinstance(value, dict):
            raise MemoryManagerError(f"OpenViking capture cursor is invalid; refusing unsafe replay (session={session_id})")
        return value

    def _save_cursor(self, session_id: str, state: dict[str, Any]) -> None:
        path = self._state_path(session_id)
        path.parent.mkdir(parents=True, exist_ok=True)
        temp_path = path.with_suffix(f".{os.getpid()}.{threading.get_ident()}.tmp")
        try:
            temp_path.write_text(
                json.dumps(state, ensure_ascii=False, indent=2),
                encoding="utf-8",
            )
            os.replace(temp_path, path)
        finally:
            try:
                temp_path.unlink(missing_ok=True)
            except OSError:

View on GitHub (pinned to 1dd6ba1acb)

Solutions

  1. Inspect {storage_path}/openviking/sessions/{session_id}.json — if it is truncated/garbage, delete it: an absent file is treated as a fresh cursor (FileNotFoundError returns {}), trading a one-time re-capture risk for recovery
  2. Fix filesystem permissions on storage_path so the Gateway process can read/write the sessions directory
  3. If write_failure_policy is 'raise', expect this to surface as MemoryManagerError on the write; with 'log_and_drop' (default) it is logged and the turn continues

Example fix

# before: corrupted cursor blocks the session
# rm .deer-flow/openviking/sessions/<session_id>.json

# after (recovery)
import pathlib
pathlib.Path(storage_path, 'openviking', 'sessions', f'{session_id}.json').unlink(missing_ok=True)  # next write starts a fresh cursor
Defensive patterns

Strategy: try-catch

Validate before calling

import json, pathlib
def cursor_readable(session_id: str, storage_path: str) -> bool:
    p = pathlib.Path(storage_path or ".", "openviking", "sessions", f"{session_id}.json")
    if not p.exists():
        return True
    try:
        json.loads(p.read_text(encoding="utf-8"))
        return True
    except (OSError, ValueError):
        return False

Try / catch

from deerflow.agents.memory.manager import MemoryManagerError
try:
    manager.add(thread_id, messages, user_id=user)
except MemoryManagerError as exc:
    if "cursor is unreadable" in str(exc):
        recover_cursor(session_id)  # delete the corrupt file to reset the cursor
    else:
        raise

Prevention

When it happens

Trigger: The cursor JSON file is corrupted (truncated by a crash mid-write — although _save_cursor uses a temp file + os.replace), has wrong permissions, sits on an unreadable mount, or contains invalid JSON. Raises on the next write for that session, after _load_cursor is called inside the session lock.

Common situations: Disk-full or hard-kill events corrupting files despite atomic replace; operators hand-editing the sessions file; storage_path on NFS with permission changes between runs.

Related errors


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