Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 17 additions & 9 deletions src/kiro_crew/memory.py
Original file line number Diff line number Diff line change
Expand Up @@ -1193,15 +1193,23 @@ def _try_create_db(self) -> sqlite3.Connection:
# the same once-per-store latch as the restrict itself.
prepare_owner_only_sqlite(self._index_db)
conn = sqlite3.connect(str(self._index_db), timeout=_DB_BUSY_TIMEOUT_SECS)
# Wait out transient 'database is locked' contention instead of letting
# it surface (where the self-heal would misread it as corruption).
conn.execute(f"PRAGMA busy_timeout={int(_DB_BUSY_TIMEOUT_SECS * 1000)}")
conn.execute(
"CREATE VIRTUAL TABLE IF NOT EXISTS memory_fts USING fts5("
"path, content, tokenize='porter unicode61')"
)
if restrict: # and cover whatever SQLite just created; retry next open on failure
self._index_owner_only = self._restrict_index_files()
try:
# Wait out transient 'database is locked' contention instead of letting
# it surface (where the self-heal would misread it as corruption).
conn.execute(f"PRAGMA busy_timeout={int(_DB_BUSY_TIMEOUT_SECS * 1000)}")
conn.execute(
"CREATE VIRTUAL TABLE IF NOT EXISTS memory_fts USING fts5("
"path, content, tokenize='porter unicode61')"
)
if restrict: # and cover whatever SQLite just created; retry next open on failure
self._index_owner_only = self._restrict_index_files()
except BaseException:
# The connection is never handed out, but the raised error's traceback
# keeps it open while _get_db's self-heal deletes the index -- and on
# Windows an open handle makes that delete fail, so the index is never
# rebuilt.
conn.close()
raise
return conn

def _restrict_index_files(self) -> bool:
Expand Down
32 changes: 32 additions & 0 deletions test/test_memory_selfheal.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
import threading
from datetime import date, datetime, timedelta

import pytest

from kiro_crew import memory as memory_module
from kiro_crew._sqlite_compat import sqlite3
from kiro_crew.memory import MemoryStore, _is_corruption_error
Expand Down Expand Up @@ -117,6 +119,36 @@ def test_genuine_corruption_still_self_heals(self, tmp_path):
results = store.search("Python")
assert len(results) >= 1

def test_self_heal_closes_the_connection_that_found_the_corruption(self, tmp_path, monkeypatch):
"""The open that met the corruption is closed before the index is deleted.

Windows refuses to delete a file that has an open handle, so a connection
left to the collector keeps the corrupt index in place and every later
open meets it again.
"""
store = MemoryStore(workspace=tmp_path)
store.init()
(tmp_path / "memory_index.db").write_bytes(b"this is not a valid sqlite database")

opened: list[sqlite3.Connection] = []
real_connect = memory_module.sqlite3.connect

def recording_connect(*args, **kwargs):
conn = real_connect(*args, **kwargs)
opened.append(conn)
return conn

monkeypatch.setattr(memory_module.sqlite3, "connect", recording_connect)
conn = store._get_db()
try:
assert len(opened) >= 2, "the corrupt index was never opened and healed"
assert opened[-1] is conn
for stale in opened[:-1]:
with pytest.raises(sqlite3.ProgrammingError, match="closed"):
stale.execute("SELECT 1")
finally:
conn.close()

def test_connection_has_busy_timeout(self, tmp_path):
"""The connection must carry a non-zero busy_timeout to wait out locks."""
store = MemoryStore(workspace=tmp_path)
Expand Down
1 change: 0 additions & 1 deletion test/windows-expected-failures.txt
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,6 @@ test/test_mcp_discovery_probe_stdio.py::TestProbeServerStderrCapture::test_stder
test/test_mcp_discovery_probe_stdio.py::TestProbeServerStderrCapture::test_successful_probe_does_not_mention_stderr
# Needs symlink-creation privilege: builds an in-vault FILE symlink
# (Alias.md -> One.md); a junction is directory-only and cannot model it.
test/test_memory_selfheal.py::TestFtsSelfHealGating::test_genuine_corruption_still_self_heals
test/test_pip_deps_consistency.py::test_all_unguarded_third_party_imports_are_declared
# Needs symlink-creation privilege: plants a FILE symlink at the output path to
# prove the writer replaces it rather than writing through. atomic_write already
Expand Down
Loading