fix(memory): use ONNX embedder for wrap --memory sync (#1092) (#1262)

## Description

`headroom wrap --memory` could never import memories: the startup sync
subprocess (`python -m
headroom.memory.sync`) and the in-process Codex memory import both built
their backend with
`LocalBackendConfig(db_path=...)`, which defaults `embedder_backend` to
`"local"` —
sentence-transformers + PyTorch (~2 GB). On the proxy extras that
dependency is absent, so sync
crashed with `ImportError: sentence-transformers is required for
LocalEmbedder` while the proxy
itself served memory fine via the torch-free ONNX backend. This routes
both paths through a
shared `_build_sync_backend` helper that uses `embedder_backend="onnx"`,
matching the proxy MCP
server (`headroom/memory/mcp_server.py`).

Closes #1092

## Type of Change

- [x] Bug fix (non-breaking change that fixes an issue)

## Changes Made

- `headroom/memory/sync.py`: added `_build_sync_backend(db_path)` that
constructs the backend
  with `embedder_backend="onnx"`; the sync CLI subprocess now uses it.
- `headroom/cli/wrap.py`: the in-process Claude→DB memory import (Codex
wrap path) now uses the
  same helper instead of the LOCAL-defaulting `LocalBackendConfig`.
- `tests/test_memory_sync.py`: added
`test_sync_backend_uses_onnx_embedder` regression test.

## Testing

- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`)
- [x] New tests added for new functionality

### Test Output

```text
$ python -m pytest tests/test_memory_sync.py -q
31 passed

$ python -m ruff check headroom/memory/sync.py headroom/cli/wrap.py tests/test_memory_sync.py
All checks passed!
```

## Real Behavior Proof

- Environment: Windows 11, Python 3.13, headroom on branch
fix/1092-memory-sync-onnx-embedder
- Exact command / steps: Ran the memory-sync suite + ruff, and an import
smoke that builds the
sync backend: `python -c "from headroom.memory.sync import
_build_sync_backend;
  print(_build_sync_backend('x.db')._config.embedder_backend)"`.
- Observed result: 31 tests pass (incl. the new regression test), ruff
clean, and the smoke
prints `onnx` — the sync backend no longer defaults to the
sentence-transformers embedder.
- Not tested: Did not run a full live `headroom wrap claude --memory`
end to end (needs the
ONNX model download + Claude memory files); the same-model
(all-MiniLM-L6-v2, 384-dim) ONNX
backend the proxy already uses keeps vectors DB-compatible, so no
migration is involved.

## Review Readiness

- [x] I have performed a self-review
- [x] This PR is ready for human review

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Parideboy 2026-06-22 05:08:12 +02:00 committed by GitHub
parent b5f63d8fa9
commit 4f9fedaa7a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 30 additions and 8 deletions

View file

@ -3719,8 +3719,7 @@ def codex(
try:
import asyncio
from headroom.memory.backends.local import LocalBackend, LocalBackendConfig
from headroom.memory.sync import sync_import
from headroom.memory.sync import _build_sync_backend, sync_import
from headroom.memory.sync_adapters.claude_code import (
ClaudeCodeAdapter,
get_claude_memory_dir,
@ -3729,8 +3728,7 @@ def codex(
claude_memory_dir = get_claude_memory_dir()
async def _import_claude_memories() -> int:
config = LocalBackendConfig(db_path=db_path)
backend = LocalBackend(config)
backend = _build_sync_backend(db_path)
await backend._ensure_initialized()
adapter = ClaudeCodeAdapter(claude_memory_dir)
count = await sync_import(backend, adapter, mem_user)

View file

@ -343,6 +343,21 @@ async def sync_export(
# ---------------------------------------------------------------------------
def _build_sync_backend(db_path: str) -> Any:
"""Build the memory backend used by the sync subprocess.
Match the proxy MCP server (see ``headroom/memory/mcp_server.py``): use the
torch-free ONNX embedder so ``wrap --memory`` sync works on the proxy extras
without sentence-transformers/PyTorch (#1092). It loads the same
``all-MiniLM-L6-v2`` 384-dim model as the local embedder, so vectors stay
compatible with what the proxy writes no DB migration.
"""
from headroom.memory.backends.local import LocalBackend, LocalBackendConfig
config = LocalBackendConfig(db_path=db_path, embedder_backend="onnx")
return LocalBackend(config)
def main() -> None:
"""CLI entry point for running sync from a subprocess."""
import argparse
@ -358,10 +373,7 @@ def main() -> None:
import json as _json
async def _run() -> None:
from headroom.memory.backends.local import LocalBackend, LocalBackendConfig
config = LocalBackendConfig(db_path=args.db)
backend = LocalBackend(config)
backend = _build_sync_backend(args.db)
await backend._ensure_initialized()
if args.agent == "claude":

View file

@ -23,6 +23,7 @@ from typing import Any
import pytest
from headroom.memory.sync import (
_build_sync_backend,
sync,
sync_export,
sync_import,
@ -675,3 +676,14 @@ class TestCrossAgentInterop:
# AGENTS.md has both (from DB)
agents_content = agents_md.read_text()
assert "FastAPI" in agents_content or "8787" in agents_content
def test_sync_backend_uses_onnx_embedder(tmp_path):
"""#1092: the sync subprocess must pick the torch-free ONNX embedder.
Defaulting to the LOCAL (sentence-transformers) embedder makes
`wrap --memory` crash with an ImportError on the proxy extras. The backend
must match the proxy MCP server, which uses ONNX.
"""
backend = _build_sync_backend(str(tmp_path / "memory.db"))
assert backend._config.embedder_backend == "onnx"