From 91f4fb457059ebed5e837824414e8c1ef3ad1df0 Mon Sep 17 00:00:00 2001 From: PeaceMaker-best Date: Tue, 10 Nov 2026 15:14:52 +0800 Subject: [PATCH] fix(memory): preserve retrieval indexes on operational startup failures (#8084) Signed-off-by: PeaceMaker-best <221849497+PeaceMaker-best@users.noreply.github.com> Co-authored-by: PeaceMaker-best <221849497+PeaceMaker-best@users.noreply.github.com> --- backend/README.md | 4 ++ .../harness/deerflow/agents/memory/AGENTS.md | 2 + .../deermem/deermem/core/retrieval.py | 5 ++- .../test_memory_index_startup_failures.py | 37 +++++++++++++++++++ 4 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 backend/tests/test_memory_index_startup_failures.py diff --git a/backend/README.md b/backend/README.md index 7f6697cdec..8cf8cf83d7 100644 --- a/backend/README.md +++ b/backend/README.md @@ -111,6 +111,10 @@ the same item or undo a pause/cancellation observed after their initial read. ### Memory System +If the local retrieval index is busy, read-only, or temporarily unavailable, +DeerMem falls back to substring search without deleting it. Automatic index +recreation is reserved for SQLite-confirmed corruption or a non-database file. + LLM-powered persistent context retention across conversations: - **Automatic extraction**: Analyzes conversations for user context, facts, and preferences diff --git a/backend/packages/harness/deerflow/agents/memory/AGENTS.md b/backend/packages/harness/deerflow/agents/memory/AGENTS.md index b59124832b..9179732eaf 100644 --- a/backend/packages/harness/deerflow/agents/memory/AGENTS.md +++ b/backend/packages/harness/deerflow/agents/memory/AGENTS.md @@ -264,6 +264,8 @@ multi-instance deployment leaves it inside `storage_path`. Chinese tokenization uses `jieba` only with the `memory-zh` extra. Malformed facts are logged and skipped during rebuild. A fatal rebuild failure keeps lazy retry active. +Index startup recreates files only for SQLite CORRUPT/NOTADB codes; operational +failures select substring fallback without deleting the existing index. Storage sends adapter updates after it releases durable locks. Adapter failures mark the scope dirty. diff --git a/backend/packages/harness/deerflow/agents/memory/backends/deermem/deermem/core/retrieval.py b/backend/packages/harness/deerflow/agents/memory/backends/deermem/deermem/core/retrieval.py index 4672bfd735..c5384d57a3 100644 --- a/backend/packages/harness/deerflow/agents/memory/backends/deermem/deermem/core/retrieval.py +++ b/backend/packages/harness/deerflow/agents/memory/backends/deermem/deermem/core/retrieval.py @@ -741,7 +741,10 @@ def create_fts5_retrieval(config: Any) -> FTS5RetrievalAdapter | None: try: return FTS5RetrievalAdapter(db_path) except sqlite3.DatabaseError as exc: - if db_path == ":memory:": + # Busy/locked, permissions, I/O and unavailable FTS5 are not evidence + # of corruption. Extended SQLite codes retain the primary low byte. + error_code = getattr(exc, "sqlite_errorcode", 0) & 0xFF + if db_path == ":memory:" or error_code not in {sqlite3.SQLITE_CORRUPT, sqlite3.SQLITE_NOTADB}: logger.warning("SQLite FTS5 is unavailable; DeerMem will use substring retrieval: %s", exc) return None diff --git a/backend/tests/test_memory_index_startup_failures.py b/backend/tests/test_memory_index_startup_failures.py new file mode 100644 index 0000000000..a69d4b41a2 --- /dev/null +++ b/backend/tests/test_memory_index_startup_failures.py @@ -0,0 +1,37 @@ +"""Operational startup failures must not be treated as index corruption.""" + +import sqlite3 + +import pytest + +from deerflow.agents.memory.backends.deermem.deermem.config import DeerMemConfig +from deerflow.agents.memory.backends.deermem.deermem.core import retrieval + + +@pytest.mark.parametrize("code", [sqlite3.SQLITE_BUSY, sqlite3.SQLITE_LOCKED, sqlite3.SQLITE_READONLY, sqlite3.SQLITE_IOERR, sqlite3.SQLITE_ERROR]) +def test_operational_startup_failure_preserves_existing_index(tmp_path, monkeypatch, code): + config = DeerMemConfig(storage_path=str(tmp_path)) + adapter = retrieval.create_fts5_retrieval(config) + adapter._engine.index_fact("existing", "remember deployment preference") + adapter.close() + index = tmp_path / ".retrieval" / "memory-fts5.sqlite3" + original = index.read_bytes() + attempts = [] + + def unavailable(self): + attempts.append(self) + error = sqlite3.OperationalError("temporary startup failure") + error.sqlite_errorcode = code + raise error + + with monkeypatch.context() as patched: + patched.setattr(retrieval.FTS5Retrieval, "_init_schema", unavailable) + assert retrieval.create_fts5_retrieval(config) is None + + assert index.read_bytes() == original + assert len(attempts) == 1 + recovered = retrieval.FTS5Retrieval(index) + try: + assert [fact["id"] for fact in recovered.search("deployment")] == ["existing"] + finally: + recovered.close()