microscope-memory 1.2.0

Pure binary cognitive memory engine with REST API bridge for LLM integration. Zero-JSON core, mmap-based, hierarchical, sub-microsecond search.
Documentation
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [1.2.0] - 2026-10-13

### Added
- `Spine::begin_publish()` returns an exclusive `Publish` batch with typed
  read/write methods, `tick_stream`, and explicit commit (drop also commits).
  The consciousness stream publishes its scalar tick and all due aggregate
  headers in one window, retaining the existing ten-cycle aggregate interval.
- `Spine::read_store_snapshot()` validates all eleven layer headers and the
  superblock together. Prefixes are 128 bytes so the 108-byte Emotion header
  fits; compile-time bounds cover every encoded header. The summary uses this
  snapshot and reports failure instead of falling back to mixed reads.
- Whole-store tearing regressions cover shared-handle threads, independent
  handles and a child process, and require observed publish progress. Unit
  tests cover empty/read-only batches, drop/unwind, short mappings, byte-64+
  Emotion fields and interrupted counters. `bench_spine_publish` measures
  batching separately from end-to-end stream execution.

### Fixed
- Every standalone header write now participates in the seqlock. The mapping
  mutex is acquired before the publisher lock: `flock` is tied to the open
  file description, so a second thread on one handle must not acquire/unlock
  that descriptor while the first thread publishes.
- Republishing after an interrupted odd counter no longer exposes a transient
  even value. Windows' shared-writer-mark fallback no longer references the
  Unix-only `as_raw_fd` API; cross-handle exclusion remains Unix/advisory only.
- Batches do not promise rollback or durability: unwinding publishes the
  already-written subset, and crash recovery cannot restore the old state.

### Fixed
- **CI never ran the tearing regression.** `build-and-test` invokes `cargo test --lib`
  and two named integration targets, so `tests/spine_torn_read.rs` compiled
  (`Build all targets`) and was never executed. It has its own step now. Same
  shape as the `Mojibake` gate sitting behind `Formatting` and never getting a
  turn: a check nobody runs is decoration.
- **Two files were committed mojibaked and nobody noticed, because the gate that
  catches it could never run.** `examples/bench_recall.rs` (497 marker hits) and
  `tests/append_cache_liveness.rs` (32) carry UTF-8 read through CP1250 and
  re-saved, so `//! Futtatás` instead of `//! Futtatás`. The <!-- mojibake-ok: a serult alakot szandekosan mutatjuk -->
  `standards` job runs `Mojibake` *after* `Formatting`, and formatting had been
  red on this tree long enough that the mojibake check never got a turn — the
  first commit to fix `cargo fmt` is also the first commit that fails on the
  real state of the text. Neither file has a clean revision in history: they were
  born damaged. Repaired by re-encoding through the CP1250 decode map, iterated
  per file until the marker set empties, and accepted only on the condition that
  the ASCII skeleton of the file is byte-identical before and after — so the
  change cannot touch code, string escapes or delimiters, only the accented
  text. `python3 scripts/check_mojibake.py --worktree` reports 360 files clean.

### Added
- `examples/bench_spine.rs` gained a **contended** section (n publishers × m readers over one `spine.bin`): throughput per configuration plus a tearing count judged from inside the page, with unpopulated pages counted separately. Measured: 2.6 M snapshots per sweep, three sweeps, zero torn reads. The single-threaded sections are now labelled as such, and the bench warns when `data/` is missing instead of silently benchmarking empty directories.

### Fixed
- **`HebbianHeader::decay_event_count` is no longer corrupted** (`src/consciousness_stream.rs`). `publish_spine` did `+= s.cycle` on every cycle, so the field grew as a sum of cycle values while nothing counted decay events — `HebbianState::apply_drift` decays energies without keeping a tally. The write is gone; the field and `ArchetypeHeader::decay_events` now say in their docs that they have no producer and no consumer, which is the honest state of a fixed-size page layout that cannot be shrunk without a version bump.
- **`UNSAFE.md` covers the spine publish window.** The new `unsafe`
  surface — `AtomicU64::from_ptr` over the mapping, the raw `flock(2)`
  declaration, and the `map_mut` of a resized file — is inventoried with what
  a reviewer has to check, and `seq_atomic` now asserts its length bound in
  release too, so a future caller that forgets its guard panics rather than
  reading outside the mapping. `UNSAFE.md`'s counts were re-derived and the
  drift against the previous pass is stated instead of hidden.
