- The repository-root
README.mdis the workspace README. It may use relative links to files, crates, assets, and sections inside this repository. - A crate-local
crates/<crate>/README.mdis package documentation: Cargo can publish it to crates.io, and readers may encounter it outside the checkout. Keep it self-contained and do not link to the workspace with paths such as../../README.mdor to sibling crate files with../...paths. - In crate-local READMEs, link public Rust APIs and sibling crates through their
absolute
https://docs.rs/<crate>/latestURLs. Link repository-only material (settings files, benchmarks, source files, and markdown documents from another crate) through absolutehttps://github.com/m62624/plugmem/...URLs. - A crate-local README may refer to an SVG or other asset shipped by that same
crate with a relative path such as
assets/chart.svg. Cargo packages the README and the crate'sassets/directory together, so this remains valid in the workspace, on GitHub, and on crates.io. Do not use relative paths to workspace files or sibling crates. - Workspace-only branding, generated top-level documentation assets, and legacy
reference material belong under the root
assets/directory. Keep legacy opaque references underassets/references/; they are design/test material, not published crate assets. RootREADME.mdmay link to these files with relative paths such asassets/logo.png.
- Benchmark SVGs are generated artifacts. Keep their source data and renderer
in
tools/bench-charts; do not hand-draw a chart that the tool cannot reproduce. - When a benchmark compares corpus sizes, the chart must be generated from the same tool input and the README must state the workload, platform, units, and whether embeddings are enabled.
plugmem is a Rust embedded memory and knowledge engine. It stores facts, temporal state, tags, entities, relationships, text indexes, and vectors. The core is designed to be usable in no_std environments; the host layer adds filesystem persistence, journals, snapshots, memory mapping, recovery, and embedding integration.
The main dependency flow is:
plugmem-arena -> plugmem-core -> plugmem-host -> plugmem-cli / plugmem-mcp / plugmem-napi / plugmem-py
^
plugmem-testgen and benchmark tools
Low-level no_std storage primitives:
- flat byte arenas and handle-based access;
Arena,BlobHeap,ChunkPool, andInterner;- packed byte/string storage;
- mmap overlays, copy-on-write behavior, and read-only views.
Arena benchmarks describe the storage primitive itself. Do not present them as measurements of the complete memory engine.
The crate has two contracts, not one: its single audited unsafe, and its allocation budget — zero allocations per operation, amortized growth only, and a published allocator-call count that a change may not raise without saying why. Both are spelled out in crates/plugmem-arena/AGENTS.md; treat them as equally binding.
The main no_std engine. It contains:
Memoryand the remember/recall orchestration;- facts,
FactId, bitemporal state, intervals, and closing facts; - entities, edges, and graph traversal;
- tags and inverted indexes;
- tokenization and BM25/full-text search;
- fixed-stride quantized vector storage;
- flat vector search and optional HNSW indexing;
- temporal filters, RRF fusion, recency, and token budgets;
- journal operation encoding and snapshots.
Important source areas include:
src/lib.rs— public API and core types;src/memory.rs— write and recall orchestration;src/vector.rs— vector storage, signatures, flat search, and HNSW;src/search.rs,src/bm25.rs,src/temporal.rs, andsrc/graph.rs— recall components;src/journal.rsandsrc/snapshot.rs— persistence formats;examples/— executable examples and focused experiments.
For vector storage, the slot stride is the fixed header plus signature bytes plus the quantized vector dimension. With dimension 384, the raw vector slot is 440 bytes before other engine structures. Flat search scans all slots, so its latency is primarily controlled by the number of stored vectors rather than by the requested result count. HNSW is advanced by maintenance/configuration paths with bounded work in Auto; remember does not automatically rebuild the graph on every write.
The std integration layer:
- filesystem and in-memory storage implementations;
- immutable generation snapshots and manifests;
- append-only journals, locks, and recovery;
- mmap read-only and overlay access;
- embedders and host-level maintenance.
The host wrapper can create embeddings when an embedder is configured. Core vector behavior should be tested separately from embedding model latency and external process overhead.
Command-line interface over the host/core APIs. It provides database opening, remember/recall, maintenance, snapshot, recovery, and diagnostic commands.
MCP adapter/server. It exposes memory operations through the host API and should not duplicate storage logic.
Node.js N-API bindings. A boundary layer only: its surface equals plugmem-host's (see the wrapper-parity rule below), the engine logic stays in host, and FFI overhead is measured separately from core and host performance.
Python bindings (PyO3), published to PyPI as plugmem. A boundary layer only,
under the same rule as plugmem-napi — with one addition that matters: its
reference is napi's surface, not host's. napi narrowed host in three places
deliberately (one export_page and no export_each, one maintain(mode) and
no maintain_with_options, scrub(budget) and no scrub_with_budget), so a
binding that re-derived those choices from host would disagree with the one
that already shipped. tests/test_parity.py reads napi's generated
index.d.ts and its error.rs and fails in both directions.
Every verb releases the GIL around the host call (Python::detach); there is
no async layer, because releasing the GIL is what makes asyncio.to_thread
and a thread pool work. Locks here guard the handle slot, not the engine —
host synchronizes itself, but several Python threads share one wrapper object.
python/plugmem/_plugmem.pyi is generated and committed, gated on git diff
and on mypy --strict, exactly as index.d.ts is gated for the Node binding.
Utilities for generating deterministic test data and scenarios. It is not part of the production storage path.
cargo-fuzz targets for the two untrusted files, the snapshot and the journal.
Its own workspace (nightly plus a sanitizer runtime), excluded from the main
one. The seed corpus is committed real images — without seeds the fuzzer never
gets past the magic number. CI runs each target briefly on every pull request;
a real campaign is a manual long run. See fuzz/README.md.
Benchmark matrix runner and result collection. Results must identify the component, data shape, configuration, and units being measured.
TSV-to-SVG benchmark chart generator. Treat baseline.tsv as checked-in measurement data, not as an automatically verified source of truth. Check units, labels, duplicate rows, and ordering when changing it.
Wasm build and compatibility probe for the core.
The write path is broadly:
remember(input)
-> apply_remember
-> tokenize/intern, BM25, tags/entities, vectors, and temporal indexes
-> journal operation
The recall path is broadly:
RecallQuery
-> tag/entity/temporal filters
-> BM25 candidates and/or flat vector/HNSW candidates
-> graph candidates
-> RRF fusion, recency, and token budget
RecallQuery supports text, vectors, tags, entities, temporal constraints, result count, token budget, closed-fact inclusion, and HNSW search parameters. A vector-only benchmark is not equivalent to a mixed text/vector/graph recall.
Use release mode for performance work and record the data shape, configuration, hardware, compiler version, warm-up policy, repetitions, units, and correctness checks.
cargo fmt --all -- --check
cargo check --workspace
cargo test --workspace
git diff --check
git status --shortFor a targeted package or example, prefer a focused command first, then run the broader workspace checks when practical. If a check is skipped, state that explicitly in the handoff.
A language binding — today plugmem-napi, and whatever follows it — exposes the whole of plugmem-host's surface.
- An omission is allowed only when the construct is unrepresentable in the target language, never because it is awkward to write. "A resumable iterator is inconvenient across this boundary" is not a reason; it is the work.
- Format concerns are not host verbs and do not live in a wrapper's Rust. JSONL import/export belongs to the CLI, or to a pure module in the target language shipped in the same package.
- Idioms the target language demands are not divergence: a Python context manager or a JavaScript promise has no counterpart to drift from.
- A divergence between wrappers that does not follow from the above is a bug, not a
decision. Two verbs are worth naming because they were once excluded on this exact
mistaken ground:
scrubandrecover.
plugmem-mcp is judged differently, and deliberately. It is not a language binding: its
caller is a model reading a tool list, where every additional tool spends context and
adds a way to go wrong, and where a path argument is chosen by the model rather than by
a programmer. Its measure is "what an agent should be handed", not "what host has". So
the salvage verbs stay out of it — but that is the only licence the difference grants.
Anything an agent legitimately uses must be whole: plugmem_export returning facts
without edges is the same defect as the equivalent gap in a binding, not an exercise of
this exemption.
The corollary for configuration: every wrapper reaches Config through the one shared
loader (Settings::load), so a setting that is not in settings_help is a setting no
wrapper can offer. Adding a knob means adding it there, not in four places.
- Read the relevant crate and its tests before changing behavior.
- Preserve existing user changes; inspect
git statusandgit diffbefore editing. - Keep arena measurements separate from full engine and host measurements.
- Keep embedding/model latency separate from storage and recall latency.
- Verify benchmark units and conversions (
ns,us,ms, per-item, total time, and throughput). - Do not infer persistent memory usage from peak allocator activity without checking retained storage, temporary allocations, journal bytes, mmap mappings, and process RSS.
- Do not compare against another database without matching data, distance metric, index type, recall target, persistence mode, concurrency, and hardware.
- Use
apply_patchfor manual file edits. - Avoid destructive commands such as
git reset --hard, broad deletion, or overwriting unrelated work unless explicitly requested.
- Keep the workspace, Cargo manifests,
Cargo.lock, and npm metadata at the current development version between releases. Theskills/plugmem/SKILL.mdmarker is the one version string a human edits by hand. - Do not manually bump package versions in a feature or performance PR. The
release workflow derives the release version from the pushed
vX.Y.Ztag, updates the workspace and npm versions on its release-candidate branch, and opens the synchronization PR back tomain. - Skill content may be updated in any normal PR, and so may the
<!-- skill-version: X.Y.Z -->marker — bump it to the version you are heading for in whichever PR before that release is a natural place for it. No rule reserves it for a release branch. - The marker exists so the skill cannot quietly fall behind the engine.
skills/plugmem/SKILL.mdis what an agent reads instead of the source, so a release that changed verbs, flags or behavior has to be checked against it: read the skill, confirm it still describes what shipped, update it if not, then set the marker. The release gate compares the marker with the tag-derived package version and fails when they differ — that failure is the reminder to do the reading, not a versioning chore. Regular CI only checks the marker is well formed and never compares it with the development version, so a bumped marker sits onmainuntil the release catches up with it. - Verify the release workflow when changing this policy:
.github/workflows/release.ymlis the source of truth for tag parsing, version synchronization, and the skill-version gate.