Make a cartridge a scope you mount, not only a bundle you export

A cartridge used to be an export: materialize it, hash it, and never let it
change behaviour again. The transactive-memory promise needs the opposite —
mounting the home cartridge must make the work cartridge's memories
unfindable, and appending to a cartridge must make a new memory findable
through it.

Cartridge scope (schema v2 -> v3):
- cartridge_atoms (which atoms are in which cartridge) and cartridge_mounts
  (which cartridges this machine has mounted now); both rebuilt on reopen,
  because a mount is a machine-local state, not a fact about the data.
- mount / switch / unmount / append refuse a cartridge that was built for a
  different runtime, naming every differing field instead of a boolean.
- append rewrites atoms.parquet and sparse_index.bm25.zst, recomputes DNA and
  the KV fingerprint, and keeps the cartridges row in one SQLite transaction;
  the files are written first so a failed export cannot leave a half-cartridge
  in the database.
- RetrievalService.search() now ranks inside the mounted scope by default, and
  an explicit cartridge_ids filter overrides the mount. Nothing mounted means
  the whole library, so an empty store behaves as it did before.
- forget() removes the forgotten atoms from every cartridge's membership:
  forgetting must not leave a memory retrievable through a cartridge.

KV fingerprint (kv_fingerprint.py, stored in the manifest):
- model / tokenizer / chat-template hashes, the RoPE block (factor,
  original_max_position_embeddings, mrope_interleaved, mrope_section,
  partial_rotary_factor), the adapter hash, the context length, the engine
  version, and the fingerprint format version.
- A snapshot we cannot read is recorded as unverifiable and listed in
  unverifiable_fields; a blind machine refuses to mount rather than pretending
  the runtime matches, because a wrong RoPE factor silently corrupts retrieval.

Two defects found by running the new tests, not by reading:
- atoms.parquet could not export a memory at all: pyarrow refuses a struct with
  no child field, so metadata={} failed the whole table. The six JSON columns
  are now written as JSON text and decoded by the cartridge reader.
- materialize() inserted cartridge membership before the cartridges row existed,
  so the foreign key rejected every cartridge.

api.py crossed 500 lines, so the cartridge and mount routes moved to
api_cartridges.py. That module must not use `from __future__ import
annotations`: FastAPI then resolves Depends(settings_dep) against module
globals, where the injected dependency is only a parameter of register(), and
every route starts demanding settings as a query parameter.

Verification: 138 pytest passed (26 new: 10 fingerprint, 16 scope), Go
build/vet/gofmt/test clean, and scripts/mcp_write_smoke.sh now proves the whole
loop across two processes - mount narrows search, a memory written after the
upload is invisible until append_to_cartridge, unmount restores the library,
and rewriting the RoPE factor makes mount_cartridge refuse with "rope: factor:
cartridge 4.0 vs local 2.0". ruff clean on every touched file (the 20 remaining
package errors are in files this change did not touch; HEAD had 25). mypy: one
error, the pre-existing np.savez stub artifact at cartridge_assets.py:329
(HEAD:320). Repo pyright: 62 errors, unchanged from the p0-3 ledger.
This commit is contained in:
ZacharyZhang-NY committed 2026-10-07 16:40:27 -04:00
1 parent 674c88e84e
commit 9fe13fffb3
21 files changed
+1492 -153

No files matched your search