- **The tearing regression proves what it claims** (`tests/spine_torn_read.rs`). Its header said every test runs an unbracketed
  control read; `torn_read_two_writers` did not have one, and the other two
  stopped their publisher after a fixed number of writes, so a writer that
  finished before the readers turned the test green and empty. Publishers now
  run until the readers are done, the two-publisher test asserts publish
  progress (its half-page control measured 0/120 000 once a second reader
  shared the handle, so it was not assertable), and the header states what
  each of the four counters measures — `open_window` is a property of the
  read path, not of the writer.
- **Spine publish is now one coherent snapshot, and it is a seqlock**
  (`src/spine.rs`). The previous commit's message claimed "1 StreamPage write
  + 1 epoch publish" with "Release/Acquire" ordering; what the code did was
  take the mapping mutex twice — once for the stream page, once for a
  millisecond timestamp in the superblock that nothing read back — and call
  the second `drop()` a release barrier. Two handles or two processes each own
  a `Mutex<MmapMut>`, so the two locks exclude nothing and there is no
  happens-before between a writer in one process and a reader in another: a
  reader could pair a new stream header with an older record, and two ticks
  inside the same millisecond were indistinguishable.
  - `tick()` now takes `flock(LOCK_EX)` on a new `spine.publish` sidecar (the
    piece that serialises publishers across processes), then the mapping lock
    once, and brackets the stream page *and* the superblock payload in a single
    seqlock window: an odd `AtomicU64` at `SEQ_SLOT` (superblock byte 56)
    while writing, the next even value stored with `Release`.
  - `Superblock` gains a monotonic `tick_seq` (superblock format version 2);
    `epoch_ms` stays as a wall-clock field, not a sequence number.
  - `Spine::read_snapshot` is the matching read: copy under the counter,
    retry if it moved, so a torn pairing is not returned. `format_summary` and
    `examples/spine_live.rs` now read through it, and `Spine::open_ro` is a
    handle that cannot mark itself a writer or resize the file underneath one.
  - `write_slot` on the superblock slot is truncated to the payload so a page
    write can never reset the counter; a window left open by a killed writer
    is repairable via `Spine::recover_stalled_publish` (`read_snapshot` calls
    it when its retry budget is exhausted rather than staying wedged).
- **`tests/spine_torn_read.rs`**: torn-read regression for the three setups the
  old code failed — two handles in one process, two publishers, two processes.
  Each asserts zero mixed/torn/window-open reads *and* runs an unbracketed
  control read on the same file that is required to still show the mixing, so
  the suite cannot pass by quietly measuring nothing. Measured on Linux with
  the dev profile: bracketed reads 0 violations in all three setups; the
  control read reported the mixing.
- `cargo fmt --all -- --check` passes again (13 files were failing, 64 hunks in
  `src/spine.rs`), which was keeping `cargo fmt` red in CI.
- Removed the unused optional `winapi` dependency: nothing referenced it and no
  feature selected it (it never reached Cargo.lock, so no lockfile churn).


## [1.0.0] - 2026-10-13

First 1.x release. The headline is recall and startup latency, measured
in-process (see BENCHMARKS.md), with the index policy and the breaking API
change stated honestly below.

### Added
- **Spine (`src/spine.rs`)**: the consciousness stream's whole scalar layer
  state (cycle, surprise, curiosity, prediction, Hebbian totals plus ten
  per-layer headers) in a single `spine.bin` mmap, published by the 10 Hz
  background tick. Startup: 19.2 ms of per-layer `load_or_init` file reads
  becomes a 1.08 ms mmap open. New deps: `byteorder`, `parking_lot`.
- **Inverted text index wired into `find_text`/`find_text_ranked`**: the TIX1
  index (`text_index.rs`, writer/reader/parser already in-tree, `build` already
  emitting it) is now actually consulted. `MicroscopeReader::candidate_blocks`
  prefilters to the index's candidate set with an exact substring predicate
  still run on every candidate, so results are bit-identical to a full scan
  (asserted per-query in `examples/bench_recall_ab.rs`).
- **`refresh_append_log` + self-validating append cache**: `find_text_all_ranked`
  no longer re-reads and re-parses `append.bin` per query; the reader caches it
  at open and re-validates against the file's (len, mtime) on every call
  (~1 us metadata check vs ~230 us re-read), so a reader held across stores
  still sees new appends. Regression tests: `tests/append_cache_liveness.rs`.
- **`examples/index_text.rs`**: regenerates `text_index.bin` for an existing
  data dir without a full rebuild.
- **`examples/bench_recall_ab.rs`**: interleaved A/B benchmark (indexed vs
  full scan, same process, alternating rounds, warm-up discarded, results
  asserted equal).
