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
- Check the underlying OSError in the exception chain (errno): EACCES means fix ownership/permissions on the memory directory; ENOSPC means free space.
- Remove the file or directory blocking the .v1.bak path if one exists with the wrong type.
- 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
- Verify write permissions and free disk space on the memory volume before upgrading.
- Ensure the process user owns the memory directory (especially in containers).
- Keep the .v1.bak pathname free of unrelated files or directories.
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
- Legacy {section} summary migration conflict in {legacy_path}
- Existing migration backup {backup_path} differs from source
- Failed to clear memory data.
- Failed to create memory fact.
- Failed to delete memory fact.
AI-assisted analysis of bytedance/deer-flow@1dd6ba1acb (2026-08-14).
Data as JSON: /api/errors/0810fc46b671865f.
Report an issue: GitHub.