bytedance/deer-flow · error · OSError

Failed to save memory data after creating fact

Error message

Failed to save memory data after creating fact

What it means

create_memory_fact retries the whole read-check-write cycle 3 times when _save_memory_to_file loses a revision race. If every attempt fails (or the storage write fails for non-race reasons), it gives up with this OSError. The apply_changes backends handle conflicts internally, so this is the legacy/conditional-save path's exhaustion signal.

Source

Thrown at backend/packages/harness/deerflow/agents/memory/backends/deermem/deermem/core/updater.py:992

            raise AssertionError("bounded create retry did not return or raise")
        # Legacy single-file path: same duplicate-rejection contract as the
        # apply_changes path above. A revision-conflicted save (False) reloads
        # the fresh snapshot and re-runs the duplicate check, so a concurrent
        # creator's commit is rejected with ValueError("Duplicate fact")
        # instead of surfacing as a generic save failure.
        for attempt in range(3):
            memory_data = self.get_memory_data(agent_name, user_id=user_id) if attempt == 0 else self.reload_memory_data(agent_name, user_id=user_id)
            _raise_if_duplicate_fact_content(memory_data, candidate_key)
            updated_memory = dict(memory_data)
            updated_memory["facts"] = _trim_facts_to_max([*memory_data.get("facts", []), copy.deepcopy(candidate)], self._config.max_facts)
            if self._save_memory_to_file(updated_memory, agent_name, user_id=user_id, expected_revision=int(memory_data.get("revision") or 0)):
                # If the cap evicted the just-added (lower-confidence) fact,
                # signal via None so callers don't report a dangling id as
                # "added".
                stored = any(f.get("id") == fact_id for f in updated_memory["facts"])
                return updated_memory, (fact_id if stored else None)
            logger.info("Retrying capped fact creation from a fresh snapshot after a revision conflict")
        raise OSError("Failed to save memory data after creating fact")

    def delete_memory_fact(self, fact_id: str, agent_name: str | None = None, *, user_id: str | None = None) -> dict[str, Any]:
        """Delete a fact by its id and persist the updated memory data."""
        if agent_name is None:
            raise ValueError("agent_name")
        if getattr(type(self._storage), "apply_changes", None) is not MemoryStorage.apply_changes and hasattr(self._storage, "get_fact"):
            deleted = self._storage.get_fact(fact_id, agent_name=agent_name, user_id=user_id)
            if deleted is None:
                raise KeyError(fact_id)
            global_memory = self.get_memory_data(user_id=user_id)
            self._storage.apply_changes(
                {"deletes": [fact_id], "deleteRevisions": {fact_id: int(deleted.get("revision") or 1)}},
                agent_name=agent_name,
                user_id=user_id,
                expected_manifest_revision=int(global_memory.get("revision") or 0),
                allow_manifest_rebase=True,
            )
            return self.get_memory_data(agent_name, user_id=user_id)

View on GitHub (pinned to 1dd6ba1acb)

Solutions

  1. Reduce concurrency: serialize fact creation per agent (lock or queue) so revision races cannot repeat
  2. Retry the create call after a short backoff — a fresh snapshot usually succeeds once the concurrent writer finishes
  3. Verify the memory directory is writable and has disk space (persistent failure also exhausts retries)
  4. Use a storage backend implementing apply_changes, which resolves conflicts server-side
Defensive patterns

Strategy: retry

Try / catch

for attempt in range(3):
    try:
        _, fact_id = memory.create_memory_fact(content, agent_name=agent)
        break
    except OSError:
        if attempt == 2:
            raise
        time.sleep(0.5 * (attempt + 1))

Prevention

When it happens

Trigger: Sustained concurrent writes to the same agent's memory file (e.g. two threads creating facts in a tight loop) that win the revision race 3 times in a row, or a persistent I/O failure (read-only file, full disk) making every save attempt fail.

Common situations: Parallel test suites hammering the same memory file, a background memory-update loop plus user-issued memory_add calls, or a broken storage mount that fails every write.

Related errors


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