- **`RecallContext` (`src/main.rs`)**: the reader plus all seventeen layer
  states opened once and reused across `recall()` calls, instead of ~20
  whole-file reads per call. `recall()` itself opens a fresh context per
  invocation (behaviour identical to before); the bench loop and future
  server paths hold one context. Saves are unchanged, so durability is
  unchanged. Steady-state recall: 2.3 ms → 0.3 ms (7x).
- **`recall_ctx` library module + MCP shared reader**: `RecallContext` moved
  to `src/recall_ctx.rs` with a generation-guarded `SharedContext`
  (reopens transparently when `meta.bin` changes, i.e. after any rebuild)
  and a process-global `with_shared()` accessor. The `memory_find` and
  `memory_recall` MCP tools now serve from the shared reader instead of
  reopening per call; mutable layer states stay per-call by design (their
  stream-vs-disk precedence and save schedule were not safe to guess —
  documented in the wiring).
- **`pub fn contains_ci` + `tests/contains_ci.rs`**: allocation-free
  case-insensitive search, kept as a tested utility. See Performance notes.

### Changed
- **BREAKING: `find_text_all_ranked` takes `&mut self`** (the cached append
  log may refresh). External callers need `let mut reader`.
- Constructor and unit-test fixture struct literals gain the new
  `append_log` / `append_log_stamp` fields.
- `Cargo.toml`: `byteorder 1.5`, `parking_lot 0.12` added as direct deps.

### Performance
- Startup (24 per-layer state files → 1 mmap): **17.8x** cold, **2.9x** warm.
  - *Labelled 2026-10-13:* this figure is `bench_spine`'s A-vs-B comparison, whose "before" side loads the layer states from a `data/` directory that is not in this repository, and whose "24 files" label counts the files the layer set opens rather than the eight the benchmark calls. It was not re-measured here (the crate cannot be built without crates.io access); the spine-side numbers that can be measured were: `Spine::open` 531 µs cold / 10.9 µs warm, `tick()` 666 ns, and `read_snapshot` 103 ns against the 805 ns two-read pattern it replaces. See the "Spine publish protocol" section of BENCHMARKS.md.
- Recall, in-process, 23,774 blocks, 19 interleaved rounds:
  **901 us → 93 us per query (9.7x)**, bit-identical results.
- The existing end-to-end finding (§"Inverted text index", 2026-09-27,
  695k blocks, ~91 ms both paths) stands: at that scale process start-up
  dominates, so the user-visible `find` is unchanged. The 9.7x is the
  in-process text path, where the loop-bound services (bridge, MCP) live.
- Attempted and **rejected**: replacing the per-block `to_lowercase()` with
  a hand-rolled allocation-free scan (`contains_ci`). Measured in-process at
  **5.4x slower** (913 ns vs 4.9 us per block-compare); std `str::contains`
  is a SIMD two-way scan and cannot be beaten by a byte loop. Documented in
  the function's doc comment and its test file so nobody re-attempts it.

### Notes for operators
- After a **light rebuild** the text index is **regenerated** from the final
  layout at the end of `build()` (cheap, content-derived), so the indexed
  recall path survives rebuilds. The stale mid-build file is still dropped
  first and never served — `test_light_rebuild_regenerates_text_index` pins
  that the regenerated file matches the new block count and returns results
  identical to a full scan.
- `cargo test --release` is unsupported by this crate's own profile
  (`[profile.release]` sets `panic = "abort"`; tests need `unwind`). The
  release gate is `cargo build --release` plus debug tests plus CI.

### Added
- **Shared hybrid lexical-spatial relevance ranker** (`relevance.rs`): query-token
  coverage, phrase evidence, spatial distance and a small importance prior replace
  the legacy clamped keyword-distance formula. Deterministic regression gate:
  Recall@3 = 1.000, MRR = 1.000 on the fixture corpus (`tests/relevance_benchmark.rs`).
- **`stats` data-footprint report**: per-file sizes of the binary index
  (`embeddings.bin`, `merkle.bin`, `links.bin`, ...), the stale `index-history/`
  snapshots, `layers/*.txt` total and the configured retention bound, so
  unbounded growth is visible from one command.

### Changed
- **Size-bounded memory eviction**: `max_blocks` (0 = unbounded) and
  `protect_min_importance` (default 8) under `[index]`. When the index exceeds
  the cap, the dream cycle evicts the lowest-scoring blocks — score =
  importance × 10 + recall energy − age penalty — and rewrites `embeddings.bin`
  in sync. Blocks with importance >= `protect_min_importance` are never evicted.
