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>
This commit is contained in:
PeaceMaker-best 2026-11-10 15:14:52 +08:00 committed by GitHub
parent 226d1bc5ce
commit 91f4fb4570
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 47 additions and 1 deletions

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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()