Commit Graph
4 Commits
Author SHA1 Message Date
ZacharyZhang-NY 9fe13fffb3 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.
2026-10-07 16:40:27 -04:00
ZacharyZhang-NY 674c88e84e Rank memory search in SQL instead of rebuilding BM25 in Python
The product path pulled every atom into Python and rebuilt a BM25Okapi
index per query: p50 20.16s over 165,973 atoms. It now runs one SQLite
statement with LIMIT/OFFSET.

- text_index.py: one normalizer for both sides of the index. CJK runs are
  split per character (unicode61 cannot segment them), camelCase and
  separators are split so "search atoms" finds SearchAtoms, latin tokens
  get a prefix match. Rows written before this existed hold raw text in
  atom_fts, so schema v3 rebuilds the index on upgrade.
- retrieval.py: filters (holder / scenario / source / modality / since /
  until / min_trust), a base64 cursor, and reciprocal rank fusion in place
  of the 0.55/0.35/0.10 blend whose graph term was always zero.
- An empty result is {status: "no_match"}, not a bare array.
- Go: search_atoms and compile_context rank through POST /api/search. The
  second Go tokenizer is deleted; two implementations could drift, which
  is what the MCP/HTTP parity requirement forbids.
- db.py passed 500 lines, so the row codec moved to row_codec.py.
- Tooling: installed the repo's own pyright. pyrightconfig.json had no
  exclude and swept the downloaded corpus (1171 errors); with excludes the
  count is 62, all pre-existing (evals 48, profiles 4, training 4,
  ingestion 3, runtime 2, cartridge_assets 1). backend/pyproject.toml
  carried a second [tool.pyright] that shadowed the root config; deleted.
- bench/scale_check.py measures the product search at corpus size. The
  100k-atom p50 number is not written until it is measured on an idle
  machine.

Verification: 112 pytest passed; go build/vet/test clean, gofmt clean;
ruff clean on every touched file; mypy clean on the 14 touched Python
files; repo pyright 62 errors, unchanged per-file ledger.
2026-10-07 16:02:40 -04:00
ZacharyZhang-NY 3d792417cf Give the write path a CLI and an MCP face
remember / supersede / forget now reach the one Python writer from a
terminal, from MCP, and from HTTP. The server, not the caller, decides
who owns a memory.

- cli: remember, supersede, forget. --holder falls back to
  settings.default_holder until the holder registry lands. Timestamps are
  parsed once with one clear message.
- ingestion: import pymupdf as fitz. The deprecated fitz import printed a
  warning to stdout, so every CLI command's JSON answer was unparseable.
- mcp: remember, supersede_memory and forget_memory forward to the Python
  writer over HTTP (ELY_EYE_API, default http://127.0.0.1:8765). SQLite
  stays read-only to the Go side, so the write rules live in one place.
- mcp reads follow schema v2: atoms.time became ingested_at, and reads go
  through v_atoms_current, so a superseded memory stops being findable.
  holder, origin, scenario and event_time now show up in fetch_atom and
  search_atoms.
- scripts/mcp_write_smoke.sh proves the loop end to end: remember, search,
  supersede, forget, then not found.

Evidence: 92 pytest passed; go build, go vet and go test clean; mypy clean
on the 10 touched source files with a fresh cache; ruff clean except a
pre-existing B008 at cli.py:292, identical at HEAD.
2026-10-07 15:09:17 -04:00
ZacharyZhang-NY 0ee99e271d Ely-Eye — local multimodal context operating system on Qwen3.5-9B
Real-models implementation: multimodal ingestion, Context Cartridge memory,
sparse + Qwen embedding retrieval, Cache Fabric, Qwen runtime adapters, verifier
contracts, Visual HashHop proofs, BF16 LoRA fine-tuning, and a local dashboard.
Verified by a 15-check PRD proof suite on the live model.
2026-05-28 22:08:08 -04:00