# v10.31.0 Implementation Plan: P4 Harvest Evolution + Sync-in-Async Refactoring

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Two independent improvements for v10.31.0 — (1) Harvest evolves existing memories instead of creating duplicates (#641), (2) sqlite_vec.py stops blocking the event loop on DB calls (#637).

**Architecture:** Track A modifies the harvest orchestrator to check semantic similarity before storing, evolving via `update_memory_versioned()` when a match is found. Track B wraps all synchronous `self.conn.execute()` calls in `asyncio.to_thread()` inside the existing `_execute_with_retry` helper, avoiding a full aiosqlite migration. Tracks are independent and can be implemented in parallel (Track A touches `harvest/`, Track B touches `storage/`). Task C1 (integration verification) must run after both tracks complete.

**Tech Stack:** Python 3.11, pytest, asyncio, sqlite3, sentence-transformers embeddings

**Design Spec:** `docs/superpowers/specs/2026-03-30-memory-evolution-p4-harvest-evolution-design.md`
**Issues:** #641 (P4 Harvest), #637 (sync-in-async)

---

## File Structure

### Track A: P4 Harvest Evolution (#641)

| Action | File | Responsibility |
|--------|------|---------------|
| Modify | `src/mcp_memory_service/harvest/models.py` | Add `similarity_threshold` and `min_confidence_to_evolve` to `HarvestConfig` |
| Modify | `src/mcp_memory_service/harvest/harvester.py` | Evolve-or-create logic in `harvest_and_store()` |
| Modify | `tests/harvest/test_harvester.py` | 6 new tests for evolve-or-create behavior + update 2 existing tests |

### Track B: Sync-in-Async Refactoring (#637)

| Action | File | Responsibility |
|--------|------|---------------|
| Modify | `src/mcp_memory_service/storage/sqlite_vec.py:291-336` | `_execute_with_retry` wraps operation in `asyncio.to_thread()` |
| Create | `tests/storage/test_async_threading.py` | Verify event loop is not blocked during DB operations |

---

## Track A: P4 Harvest Evolution (#641)

### Task A1: Add evolution config fields to HarvestConfig

**Files:**
- Modify: `src/mcp_memory_service/harvest/models.py:33-42`

- [ ] **Step 1: Write the failing test**

Create test in `tests/harvest/test_harvester.py` that verifies config fields exist:

```python
# Add at the bottom of the file
class TestHarvestEvolutionConfig:
    def test_harvest_config_has_evolution_fields(self):
        """HarvestConfig should have similarity_threshold and min_confidence_to_evolve."""
        config = HarvestConfig()
        assert config.similarity_threshold == 0.85
        assert config.min_confidence_to_evolve == 0.3
```

- [ ] **Step 2: Run test to verify it fails**

Run: `.venv/bin/pytest tests/harvest/test_harvester.py::TestHarvestEvolutionConfig::test_harvest_config_has_evolution_fields -v`
Expected: FAIL with `AttributeError: 'HarvestConfig' object has no attribute 'similarity_threshold'`

- [ ] **Step 3: Add fields to HarvestConfig**

In `src/mcp_memory_service/harvest/models.py`, add two fields to the `HarvestConfig` dataclass:

```python
@dataclass
class HarvestConfig:
    """Configuration for harvest operation."""
    sessions: int = 1
    session_ids: Optional[List[str]] = None
    types: List[str] = field(default_factory=lambda: list(HARVEST_TYPES))
    min_confidence: float = 0.6
    dry_run: bool = True
    project_path: Optional[str] = None  # Override project dir
    use_llm: bool = False  # Phase 2: LLM-based classification
    # P4: Harvest evolution — evolve existing memories instead of duplicating
    similarity_threshold: float = 0.85  # Cosine similarity to trigger evolution
    min_confidence_to_evolve: float = 0.3  # Skip evolution for very stale memories
```

Then add `import os` at the top of `models.py` (needed for the factory function below).

Add a factory function below the `HarvestConfig` dataclass:

```python
def harvest_config_from_env(**overrides) -> HarvestConfig:
    """Create HarvestConfig with environment variable overrides."""
    defaults = {}
    threshold = os.environ.get("MCP_HARVEST_SIMILARITY_THRESHOLD")
    if threshold is not None:
        defaults["similarity_threshold"] = float(threshold)
    min_conf = os.environ.get("MCP_HARVEST_MIN_CONFIDENCE_TO_EVOLVE")
    if min_conf is not None:
        defaults["min_confidence_to_evolve"] = float(min_conf)
    defaults.update(overrides)
    return HarvestConfig(**defaults)
```

- [ ] **Step 4: Run test to verify it passes**

Run: `.venv/bin/pytest tests/harvest/test_harvester.py::TestHarvestEvolutionConfig -v`
Expected: PASS

- [ ] **Step 5: Commit**

```bash
git add src/mcp_memory_service/harvest/models.py tests/harvest/test_harvester.py
git commit -m "feat(harvest): add evolution config fields to HarvestConfig (P4)"
```

---

### Task A2: Test env-var factory function

**Files:**
- Test: `tests/harvest/test_harvester.py`

- [ ] **Step 1: Write the failing test**

```python
# Add to TestHarvestEvolutionConfig class
import os

class TestHarvestEvolutionConfig:
    # ... existing test ...

    def test_harvest_config_from_env(self, monkeypatch):
        """Environment variables should override defaults."""
        from mcp_memory_service.harvest.models import harvest_config_from_env
        monkeypatch.setenv("MCP_HARVEST_SIMILARITY_THRESHOLD", "0.90")
        monkeypatch.setenv("MCP_HARVEST_MIN_CONFIDENCE_TO_EVOLVE", "0.5")
        config = harvest_config_from_env()
        assert config.similarity_threshold == 0.90
        assert config.min_confidence_to_evolve == 0.5

    def test_harvest_config_from_env_with_overrides(self, monkeypatch):
        """Explicit overrides should beat env vars."""
        from mcp_memory_service.harvest.models import harvest_config_from_env
        monkeypatch.setenv("MCP_HARVEST_SIMILARITY_THRESHOLD", "0.90")
        config = harvest_config_from_env(similarity_threshold=0.75)
        assert config.similarity_threshold == 0.75
```

- [ ] **Step 2: Run tests to verify they pass**

Run: `.venv/bin/pytest tests/harvest/test_harvester.py::TestHarvestEvolutionConfig -v`
Expected: PASS (implementation already done in A1)

- [ ] **Step 3: Commit**

```bash
git add tests/harvest/test_harvester.py
git commit -m "test(harvest): add env-var config tests for P4 evolution"
```

---

### Task A3: Evolve-or-create logic — similar memory triggers versioned update

**Files:**
- Modify: `src/mcp_memory_service/harvest/harvester.py:44-74`
- Test: `tests/harvest/test_harvester.py`

- [ ] **Step 1: Write the failing test**

```python
# Add new test class to tests/harvest/test_harvester.py

class TestHarvestEvolution:
    """Tests for P4 evolve-instead-of-duplicate behavior."""

    @pytest.mark.asyncio
    async def test_evolve_similar_memory(self, sample_project_dir):
        """When a similar active memory exists (score > threshold), evolve it."""
        # Mock memory_service with storage that returns a similar match
        mock_service = AsyncMock()
        mock_query_result = MagicMock()
        mock_query_result.relevance_score = 0.92  # Above 0.85 threshold
        mock_query_result.memory = MagicMock()
        mock_query_result.memory.content_hash = "existing-hash-123"
        mock_service.storage = AsyncMock()
        mock_service.storage.retrieve = AsyncMock(return_value=[mock_query_result])
        mock_service.storage.update_memory_versioned = AsyncMock(
            return_value=(True, "Updated", "new-hash-456")
        )

        config = HarvestConfig(sessions=1, dry_run=False, similarity_threshold=0.85)
        harvester = SessionHarvester(
            project_dir=sample_project_dir, memory_service=mock_service
        )
        results = await harvester.harvest_and_store(config)
        result = results[0]

        if result.found > 0:
            # Should call retrieve to check similarity, NOT store_memory
            mock_service.storage.retrieve.assert_called()
            mock_service.storage.update_memory_versioned.assert_called()
            mock_service.store_memory.assert_not_called()
            assert result.stored == result.found
```

- [ ] **Step 2: Run test to verify it fails**

Run: `.venv/bin/pytest tests/harvest/test_harvester.py::TestHarvestEvolution::test_evolve_similar_memory -v`
Expected: FAIL — `store_memory` is called instead of `update_memory_versioned`

- [ ] **Step 3: Update existing tests to handle _try_evolve's storage access**

After this change, `_try_evolve` will access `self.memory_service.storage.retrieve(...)`. Existing tests use `AsyncMock()` which auto-creates `.storage` as a `MagicMock` — this would cause unpredictable behavior. Fix by explicitly setting `mock_service.storage.retrieve` to return an empty list (no similar memories → falls through to `store_memory` as before).

In `tests/harvest/test_harvester.py`, update `test_harvest_store` (line 44):

```python
    @pytest.mark.asyncio
    async def test_harvest_store(self, sample_project_dir):
        """Non-dry-run should call memory_service.store_memory."""
        mock_service = AsyncMock()
        mock_service.storage = AsyncMock()
        mock_service.storage.retrieve = AsyncMock(return_value=[])  # No similar → store new
        mock_result = MagicMock()
        mock_result.success = True
        mock_service.store_memory.return_value = mock_result
        config = HarvestConfig(sessions=1, dry_run=False)
        harvester = SessionHarvester(project_dir=sample_project_dir, memory_service=mock_service)
        results = await harvester.harvest_and_store(config)
        result = results[0]
        if result.found > 0:
            assert mock_service.store_memory.call_count == result.found
            assert result.stored == result.found
```

Update `test_harvest_store_partial_failure` (line 58) similarly — add these two lines after `mock_service = AsyncMock()`:

```python
        mock_service.storage = AsyncMock()
        mock_service.storage.retrieve = AsyncMock(return_value=[])  # No similar → store new
```

- [ ] **Step 4: Implement evolve-or-create logic in harvest_and_store**

Replace the storage loop in `src/mcp_memory_service/harvest/harvester.py` (lines 54-71):

```python
    async def harvest_and_store(self, config: HarvestConfig) -> List[HarvestResult]:
        """Parse, extract, and store candidates via MemoryService.

        P4 Evolution: Before storing, checks for semantically similar active
        memories. If found above similarity_threshold, evolves via versioned
        update instead of creating a duplicate.
        """
        session_files = self._resolve_sessions(config)
        if not session_files:
            return []

        results = []
        for filepath in session_files:
            result = self._harvest_file(filepath, config)

            if not config.dry_run and self.memory_service and result.candidates:
                stored = 0
                for candidate in result.candidates:
                    try:
                        tags = ["session-harvest"] + candidate.tags
                        evolved = await self._try_evolve(candidate, config)
                        if evolved:
                            stored += 1
                        else:
                            resp = await self.memory_service.store_memory(
                                content=candidate.content,
                                tags=tags,
                                memory_type=candidate.memory_type,
                                metadata={
                                    "confidence": candidate.confidence,
                                    "source": "harvest",
                                },
                            )
                            if isinstance(resp, dict) and resp.get("success"):
                                stored += 1
                            elif hasattr(resp, "success") and resp.success:
                                stored += 1
                    except Exception as e:
                        logger.warning(f"Failed to store harvest candidate: {e}")
                result.stored = stored

            results.append(result)
        return results

    async def _try_evolve(self, candidate, config: "HarvestConfig") -> bool:
        """Check for similar active memory; if found, evolve it.

        Returns True if an existing memory was evolved, False if caller
        should fall back to store_memory().
        """
        if not hasattr(self.memory_service, "storage") or not self.memory_service.storage:
            return False

        try:
            similar = await self.memory_service.storage.retrieve(
                candidate.content,
                n_results=1,
                min_confidence=config.min_confidence_to_evolve,
            )
        except Exception as e:
            logger.debug(f"Similarity check failed, falling back to store: {e}")
            return False

        if not similar or similar[0].relevance_score <= config.similarity_threshold:
            return False

        existing_hash = similar[0].memory.content_hash
        try:
            from datetime import datetime

            ok, msg, new_hash = await self.memory_service.storage.update_memory_versioned(
                existing_hash,
                candidate.content,
                new_tags=["session-harvest"] + candidate.tags,
                new_memory_type=candidate.memory_type,
                reason=f"Session harvest: {datetime.now().isoformat()}",
            )
            if ok:
                logger.info(f"Evolved memory {existing_hash[:8]}→{new_hash[:8] if new_hash else '?'}")
                return True
            else:
                logger.debug(f"Evolution failed ({msg}), falling back to store")
                return False
        except Exception as e:
            logger.debug(f"Evolution error, falling back to store: {e}")
            return False
```

Add `from datetime import datetime` to the imports at the top of `harvester.py` and remove the `from datetime import datetime` local import inside `_try_evolve`.

- [ ] **Step 5: Run test to verify it passes**

Run: `.venv/bin/pytest tests/harvest/test_harvester.py::TestHarvestEvolution::test_evolve_similar_memory -v`
Expected: PASS

- [ ] **Step 6: Verify existing tests still pass**

Run: `.venv/bin/pytest tests/harvest/test_harvester.py::TestSessionHarvester -v`
Expected: ALL PASS (existing tests unbroken)

- [ ] **Step 7: Commit**

```bash
git add src/mcp_memory_service/harvest/harvester.py tests/harvest/test_harvester.py
git commit -m "feat(harvest): evolve similar memories instead of duplicating (P4 #641)"
```

---

### Task A4: Novel content falls through to store_memory

**Files:**
- Test: `tests/harvest/test_harvester.py`

- [ ] **Step 1: Write the failing test**

```python
    @pytest.mark.asyncio
    async def test_store_novel_content(self, sample_project_dir):
        """When no similar memory exists, fall back to normal store_memory."""
        mock_service = AsyncMock()
        mock_service.storage = AsyncMock()
        mock_service.storage.retrieve = AsyncMock(return_value=[])  # No matches
        mock_result = MagicMock()
        mock_result.success = True
        mock_service.store_memory.return_value = mock_result

        config = HarvestConfig(sessions=1, dry_run=False)
        harvester = SessionHarvester(
            project_dir=sample_project_dir, memory_service=mock_service
        )
        results = await harvester.harvest_and_store(config)
        result = results[0]

        if result.found > 0:
            mock_service.store_memory.assert_called()
            mock_service.storage.update_memory_versioned.assert_not_called()
            assert result.stored == result.found
```

- [ ] **Step 2: Run test to verify it passes**

Run: `.venv/bin/pytest tests/harvest/test_harvester.py::TestHarvestEvolution::test_store_novel_content -v`
Expected: PASS (logic already implemented in A3)

- [ ] **Step 3: Commit**

```bash
git add tests/harvest/test_harvester.py
git commit -m "test(harvest): verify novel content creates new memory (P4)"
```

---

### Task A5: Stale memory below min_confidence skips evolution

**Files:**
- Test: `tests/harvest/test_harvester.py`

- [ ] **Step 1: Write the test**

```python
    @pytest.mark.asyncio
    async def test_skip_evolve_stale_memory(self, sample_project_dir):
        """Stale memory below min_confidence_to_evolve should not be evolved."""
        # retrieve returns a match, but it's below min_confidence threshold
        # (min_confidence is passed to retrieve, which filters at the storage layer)
        mock_service = AsyncMock()
        mock_service.storage = AsyncMock()
        # Simulate: retrieve with high min_confidence returns nothing
        mock_service.storage.retrieve = AsyncMock(return_value=[])
        mock_result = MagicMock()
        mock_result.success = True
        mock_service.store_memory.return_value = mock_result

        config = HarvestConfig(
            sessions=1, dry_run=False, min_confidence_to_evolve=0.8
        )
        harvester = SessionHarvester(
            project_dir=sample_project_dir, memory_service=mock_service
        )
        results = await harvester.harvest_and_store(config)
        result = results[0]

        if result.found > 0:
            # retrieve was called with high min_confidence, returned nothing
            call_args = mock_service.storage.retrieve.call_args
            assert call_args.kwargs.get("min_confidence") == 0.8
            # Fell through to store_memory
            mock_service.store_memory.assert_called()
```

- [ ] **Step 2: Run test to verify it passes**

Run: `.venv/bin/pytest tests/harvest/test_harvester.py::TestHarvestEvolution::test_skip_evolve_stale_memory -v`
Expected: PASS

- [ ] **Step 3: Commit**

```bash
git add tests/harvest/test_harvester.py
git commit -m "test(harvest): stale memories below threshold skip evolution (P4)"
```

---

### Task A6: Below-threshold similarity falls through to store

**Files:**
- Test: `tests/harvest/test_harvester.py`

- [ ] **Step 1: Write the test**

```python
    @pytest.mark.asyncio
    async def test_below_threshold_similarity_stores_new(self, sample_project_dir):
        """Match below similarity_threshold should create new memory, not evolve."""
        mock_service = AsyncMock()
        mock_query_result = MagicMock()
        mock_query_result.relevance_score = 0.60  # Below 0.85 threshold
        mock_query_result.memory = MagicMock()
        mock_query_result.memory.content_hash = "low-sim-hash"
        mock_service.storage = AsyncMock()
        mock_service.storage.retrieve = AsyncMock(return_value=[mock_query_result])
        mock_result = MagicMock()
        mock_result.success = True
        mock_service.store_memory.return_value = mock_result

        config = HarvestConfig(sessions=1, dry_run=False, similarity_threshold=0.85)
        harvester = SessionHarvester(
            project_dir=sample_project_dir, memory_service=mock_service
        )
        results = await harvester.harvest_and_store(config)
        result = results[0]

        if result.found > 0:
            mock_service.store_memory.assert_called()
            mock_service.storage.update_memory_versioned.assert_not_called()
```

- [ ] **Step 2: Run test to verify it passes**

Run: `.venv/bin/pytest tests/harvest/test_harvester.py::TestHarvestEvolution::test_below_threshold_similarity_stores_new -v`
Expected: PASS

- [ ] **Step 3: Commit**

```bash
git add tests/harvest/test_harvester.py
git commit -m "test(harvest): below-threshold similarity creates new memory (P4)"
```

---

### Task A7: Superseded memory is not evolved (spec bullet 4)

**Files:**
- Test: `tests/harvest/test_harvester.py`

**Context:** `retrieve()` in sqlite_vec.py already filters out superseded versions via `superseded_by IS NULL` (line 1592). This test documents that assumption: if `retrieve` returns nothing because the only match is superseded, harvest should create a new memory.

- [ ] **Step 1: Write the test**

```python
    @pytest.mark.asyncio
    async def test_superseded_memory_not_evolved(self, sample_project_dir):
        """Superseded memories should not be evolved — retrieve filters them out.

        The storage layer's retrieve() excludes superseded versions
        (WHERE superseded_by IS NULL). If the only similar memory is
        superseded, retrieve returns empty and we create a new memory.
        """
        mock_service = AsyncMock()
        mock_service.storage = AsyncMock()
        # retrieve returns empty because the only match is superseded (filtered by storage)
        mock_service.storage.retrieve = AsyncMock(return_value=[])
        mock_result = MagicMock()
        mock_result.success = True
        mock_service.store_memory.return_value = mock_result

        config = HarvestConfig(sessions=1, dry_run=False)
        harvester = SessionHarvester(
            project_dir=sample_project_dir, memory_service=mock_service
        )
        results = await harvester.harvest_and_store(config)
        result = results[0]

        if result.found > 0:
            # Falls through to store_memory since retrieve found no active match
            mock_service.store_memory.assert_called()
            mock_service.storage.update_memory_versioned.assert_not_called()
```

- [ ] **Step 2: Run test to verify it passes**

Run: `.venv/bin/pytest tests/harvest/test_harvester.py::TestHarvestEvolution::test_superseded_memory_not_evolved -v`
Expected: PASS

- [ ] **Step 3: Commit**

```bash
git add tests/harvest/test_harvester.py
git commit -m "test(harvest): superseded memories not evolved — documented assumption (P4)"
```

---

### Task A8: Graceful fallback when storage has no retrieve

**Files:**
- Test: `tests/harvest/test_harvester.py`

- [ ] **Step 1: Write the test**

```python
    @pytest.mark.asyncio
    async def test_fallback_when_no_storage(self, sample_project_dir):
        """If memory_service has no storage attr, fall back to store_memory."""
        mock_service = AsyncMock()
        # No .storage attribute — simulates pre-P4 MemoryService
        del mock_service.storage
        mock_result = MagicMock()
        mock_result.success = True
        mock_service.store_memory.return_value = mock_result

        config = HarvestConfig(sessions=1, dry_run=False)
        harvester = SessionHarvester(
            project_dir=sample_project_dir, memory_service=mock_service
        )
        results = await harvester.harvest_and_store(config)
        result = results[0]

        if result.found > 0:
            mock_service.store_memory.assert_called()
            assert result.stored == result.found
```

- [ ] **Step 2: Run test to verify it passes**

Run: `.venv/bin/pytest tests/harvest/test_harvester.py::TestHarvestEvolution::test_fallback_when_no_storage -v`
Expected: PASS

- [ ] **Step 3: Commit**

```bash
git add tests/harvest/test_harvester.py
git commit -m "test(harvest): graceful fallback when storage unavailable (P4)"
```

---

### Task A9: Run full harvest test suite

- [ ] **Step 1: Run all harvest tests**

Run: `.venv/bin/pytest tests/harvest/ -v`
Expected: ALL PASS

- [ ] **Step 2: Run full test suite to check for regressions**

Run: `.venv/bin/pytest --timeout=120 -x -q`
Expected: ALL PASS (1500+ tests)

---

## Track B: Sync-in-Async Refactoring (#637)

### Design Decision: `asyncio.to_thread()` vs `aiosqlite`

**Chosen approach: `asyncio.to_thread()` via `_execute_with_retry`**

Rationale:
- `aiosqlite` is already a dependency but migrating to it changes the connection model (`sqlite3.connect()` → `await aiosqlite.connect()`), affecting `__init__`, `initialize()`, and 119 call sites
- `asyncio.to_thread()` is a minimal change: modify `_execute_with_retry` to wrap the synchronous callable, then ensure all DB operations go through it
- Risk is lower: same sqlite3 connection, same API, just offloaded to a thread
- Performance overhead is ~50-100us per call — negligible for MCP use case
- A future PR can evaluate full aiosqlite migration if needed

**Scope constraint:** This task modifies `_execute_with_retry` only. The 119 direct `self.conn.execute()` calls that bypass `_execute_with_retry` are pre-existing technical debt outside this PR's scope. We add a TODO comment and open a follow-up issue for the full migration.

---

### Task B1: Test that _execute_with_retry doesn't block event loop

**Files:**
- Create: `tests/storage/test_async_threading.py`

- [ ] **Step 1: Write the failing test**

Create `tests/storage/test_async_threading.py`:

```python
"""Tests for async threading behavior in sqlite_vec storage."""

import asyncio
import time
import pytest
import sqlite3
from unittest.mock import MagicMock


class TestExecuteWithRetryThreading:
    """Verify _execute_with_retry offloads work to a thread."""

    @pytest.mark.asyncio
    async def test_execute_with_retry_does_not_block_loop(self, tmp_path):
        """DB operations inside _execute_with_retry should not block the event loop."""
        from mcp_memory_service.storage.sqlite_vec import SQLiteVecStorage

        storage = SQLiteVecStorage.__new__(SQLiteVecStorage)
        storage.conn = sqlite3.connect(str(tmp_path / "test.db"))
        storage.conn.execute("CREATE TABLE t (id INTEGER)")

        # A slow operation that takes 200ms
        def slow_insert():
            time.sleep(0.2)
            return storage.conn.execute("INSERT INTO t VALUES (1)")

        # Track whether the event loop stayed responsive
        loop_was_free = False

        async def check_loop():
            nonlocal loop_was_free
            await asyncio.sleep(0.05)  # Should complete while slow_insert runs
            loop_was_free = True

        # Run both concurrently — if _execute_with_retry blocks, check_loop won't run
        await asyncio.gather(
            storage._execute_with_retry(slow_insert),
            check_loop(),
        )
        assert loop_was_free, "Event loop was blocked during _execute_with_retry"
        storage.conn.close()

    @pytest.mark.asyncio
    async def test_execute_with_retry_returns_result(self, tmp_path):
        """_execute_with_retry should return the operation's result."""
        from mcp_memory_service.storage.sqlite_vec import SQLiteVecStorage

        storage = SQLiteVecStorage.__new__(SQLiteVecStorage)
        storage.conn = sqlite3.connect(str(tmp_path / "test.db"))
        storage.conn.execute("CREATE TABLE t (val TEXT)")
        storage.conn.execute("INSERT INTO t VALUES ('hello')")

        def query():
            return storage.conn.execute("SELECT val FROM t").fetchone()[0]

        result = await storage._execute_with_retry(query)
        assert result == "hello"
        storage.conn.close()

    @pytest.mark.asyncio
    async def test_execute_with_retry_retries_on_locked(self, tmp_path):
        """Should retry with backoff on OperationalError('database is locked')."""
        from mcp_memory_service.storage.sqlite_vec import SQLiteVecStorage

        storage = SQLiteVecStorage.__new__(SQLiteVecStorage)
        storage.conn = sqlite3.connect(str(tmp_path / "test.db"))

        call_count = 0

        def flaky_op():
            nonlocal call_count
            call_count += 1
            if call_count < 3:
                raise sqlite3.OperationalError("database is locked")
            return "success"

        result = await storage._execute_with_retry(flaky_op, max_retries=5, initial_delay=0.01)
        assert result == "success"
        assert call_count == 3
        storage.conn.close()
```

- [ ] **Step 2: Run test to verify the blocking test fails**

Run: `.venv/bin/pytest tests/storage/test_async_threading.py::TestExecuteWithRetryThreading::test_execute_with_retry_does_not_block_loop -v`
Expected: FAIL — `loop_was_free` is False because current `_execute_with_retry` calls `operation()` synchronously

- [ ] **Step 3: Implement asyncio.to_thread in _execute_with_retry**

In `src/mcp_memory_service/storage/sqlite_vec.py`, modify `_execute_with_retry` (lines 291-336). The **only change** is on line 311:

```diff
-                return operation()
+                return await asyncio.to_thread(operation)
```

The full method after the change (preserving all existing behavior — `continue`, `logger.error`, comments):

```python
    async def _execute_with_retry(self, operation: Callable, max_retries: int = 5, initial_delay: float = 0.2):
        """
        Execute a database operation with exponential backoff retry logic.

        The operation is offloaded to a thread via asyncio.to_thread() to
        avoid blocking the event loop during synchronous sqlite3 calls.

        Args:
            operation: The database operation to execute
            max_retries: Maximum number of retry attempts
            initial_delay: Initial delay in seconds before first retry

        Returns:
            The result of the operation

        Raises:
            The last exception if all retries fail
        """
        last_exception = None
        delay = initial_delay

        for attempt in range(max_retries + 1):
            try:
                return await asyncio.to_thread(operation)
            except sqlite3.OperationalError as e:
                last_exception = e
                error_msg = str(e).lower()

                # Check if error is related to database locking
                if "locked" in error_msg or "busy" in error_msg:
                    if attempt < max_retries:
                        # Add jitter to prevent thundering herd
                        jittered_delay = delay * (1 + random.uniform(-0.1, 0.1))
                        logger.warning(f"Database locked, retrying in {jittered_delay:.2f}s (attempt {attempt + 1}/{max_retries})")
                        await asyncio.sleep(jittered_delay)
                        # Exponential backoff
                        delay *= 2
                        continue
                    else:
                        logger.error(f"Database locked after {max_retries} retries")
                else:
                    # Non-retryable error
                    raise
            except Exception as e:
                # Non-SQLite errors are not retried
                raise

        # If we get here, all retries failed
        raise last_exception
```

- [ ] **Step 4: Run all three tests to verify they pass**

Run: `.venv/bin/pytest tests/storage/test_async_threading.py -v`
Expected: ALL 3 PASS

- [ ] **Step 5: Commit**

```bash
git add src/mcp_memory_service/storage/sqlite_vec.py tests/storage/test_async_threading.py
git commit -m "refactor(storage): wrap _execute_with_retry in asyncio.to_thread (#637)"
```

---

### Task B2: Add TODO and verify no regressions

**Files:**
- Modify: `src/mcp_memory_service/storage/sqlite_vec.py`

- [ ] **Step 1: Add TODO comment for remaining direct calls**

Near the top of `sqlite_vec.py`, after the class docstring, add:

```python
    # TODO(#637): ~119 direct self.conn.execute() calls bypass _execute_with_retry
    # and still block the event loop. A follow-up PR should either:
    # (a) route all DB calls through _execute_with_retry, or
    # (b) migrate to aiosqlite for native async sqlite access.
```

- [ ] **Step 2: Run full storage test suite**

Run: `.venv/bin/pytest tests/storage/ -v --timeout=120`
Expected: ALL PASS

- [ ] **Step 3: Run full test suite**

Run: `.venv/bin/pytest --timeout=120 -x -q`
Expected: ALL PASS (1500+ tests)

- [ ] **Step 4: Commit**

```bash
git add src/mcp_memory_service/storage/sqlite_vec.py
git commit -m "chore(storage): add TODO for remaining sync-in-async calls (#637)"
```

---

## Final: Integration Verification

### Task C1: Full test suite + pre-PR check

- [ ] **Step 1: Run full test suite**

Run: `.venv/bin/pytest --timeout=120 -q`
Expected: ALL PASS

- [ ] **Step 2: Run pre-PR validation**

Run: `bash scripts/pr/pre_pr_check.sh`
Expected: PASS

- [ ] **Step 3: Create feature branch and PR**

Use `github-release-manager` agent to handle version bump to v10.31.0, CHANGELOG update, and PR creation targeting main. PR should reference both #641 and #637.