- **Durable importance**: layer entries now carry a leading `(imp=N)` marker and
  the build reads it instead of flattening every layer block to importance 5, so
  rebuilds no longer erase stored importance. The dream cycle automatically
  promotes frequently recalled blocks (`promote_energy_threshold`, default
  0.35): energy >= threshold bumps importance by one, capped at the protection
  floor, mirrored back into the layer source.

### Fixed
- **Precise layer promotion**: the dream's importance mirror now matches the
  layer entry by exact FNV-1a hash equality of the marker-stripped text instead
  of substring matching, so overlapping entries can never receive a wrong bump
  and at most one entry changes per promoted block.
- **Release-safe header reads**: `MicroscopeReader::header` is now bounds
  checked with `assert!` in all builds (previously `debug_assert!`, which is
  compiled out of release). The hot whole-index loops use the explicitly
  `unsafe header_unchecked`, so an out-of-range index can never read outside
  the mmap region again.
- **Bounded MCP framing**: the stdio MCP server rejects `Content-Length` values
  above 16 MiB before allocating the body buffer (previously an unbounded
  `vec![0u8; len]`), and caps header lines at 64 KiB so a client that never
  terminates its headers cannot grow memory without bound.
- **Dependency audit**: cleared all seven `cargo audit` vulnerabilities —
  pyo3 0.20 → 0.29 (also adds Python 3.14 support and fixes the Cargo.toml
  target-section bug that made the optional native deps unresolvable on native
  builds), crossbeam-epoch 0.9.20, quinn-proto 0.11.16, rustls-webpki 0.103.13;
  anyhow/memmap2/rand updated to clear the unsound advisories.
- **NaN-safe ranking**: 64 `partial_cmp(&...).unwrap()` f32 comparators across
  the recall/rank/sort paths replaced with `total_cmp`, so a NaN score can
  never panic a hot path.
- **Bridge auth**: optional `[server] api_key` — when set, every inbound bridge
  request must carry `X-API-Key: <key>` (checked before the handlers, after
  CORS so preflight keeps working). The bridge refuses to start on a
  non-loopback host without a key, and the single-user localhost default is
  documented in the README.
- **Per-user bridge tokens**: `microscope-mem token <user_id>` generates an
  HMAC-SHA256-signed bearer token (`user_id.<hmac>`, constant-time verified).
  The middleware binds the memory scope to the verified `user_id`, so users
  are separated from each other, not just from outsiders; the shared
  `X-API-Key` mode remains as the trusted-team fallback.
- **Poison-safe locks**: 200 `RwLock`/`Mutex` `.read()/.write()/.lock().unwrap()`
  sites across the codebase now go through `sync_guard::{read_lock, write_lock,
  mutex_lock}`, which recover from a poisoned lock instead of panicking forever.
  One stray panic while holding a lock can no longer permanently break a
  subsystem for the rest of the process lifetime.
- **Self-defensive multimodal decoding**: `decode_image_meta` and
  `decode_audio_meta` now return `Option` and reject short input up front
  instead of relying on every caller to bounds-check first.
- **Cross-process snapshot mmap**: `consciousness_seqlock` now ships a real
  file-backed mmap layer (`MappedSnapshot` / `MappedSnapshotWriter`) mapping
  the `#[repr(C)]` seqlock header + `SnapshotData` block via `memmap2`, so two
  processes sharing a file see the same snapshot with the same seqlock
  protocol (odd/even sequence, retry on torn reads). The heap-backed fast-path
  fields remain in-process only. The `ConsciousnessStream` background cycle now
  publishes to `<output_dir>/consciousness_snapshot.bin` every tick, so another
  process can map the file and read the live consciousness snapshot. Covered by
  roundtrip, uninitialized-file, no-torn-reads-under-concurrency and
  stream-publishes-to-mmap tests.
