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
2 changes: 0 additions & 2 deletions agent/embeddings/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from .cache import (
DEFAULT_CACHE_DIR,
DEFAULT_CACHE_FILENAME,
LEGACY_CACHE_FILENAME,
EmbeddingCache,
default_embedding_cache_path,
)
Expand All @@ -27,7 +26,6 @@
__all__ = [
"DEFAULT_CACHE_DIR",
"DEFAULT_CACHE_FILENAME",
"LEGACY_CACHE_FILENAME",
"DEFAULT_CHUNK_CHAR_LIMIT",
"EmbeddingCache",
"EmbeddingAdapter",
Expand Down
56 changes: 3 additions & 53 deletions agent/embeddings/cache.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import json
import os
from dataclasses import dataclass, field
from pathlib import Path
Expand All @@ -11,7 +10,6 @@

DEFAULT_CACHE_DIR = ".code-orbit"
DEFAULT_CACHE_FILENAME = "embeddings_cache.npz"
LEGACY_CACHE_FILENAME = "embeddings_cache.json"
_CACHE_FORMAT_VERSION = 2


Expand All @@ -25,19 +23,12 @@ class EmbeddingCache:
@classmethod
def load(cls, path: Path) -> EmbeddingCache:
if not path.exists():
legacy_path = _legacy_cache_sibling_path(path)
if legacy_path.exists():
cache = _load_legacy_json_cache(legacy_path)
if cache is not None:
return cache
return cls()

if _looks_like_npz(path):
cache = _load_npz_cache(path)
if cache is not None:
return cache
if not _looks_like_npz(path):
return cls()

cache = _load_legacy_json_cache(path)
cache = _load_npz_cache(path)
if cache is not None:
return cache

Expand Down Expand Up @@ -72,15 +63,6 @@ def default_embedding_cache_path(root: str | Path) -> Path:
return root_path / DEFAULT_CACHE_DIR / DEFAULT_CACHE_FILENAME


def _legacy_embedding_cache_path(root: str | Path) -> Path:
root_path = Path(root).resolve()
return root_path / DEFAULT_CACHE_DIR / LEGACY_CACHE_FILENAME


def _legacy_cache_sibling_path(path: Path) -> Path:
return path.parent / LEGACY_CACHE_FILENAME


def _looks_like_npz(path: Path) -> bool:
try:
with path.open("rb") as file_handle:
Expand Down Expand Up @@ -148,38 +130,6 @@ def _serialize_cache_arrays(
return metadata, vectors_array


def _load_legacy_json_cache(path: Path) -> EmbeddingCache | None:
try:
raw = json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
return None

files: dict[str, FileEmbeddingRecord] = {}
for rel_path, payload in raw.get("files", {}).items():
chunks = tuple(
ChunkEmbedding(
index=int(chunk["index"]),
vector=tuple(float(value) for value in chunk["vector"]),
start_line=int(chunk["start_line"]),
end_line=int(chunk["end_line"]),
content_hash=str(chunk["content_hash"]),
)
for chunk in payload.get("chunks", [])
)
files[rel_path] = FileEmbeddingRecord(
path=rel_path,
sha256=str(payload.get("sha256", "")),
chunks=chunks,
)

return EmbeddingCache(
version=int(raw.get("version", _CACHE_FORMAT_VERSION)),
model=str(raw.get("model", "")),
api_base=str(raw.get("api_base", "")),
files=files,
)


def _load_npz_cache(path: Path) -> EmbeddingCache | None:
try:
with np.load(path, allow_pickle=False) as archive:
Expand Down
15 changes: 3 additions & 12 deletions agent/embeddings/index.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,7 @@
from typing import Protocol, runtime_checkable

from ..config import Config
from .cache import (
EmbeddingCache,
_legacy_embedding_cache_path,
default_embedding_cache_path,
)
from .cache import EmbeddingCache, default_embedding_cache_path
from .adapters import EmbeddingAdapter
from .chunking import chunk_file
from .factory import create_embedding_adapter
Expand Down Expand Up @@ -109,13 +105,8 @@ async def build_embedding_index(
"""
root_path = Path(root).resolve()
cache_file = cache_path or default_embedding_cache_path(root_path)
cache_load_path = cache_file
if cache_path is None and not cache_file.exists():
legacy_cache_file = _legacy_embedding_cache_path(root_path)
if legacy_cache_file.exists():
cache_load_path = legacy_cache_file

cache = EmbeddingCache.load(cache_load_path)
cache = EmbeddingCache.load(cache_file)
if not cache.is_compatible(config):
cache = EmbeddingCache(
model=config.embedding_model,
Expand Down Expand Up @@ -240,7 +231,7 @@ async def guarded_embed_batch(

# Dirty-check: skip the full .npz rewrite when nothing changed and a
# cache file already exists. A missing file is always (re)written so
# fresh checkouts and legacy-JSON migrations materialize it once.
# fresh checkouts materialize it once.
if updated_files or stale_paths or not cache_file.exists():
cache.save(cache_file)
vector_store = VectorStore(cache.files.values())
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "code-orbit"
version = "1.8.8"
version = "1.8.10"
description = "AI-powered codebase editor using local llama.cpp models"
readme = "README.md"
requires-python = ">=3.14"
Expand Down
123 changes: 14 additions & 109 deletions tests/test_embeddings.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import json
import hashlib
from pathlib import Path

Expand Down Expand Up @@ -161,125 +160,35 @@ def test_embedding_cache_round_trips_npz(tmp_path: Path) -> None:
assert loaded.files == cache.files


def test_embedding_cache_loads_legacy_json(tmp_path: Path) -> None:
def test_embedding_cache_ignores_non_npz_file(tmp_path: Path) -> None:
cache_path = tmp_path / "embeddings_cache.json"
cache_path.write_text(
json.dumps(
{
"version": 1,
"model": "legacy-model",
"api_base": "http://legacy.invalid/v1",
"files": {
"src/app.py": {
"sha256": "abc123",
"chunks": [
{
"index": 0,
"vector": [0.4, 0.5],
"start_line": 1,
"end_line": 2,
"content_hash": "legacy-chunk",
}
],
}
},
}
),
encoding="utf-8",
)
cache_path.write_text('{"version": 1, "files": {}}', encoding="utf-8")

cache = EmbeddingCache.load(cache_path)
assert cache.version == 1
assert cache.model == "legacy-model"
assert cache.api_base == "http://legacy.invalid/v1"
assert cache.files["src/app.py"].sha256 == "abc123"
assert cache.files["src/app.py"].chunks[0].vector == (0.4, 0.5)
assert cache.files == {}


def test_embedding_cache_loads_legacy_json_sibling_for_missing_npz(
def test_embedding_cache_ignores_legacy_json_sibling_for_missing_npz(
tmp_path: Path,
) -> None:
cache_path = tmp_path / ".code-orbit" / "embeddings_cache.npz"
cache_path.parent.mkdir()
legacy_path = cache_path.parent / "embeddings_cache.json"
legacy_path.write_text(
json.dumps(
{
"version": 1,
"model": "legacy-model",
"api_base": "http://legacy.invalid/v1",
"files": {
"src/app.py": {
"sha256": "abc123",
"chunks": [
{
"index": 0,
"vector": [0.4, 0.5],
"start_line": 1,
"end_line": 2,
"content_hash": "legacy-chunk",
}
],
}
},
}
),
encoding="utf-8",
)
legacy_path.write_text('{"version": 1, "files": {}}', encoding="utf-8")

cache = EmbeddingCache.load(cache_path)
assert cache.model == "legacy-model"
assert cache.files["src/app.py"].chunks[0].vector == (0.4, 0.5)
assert cache.files == {}


def test_build_embedding_sync_migrates_legacy_cache(tmp_path: Path) -> None:
def test_build_embedding_sync_does_not_reuse_legacy_json_cache(
tmp_path: Path,
) -> None:
_write_codebase(tmp_path)
config = Config(ignore_patterns=[".git", "node_modules"])
legacy_cache_dir = tmp_path / ".code-orbit"
legacy_cache_dir.mkdir()
legacy_cache_path = legacy_cache_dir / "embeddings_cache.json"

middleware_hash = hashlib.sha256(
(tmp_path / "src" / "auth" / "middleware.py").read_bytes()
).hexdigest()
tests_hash = hashlib.sha256(
(tmp_path / "src" / "tests.py").read_bytes()
).hexdigest()
legacy_cache_path.write_text(
json.dumps(
{
"version": 1,
"model": config.embedding_model,
"api_base": config.embedding_api_base,
"files": {
"src/auth/middleware.py": {
"sha256": middleware_hash,
"chunks": [
{
"index": 0,
"vector": [0.9, 0.1],
"start_line": 1,
"end_line": 2,
"content_hash": "middleware-chunk",
}
],
},
"src/tests.py": {
"sha256": tests_hash,
"chunks": [
{
"index": 0,
"vector": [0.1, 0.9],
"start_line": 1,
"end_line": 2,
"content_hash": "tests-chunk",
}
],
},
},
}
),
encoding="utf-8",
(legacy_cache_dir / "embeddings_cache.json").write_text(
'{"version": 1, "files": {}}', encoding="utf-8"
)

client = FakeEmbeddingClient()
Expand All @@ -292,16 +201,12 @@ def test_build_embedding_sync_migrates_legacy_cache(tmp_path: Path) -> None:
)

assert result.cache_path.suffix == ".npz"
assert result.updated_files == ()
assert set(result.reused_files) == {
"src/auth/middleware.py",
"src/tests.py",
}
assert client.requests == []
assert set(EmbeddingCache.load(result.cache_path).files) == {
assert set(result.updated_files) == {
"src/auth/middleware.py",
"src/tests.py",
}
assert result.reused_files == ()
assert client.requests != []


def test_build_embedding_sync_reads_each_file_once(tmp_path: Path, monkeypatch) -> None:
Expand Down
Loading