bytedance/deer-flow · critical · OSError

Failed to create durable migration backup {backup_path}: {ex

Error message

Failed to create durable migration backup {backup_path}: {exc}

What it means

An OSError raised while creating or reading the durable .v1.bak migration backup is re-wrapped with context naming the backup path and the underlying error. It fires when reading source bytes or atomically writing the backup fails at the filesystem level. The migration is aborted before any canonical data is touched, so the source remains intact.

Source

Thrown at backend/packages/harness/deerflow/agents/memory/backends/deermem/deermem/core/storage.py:153

    except OSError:
        return None


def _ensure_migration_backup(source_path: Path) -> Path:
    """Durably preserve one immutable pre-migration JSON source beside it."""
    backup_path = source_path.with_name(f"{source_path.name}.v1.bak")
    try:
        source_bytes = source_path.read_bytes()
        if backup_path.exists():
            if backup_path.read_bytes() != source_bytes:
                raise MemoryStorageCorruption(f"Existing migration backup {backup_path} differs from source {source_path}; the original backup was kept and migration was stopped")
            return backup_path
        _atomic_write(backup_path, source_bytes)
        return backup_path
    except MemoryStorageCorruption:
        raise
    except OSError as exc:
        raise OSError(f"Failed to create durable migration backup {backup_path}: {exc}") from exc


def _normalize_category(fact: dict[str, Any]) -> None:
    raw_category = fact.get("category", "context")
    if not isinstance(raw_category, str):
        raise ValueError("fact.category must be a string")
    category = raw_category or "context"
    if category not in CORE_CATEGORIES:
        fact.setdefault("categoryExtension", category)
        fact["category"] = "other"


def _require_string_list(fact: dict[str, Any], field: str) -> None:
    value = fact.get(field, [])
    if not isinstance(value, list) or any(not isinstance(item, str) for item in value):
        raise ValueError(f"fact.{field} must be a list of strings")
    fact[field] = value

View on GitHub (pinned to 1dd6ba1acb)

Solutions

  1. Check the underlying OSError in the exception chain (errno): EACCES means fix ownership/permissions on the memory directory; ENOSPC means free space.
  2. Remove the file or directory blocking the .v1.bak path if one exists with the wrong type.
  3. Verify the memory directory is on a writable filesystem and fix the root cause before retrying; do not retry unchanged.
Defensive patterns

Strategy: retry

Validate before calling

import os

def memory_dir_writable(path: str) -> bool:
    return os.access(path, os.R_OK | os.W_OK)

Try / catch

import errno, time
for attempt in range(3):
    try:
        store.migrate()
        break
    except OSError as exc:
        if attempt == 2 or exc.errno not in (errno.EAGAIN, errno.EBUSY):
            raise  # permanent (EACCES/ENOSPC) - fix cause first
        time.sleep(0.5 * (attempt + 1))

Prevention

When it happens

Trigger: Read-only or full disk; permission/ownership mismatch on the memory directory (e.g. container user changed); path length limits; the .v1.bak name already existing as a directory; NAS/network filesystem returning I/O errors.

Common situations: Docker volume mounted read-only or owned by root while the app runs as another user; disk-full conditions; migrating a memory directory copied between hosts with different UID mappings.

Related errors


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