- **Honored `[server] cors_origin`**: the bridge previously hardcoded
  `allow_origin(Any)` and silently ignored the config value. It now uses the
  configured origin (exact match; the requester's origin is never reflected),
  with `"*"`/unset keeping the open default. Covered by a CORS test.
- **Deterministic morphogenesis**: the growth functions derive their RNG from
  `Seed::rng()` (FNV hash of the seed id + position), so the same seed always
  produces the same organism — the previously flaky, RNG-driven tests are now
  fully deterministic. The evolution loop's mutation RNG is now seeded from
  the generation number too, so a whole `evolve_population` run is
  reproducible for the same initial seeds (verified byte-identical).
- **Poison-safe locks completed**: the earlier sweep missed 34 multi-line
  `RwLock`/`Mutex` `.read()/.write()/.lock().unwrap()` sites (receiver and
  call on separate lines); all now go through `sync_guard` as well.
- **Debug CLI stack overflow fixed**: `main` now runs the async runtime on a
  16 MiB thread, so debug builds no longer overflow the 1 MiB OS main-thread
  stack at startup (`cargo run` works again).

### Changed
- **Bounded layer retention**: `layer_retention_entries` default is now 2000
  (was 0 / unlimited). Each layer file keeps at most 2000 newest entries and
  trims the oldest past that cap, so `layers/*.txt` cannot grow without bound.

## [0.8.2] - 2026-07-17

### Added
- **auto-context CORE MEMORIES section**: `auto_context.rs` now embeds full text (up to 500 chars) of the top 3 highest-importance memories (imp >= 7) directly into every auto-context snapshot. LLMs receive actual memory content, not just metadata — eliminating hallucination on foundation memories.

### Changed
- **Recall truncation limit**: `blade-microscope-memory` recall output increased from 150 to 4000 chars per block, ensuring full context reaches the LLM via MCP/tool-server.

## [0.8.1] - 2026-06-23

### Fixed
- **Crash safety**: 50 files — all `fs::write()` calls replaced with temp file + atomic rename pattern
- **NaN propagation**: emotional_21d, attention, and 3 other files — NaN/inf sanitization on load
- **Data loss**: emotion vector now persisted in `store_memory_with_emotion`
- **Planning**: added `fail_step()` rollback method
- **Memory leak**: mental_sandbox capped at `MAX_SCENARIOS=100`
- **Redundant I/O**: removed duplicate `HebbianState::load_or_init` in hot recall path

### Changed
- **Hook script**: `scripts/microscope-recall-hook.ps1` — now uses `-Action` parameter instead of `$env:CLAUDE_HOOK_TYPE`; UserPromptSubmit does store + recall + inject; Stop stores to long_term
- **README**: corrected layer list (15 layers, deduplicated), module count (84), LOC (36,317)
- **Version**: bumped to 0.8.1

## [0.8.0] - 2026-06-17

### Added
- **morphogenesis.rs** — 4 biological growth algorithms (mycelium, capillary, slime mold, fractal L-system) + evolutionary engine
- **pattern_recognition.rs** — sequence, temporal, structural & cluster pattern detection with cross-domain correlation
- **executive.rs** — cognitive conductor: module scheduling, resource allocation, homeostasis
- **planning.rs** — HTN goal decomposition, action planning, replanning on changing conditions
- **autopoiesis.rs** — template-based code generation, versioned mutations, rollback mechanism
- **code_memory.rs** — dedicated memory layer for coding agents (code snippets, symbols, error↔solution pairs)
- **chatgpt.rs** — ChatGPT export parser with Google Drive import support (--gdrive, --gdrive-folder)
- **PWA chat** — Progressive Web App with manifest, service worker, installable on mobile
- **MCP integration** — Model Context Protocol server for Claude Code, Cline, Kilo Code, OpenCode
- **25 integration examples** — LangChain, OpenAI Assistant, Ollama RAG, Discord, Slack, WhatsApp, n8n, Docker, Home Assistant, Streamlit, Obsidian, AutoGPT, Cloudflare Worker

### Changed
- CLI: added `morph`, `code`, `import-chat-gpt` commands
- Serve: binds to 0.0.0.0, serves PWA on /chat.html, displays local IP for phone access
- Scripts: updated binary name, ports, and removed obsolete TTS references
- Layers: added missing layer files (identity, emotional, relational, reflections, crypto_chain, rust_state, code)
- README: full rewrite with v0.8.0 features and Vector DB comparison benchmarks
- BENCHMARKS.md: added comparison table (FAISS, Pinecone, ChromaDB, Qdrant)

### Removed
- demo.html (replaced by PWA chat)
- website/ directory (landing page)
- server-data/ (obsolete server duplicate, 606MB)
- backup/ directory
- tools/edge_tts_server.py (obsolete, replaced by voice-mcp)
- examples/index.html

### Performance
- Overall query: 87 µs avg across 9 depths (20323 blocks)
- 4D soft zoom: 249 µs/query
- 265 tests, all passing

## [0.7.0] - 2026-04-08

### Added
- Comprehensive MQL integration tests
- Python API documentation
- WASM browser integration documentation

### Changed
- Refined CLI command descriptions
- Improved config.example.toml comments

## [0.1.0] - 2026-03-21

### Added
- Initial release: 9-level hierarchical depth system (D0-D8)
- Pure binary storage with mmap
- Sub-microsecond query performance
- 3D spatial indexing with L2 distance search
- Natural language recall with auto-zoom
- 9 cognitive memory layers
- GitHub Actions CI/CD pipeline