+2 -43
View File
@@ -10,6 +10,7 @@ from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel from pydantic import BaseModel
from . import __version__ from . import __version__
from .api_cartridges import register as register_cartridge_routes
from .benchmarks import ( from .benchmarks import (
benchmark_prediction_report, benchmark_prediction_report,
benchmark_registry_report, benchmark_registry_report,
@@ -18,7 +19,6 @@ from .benchmarks import (
) )
from .cache_fabric import CacheFabric from .cache_fabric import CacheFabric
from .cartridge import CartridgeService from .cartridge import CartridgeService
from .cartridge_assets import CartridgeAssetService
from .compiler import ContextCompiler from .compiler import ContextCompiler
from .config import Settings, get_settings from .config import Settings, get_settings
from .db import Database from .db import Database
@@ -44,7 +44,6 @@ from .schemas import (
AdapterManifest, AdapterManifest,
AdapterStatus, AdapterStatus,
AnswerResponse, AnswerResponse,
CartridgeAssetReport,
EvidenceAtom, EvidenceAtom,
ForgetReport, ForgetReport,
ForgetRequest, ForgetRequest,
@@ -246,47 +245,7 @@ def create_app() -> FastAPI:
except ValueError as exc: except ValueError as exc:
raise HTTPException(status_code=422, detail=str(exc)) from exc raise HTTPException(status_code=422, detail=str(exc)) from exc
@app.get("/api/cartridges") register_cartridge_routes(app, settings_dep)
def cartridges(settings: Annotated[Settings, Depends(settings_dep)]):
return CartridgeService(settings).list_manifests()
@app.post("/api/cartridges/{cartridge_id}/finalize", response_model=CartridgeAssetReport)
def finalize_cartridge(
cartridge_id: str,
settings: Annotated[Settings, Depends(settings_dep)],
) -> CartridgeAssetReport:
try:
return CartridgeAssetService(settings).finalize(cartridge_id)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
@app.post("/api/cartridges/{cartridge_id}/refresh")
def refresh_cartridge(
cartridge_id: str,
settings: Annotated[Settings, Depends(settings_dep)],
):
db = Database(settings)
try:
return CartridgeService(settings, db).rebuild(
cartridge_id=cartridge_id,
atoms=db.list_atoms(),
sources=db.list_sources(),
)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
@app.get("/api/cartridges/{cartridge_id}/assets", response_model=CartridgeAssetReport)
def cartridge_assets(
cartridge_id: str,
settings: Annotated[Settings, Depends(settings_dep)],
) -> CartridgeAssetReport:
try:
report = CartridgeAssetService(settings).status(cartridge_id)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
if report is None:
raise HTTPException(status_code=404, detail="Cartridge assets have not been finalized.")
return report
@app.get("/api/adapters", response_model=list[AdapterStatus]) @app.get("/api/adapters", response_model=list[AdapterStatus])
def adapters(settings: Annotated[Settings, Depends(settings_dep)]) -> list[AdapterStatus]: def adapters(settings: Annotated[Settings, Depends(settings_dep)]) -> list[AdapterStatus]:
+107
View File
@@ -0,0 +1,107 @@
"""Cartridge and mount routes: a cartridge is a retrieval scope, not only an export."""
# No `from __future__ import annotations` here: FastAPI reads the route signatures, and
# string annotations would be resolved against this module's globals, where the injected
# settings_dep lives only as a parameter of register().
from collections.abc import Callable
from typing import Annotated
from fastapi import Depends, FastAPI, HTTPException
from .cartridge import CartridgeService
from .cartridge_assets import CartridgeAssetService
from .config import Settings
from .db import Database
from .schemas import AppendRequest, CartridgeAssetReport, CartridgeManifest, MountState
def register(app: FastAPI, settings_dep: Callable[[], Settings]) -> None:
"""Mounting decides what a search may see; the KV fingerprint decides if it may mount here."""
@app.get("/api/cartridges")
def cartridges(settings: Annotated[Settings, Depends(settings_dep)]):
return CartridgeService(settings).list_manifests()
@app.get("/api/mounts", response_model=MountState)
def mounts(settings: Annotated[Settings, Depends(settings_dep)]) -> MountState:
return CartridgeService(settings).mount_state()
@app.post("/api/cartridges/{cartridge_id}/mount", response_model=CartridgeManifest)
def mount_cartridge(
cartridge_id: str,
settings: Annotated[Settings, Depends(settings_dep)],
) -> CartridgeManifest:
try:
return CartridgeService(settings).mount(cartridge_id)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
@app.post("/api/cartridges/{cartridge_id}/switch", response_model=CartridgeManifest)
def switch_cartridge(
cartridge_id: str,
settings: Annotated[Settings, Depends(settings_dep)],
) -> CartridgeManifest:
try:
return CartridgeService(settings).switch(cartridge_id)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
@app.post("/api/cartridges/{cartridge_id}/unmount", response_model=MountState)
def unmount_cartridge(
cartridge_id: str,
settings: Annotated[Settings, Depends(settings_dep)],
) -> MountState:
try:
return CartridgeService(settings).unmount(cartridge_id)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
@app.post("/api/cartridges/{cartridge_id}/append", response_model=CartridgeManifest)
def append_cartridge(
cartridge_id: str,
request: AppendRequest,
settings: Annotated[Settings, Depends(settings_dep)],
) -> CartridgeManifest:
try:
return CartridgeService(settings).append(cartridge_id, request.atom_ids)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
@app.post("/api/cartridges/{cartridge_id}/finalize", response_model=CartridgeAssetReport)
def finalize_cartridge(
cartridge_id: str,
settings: Annotated[Settings, Depends(settings_dep)],
) -> CartridgeAssetReport:
try:
return CartridgeAssetService(settings).finalize(cartridge_id)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
@app.post("/api/cartridges/{cartridge_id}/refresh")
def refresh_cartridge(
cartridge_id: str,
settings: Annotated[Settings, Depends(settings_dep)],
):
db = Database(settings)
try:
return CartridgeService(settings, db).rebuild(
cartridge_id=cartridge_id,
atoms=db.list_atoms(),
sources=db.list_sources(),
)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
@app.get("/api/cartridges/{cartridge_id}/assets", response_model=CartridgeAssetReport)
def cartridge_assets(
cartridge_id: str,
settings: Annotated[Settings, Depends(settings_dep)],
) -> CartridgeAssetReport:
try:
report = CartridgeAssetService(settings).status(cartridge_id)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
if report is None:
raise HTTPException(status_code=404, detail="Cartridge assets have not been finalized.")
return report
+132 -90
View File
@@ -4,16 +4,34 @@ import hashlib
import json import json
import shutil import shutil
import sqlite3 import sqlite3
from datetime import datetime, timezone from datetime import UTC, datetime
from pathlib import Path from pathlib import Path
import pyarrow as pa import pyarrow as pa
import pyarrow.parquet as pq import pyarrow.parquet as pq
import zstandard as zstd import zstandard as zstd
from . import cartridge_store
from .config import Settings, get_settings from .config import Settings, get_settings
from .db import Database from .db import Database
from .schemas import AdapterManifest, CartridgeManifest, EvidenceAtom, SourceRecord from .kv_fingerprint import compute_fingerprint, fingerprint_diff, unverifiable_fields
from .schemas import (
AdapterManifest,
CartridgeManifest,
EvidenceAtom,
MountState,
SourceRecord,
utc_now,
)
JSON_PARQUET_COLUMNS = (
"embedding_refs",
"layout",
"relations",
"trust",
"metadata",
"visibility",
)
class CartridgeService: class CartridgeService:
@@ -27,7 +45,7 @@ class CartridgeService:
atoms: list[EvidenceAtom], atoms: list[EvidenceAtom],
sources: list[SourceRecord], sources: list[SourceRecord],
) -> CartridgeManifest: ) -> CartridgeManifest:
created_at = datetime.now(timezone.utc) created_at = datetime.now(UTC)
cartridge_id = self._cartridge_id(name, created_at) cartridge_id = self._cartridge_id(name, created_at)
root = self.settings.cartridge_dir / cartridge_id root = self.settings.cartridge_dir / cartridge_id
eval_dir = root / "eval_proofs" eval_dir = root / "eval_proofs"
@@ -55,6 +73,7 @@ class CartridgeService:
source_count=len(sources), source_count=len(sources),
atom_count=len(atoms), atom_count=len(atoms),
token_equivalent=sum(atom.token_equivalent for atom in atoms), token_equivalent=sum(atom.token_equivalent for atom in atoms),
kv_fingerprint=compute_fingerprint(self.settings, root / "adapters"),
artifacts={ artifacts={
"manifest": "manifest.json", "manifest": "manifest.json",
"atoms": "atoms.parquet", "atoms": "atoms.parquet",
@@ -64,20 +83,8 @@ class CartridgeService:
"eval_proofs": "eval_proofs/", "eval_proofs": "eval_proofs/",
}, },
) )
manifest_path = root / "manifest.json" self._commit(manifest, root)
manifest_path.write_text(manifest.model_dump_json(indent=2), encoding="utf-8") cartridge_store.set_membership(self.db, cartridge_id, [atom.atom_id for atom in atoms])
dna = self.compute_dna(root)
manifest.dna = dna
manifest_path.write_text(manifest.model_dump_json(indent=2), encoding="utf-8")
self.db.write_cartridge(
cartridge_id=cartridge_id,
name=name,
root_path=root,
manifest_json=manifest.model_dump_json(),
dna=dna,
token_equivalent=manifest.token_equivalent,
created_at=created_at,
)
return manifest return manifest
def compute_dna(self, root: Path) -> str: def compute_dna(self, root: Path) -> str:
@@ -96,18 +103,102 @@ class CartridgeService:
manifests.append(CartridgeManifest.model_validate_json(row["manifest_json"])) manifests.append(CartridgeManifest.model_validate_json(row["manifest_json"]))
return manifests return manifests
def mount_state(self) -> MountState:
mounted = cartridge_store.mounted_ids(self.db)
return MountState(
mounted=mounted,
scoped=bool(mounted),
atoms_in_scope=len(cartridge_store.atoms_of_cartridges(self.db, mounted)),
)
def mount(self, cartridge_id: str) -> CartridgeManifest:
"""Add one cartridge to the mounted set; a mounted search answers only from it."""
root, manifest = self._require(cartridge_id)
self._refuse_if_incompatible(manifest, root)
mounted = cartridge_store.mounted_ids(self.db)
if cartridge_id not in mounted:
cartridge_store.set_mounts(self.db, [*mounted, cartridge_id], utc_now())
return manifest
def switch(self, cartridge_id: str) -> CartridgeManifest:
"""Mount one cartridge and nothing else."""
root, manifest = self._require(cartridge_id)
self._refuse_if_incompatible(manifest, root)
cartridge_store.set_mounts(self.db, [cartridge_id], utc_now())
return manifest
def unmount(self, cartridge_id: str) -> MountState:
self._require(cartridge_id)
mounted = cartridge_store.mounted_ids(self.db)
if cartridge_id not in mounted:
raise ValueError(f"{cartridge_id} is not mounted; nothing to unmount")
cartridge_store.set_mounts(
self.db, [held for held in mounted if held != cartridge_id], utc_now()
)
return self.mount_state()
def append(self, cartridge_id: str, atom_ids: list[str]) -> CartridgeManifest:
"""Add memories to an existing cartridge; the bundle and its DNA follow the membership."""
self._require(cartridge_id)
if not atom_ids:
raise ValueError("append needs at least one atom")
atoms = self.db.get_atoms(atom_ids)
found = {atom.atom_id for atom in atoms}
missing = [atom_id for atom_id in atom_ids if atom_id not in found]
if missing:
raise ValueError(f"cannot append, these atoms are not in the store: {', '.join(missing)}")
cartridge_store.add_membership(self.db, cartridge_id, atom_ids)
held = self.db.get_atoms(sorted(cartridge_store.atoms_of_cartridge(self.db, cartridge_id)))
sources = self.db.get_sources(sorted({atom.source_id for atom in held}))
return self.rebuild(cartridge_id, held, sources)
def _require(self, cartridge_id: str) -> tuple[Path, CartridgeManifest]:
rows = [row for row in self.db.list_cartridges() if row["cartridge_id"] == cartridge_id]
if not rows:
raise ValueError(f"Unknown cartridge: {cartridge_id}")
row = rows[0]
return Path(row["root_path"]), CartridgeManifest.model_validate_json(row["manifest_json"])
def _refuse_if_incompatible(self, manifest: CartridgeManifest, root: Path) -> None:
"""KV bytes are only valid under the settings that produced them."""
stored = manifest.kv_fingerprint
if stored is None:
raise ValueError(f"{manifest.cartridge_id} carries no kv fingerprint; rebuild it before mounting")
live = compute_fingerprint(self.settings, root / "adapters")
gaps = unverifiable_fields(live)
if gaps:
raise ValueError(
f"cannot verify {manifest.cartridge_id} on this machine: {', '.join(gaps)} are unknown; "
"set ELY_EYE_MODEL_DIR to the model snapshot"
)
differences = fingerprint_diff(stored, live)
if differences:
raise ValueError(
f"{manifest.cartridge_id} was built for a different runtime; refusing to mount: "
+ "; ".join(differences)
)
def _commit(self, manifest: CartridgeManifest, root: Path) -> None:
"""Re-hash the bundle, then record the manifest that describes those bytes."""
manifest.dna = self.compute_dna(root)
(root / "manifest.json").write_text(manifest.model_dump_json(indent=2), encoding="utf-8")
self.db.write_cartridge(
cartridge_id=manifest.cartridge_id,
name=manifest.name,
root_path=root,
manifest_json=manifest.model_dump_json(),
dna=manifest.dna,
token_equivalent=manifest.token_equivalent,
created_at=manifest.created_at,
)
def rebuild( def rebuild(
self, self,
cartridge_id: str, cartridge_id: str,
atoms: list[EvidenceAtom], atoms: list[EvidenceAtom],
sources: list[SourceRecord], sources: list[SourceRecord],
) -> CartridgeManifest: ) -> CartridgeManifest:
rows = [row for row in self.db.list_cartridges() if row["cartridge_id"] == cartridge_id] root, manifest = self._require(cartridge_id)
if not rows:
raise ValueError(f"Unknown cartridge: {cartridge_id}")
row = rows[0]
root = Path(row["root_path"])
manifest = CartridgeManifest.model_validate_json(row["manifest_json"])
root.mkdir(parents=True, exist_ok=True) root.mkdir(parents=True, exist_ok=True)
self._write_atoms_parquet(root / "atoms.parquet", atoms) self._write_atoms_parquet(root / "atoms.parquet", atoms)
@@ -133,24 +224,11 @@ class CartridgeService:
) )
manifest.dna = self.compute_dna(root) manifest.dna = self.compute_dna(root)
(root / "manifest.json").write_text(manifest.model_dump_json(indent=2), encoding="utf-8") (root / "manifest.json").write_text(manifest.model_dump_json(indent=2), encoding="utf-8")
self.db.write_cartridge( self._commit(manifest, root)
cartridge_id=manifest.cartridge_id,
name=manifest.name,
root_path=root,
manifest_json=manifest.model_dump_json(),
dna=manifest.dna,
token_equivalent=manifest.token_equivalent,
created_at=manifest.created_at,
)
return manifest return manifest
def attach_adapter(self, cartridge_id: str, adapter_dir: Path) -> CartridgeManifest: def attach_adapter(self, cartridge_id: str, adapter_dir: Path) -> CartridgeManifest:
rows = [row for row in self.db.list_cartridges() if row["cartridge_id"] == cartridge_id] root, manifest = self._require(cartridge_id)
if not rows:
raise ValueError(f"Unknown cartridge: {cartridge_id}")
row = rows[0]
root = Path(row["root_path"])
manifest = CartridgeManifest.model_validate_json(row["manifest_json"])
adapter_manifest_path = adapter_dir / "adapter_manifest.json" adapter_manifest_path = adapter_dir / "adapter_manifest.json"
if not adapter_manifest_path.exists(): if not adapter_manifest_path.exists():
raise ValueError(f"Missing adapter manifest: {adapter_manifest_path}") raise ValueError(f"Missing adapter manifest: {adapter_manifest_path}")
@@ -163,18 +241,7 @@ class CartridgeService:
manifest.artifacts[f"adapter_{adapter_manifest.kind.value}"] = ( manifest.artifacts[f"adapter_{adapter_manifest.kind.value}"] = (
adapter_root.relative_to(root).as_posix() + "/" adapter_root.relative_to(root).as_posix() + "/"
) )
manifest_path = root / "manifest.json" self._commit(manifest, root)
manifest.dna = self.compute_dna(root)
manifest_path.write_text(manifest.model_dump_json(indent=2), encoding="utf-8")
self.db.write_cartridge(
cartridge_id=manifest.cartridge_id,
name=manifest.name,
root_path=root,
manifest_json=manifest.model_dump_json(),
dna=manifest.dna,
token_equivalent=manifest.token_equivalent,
created_at=manifest.created_at,
)
return manifest return manifest
def update_artifacts( def update_artifacts(
@@ -183,59 +250,34 @@ class CartridgeService:
artifacts: dict[str, str], artifacts: dict[str, str],
token_equivalent: int | None = None, token_equivalent: int | None = None,
) -> CartridgeManifest: ) -> CartridgeManifest:
rows = [row for row in self.db.list_cartridges() if row["cartridge_id"] == cartridge_id] root, manifest = self._require(cartridge_id)
if not rows:
raise ValueError(f"Unknown cartridge: {cartridge_id}")
row = rows[0]
root = Path(row["root_path"])
manifest = CartridgeManifest.model_validate_json(row["manifest_json"])
manifest.artifacts.update(artifacts) manifest.artifacts.update(artifacts)
if token_equivalent is not None: if token_equivalent is not None:
manifest.token_equivalent = token_equivalent manifest.token_equivalent = token_equivalent
manifest_path = root / "manifest.json" self._commit(manifest, root)
manifest.dna = self.compute_dna(root)
manifest_path.write_text(manifest.model_dump_json(indent=2), encoding="utf-8")
self.db.write_cartridge(
cartridge_id=manifest.cartridge_id,
name=manifest.name,
root_path=root,
manifest_json=manifest.model_dump_json(),
dna=manifest.dna,
token_equivalent=manifest.token_equivalent,
created_at=manifest.created_at,
)
return manifest return manifest
def attach_eval_proof(self, cartridge_id: str, proof_dir: Path) -> CartridgeManifest: def attach_eval_proof(self, cartridge_id: str, proof_dir: Path) -> CartridgeManifest:
rows = [row for row in self.db.list_cartridges() if row["cartridge_id"] == cartridge_id] root, manifest = self._require(cartridge_id)
if not rows:
raise ValueError(f"Unknown cartridge: {cartridge_id}")
if not proof_dir.exists() or not proof_dir.is_dir(): if not proof_dir.exists() or not proof_dir.is_dir():
raise ValueError(f"Missing proof directory: {proof_dir}") raise ValueError(f"Missing proof directory: {proof_dir}")
row = rows[0]
root = Path(row["root_path"])
manifest = CartridgeManifest.model_validate_json(row["manifest_json"])
target = root / "eval_proofs" / proof_dir.name target = root / "eval_proofs" / proof_dir.name
target.parent.mkdir(parents=True, exist_ok=True) target.parent.mkdir(parents=True, exist_ok=True)
shutil.copytree(proof_dir, target, dirs_exist_ok=True) shutil.copytree(proof_dir, target, dirs_exist_ok=True)
manifest.artifacts[f"eval_proof_{proof_dir.name}"] = target.relative_to(root).as_posix() + "/" manifest.artifacts[f"eval_proof_{proof_dir.name}"] = target.relative_to(root).as_posix() + "/"
manifest_path = root / "manifest.json" self._commit(manifest, root)
manifest.dna = self.compute_dna(root)
manifest_path.write_text(manifest.model_dump_json(indent=2), encoding="utf-8")
self.db.write_cartridge(
cartridge_id=manifest.cartridge_id,
name=manifest.name,
root_path=root,
manifest_json=manifest.model_dump_json(),
dna=manifest.dna,
token_equivalent=manifest.token_equivalent,
created_at=manifest.created_at,
)
return manifest return manifest
def _write_atoms_parquet(self, path: Path, atoms: list[EvidenceAtom]) -> None: def _write_atoms_parquet(self, path: Path, atoms: list[EvidenceAtom]) -> None:
rows = [atom.model_dump(mode="json") for atom in atoms] rows = []
table = pa.Table.from_pylist(rows) for atom in atoms:
row = atom.model_dump(mode="json")
for field in JSON_PARQUET_COLUMNS:
row[field] = json.dumps(row[field], ensure_ascii=False, sort_keys=True)
rows.append(row)
# An empty struct column has no child field, and Parquet cannot write one: a memory
# atom carries metadata={} and would crash the export.
table = pa.Table.from_pylist(rows) if rows else pa.Table.from_pydict({"atom_id": []})
pq.write_table(table, path, compression="zstd") pq.write_table(table, path, compression="zstd")
def _write_sparse_index(self, path: Path, atoms: list[EvidenceAtom]) -> None: def _write_sparse_index(self, path: Path, atoms: list[EvidenceAtom]) -> None:
@@ -289,13 +331,13 @@ class CartridgeService:
connection.close() connection.close()
def _cartridge_id(self, name: str, created_at: datetime) -> str: def _cartridge_id(self, name: str, created_at: datetime) -> str:
raw = f"{name}:{created_at.isoformat()}".encode("utf-8") raw = f"{name}:{created_at.isoformat()}".encode()
safe_name = "".join(char if char.isalnum() else "_" for char in name.lower()).strip("_")[:40] safe_name = "".join(char if char.isalnum() else "_" for char in name.lower()).strip("_")[:40]
return f"{safe_name or 'cartridge'}_{hashlib.sha256(raw).hexdigest()[:12]}" return f"{safe_name or 'cartridge'}_{hashlib.sha256(raw).hexdigest()[:12]}"
class _DigestWriter: class _DigestWriter:
def __init__(self, digest: "hashlib._Hash") -> None: def __init__(self, digest: hashlib._Hash) -> None:
self.digest = digest self.digest = digest
def write(self, data: bytes) -> int: def write(self, data: bytes) -> int:
+13 -4
View File
@@ -14,13 +14,12 @@ import pyarrow.parquet as pq
import zstandard as zstd import zstandard as zstd
from PIL import Image from PIL import Image
from .cartridge import CartridgeService from .cartridge import JSON_PARQUET_COLUMNS, CartridgeService
from .config import Settings, get_settings from .config import Settings, get_settings
from .db import Database from .db import Database
from .schemas import CartridgeAssetReport, CartridgeAssetStatus from .schemas import CartridgeAssetReport, CartridgeAssetStatus
from .storage import ObjectStore, sha256_file from .storage import ObjectStore, sha256_file
TEXT_VECTOR_DIMS = 384 TEXT_VECTOR_DIMS = 384
VISUAL_VECTOR_DIMS = 256 VISUAL_VECTOR_DIMS = 256
MEMORY_CAPSULE_SEGMENTS = 4096 MEMORY_CAPSULE_SEGMENTS = 4096
@@ -269,7 +268,17 @@ class CartridgeAssetService:
def load_cartridge_atoms(path: Path) -> list[dict[str, Any]]: def load_cartridge_atoms(path: Path) -> list[dict[str, Any]]:
if not path.exists(): if not path.exists():
raise FileNotFoundError(path) raise FileNotFoundError(path)
return pq.read_table(path).to_pylist() rows = pq.read_table(path).to_pylist()
for row in rows:
for field in JSON_PARQUET_COLUMNS:
try:
row[field] = json.loads(row[field])
except (TypeError, json.JSONDecodeError) as error:
raise ValueError(
f"{path}: column {field!r} is not readable JSON text ({error}); "
"rebuild the cartridge"
) from error
return rows
def hashed_text_vector(text: str, dims: int) -> np.ndarray: def hashed_text_vector(text: str, dims: int) -> np.ndarray:
@@ -298,7 +307,7 @@ def visual_image_vector(path: Path, dims: int) -> np.ndarray:
def capsule_hash(seed: str, index: int, label: str) -> str: def capsule_hash(seed: str, index: int, label: str) -> str:
return hashlib.sha256(f"{seed}:{index}:{label}".encode("utf-8")).hexdigest() return hashlib.sha256(f"{seed}:{index}:{label}".encode()).hexdigest()
def merkle_root(hashes: list[str]) -> str: def merkle_root(hashes: list[str]) -> str:
+106
View File
@@ -0,0 +1,106 @@
"""Membership and mount state for cartridges, kept out of db.py by the size rule.
A cartridge used to exist only as files copied out of the store, so "mount this cartridge"
had nothing to filter against. Membership now lives beside the atoms, and the mount table
is what a search scopes itself to when the caller does not name a scope.
"""
from __future__ import annotations
import sqlite3
from datetime import datetime
from .db import Database
from .row_codec import to_json
from .schemas import utc_now
def set_membership(database: Database, cartridge_id: str, atom_ids: list[str]) -> None:
"""Replace the atom set a cartridge holds."""
added_at = utc_now().isoformat()
with database.connect() as connection:
connection.execute("DELETE FROM cartridge_atoms WHERE cartridge_id = ?", (cartridge_id,))
connection.executemany(
"INSERT OR IGNORE INTO cartridge_atoms(cartridge_id, atom_id, added_at) VALUES (?, ?, ?)",
[(cartridge_id, atom_id, added_at) for atom_id in atom_ids],
)
def add_membership(database: Database, cartridge_id: str, atom_ids: list[str]) -> int:
"""Add atoms to a cartridge; returns how many were new."""
added_at = utc_now().isoformat()
with database.connect() as connection:
before = _count(connection, cartridge_id)
connection.executemany(
"INSERT OR IGNORE INTO cartridge_atoms(cartridge_id, atom_id, added_at) VALUES (?, ?, ?)",
[(cartridge_id, atom_id, added_at) for atom_id in atom_ids],
)
after = _count(connection, cartridge_id)
return after - before
def membership_count(database: Database, cartridge_id: str) -> int:
with database.connect() as connection:
return _count(connection, cartridge_id)
def cartridges_holding_atom(database: Database, atom_id: str) -> list[str]:
with database.connect() as connection:
rows = connection.execute(
"SELECT cartridge_id FROM cartridge_atoms WHERE atom_id = ? ORDER BY cartridge_id",
(atom_id,),
).fetchall()
return [str(row["cartridge_id"]) for row in rows]
def atoms_of_cartridge(database: Database, cartridge_id: str) -> list[str]:
with database.connect() as connection:
rows = connection.execute(
"SELECT atom_id FROM cartridge_atoms WHERE cartridge_id = ? ORDER BY atom_id",
(cartridge_id,),
).fetchall()
return [str(row["atom_id"]) for row in rows]
def atoms_of_cartridges(database: Database, cartridge_ids: list[str]) -> set[str]:
"""Every atom the named cartridges hold; the scope a search must not leak outside."""
if not cartridge_ids:
return set()
with database.connect() as connection:
rows = connection.execute(
"SELECT DISTINCT atom_id FROM cartridge_atoms "
"WHERE cartridge_id IN (SELECT value FROM json_each(?))",
(to_json(cartridge_ids),),
).fetchall()
return {str(row["atom_id"]) for row in rows}
def mounted_ids(database: Database) -> list[str]:
with database.connect() as connection:
rows = connection.execute(
"SELECT cartridge_id, mounted_at FROM mounts ORDER BY mounted_at, cartridge_id"
).fetchall()
return [str(row["cartridge_id"]) for row in rows]
def set_mounts(database: Database, cartridge_ids: list[str], mounted_at: datetime) -> None:
"""Replace the mounted set; mounting is a whole-state write, not a toggle."""
stamp = mounted_at.isoformat()
with database.connect() as connection:
connection.execute("DELETE FROM mounts")
connection.executemany(
"INSERT INTO mounts(cartridge_id, mounted_at) VALUES (?, ?)",
[(cartridge_id, stamp) for cartridge_id in cartridge_ids],
)
def clear_mounts(database: Database) -> None:
with database.connect() as connection:
connection.execute("DELETE FROM mounts")
def _count(connection: sqlite3.Connection, cartridge_id: str) -> int:
row = connection.execute(
"SELECT COUNT(*) AS n FROM cartridge_atoms WHERE cartridge_id = ?", (cartridge_id,)
).fetchone()
return int(row["n"])
+33 -1
View File
@@ -168,7 +168,7 @@ def forget(atom_id: str = "", source_id: str = "", reason: str = "") -> None:
def build_dense_index() -> None: def build_dense_index() -> None:
index = EmbeddingService(get_settings()).build_index() index = EmbeddingService(get_settings()).build_index()
dimensions = int(index.vectors.shape[1]) if index.vectors.size else 0 dimensions = int(index.vectors.shape[1]) if index.vectors.size else 0
typer.echo({"atoms": len(index.atom_ids), "dimensions": dimensions}) typer.echo(json_dump({"atoms": len(index.atom_ids), "dimensions": dimensions}))
@app.command("list-cartridges") @app.command("list-cartridges")
@@ -177,6 +177,38 @@ def list_cartridges() -> None:
typer.echo(manifest.model_dump_json(indent=2)) typer.echo(manifest.model_dump_json(indent=2))
@app.command("mounts")
def mounts() -> None:
typer.echo(CartridgeService(get_settings()).mount_state().model_dump_json(indent=2))
@app.command()
def mount(cartridge_id: str) -> None:
"""Add a cartridge to the mounted set; a mounted search answers only from it."""
typer.echo(CartridgeService(get_settings()).mount(cartridge_id).model_dump_json(indent=2))
@app.command()
def switch(cartridge_id: str) -> None:
"""Mount one cartridge and nothing else."""
typer.echo(CartridgeService(get_settings()).switch(cartridge_id).model_dump_json(indent=2))
@app.command()
def unmount(cartridge_id: str) -> None:
typer.echo(CartridgeService(get_settings()).unmount(cartridge_id).model_dump_json(indent=2))
@app.command()
def append(
cartridge_id: str,
atom_id: list[str] = typer.Option(..., "--atom-id"), # noqa: B008 # typer reads option defaults itself
) -> None:
"""Add memories to an existing cartridge; the bundle and its DNA follow."""
service = CartridgeService(get_settings())
typer.echo(service.append(cartridge_id, list(atom_id)).model_dump_json(indent=2))
@app.command("hashhop") @app.command("hashhop")
def hashhop(kind: str = "visual", hops: int = 2, token_equivalent: int = 262_144) -> None: def hashhop(kind: str = "visual", hops: int = 2, token_equivalent: int = 262_144) -> None:
evaluator = HashHopEvaluator(get_settings()) evaluator = HashHopEvaluator(get_settings())
+2
View File
@@ -14,6 +14,8 @@ class Settings(BaseSettings):
home: Path = Field(default_factory=lambda: Path.cwd() / ".ely_eye") home: Path = Field(default_factory=lambda: Path.cwd() / ".ely_eye")
default_holder: str = "local" default_holder: str = "local"
model_id: str = "Qwen/Qwen3.5-9B" model_id: str = "Qwen/Qwen3.5-9B"
model_dir: Path | None = None
kv_cache_dtype: str = "auto"
planner_model_id: str = "Qwen/Qwen3.5-0.8B" planner_model_id: str = "Qwen/Qwen3.5-0.8B"
embedding_model_id: str = "Qwen/Qwen3-VL-Embedding-8B" embedding_model_id: str = "Qwen/Qwen3-VL-Embedding-8B"
adapter_dir: Path | None = None adapter_dir: Path | None = None
+19
View File
@@ -187,6 +187,7 @@ class Database:
since: datetime | None = None, since: datetime | None = None,
until: datetime | None = None, until: datetime | None = None,
min_trust: float | None = None, min_trust: float | None = None,
cartridge_ids: list[str] | None = None,
) -> FtsPage: ) -> FtsPage:
"""BM25 over the FTS index joined to memories that are open right now. """BM25 over the FTS index joined to memories that are open right now.
@@ -215,6 +216,11 @@ class Database:
json_extract(a.trust_json, '$.model') json_extract(a.trust_json, '$.model')
) >= ? ) >= ?
)) ))
AND (? IS NULL OR EXISTS (
SELECT 1 FROM cartridge_atoms AS ca
WHERE ca.atom_id = f.atom_id
AND ca.cartridge_id IN (SELECT value FROM json_each(?))
))
ORDER BY score ORDER BY score
LIMIT ? OFFSET ? LIMIT ? OFFSET ?
""", """,
@@ -234,6 +240,8 @@ class Database:
None if until is None else until.isoformat(), None if until is None else until.isoformat(),
min_trust, min_trust,
min_trust, min_trust,
None if cartridge_ids is None else to_json(cartridge_ids),
None if cartridge_ids is None else to_json(cartridge_ids),
limit + 1, limit + 1,
offset, offset,
), ),
@@ -330,6 +338,17 @@ class Database:
).fetchall() ).fetchall()
return [row_to_source(row) for row in rows] return [row_to_source(row) for row in rows]
def get_sources(self, source_ids: list[str]) -> list[SourceRecord]:
if not source_ids:
return []
with self.connect() as connection:
rows = connection.execute(
"SELECT source_id, path, kind, mime, sha256, size_bytes, created_at, metadata_json "
"FROM sources WHERE source_id IN (SELECT value FROM json_each(?))",
(to_json(source_ids),),
).fetchall()
return [row_to_source(row) for row in rows]
def cartridge_count(self) -> int: def cartridge_count(self) -> int:
with self.connect() as connection: with self.connect() as connection:
return int(connection.execute("SELECT COUNT(*) FROM cartridges").fetchone()[0]) return int(connection.execute("SELECT COUNT(*) FROM cartridges").fetchone()[0])
+196
View File
@@ -0,0 +1,196 @@
"""The compatibility header a cartridge carries, and the check that gates a mount.
KV bytes are only valid under the RoPE, quantization and engine settings that produced
them (profiles.py launches YaRN factor 4.0 and llama.cpp --cache-type-k q4_0). A cartridge
mounted under different settings does not fail: it silently attends to wrong positions. So
the manifest carries a fingerprint and the mount compares it field by field.
"""
from __future__ import annotations
import hashlib
import json
from importlib import metadata
from pathlib import Path
from typing import Any
from pydantic import BaseModel
from .config import Settings
FORMAT_SEMVER = "1.0.0"
UNKNOWN = ""
class KvFingerprint(BaseModel):
format_semver: str = FORMAT_SEMVER
model_id: str = UNKNOWN
model_hash: str = UNKNOWN
tokenizer_hash: str = UNKNOWN
chat_template_hash: str = UNKNOWN
rope: dict[str, Any] = {}
context_tokens: int = 0
kv_dtype: str = UNKNOWN
engine: str = UNKNOWN
engine_version: str = UNKNOWN
adapter_hashes: dict[str, str] = {}
def _read_bytes(path: Path) -> bytes | None:
if not path.is_file():
return None
return path.read_bytes()
def _hex(data: bytes) -> str:
return hashlib.sha256(data).hexdigest()
def _config_json(model_dir: Path) -> dict[str, Any]:
data = _read_bytes(model_dir / "config.json")
if data is None:
return {}
try:
parsed = json.loads(data)
except json.JSONDecodeError as error:
raise ValueError(f"{model_dir / 'config.json'}: the model config is unreadable ({error})") from error
if not isinstance(parsed, dict):
raise ValueError(f"{model_dir / 'config.json'}: expected a JSON object") # noqa: TRY004 # the file's bytes are wrong, not the caller's argument
return parsed
def _model_hash(model_dir: Path) -> str:
config = _read_bytes(model_dir / "config.json")
if config is None:
return UNKNOWN
digest = hashlib.sha256(config)
index = _read_bytes(model_dir / "model.safetensors.index.json")
if index is not None:
digest.update(index)
return digest.hexdigest()
for shard in sorted(model_dir.glob("*.safetensors")):
digest.update(f"{shard.name}:{shard.stat().st_size}".encode())
return digest.hexdigest()
def _rope(config: dict[str, Any]) -> dict[str, Any]:
rope: dict[str, Any] = {}
for key in ("rope_parameters", "rope_scaling"):
block = config.get(key)
if isinstance(block, dict):
rope.update(block)
for key in ("partial_rotary_factor", "max_position_embeddings", "sliding_window"):
value = config.get(key)
if value is not None:
rope[key] = value
return rope
def _chat_template_hash(model_dir: Path, config: dict[str, Any]) -> str:
template = config.get("chat_template")
if not isinstance(template, str):
data = _read_bytes(model_dir / "chat_template.json")
if data is None:
return UNKNOWN
try:
parsed = json.loads(data)
except json.JSONDecodeError as error:
raise ValueError(f"{model_dir / 'chat_template.json'}: unreadable ({error})") from error
template = parsed.get("chat_template") if isinstance(parsed, dict) else None
return _hex(template.encode("utf-8")) if isinstance(template, str) else UNKNOWN
def adapter_hashes(adapter_root: Path) -> dict[str, str]:
"""Hash every adapter weight under a directory, keyed by adapter kind."""
found: dict[str, str] = {}
if not adapter_root.is_dir():
return found
for weight in sorted(adapter_root.rglob("adapter_model.safetensors")):
kind = weight.parent.name
found[kind] = _hex(weight.read_bytes())
return found
def compute_fingerprint(settings: Settings, adapter_root: Path | None = None) -> KvFingerprint:
"""Read what this machine can prove; unverifiable fields come back empty."""
engine_version = UNKNOWN
try:
engine_version = metadata.version(
"transformers" if settings.runtime_backend == "transformers" else "sglang"
)
except metadata.PackageNotFoundError:
pass # an engine that is not installed here is a fact to report, not a crash
model_dir = settings.model_dir
config = _config_json(model_dir) if model_dir is not None else {}
tokenizer = _read_bytes(model_dir / "tokenizer.json") if model_dir is not None else None
if tokenizer is None and model_dir is not None:
tokenizer = _read_bytes(model_dir / "tokenizer_config.json")
return KvFingerprint(
model_id=settings.model_id,
model_hash=_model_hash(model_dir) if model_dir is not None else UNKNOWN,
tokenizer_hash=_hex(tokenizer) if tokenizer is not None else UNKNOWN,
chat_template_hash=_chat_template_hash(model_dir, config) if model_dir is not None else UNKNOWN,
rope=_rope(config),
context_tokens=settings.live_context_tokens,
kv_dtype=settings.kv_cache_dtype,
engine=settings.runtime_backend,
engine_version=engine_version,
adapter_hashes=adapter_hashes(adapter_root) if adapter_root is not None else {},
)
def unverifiable_fields(fingerprint: KvFingerprint) -> list[str]:
"""Fields this machine cannot prove; a mount must not wave them through."""
missing: list[str] = []
for field in ("model_hash", "tokenizer_hash", "chat_template_hash", "engine_version"):
if not getattr(fingerprint, field):
missing.append(field)
if not fingerprint.rope:
missing.append("rope")
return missing
def fingerprint_diff(stored: KvFingerprint, live: KvFingerprint) -> list[str]:
"""Named differences between a cartridge's header and this machine."""
differences: list[str] = []
if stored.format_semver != live.format_semver:
differences.append(
f"format_semver: cartridge {stored.format_semver} vs this build {live.format_semver}"
)
for field in (
"model_id",
"model_hash",
"tokenizer_hash",
"chat_template_hash",
"context_tokens",
"kv_dtype",
"engine",
"engine_version",
):
left, right = getattr(stored, field), getattr(live, field)
if left != right:
differences.append(f"{field}: cartridge {left!r} vs local {right!r}")
if stored.rope != live.rope:
keys = sorted(set(stored.rope) | set(live.rope))
changes = [
f"{key}: cartridge {stored.rope.get(key)!r} vs local {live.rope.get(key)!r}"
for key in keys
if stored.rope.get(key) != live.rope.get(key)
]
differences.append("rope: " + "; ".join(changes))
if stored.adapter_hashes != live.adapter_hashes:
kinds = sorted(set(stored.adapter_hashes) | set(live.adapter_hashes))
changes = [
f"{kind}: cartridge {_short(stored.adapter_hashes.get(kind))} vs local "
f"{_short(live.adapter_hashes.get(kind))}"
for kind in kinds
if stored.adapter_hashes.get(kind) != live.adapter_hashes.get(kind)
]
differences.append("adapters: " + "; ".join(changes))
return differences
def _short(value: str | None) -> str:
if not value:
return "absent"
return value[:12]
+15 -1
View File
@@ -14,7 +14,7 @@ import sqlite3
from .text_index import index_text from .text_index import index_text
SCHEMA_VERSION = 3 SCHEMA_VERSION = 4
ATOM_COLUMNS_V2 = """ ATOM_COLUMNS_V2 = """
atom_id TEXT PRIMARY KEY, atom_id TEXT PRIMARY KEY,
@@ -68,6 +68,20 @@ _BASE_TABLES = """
created_at TEXT NOT NULL created_at TEXT NOT NULL
); );
-- Membership lives in the live store, not only in the copied parquet, otherwise
-- "mount this cartridge" cannot scope a query against the atoms it holds.
CREATE TABLE IF NOT EXISTS cartridge_atoms (
cartridge_id TEXT NOT NULL REFERENCES cartridges(cartridge_id) ON DELETE CASCADE,
atom_id TEXT NOT NULL REFERENCES atoms(atom_id) ON DELETE CASCADE,
added_at TEXT NOT NULL,
PRIMARY KEY (cartridge_id, atom_id)
);
CREATE TABLE IF NOT EXISTS mounts (
cartridge_id TEXT PRIMARY KEY REFERENCES cartridges(cartridge_id) ON DELETE CASCADE,
mounted_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS cache_events ( CREATE TABLE IF NOT EXISTS cache_events (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
ts TEXT NOT NULL, ts TEXT NOT NULL,
+10 -1
View File
@@ -11,6 +11,7 @@ from __future__ import annotations
import base64 import base64
from collections import defaultdict from collections import defaultdict
from . import cartridge_store
from .config import Settings, get_settings from .config import Settings, get_settings
from .db import Database from .db import Database
from .embeddings import EmbeddingService from .embeddings import EmbeddingService
@@ -44,11 +45,16 @@ class RetrievalService:
if limit <= 0: if limit <= 0:
raise ValueError(f"search needs a positive limit, got {limit}") raise ValueError(f"search needs a positive limit, got {limit}")
offset = decode_cursor(cursor) offset = decode_cursor(cursor)
criteria = (filters or SearchFilters()).model_dump(exclude_none=True)
scope = criteria.get("cartridge_ids")
if scope is None:
scope = cartridge_store.mounted_ids(self.db) or None
criteria["cartridge_ids"] = scope
page = self.db.search_atoms( page = self.db.search_atoms(
query, query,
limit=limit * CANDIDATE_WINDOW, limit=limit * CANDIDATE_WINDOW,
offset=offset, offset=offset,
**(filters or SearchFilters()).model_dump(exclude_none=True), **criteria,
) )
rankings: list[list[str]] = [[atom_id for atom_id, _ in page.rows]] rankings: list[list[str]] = [[atom_id for atom_id, _ in page.rows]]
scores: dict[str, dict[str, float]] = defaultdict(dict) scores: dict[str, dict[str, float]] = defaultdict(dict)
@@ -56,6 +62,9 @@ class RetrievalService:
scores[atom_id]["sparse_score"] = bm25 scores[atom_id]["sparse_score"] = bm25
if self.embeddings.enabled(): if self.embeddings.enabled():
dense = self._dense_search(query, limit * CANDIDATE_WINDOW) dense = self._dense_search(query, limit * CANDIDATE_WINDOW)
if scope:
allowed = cartridge_store.atoms_of_cartridges(self.db, scope)
dense = {atom_id: score for atom_id, score in dense.items() if atom_id in allowed}
rankings.append(list(dense)) rankings.append(list(dense))
for atom_id, cosine in dense.items(): for atom_id, cosine in dense.items():
scores[atom_id]["dense_score"] = cosine scores[atom_id]["dense_score"] = cosine
+16
View File
@@ -7,6 +7,8 @@ from typing import Any, Literal
from pydantic import BaseModel, Field, field_validator, model_validator from pydantic import BaseModel, Field, field_validator, model_validator
from .kv_fingerprint import KvFingerprint
def utc_now() -> datetime: def utc_now() -> datetime:
return datetime.now(UTC) return datetime.now(UTC)
@@ -162,6 +164,7 @@ class SearchFilters(BaseModel):
scenario: str | None = None scenario: str | None = None
source_id: str | None = None source_id: str | None = None
modality: Modality | None = None modality: Modality | None = None
cartridge_ids: list[str] | None = None
since: datetime | None = None since: datetime | None = None
until: datetime | None = None until: datetime | None = None
min_trust: float | None = Field(default=None, ge=0.0, le=1.0) min_trust: float | None = Field(default=None, ge=0.0, le=1.0)
@@ -256,6 +259,18 @@ class SystemStatus(BaseModel):
profiles: dict[str, dict[str, Any]] profiles: dict[str, dict[str, Any]]
data_home: str data_home: str
class MountState(BaseModel):
"""What is mounted now, and whether that narrows a search."""
mounted: list[str]
scoped: bool
atoms_in_scope: int
class AppendRequest(BaseModel):
atom_ids: list[str]
class CartridgeManifest(BaseModel): class CartridgeManifest(BaseModel):
cartridge_id: str cartridge_id: str
name: str name: str
@@ -265,6 +280,7 @@ class CartridgeManifest(BaseModel):
atom_count: int atom_count: int
token_equivalent: int token_equivalent: int
artifacts: dict[str, str] artifacts: dict[str, str]
kv_fingerprint: KvFingerprint | None = None
dna: str | None = None dna: str | None = None
class AdapterManifest(BaseModel): class AdapterManifest(BaseModel):
+53
View File
@@ -0,0 +1,53 @@
"""Shared fixtures: a model snapshot the fingerprint can read without a 9B download."""
from __future__ import annotations
import json
from collections.abc import Callable
from pathlib import Path
from typing import Any
import pytest
ROPE: dict[str, Any] = {
"rope_type": "yarn",
"factor": 4.0,
"original_max_position_embeddings": 262_144,
"mrope_interleaved": True,
"mrope_section": [11, 11, 10],
}
TEMPLATE = "{% for message in messages %}{{ message['content'] }}{% endfor %}"
def write_model(model_dir: Path, **config_overrides: Any) -> Path:
config: dict[str, Any] = {
"model_type": "qwen3_5",
"max_position_embeddings": 262_144,
"partial_rotary_factor": 0.25,
"rope_parameters": dict(ROPE),
"chat_template": TEMPLATE,
}
config.update(config_overrides)
model_dir.mkdir(parents=True, exist_ok=True)
(model_dir / "config.json").write_text(json.dumps(config), encoding="utf-8")
(model_dir / "tokenizer.json").write_text('{"version": "1.0"}', encoding="utf-8")
(model_dir / "model.safetensors.index.json").write_text(
json.dumps({"metadata": {}, "weight_map": {"model-00001.safetensors": "0"}}),
encoding="utf-8",
)
return model_dir
@pytest.fixture
def chat_template() -> str:
return TEMPLATE
@pytest.fixture
def write_model_fixture() -> Callable[..., Path]:
return write_model
@pytest.fixture
def model_dir(tmp_path: Path) -> Path:
return write_model(tmp_path / "model")
+284
View File
@@ -0,0 +1,284 @@
"""A cartridge is a retrieval scope: mount it and only it answers, append and the bundle follows."""
from __future__ import annotations
import json
from collections.abc import Callable
from datetime import datetime
from pathlib import Path
from typing import Any
import pyarrow.parquet as pq
import pytest
from fastapi.testclient import TestClient
from typer.testing import CliRunner
import ely_eye.api as api_module
import ely_eye.cli as cli_module
from ely_eye import cartridge_store
from ely_eye.cartridge import CartridgeService
from ely_eye.cli import app
from ely_eye.config import Settings
from ely_eye.db import Database
from ely_eye.memory import MemoryService
from ely_eye.migrations import SCHEMA_VERSION
from ely_eye.retrieval import RetrievalService
from ely_eye.schemas import MemoryOrigin, SearchFilters
HOME_KEYS = "我们的钥匙放在玄关抽屉里"
WORK_KEYS = "备用钥匙在办公室第二个抽屉里"
class Store:
"""One SQLite file, the four services that read and write it."""
def __init__(self, settings: Settings) -> None:
self.settings = settings
self.memory = MemoryService(settings=settings)
self.db = self.memory.db
self.cartridges = CartridgeService(settings=settings, database=self.db)
self.search = RetrievalService(settings=settings, database=self.db)
def remember(self, text: str, scenario: str) -> str:
atom = self.memory.remember(
text, holder_id="wife", origin=MemoryOrigin.self_, scenario=scenario
)
return atom.atom_id
def cartridge(self, name: str, atom_ids: list[str]) -> str:
atoms = self.db.get_atoms(atom_ids)
sources = self.db.get_sources(sorted({atom.source_id for atom in atoms}))
return self.cartridges.materialize(name, atoms, sources).cartridge_id
def texts(self, query: str = "钥匙", **filters: Any) -> list[str]:
result = self.search.search(query, filters=SearchFilters(**filters) if filters else None)
return [hit.atom.text for hit in result.hits]
@pytest.fixture
def store(tmp_path: Path, model_dir: Path) -> Store:
return Store(Settings(home=tmp_path / "home", model_dir=model_dir, default_holder="wife"))
def test_mounting_one_cartridge_stops_the_other_from_answering(store: Store) -> None:
home = store.cartridge("home", [store.remember(HOME_KEYS, "home")])
work = store.cartridge("work", [store.remember(WORK_KEYS, "work")])
assert sorted(store.texts()) == sorted([HOME_KEYS, WORK_KEYS])
store.cartridges.switch(home)
assert store.texts() == [HOME_KEYS]
store.cartridges.switch(work)
assert store.texts() == [WORK_KEYS]
def test_an_explicit_scope_overrides_what_is_mounted(store: Store) -> None:
home = store.cartridge("home", [store.remember(HOME_KEYS, "home")])
work = store.cartridge("work", [store.remember(WORK_KEYS, "work")])
store.cartridges.switch(home)
assert store.texts(cartridge_ids=[work]) == [WORK_KEYS]
def test_nothing_mounted_answers_from_the_whole_library(store: Store) -> None:
store.cartridge("home", [store.remember(HOME_KEYS, "home")])
state = store.cartridges.mount_state()
assert state.mounted == []
assert state.scoped is False
assert state.atoms_in_scope == 0
assert store.texts() == [HOME_KEYS]
def test_mounting_two_cartridges_answers_from_both(store: Store) -> None:
home = store.cartridge("home", [store.remember(HOME_KEYS, "home")])
work = store.cartridge("work", [store.remember(WORK_KEYS, "work")])
store.cartridges.mount(home)
store.cartridges.mount(work)
state = store.cartridges.mount_state()
assert sorted(state.mounted) == sorted([home, work])
assert state.atoms_in_scope == 2
assert sorted(store.texts()) == sorted([HOME_KEYS, WORK_KEYS])
def test_unmounting_reports_the_state_and_refuses_a_stranger(store: Store) -> None:
home = store.cartridge("home", [store.remember(HOME_KEYS, "home")])
store.cartridges.mount(home)
state = store.cartridges.unmount(home)
assert state.mounted == [] and state.scoped is False
with pytest.raises(ValueError, match=f"{home} is not mounted; nothing to unmount"):
store.cartridges.unmount(home)
def test_append_adds_a_memory_and_the_bundle_follows(store: Store) -> None:
home = store.cartridge("home", [store.remember(HOME_KEYS, "home")])
before = next(m for m in store.cartridges.list_manifests() if m.cartridge_id == home)
later = store.remember("备用钥匙在办公室第二个抽屉里", "home")
after = store.cartridges.append(home, [later])
assert after.dna != before.dna
assert after.atom_count == before.atom_count + 1
assert cartridge_store.membership_count(store.db, home) == 2
rows = pq.read_table(store.settings.cartridge_dir / home / "atoms.parquet").num_rows
assert rows == before.atom_count + 1
store.cartridges.switch(home)
assert sorted(store.texts()) == sorted([HOME_KEYS, WORK_KEYS])
def test_append_refuses_atoms_that_are_not_in_the_store(store: Store) -> None:
home = store.cartridge("home", [store.remember(HOME_KEYS, "home")])
with pytest.raises(ValueError, match="not in the store: mem-missing:text"):
store.cartridges.append(home, ["mem-missing:text"])
def test_append_needs_something_to_append(store: Store) -> None:
home = store.cartridge("home", [store.remember(HOME_KEYS, "home")])
with pytest.raises(ValueError, match="append needs at least one atom"):
store.cartridges.append(home, [])
def test_a_cartridge_without_a_fingerprint_cannot_be_mounted(store: Store) -> None:
home = store.cartridge("home", [store.remember(HOME_KEYS, "home")])
row = next(r for r in store.db.list_cartridges() if r["cartridge_id"] == home)
manifest = json.loads(row["manifest_json"])
manifest["kv_fingerprint"] = None
store.db.write_cartridge(
cartridge_id=home,
name=row["name"],
root_path=Path(row["root_path"]),
manifest_json=json.dumps(manifest),
dna=row["dna"],
token_equivalent=row["token_equivalent"],
created_at=datetime.fromisoformat(row["created_at"]),
)
with pytest.raises(ValueError, match="carries no kv fingerprint; rebuild it before mounting"):
store.cartridges.mount(home)
def test_a_mount_refuses_a_different_rope_and_names_the_field(
store: Store, model_dir: Path, write_model_fixture: Callable[..., Path]
) -> None:
home = store.cartridge("home", [store.remember(HOME_KEYS, "home")])
write_model_fixture(model_dir, rope_parameters={"rope_type": "yarn", "factor": 2.0})
with pytest.raises(ValueError, match="rope: factor: cartridge 4.0 vs local 2.0"):
store.cartridges.mount(home)
def test_a_mount_refuses_when_the_machine_cannot_prove_the_model(store: Store) -> None:
home = store.cartridge("home", [store.remember(HOME_KEYS, "home")])
blind = CartridgeService(
settings=Settings(home=store.settings.home, model_dir=None), database=store.db
)
with pytest.raises(ValueError, match="model_hash, tokenizer_hash, chat_template_hash, rope"):
blind.mount(home)
def test_forgetting_a_memory_takes_it_out_of_the_cartridge(store: Store) -> None:
atom_id = store.remember(HOME_KEYS, "home")
home = store.cartridge("home", [atom_id])
store.cartridges.mount(home)
store.memory.forget(atom_id=atom_id, reason="moved")
assert cartridge_store.membership_count(store.db, home) == 0
assert store.texts() == []
def upload_home(client: TestClient) -> str:
answer = client.post(
"/api/ingest/upload?cartridge_name=home",
files={"files": ("pantry.txt", b"the pantry keeps flour", "text/plain")},
)
assert answer.status_code == 200, answer.text
return answer.json()["cartridge_id"]
def test_the_api_mounts_and_reports_the_scope(
tmp_path: Path, model_dir: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
settings = Settings(home=tmp_path / "home", model_dir=model_dir, default_holder="wife")
monkeypatch.setattr(api_module, "get_settings", lambda: settings)
client = TestClient(api_module.create_app())
atom = client.post("/api/memory", json={"text": HOME_KEYS, "scenario": "home"}).json()
assert client.get("/api/mounts").json() == {"mounted": [], "scoped": False, "atoms_in_scope": 0}
cartridge_id = upload_home(client)
answer = client.post(f"/api/cartridges/{cartridge_id}/mount")
assert answer.status_code == 200
assert answer.json()["kv_fingerprint"]["rope"]["factor"] == 4.0
assert client.get("/api/mounts").json() == {
"mounted": [cartridge_id],
"scoped": True,
"atoms_in_scope": 2,
}
hits = client.post("/api/search", json={"query": "钥匙"}).json()["hits"]
assert [hit["atom"]["atom_id"] for hit in hits] == [atom["atom_id"]]
stranger = client.post("/api/cartridges/cartridge-does-not-exist/mount")
assert stranger.status_code == 400 and "Unknown cartridge" in stranger.json()["detail"]
assert client.post(f"/api/cartridges/{cartridge_id}/unmount").status_code == 200
assert client.post(f"/api/cartridges/{cartridge_id}/unmount").status_code == 400
def test_the_api_appends_and_refuses_an_unknown_atom(
tmp_path: Path, model_dir: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
settings = Settings(home=tmp_path / "home", model_dir=model_dir, default_holder="wife")
monkeypatch.setattr(api_module, "get_settings", lambda: settings)
client = TestClient(api_module.create_app())
client.post("/api/memory", json={"text": HOME_KEYS, "scenario": "home"})
cartridge_id = upload_home(client)
answer = client.post(
f"/api/cartridges/{cartridge_id}/append", json={"atom_ids": ["mem-nope:text"]}
)
assert answer.status_code == 400 and "not in the store" in answer.json()["detail"]
extra = client.post("/api/memory", json={"text": WORK_KEYS, "scenario": "home"}).json()
grown = client.post(
f"/api/cartridges/{cartridge_id}/append", json={"atom_ids": [extra["atom_id"]]}
)
assert grown.status_code == 200 and grown.json()["atom_count"] == 3
def test_the_cli_mounts_and_answers_in_json(
tmp_path: Path, model_dir: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
settings = Settings(home=tmp_path / "home", model_dir=model_dir, default_holder="wife")
monkeypatch.setattr(cli_module, "get_settings", lambda: settings)
runner = CliRunner()
store = Store(settings)
cartridge_id = store.cartridge("home", [store.remember(HOME_KEYS, "home")])
mounted = runner.invoke(app, ["mount", cartridge_id])
assert mounted.exit_code == 0, mounted.output
assert json.loads(mounted.output)["cartridge_id"] == cartridge_id
state = json.loads(runner.invoke(app, ["mounts"]).output)
assert state == {"mounted": [cartridge_id], "scoped": True, "atoms_in_scope": 1}
refused = runner.invoke(app, ["mount", "cartridge-does-not-exist"])
assert isinstance(refused.exception, ValueError) and "Unknown cartridge" in str(
refused.exception
)
def test_a_third_version_store_gains_the_scope_tables_on_upgrade(
tmp_path: Path, model_dir: Path
) -> None:
settings = Settings(home=tmp_path / "home", model_dir=model_dir)
first = Database(settings)
with first.connect() as connection:
connection.execute("DROP TABLE cartridge_atoms")
connection.execute("DROP TABLE mounts")
connection.execute("PRAGMA user_version = 3")
reopened = Database(settings)
with reopened.connect() as connection:
tables = {
row[0]
for row in connection.execute(
"SELECT name FROM sqlite_master WHERE type = 'table'"
).fetchall()
}
version = connection.execute("PRAGMA user_version").fetchone()[0]
assert {"cartridge_atoms", "mounts"} <= tables
assert version == SCHEMA_VERSION
assert cartridge_store.mounted_ids(reopened) == []
+138
View File
@@ -0,0 +1,138 @@
"""KV bytes are only valid under the settings that produced them, so the header must say so."""
from __future__ import annotations
import json
from collections.abc import Callable
from importlib import metadata
from pathlib import Path
from typing import Any
import pytest
from ely_eye.config import Settings
from ely_eye.kv_fingerprint import (
FORMAT_SEMVER,
KvFingerprint,
adapter_hashes,
compute_fingerprint,
fingerprint_diff,
unverifiable_fields,
)
def settings_for(model_dir: Path | None, home: Path, **overrides: Any) -> Settings:
fields: dict[str, Any] = {"runtime_backend": "transformers", **overrides}
return Settings(home=home, model_dir=model_dir, **fields)
def test_a_readable_snapshot_leaves_nothing_unverifiable(
tmp_path: Path, write_model_fixture: Callable[..., Path]
) -> None:
fingerprint = compute_fingerprint(
settings_for(write_model_fixture(tmp_path / "model"), tmp_path)
)
assert unverifiable_fields(fingerprint) == []
assert fingerprint.format_semver == FORMAT_SEMVER
assert fingerprint.rope["factor"] == 4.0
assert fingerprint.engine_version == metadata.version("transformers")
def test_a_machine_without_the_model_says_what_it_cannot_prove(tmp_path: Path) -> None:
gaps = unverifiable_fields(compute_fingerprint(settings_for(None, tmp_path)))
assert gaps == ["model_hash", "tokenizer_hash", "chat_template_hash", "rope"]
def test_an_engine_that_is_not_installed_is_reported_not_guessed(
tmp_path: Path, write_model_fixture: Callable[..., Path]
) -> None:
settings = settings_for(
write_model_fixture(tmp_path / "model"), tmp_path, runtime_backend="sglang"
)
assert "engine_version" in unverifiable_fields(compute_fingerprint(settings))
def test_different_weights_change_the_model_hash_and_are_named(
tmp_path: Path, write_model_fixture: Callable[..., Path]
) -> None:
model = write_model_fixture(tmp_path / "model")
before = compute_fingerprint(settings_for(model, tmp_path))
(model / "model.safetensors.index.json").write_text(
json.dumps({"metadata": {}, "weight_map": {"model-00002.safetensors": "0"}}),
encoding="utf-8",
)
differences = fingerprint_diff(before, compute_fingerprint(settings_for(model, tmp_path)))
assert len(differences) == 1
assert differences[0].startswith("model_hash: cartridge ")
def test_a_rope_change_names_the_field_it_moves(
tmp_path: Path, write_model_fixture: Callable[..., Path]
) -> None:
model = write_model_fixture(tmp_path / "model")
before = compute_fingerprint(settings_for(model, tmp_path))
write_model_fixture(model, rope_parameters={"rope_type": "yarn", "factor": 2.0})
differences = fingerprint_diff(before, compute_fingerprint(settings_for(model, tmp_path)))
rope = [line for line in differences if line.startswith("rope: ")]
assert len(rope) == 1
assert "factor: cartridge 4.0 vs local 2.0" in rope[0]
assert any(line.startswith("model_hash: ") for line in differences)
def test_a_template_in_a_sidecar_file_hashes_the_same(
tmp_path: Path, write_model_fixture: Callable[..., Path], chat_template: str
) -> None:
in_config = write_model_fixture(tmp_path / "a")
sidecar = write_model_fixture(tmp_path / "b", chat_template=None)
(sidecar / "chat_template.json").write_text(
json.dumps({"chat_template": chat_template}), encoding="utf-8"
)
assert compute_fingerprint(settings_for(in_config, tmp_path)).chat_template_hash == (
compute_fingerprint(settings_for(sidecar, tmp_path)).chat_template_hash
)
def test_adapter_weights_are_hashed_by_kind(tmp_path: Path) -> None:
adapters = tmp_path / "adapters" / "hme-router"
adapters.mkdir(parents=True)
(adapters / "adapter_model.safetensors").write_bytes(b"weights-one")
found = adapter_hashes(tmp_path / "adapters")
assert list(found) == ["hme-router"]
(adapters / "adapter_model.safetensors").write_bytes(b"weights-two")
assert found != adapter_hashes(tmp_path / "adapters")
def test_a_missing_adapter_shows_up_as_absent(
tmp_path: Path, write_model_fixture: Callable[..., Path]
) -> None:
adapters = tmp_path / "adapters" / "hme-router"
adapters.mkdir(parents=True)
(adapters / "adapter_model.safetensors").write_bytes(b"weights")
model = write_model_fixture(tmp_path / "model")
stored = compute_fingerprint(settings_for(model, tmp_path), tmp_path / "adapters")
live = compute_fingerprint(settings_for(model, tmp_path), tmp_path / "gone")
differences = fingerprint_diff(stored, live)
assert len(differences) == 1
assert differences[0].startswith("adapters: hme-router: cartridge ")
assert differences[0].endswith("vs local absent")
def test_a_fingerprint_format_change_refuses_the_mount(
tmp_path: Path, write_model_fixture: Callable[..., Path]
) -> None:
live = compute_fingerprint(
settings_for(write_model_fixture(tmp_path / "model"), tmp_path)
)
stored = KvFingerprint.model_validate({**live.model_dump(), "format_semver": "0.9.0"})
assert fingerprint_diff(stored, live) == [
f"format_semver: cartridge 0.9.0 vs this build {FORMAT_SEMVER}"
]
def test_an_unreadable_model_config_fails_loudly(
tmp_path: Path, write_model_fixture: Callable[..., Path]
) -> None:
model = write_model_fixture(tmp_path / "model")
(model / "config.json").write_text("not json", encoding="utf-8")
with pytest.raises(ValueError, match="the model config is unreadable"):
compute_fingerprint(settings_for(model, tmp_path))
+21 -9
View File
@@ -34,19 +34,16 @@ func New(baseURL string) *Client {
} }
} }
// Post sends body as JSON and decodes the response into out. A rejected request // Get fetches a JSON resource into out.
// carries the server's own reason, which is the rule the caller broke. func (c *Client) Get(ctx context.Context, path string, out any) error {
func (c *Client) Post(ctx context.Context, path string, body any, out any) error { request, err := http.NewRequestWithContext(ctx, http.MethodGet, c.BaseURL+path, nil)
payload, err := json.Marshal(body)
if err != nil {
return fmt.Errorf("cannot encode the request for %s: %w", path, err)
}
request, err := http.NewRequestWithContext(ctx, http.MethodPost, c.BaseURL+path, bytes.NewReader(payload))
if err != nil { if err != nil {
return fmt.Errorf("cannot build the request for %s: %w", path, err) return fmt.Errorf("cannot build the request for %s: %w", path, err)
} }
request.Header.Set("Content-Type", "application/json") return c.do(request, path, out)
}
func (c *Client) do(request *http.Request, path string, out any) error {
response, err := c.HTTP.Do(request) response, err := c.HTTP.Do(request)
if err != nil { if err != nil {
return fmt.Errorf("the Ely-Eye API at %s is unreachable; start it with `ely-eye serve`: %w", c.BaseURL, err) return fmt.Errorf("the Ely-Eye API at %s is unreachable; start it with `ely-eye serve`: %w", c.BaseURL, err)
@@ -69,6 +66,21 @@ func (c *Client) Post(ctx context.Context, path string, body any, out any) error
return nil return nil
} }
// Post sends body as JSON and decodes the response into out. A rejected request
// carries the server's own reason, which is the rule the caller broke.
func (c *Client) Post(ctx context.Context, path string, body any, out any) error {
payload, err := json.Marshal(body)
if err != nil {
return fmt.Errorf("cannot encode the request for %s: %w", path, err)
}
request, err := http.NewRequestWithContext(ctx, http.MethodPost, c.BaseURL+path, bytes.NewReader(payload))
if err != nil {
return fmt.Errorf("cannot build the request for %s: %w", path, err)
}
request.Header.Set("Content-Type", "application/json")
return c.do(request, path, out)
}
// serverReason prefers FastAPI's {"detail": "..."} so the caller sees the broken // serverReason prefers FastAPI's {"detail": "..."} so the caller sees the broken
// rule instead of an HTML error page. // rule instead of an HTML error page.
func serverReason(status int, data []byte) string { func serverReason(status int, data []byte) string {
+22
View File
@@ -71,6 +71,28 @@ type CartridgeDetail struct {
AssetReport any `json:"asset_report,omitempty"` AssetReport any `json:"asset_report,omitempty"`
} }
// MountState is what the server lets a search see right now.
type MountState struct {
Mounted []string `json:"mounted"`
Scoped bool `json:"scoped"`
AtomsInScope int `json:"atoms_in_scope"`
}
// CartridgeManifest is the header a cartridge carries, including the KV fingerprint
// that decides whether it may be mounted on this machine.
type CartridgeManifest struct {
CartridgeID string `json:"cartridge_id"`
Name string `json:"name"`
CreatedAt string `json:"created_at"`
ModelBase string `json:"model_base"`
SourceCount int `json:"source_count"`
AtomCount int `json:"atom_count"`
TokenEquivalent int64 `json:"token_equivalent"`
Artifacts map[string]string `json:"artifacts"`
KVFingerprint map[string]any `json:"kv_fingerprint,omitempty"`
DNA string `json:"dna,omitempty"`
}
type Atom struct { type Atom struct {
AtomID string `json:"atom_id"` AtomID string `json:"atom_id"`
SourceID string `json:"source_id"` SourceID string `json:"source_id"`
@@ -0,0 +1,111 @@
package mcpserver
import (
"context"
"errors"
"github.com/ZacharyZhang-NY/ely-eye/ely-eye-mcp/internal/domain"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
type CartridgeIDInput struct {
CartridgeID string `json:"cartridge_id" jsonschema:"exact cartridge id"`
}
type AppendInput struct {
CartridgeID string `json:"cartridge_id" jsonschema:"exact cartridge id"`
AtomIDs []string `json:"atom_ids" jsonschema:"atom ids to add to this cartridge"`
}
type MountOutput struct {
Manifest domain.CartridgeManifest `json:"manifest"`
}
type MountStateOutput struct {
State domain.MountState `json:"state"`
}
// registerMountTools exposes the cartridge as a retrieval scope: the point of a
// cartridge is that the model stops hunting the whole library and asks one cartridge.
func (s *Server) registerMountTools(server *mcp.Server) {
mcp.AddTool(server, &mcp.Tool{
Name: "mounted_cartridges",
Title: "Mounted cartridges",
Description: "Report which cartridges are mounted. A mounted cartridge narrows every later search; nothing mounted means the whole library is searchable.",
}, s.mountsTool)
mcp.AddTool(server, &mcp.Tool{
Name: "mount_cartridge",
Title: "Mount a cartridge",
Description: "Add one cartridge to the mounted set. The server refuses a cartridge whose KV fingerprint does not match this machine, because mismatched RoPE or quantization settings attend to wrong positions instead of failing.",
}, s.mountTool)
mcp.AddTool(server, &mcp.Tool{
Name: "switch_cartridge",
Title: "Switch to one cartridge",
Description: "Mount one cartridge and nothing else.",
}, s.switchTool)
mcp.AddTool(server, &mcp.Tool{
Name: "unmount_cartridge",
Title: "Unmount a cartridge",
Description: "Remove one cartridge from the mounted set and report what is left.",
}, s.unmountTool)
mcp.AddTool(server, &mcp.Tool{
Name: "append_to_cartridge",
Title: "Append memories to a cartridge",
Description: "Add atoms to an existing cartridge. The bundle and its DNA are rewritten, so the cartridge id keeps describing the bytes it holds.",
}, s.appendTool)
}
func (s *Server) mountsTool(ctx context.Context, _ *mcp.CallToolRequest, _ struct{}) (*mcp.CallToolResult, MountStateOutput, error) {
var state domain.MountState
if err := s.api.Get(ctx, "/api/mounts", &state); err != nil {
return nil, MountStateOutput{}, err
}
return nil, MountStateOutput{State: state}, nil
}
func (s *Server) mountTool(ctx context.Context, _ *mcp.CallToolRequest, input CartridgeIDInput) (*mcp.CallToolResult, MountOutput, error) {
if input.CartridgeID == "" {
return nil, MountOutput{}, errors.New("cartridge_id is required")
}
var manifest domain.CartridgeManifest
if err := s.api.Post(ctx, "/api/cartridges/"+input.CartridgeID+"/mount", struct{}{}, &manifest); err != nil {
return nil, MountOutput{}, err
}
return nil, MountOutput{Manifest: manifest}, nil
}
func (s *Server) switchTool(ctx context.Context, _ *mcp.CallToolRequest, input CartridgeIDInput) (*mcp.CallToolResult, MountOutput, error) {
if input.CartridgeID == "" {
return nil, MountOutput{}, errors.New("cartridge_id is required")
}
var manifest domain.CartridgeManifest
if err := s.api.Post(ctx, "/api/cartridges/"+input.CartridgeID+"/switch", struct{}{}, &manifest); err != nil {
return nil, MountOutput{}, err
}
return nil, MountOutput{Manifest: manifest}, nil
}
func (s *Server) unmountTool(ctx context.Context, _ *mcp.CallToolRequest, input CartridgeIDInput) (*mcp.CallToolResult, MountStateOutput, error) {
if input.CartridgeID == "" {
return nil, MountStateOutput{}, errors.New("cartridge_id is required")
}
var state domain.MountState
if err := s.api.Post(ctx, "/api/cartridges/"+input.CartridgeID+"/unmount", struct{}{}, &state); err != nil {
return nil, MountStateOutput{}, err
}
return nil, MountStateOutput{State: state}, nil
}
func (s *Server) appendTool(ctx context.Context, _ *mcp.CallToolRequest, input AppendInput) (*mcp.CallToolResult, MountOutput, error) {
if input.CartridgeID == "" {
return nil, MountOutput{}, errors.New("cartridge_id is required")
}
if len(input.AtomIDs) == 0 {
return nil, MountOutput{}, errors.New("atom_ids needs at least one atom")
}
var manifest domain.CartridgeManifest
if err := s.api.Post(ctx, "/api/cartridges/"+input.CartridgeID+"/append", input, &manifest); err != nil {
return nil, MountOutput{}, err
}
return nil, MountOutput{Manifest: manifest}, nil
}
@@ -0,0 +1,146 @@
package mcpserver
import (
"context"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/ZacharyZhang-NY/ely-eye/ely-eye-mcp/internal/apiclient"
)
const manifestBody = `{"cartridge_id":"home_1","name":"home","created_at":"2026-05-01T12:00:00Z",` +
`"model_base":"Qwen/Qwen3.5-9B","source_count":1,"atom_count":2,"token_equivalent":40,` +
`"artifacts":{"atoms":"atoms.parquet"},"dna":"deadbeef",` +
`"kv_fingerprint":{"format_semver":"1.0.0","rope":{"factor":4.0}}}`
func mountServer(t *testing.T, status int, body string, seen *[]string) *httptest.Server {
t.Helper()
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
*seen = append(*seen, r.Method+" "+r.URL.Path)
w.WriteHeader(status)
_, _ = w.Write([]byte(body))
}))
}
func TestMountedCartridgesAsksTheServer(t *testing.T) {
var seen []string
server := mountServer(t, http.StatusOK, `{"mounted":["home_1"],"scoped":true,"atoms_in_scope":2}`, &seen)
defer server.Close()
s := &Server{api: apiclient.New(server.URL)}
_, out, err := s.mountsTool(context.Background(), nil, struct{}{})
if err != nil {
t.Fatalf("mounted_cartridges failed: %v", err)
}
if !out.State.Scoped || out.State.AtomsInScope != 2 || len(out.State.Mounted) != 1 {
t.Errorf("mount state = %+v, want the server's scope", out.State)
}
if len(seen) != 1 || seen[0] != "GET /api/mounts" {
t.Errorf("requests = %v, want one GET to /api/mounts", seen)
}
}
func TestMountCartridgeReturnsTheManifestItMounted(t *testing.T) {
var seen []string
server := mountServer(t, http.StatusOK, manifestBody, &seen)
defer server.Close()
s := &Server{api: apiclient.New(server.URL)}
_, out, err := s.mountTool(context.Background(), nil, CartridgeIDInput{CartridgeID: "home_1"})
if err != nil {
t.Fatalf("mount_cartridge failed: %v", err)
}
if out.Manifest.CartridgeID != "home_1" || out.Manifest.DNA != "deadbeef" {
t.Errorf("manifest = %+v, want the mounted cartridge", out.Manifest)
}
if out.Manifest.KVFingerprint["rope"] == nil {
t.Error("manifest lost the kv fingerprint that gated the mount")
}
if len(seen) != 1 || seen[0] != "POST /api/cartridges/home_1/mount" {
t.Errorf("requests = %v, want one POST to the mount route", seen)
}
}
func TestSwitchAndUnmountUseTheirOwnRoutes(t *testing.T) {
var seen []string
server := mountServer(t, http.StatusOK, manifestBody, &seen)
defer server.Close()
s := &Server{api: apiclient.New(server.URL)}
if _, _, err := s.switchTool(context.Background(), nil, CartridgeIDInput{CartridgeID: "home_1"}); err != nil {
t.Fatalf("switch_cartridge failed: %v", err)
}
if _, _, err := s.unmountTool(context.Background(), nil, CartridgeIDInput{CartridgeID: "home_1"}); err != nil {
t.Fatalf("unmount_cartridge failed: %v", err)
}
want := []string{"POST /api/cartridges/home_1/switch", "POST /api/cartridges/home_1/unmount"}
if strings.Join(seen, ",") != strings.Join(want, ",") {
t.Errorf("requests = %v, want %v", seen, want)
}
}
func TestMountRefusalCarriesTheServersReason(t *testing.T) {
var seen []string
server := mountServer(t, http.StatusBadRequest,
`{"detail":"home_1 was built for a different runtime; refusing to mount: rope: factor: cartridge 4.0 vs local 2.0"}`,
&seen)
defer server.Close()
s := &Server{api: apiclient.New(server.URL)}
_, _, err := s.mountTool(context.Background(), nil, CartridgeIDInput{CartridgeID: "home_1"})
if err == nil {
t.Fatal("an incompatible cartridge must not mount quietly")
}
if !strings.Contains(err.Error(), "rope: factor: cartridge 4.0 vs local 2.0") {
t.Errorf("error = %v, want the field the server named", err)
}
}
func TestMountToolsRefuseAnEmptyCartridgeID(t *testing.T) {
var seen []string
server := mountServer(t, http.StatusOK, manifestBody, &seen)
defer server.Close()
s := &Server{api: apiclient.New(server.URL)}
if _, _, err := s.mountTool(context.Background(), nil, CartridgeIDInput{}); err == nil {
t.Error("mount without an id must fail before the network")
}
if _, _, err := s.switchTool(context.Background(), nil, CartridgeIDInput{}); err == nil {
t.Error("switch without an id must fail before the network")
}
if _, _, err := s.unmountTool(context.Background(), nil, CartridgeIDInput{}); err == nil {
t.Error("unmount without an id must fail before the network")
}
if _, _, err := s.appendTool(context.Background(), nil, AppendInput{CartridgeID: "home_1"}); err == nil {
t.Error("append without atoms must fail before the network")
}
if len(seen) != 0 {
t.Errorf("requests = %v, want none", seen)
}
}
func TestAppendSendsTheAtomsItNames(t *testing.T) {
var seen []string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
body := make([]byte, 512)
n, _ := r.Body.Read(body)
seen = append(seen, r.URL.Path+" "+string(body[:n]))
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(manifestBody))
}))
defer server.Close()
s := &Server{api: apiclient.New(server.URL)}
_, out, err := s.appendTool(context.Background(), nil, AppendInput{CartridgeID: "home_1", AtomIDs: []string{"mem-1:text"}})
if err != nil {
t.Fatalf("append_to_cartridge failed: %v", err)
}
if out.Manifest.AtomCount != 2 {
t.Errorf("manifest = %+v, want the rewritten atom count", out.Manifest)
}
if len(seen) != 1 || !strings.Contains(seen[0], "/append") || !strings.Contains(seen[0], "mem-1:text") {
t.Errorf("requests = %v, want one append carrying the atom id", seen)
}
}
+2 -1
View File
@@ -33,10 +33,11 @@ func Run(ctx context.Context, cfg config.Config, version string) error {
Title: "Ely-Eye Context Cartridge MCP", Title: "Ely-Eye Context Cartridge MCP",
Version: version, Version: version,
}, &mcp.ServerOptions{ }, &mcp.ServerOptions{
Instructions: "Use Ely-Eye tools to retrieve local cartridge context, evidence atoms, proof suites, and compiled evidence packs. Read tools mirror local Ely-Eye state without writing. remember, supersede_memory and forget_memory mutate memory through the Ely-Eye API, which must be running (`ely-eye serve`); the server, not the caller, decides who owns a memory.", Instructions: "Use Ely-Eye tools to retrieve local cartridge context, evidence atoms, proof suites, and compiled evidence packs. Read tools mirror local Ely-Eye state without writing. remember, supersede_memory and forget_memory mutate memory through the Ely-Eye API, which must be running (`ely-eye serve`); the server, not the caller, decides who owns a memory. mount_cartridge and switch_cartridge decide what a search may see: mount a cartridge to ask only that cartridge, and unmount everything to ask the whole library.",
}) })
server.registerTools(mcpServer) server.registerTools(mcpServer)
server.registerWriteTools(mcpServer) server.registerWriteTools(mcpServer)
server.registerMountTools(mcpServer)
server.registerResources(mcpServer) server.registerResources(mcpServer)
if err := mcpServer.Run(ctx, &mcp.StdioTransport{}); err != nil { if err := mcpServer.Run(ctx, &mcp.StdioTransport{}); err != nil {
if errors.Is(err, io.EOF) || strings.HasSuffix(err.Error(), "EOF") { if errors.Is(err, io.EOF) || strings.HasSuffix(err.Error(), "EOF") {
+64 -3
View File
@@ -11,6 +11,19 @@ export ELY_EYE_API="http://127.0.0.1:${port}"
python_bin="$root/.venv/bin/python" python_bin="$root/.venv/bin/python"
[ -x "$python_bin" ] || python_bin="python3" [ -x "$python_bin" ] || python_bin="python3"
# A model snapshot the KV fingerprint can read: without it every mount is refused as
# unverifiable, which is the behaviour, but the smoke test also needs a mount to succeed.
model_dir="$home/model"
mkdir -p "$model_dir"
cat > "$model_dir/config.json" <<'JSON'
{"model_type": "qwen3_5", "max_position_embeddings": 262144, "partial_rotary_factor": 0.25,
"rope_parameters": {"rope_type": "yarn", "factor": 4.0, "mrope_section": [11, 11, 10]},
"chat_template": "{% for message in messages %}{{ message['content'] }}{% endfor %}"}
JSON
printf '{"version": "1.0"}' > "$model_dir/tokenizer.json"
printf '{"metadata": {}, "weight_map": {"model-00001.safetensors": "0"}}' > "$model_dir/model.safetensors.index.json"
export ELY_EYE_MODEL_DIR="$model_dir"
api="" api=""
cleanup() { cleanup() {
if [ -n "$api" ]; then kill "$api" 2>/dev/null || true; fi if [ -n "$api" ]; then kill "$api" 2>/dev/null || true; fi
@@ -34,12 +47,24 @@ fi
(cd "$root/ely-eye-mcp" && go build -o "$home/ely-eye-mcp" ./cmd/ely-eye-mcp) (cd "$root/ely-eye-mcp" && go build -o "$home/ely-eye-mcp" ./cmd/ely-eye-mcp)
"$python_bin" - "$home/ely-eye-mcp" <<'PY' printf 'the pantry keeps flour\n' > "$home/pantry.txt"
uploaded="$(curl -sf -X POST "http://127.0.0.1:${port}/api/ingest/upload?cartridge_name=home" \
-F "files=@${home}/pantry.txt;type=text/plain")"
cartridge_id="$(printf '%s' "$uploaded" | "$python_bin" -c 'import json,sys; print(json.load(sys.stdin)["cartridge_id"])')"
if [ -z "$cartridge_id" ] || [ "$cartridge_id" = "None" ]; then
echo "FAIL: the upload did not materialize a cartridge: $uploaded"
exit 1
fi
"$python_bin" - "$home/ely-eye-mcp" "$cartridge_id" "$model_dir" <<'PY'
import json import json
import pathlib
import subprocess import subprocess
import sys import sys
binary = sys.argv[1] binary = sys.argv[1]
cartridge_id = sys.argv[2]
model_dir = pathlib.Path(sys.argv[3])
proc = subprocess.Popen( proc = subprocess.Popen(
[binary, "server"], [binary, "server"],
stdin=subprocess.PIPE, stdin=subprocess.PIPE,
@@ -82,7 +107,8 @@ call(1, "initialize", {
send({"jsonrpc": "2.0", "method": "notifications/initialized"}) send({"jsonrpc": "2.0", "method": "notifications/initialized"})
names = [item["name"] for item in call(2, "tools/list", {})["tools"]] names = [item["name"] for item in call(2, "tools/list", {})["tools"]]
for required in ("remember", "supersede_memory", "forget_memory"): for required in ("remember", "supersede_memory", "forget_memory", "mounted_cartridges",
"mount_cartridge", "switch_cartridge", "unmount_cartridge", "append_to_cartridge"):
if required not in names: if required not in names:
raise SystemExit(f"FAIL: {required} is not registered; tools are {names}") raise SystemExit(f"FAIL: {required} is not registered; tools are {names}")
@@ -121,5 +147,40 @@ gone = call(3, "tools/call", {"name": "fetch_atom", "arguments": {"atom_id": ato
if not gone.get("isError") or "not found" not in gone["content"][0]["text"]: if not gone.get("isError") or "not found" not in gone["content"][0]["text"]:
raise SystemExit(f"FAIL: the forgotten memory is still readable: {gone}") raise SystemExit(f"FAIL: the forgotten memory is still readable: {gone}")
print("PASS: remember, supersede, search and forget work through MCP") state = tool("mounted_cartridges", {})["state"]
if state["mounted"] or state["scoped"]:
raise SystemExit(f"FAIL: a fresh store reports a scope: {state}")
outside = tool("remember", {"text": "the spare key hides in the garage", "scenario": "work"})["atom"]
manifest = tool("mount_cartridge", {"cartridge_id": cartridge_id})["manifest"]
if manifest["cartridge_id"] != cartridge_id or not manifest.get("dna"):
raise SystemExit(f"FAIL: mount returned the wrong header: {manifest}")
state = tool("mounted_cartridges", {})["state"]
if not state["scoped"] or state["mounted"] != [cartridge_id]:
raise SystemExit(f"FAIL: the mount did not take effect: {state}")
if tool("search_atoms", {"query": "garage"})["hits"]:
raise SystemExit("FAIL: a mounted cartridge let a memory outside it answer")
grown = tool("append_to_cartridge", {"cartridge_id": cartridge_id, "atom_ids": [outside["atom_id"]]})["manifest"]
if grown["dna"] == manifest["dna"]:
raise SystemExit("FAIL: append rewrote nothing; the DNA is unchanged")
if not any(hit["atom"]["atom_id"] == outside["atom_id"] for hit in tool("search_atoms", {"query": "garage"})["hits"]):
raise SystemExit("FAIL: an appended memory is still not reachable")
tool("unmount_cartridge", {"cartridge_id": cartridge_id})
if tool("mounted_cartridges", {})["state"]["scoped"]:
raise SystemExit("FAIL: unmount left the store scoped")
(model_dir / "config.json").write_text(json.dumps({
"model_type": "qwen3_5",
"max_position_embeddings": 262144,
"partial_rotary_factor": 0.25,
"rope_parameters": {"rope_type": "yarn", "factor": 2.0},
"chat_template": "{% for message in messages %}{{ message['content'] }}{% endfor %}",
}), encoding="utf-8")
refused = call(3, "tools/call", {"name": "mount_cartridge", "arguments": {"cartridge_id": cartridge_id}})
if not refused.get("isError") or "rope" not in refused["content"][0]["text"]:
raise SystemExit(f"FAIL: a cartridge built for RoPE factor 4.0 mounted on a factor 2.0 machine: {refused}")
print("PASS: remember, supersede, search, forget and cartridge mounting work through MCP")
PY PY