English | 中文
Current version: v1.4.5 — start.sh registry self-check (issue #31, config governance Phase 3.3):
start.sh startnow verifies its ownport-registry.yamlentry before serving — an unregistered port fails at start (exit 1) instead of being caught later by CI check-ports. Missing registry file = warn + continue (standalone deploy);FUSION_SKIP_REGISTRY_CHECK=1bypasses. v1.4.4 env contract documentation (issue #29, config governance Phase 2.2): consolidatedFUSION_MEMORY_*environment-variable tables in this README, plus a check-env false-positive fix (compliantFUSION_MEMORY_MLX_API_KEYwas substring-matched as legacyMLX_API_KEY; the workspace checker now matches exact tokens). The repo itself needed zero renames — acceptancepython scripts/check-env.py: 0 findings. v1.4.3 CI-red fix (issue #27): the cross-repo path dependencies (fg-redacton fusion-guard,fs-coreon fusion-store) broke CI's standalone checkout; both replaced with pinned git tag deps (zero drift vs the local trees) and verified against a fresh lone clone. v1.4.2 retrieval quality ablation (issue #25): full fusion scoring beats plain cosine by +0.40 recall@5 (0.8270 vs 0.4250, full-scale corpus indocs/benchmarks/retrieval-quality-2026-10-25.md), and the benchmark's first finding forced a retune — the fusion decay term now uses normalized freshness instead of rawW(t)(raw-W(t)fusion measured worse than plain cosine). CI gate: fusion must never score below plain cosine. v1.4.1 MLX embedding API key TOML mirror (issue #23), v1.4.0 scoped memory namespaces (issue #21), v1.3.0 fusion-identity integration (issue #18), v1.2.0 multi-tenant isolation (issue #16) landed earlier. Known limitations inCHANGELOG.md"Known limitations", non-blocking.
The system-level long/short-term memory and cognitive graph hub of the Fusion ecosystem ("一核九端"). Solves the Agent cross-session state gap, repeated questions, and context-window blowup. Goal: the more it is used, the better it knows the user.
- Authoritative PRD:
architecture/fusion-memory-prd-0825.md - Landing architecture:
~/fusion/fusion-memory-prd-plan-0826.md - Audit report:
~/fusion/audit/fusion-memory-audit-0827.md
v1.0.0 — API stability commitment (2026-08-28): 11 crates locked at 1.0.0. From here follows the semantic versioning (SemVer) contract:
MAJOR(2.0+): only backward-incompatible breaking changes, must be announced in changelog in advance + migration guide. Breaking changes include: removing/renaming existing RPC methods, changing HTTP paths, changing field semantics, changing default behavior.MINOR(1.x): backward-compatible new methods/fields/endpoints/perf improvements; clients must not break on a MINOR upgrade.PATCH(1.0.x): bug fixes, no behavior change.- Frozen wire contract: UDS JSON-RPC method set +
v1.<method>prefix routing +jsonrpc=="2.0"validation (seejsonrpc::API_VERSION=1); HTTP/v1/memory/*paths + Bearer auth +confirmguard. Both unchanged across the entire 1.x cycle. - Client negotiation: call the
versionRPC orGET /v1/memory/versionto getapi_version, branch on the version number. - Pre-1.0 0.x versions are technical previews, no stability commitment.
store-fusion adapter + fg-redact credential redaction landed (2026-08-28, unreleased): user request "build store-fusion adapter now, then switch upstream fg-redact" — both parts fully landed.
- store-fusion adapter (
fm-store/src/fusion.rs, featurestore-fusion): implements the fm-storeFusionStoreEnginetrait, wrapping upstream fusion-storefs-coreEngine(HNSW + mmap KV). Distance semantics bridge: fs-core returnsdistance = 1 - cos_sim, the adapter convertssimilarity = 1.0 - distanceto align with the fm-store contract (same formula as local.rs). UFCS calls to the fs-core trait (both crates have a trait of the same nameFusionStoreEngine). ZeroCopyBuffer mmap→owned bridge. 6 tests green (kv roundtrip / vector insert+get+search / dim mismatch reject / search dim mismatch / delete→None / list_ids excludes soft-deleted). Additive, not exclusive: coexists with local-store (local-store default + always-on; store-fusion optional, default off), both compile together. Closes RC known-limitation #2 (store-stub naming — store-fusion is now a real fusion-store-backed alternative, not just "stub"). - fg-redact credential redaction (
fm-engine/src/redact.rs): stage-1 credential redaction delegates to upstreamfg-redact::Redactor::redact_credentials()(fusion-guard PR #11 / issue #10). fg-redact adds 10 credential classes fusion-memory previously lacked (JWT/private_key/oauth_bearer/api_key/conn_string/password/secret_kv/env_kv/netrc/aws_secret). Stage-2 PII remains fusion-memory-local (phone+86/0086/email/idcard/bankcard+Luhn/IPv4/passport/IPv6/intl phone) — fg-redact's PII behavior is worse (idcard wrongly eaten by credit_card / id_number wrongly eats long digits / +86 phone rejected by border validator), so PII does not go through fg-redact, see redact.rs module doc. Closes RC known-limitation #3 credential part (credentials now upstream; PII stays local by design). 4 new tests (jwt / password / credential+PII same segment / idcard stays local not bankcard). Idempotent: credential placeholder[REDACTED:jwt]has no digits → PII regex no re-match. - Test counts: default features 425→429 (+4 credential tests);
--features fm-store/store-fusion435 (429 + 6 store-fusion tests). Gates all green (fmt / clippy -D warnings / check / test). - Upstream: fusion-guard #10/#11 credential API landed (issue filed + PR #11 implemented + 8 issue10_* tests), fusion-memory consumes
redact_credentials(). fusion-store #3/#4 still tracked (adapter built consuming fs-core via path dep; store-stub remains default production backend).
Multi-tenant isolation landed (2026-09-02, issue #16): backend half of fusion-gateway #150 Gap1c. Canonical pattern follows fusion-model-hub#53.
- Tenant field on data (
fm-core):MemoryItem.tenant,Interaction.tenant(#[serde(default)]),RetrieveQuery.tenant(#[serde(default)]). Tenant flows through data, NOT through coreFusionMemoryEnginetrait method signatures — preserves v1.0 API freeze.tenant=""= default tenant (single-tenant backward compatible). - Additive tenant-scoped trait methods:
get_memory_tenant/delete_memory_tenant/delete_scope_tenant/count_tenant— default-delegate to non-tenant variants; impls override for real scoping. Backward compatible. - Schema v3 migration (
fm-persist):tenant TEXT NOT NULL DEFAULT ''column +idx_memory_tenantindex. IdempotentALTER TABLEviapragma_table_infocheck. - Engine scoping (
fm-engine):MemoryEngine.tenant(startup default,""),with_tenantbuilder. commit/summarize/retrieve/consolidate scope byself.tenant. Cross-tenant: get → None (no existence leak), delete → NotFound, missing-row delete → Ok (soft-delete preserved). - Gateway-origin enforcement (
fm-server/src/tenant.rs):X-Fusion-Route: gateway-decisionheader (configgateway_origin_required: bool = falsedefault off → 403 when missing);X-Fusion-Tenantauthoritative tenant (header >default_tenantconfig > ""). HTTPhandle_rpc+get_memoryenforce before dispatch. UDS (trusted-local) uses default tenant. Config env:FUSION_MEMORY_GATEWAY_ORIGIN_REQUIRED,FUSION_MEMORY_DEFAULT_TENANT. - Test counts: default features 429→433 (+4 gateway-origin/tenant tests in fm-server);
--features fm-store/store-fusion435→439. Gates all green (fmt / clippy -D warnings / check / test). Version bump all 12 crates 1.1.0→1.2.0 (additive, no breaking change).
fusion-identity integration landed (2026-09-03, issue #18): fm-server HTTP boundary enforces caller JWT verified by fusion-identity, tid↔tenant match, and empty-tenant rejection in multi-tenant mode (multi-tenant PRD §4 red line 1 fail-closed). Single-tenant backward compatible (default multi_tenant: false = no identity verify).
- Identity verifier (
fm-server/src/identity.rs):IdentityVerifiertrait (verify+report_usage) for 100% offline testability.RealVerifier(reqwest to127.0.0.1only, 30s TTL result cache) — fail-closed on HTTP error.NoopVerifierfor single-tenant backward compat.enforce_multi_tenant:multi_tenant=falseshort-circuits; true + empty tenant → 401; missing Bearer JWT → 401; verify → tid != tenant → 403; match → Ok(tid). - Config (
fm-server/src/config.rs):multi_tenant: bool(default false),identity_url(defaulthttp://127.0.0.1:11470),identity_service_token(+_filesecret). Env:FUSION_MEMORY_MULTI_TENANT/FUSION_MEMORY_IDENTITY_URL/FUSION_MEMORY_IDENTITY_SERVICE_TOKEN/FUSION_MEMORY_IDENTITY_SERVICE_TOKEN_FILE.validate():multi_tenant=truerequires http identity_url AND non-empty service_token. - HTTP boundary (
fm-server/src/http.rs): in multi-tenant modecheck_bearer(static api_key) is skipped — Bearer is the JWT (identity-verified).handle_rpc+get_memorycallenforce_multi_tenantafter tenant extraction. - Test counts: default features 433→452 (+19 identity tests: 4 config + 7 http + 8 identity unit/e2e);
--features fm-store/store-fusion439→458. Gates all green (fmt / clippy -D warnings / check / test). Version bump all 12 crates 1.2.0→1.3.0 (additive, no breaking change). - Upstream: fusion-identity consumed (not modified) — sole JWT issuer + tenant registry. fm-server calls
POST /api/v1/auth/verify(service-token gated) +POST /api/v1/tenants/{tenant_id}/usage(best-effort). Three red lines honored: fail-closed, cross-tenant denied, data isolation layering.
Scoped memory namespaces landed (2026-10-21, issue #21): second visibility dimension orthogonal to tenant — namespace (user / project / local / empty) with namespace_key (project path / machine id / user id). Fully additive: empty namespace = global (old behavior); all fields #[serde(default)] so old JSON and WOP payloads replay unchanged.
- Data (
fm-core):MemoryItem/Interaction/RetrieveQuerygainnamespace+namespace_key;MemoryItem::with_namespacebuilder keeps the existing 8-arg skeleton signature. - Stacking rule:
projectsees its own key ∪user;localsees only its own key;usersees user only; empty = no filter. Unknown values fail closed (-32602) rather than silently widening the read. - Schema v4 (
fm-persist):namespace/namespace_keycolumns +idx_memory_namespace; idempotent v3→v4ALTER; migration order fixed (table → ALTER → index DDL). - Engine (
fm-engine): commit stamps namespace onto turn items; retrieve filters the KNN hit lookup by the SQL predicate and over-fetchestop_k × 4when a filter is active (the store trait has no metadata filter);consolidate_memories_scoped()restricts recycle/promote/merge/summarize/reconcile to the visible domain and deliberately does not advance the global consolidation watermark, so a scoped backfill cannot swallow other namespaces' pending changes. - RPC (
fm-server): optionalnamespace/namespace_keyoncommit,retrieve,memory.retrieve_context,consolidate({}/ null params still valid); commit params override interaction values. Namednamespaceto avoid colliding with the existingdelete_scope/countscope(session_id) meaning. - Test counts: default features 452→468 (+16 namespace tests: filter unit / persist scoped + v3→v4 migration / engine stacking + watermark / jsonrpc passthrough + validation / HTTP end-to-end scoping). Gates all green. Version bump all 12 crates 1.3.0→1.4.0 (additive, SemVer MINOR).
- Consumer side (out of this repo): fusion-agent-studio#434 — subagent
memory:field, project-root key derivation, per-subagent toggle. Wire contract:clients/README.md.
M2 completed: real bge-m3 embedding (dim=1024) + entity extraction (injection-proof prompt + strict JSON parsing) + SQLite recursive CTE graph traversal + rule-priority entity alignment + fused scoring (cosine+decay+graph_affinity) + agent-studio history memory import. Acceptance: entity extraction JSON parse success rate 100% (>90%), rule-priority alignment correct (same name same type merge / same name different type no merge), real embedding roundtrip dim=1024. Test coverage lines 90.59% / regions 92.17%. 162 tests all green, clippy -D warnings passed.
M3 completed: fm-server (UDS JSON-RPC 0600 + HTTP axum enforced Bearer B5, port 11435, refuses to start HTTP without API_KEY) + fm-py PyO3 binding (allow_threads GIL-safe C2) + consolidate_memories saga (incremental forgetting + merge/summarize/reconcile, cross-DB reconciliation + merge_log + unmerge) + fm-cli (consolidate/merges/unmerge/reconcile) + start.sh (start/stop/restart/status/log/doctor). Acceptance: PyO3 roundtrip GIL not frozen (commit→2 ids / retrieve→block / consolidate→report); HTTP rejected without token + DELETE rejected without confirm + HTTP refused without API_KEY; consolidate report fields complete + reconciliation diff detected; start.sh three commands usable. 242 offline + live tests all green, regions offline 90.63% / live 92.07%.
M4 in-scope completed (consumer integration reference impl + contract tests, in this repo): clients/ three consumer reference clients — TS HTTP client (ts/fusionMemoryClient.ts, fusion-code vendor, default port 11440 to avoid clashing with fusion-kb 11435) + Python HTTP client (python/fusion_memory_client.py, cowork/agent-studio alternate path, default 11435) + clients/README.md integration doc (protocol matrix + wire contract + three consumer integration seams + port conflict warning + agent-studio 9 handler→6 RPC mapping table). Contract scenario tests crates/fm-server/tests/consumer_scenarios.rs (3 scenarios: cowork memory_commit/retrieve node flow, fusion-code retrieve inject→commit→cross-turn recall, agent-studio 9 handler backend-replace mapping + delete without confirm -32602). Stub engine HTTP oneshot roundtrip, offline no mlx. Acceptance: 3 contract scenarios pass + 248 offline tests all green + clippy/fmt clean + regions 91.76% (up, new scenarios expand trait path coverage). Outward PRs landed (cross-repo, 3 consumer repos issue→PR→land): fusion-cowork #67→#68 (merged, memory_commit/retrieve two nodes), fusion-agent-studio #246→#247 (merged, FusionMemoryAdapter 9 handler→6 RPC env-gated swap), fusion-code #150→#151 (merged 5311b00, turn-end commit; retrieve-inject half-deferred, tracked #154). All three consumer integration files verified present on each repo's main.
M1 completed: store-stub backend (hnsw_rs + sled) + SQLite WAL persistence + StubEngine (deterministic stub embedding) + CLI (commit/query/stats/delete/doctor). Acceptance: CLI writes 100 entries (50 interaction × 2 turn) → query aggregates each block restoring 2 turns → doctor reports component status. Test coverage lines 94.6% / regions 91.1% (cargo-llvm-cov).
M6 completed: cluster sync leader-follower (PRD §16 intranet offline cluster, not public cloud). New crate fm-cluster: role injection (standalone/leader/follower, env FUSION_MEMORY_ROLE > home/role file > standalone) + wop_log replication (leader single write point + append_wop, follower pulls SyncRequest → local replay commit/delete, summarize audit skipped) + TCP transport (4B length prefix + JSON wire frame, Hello/SyncRequest/SyncResponse/Ping/Pong, intranet port 11436) + heartbeat (5s ping, 3 consecutive failures = LeaderDown) + manual failover (fm cluster promote writes home/role=leader, requires fm-server restart to take effect, auto-election deferred). fm-server spawn_cluster(engine, role, set) role injection eliminates env races. fm-cli cluster status/promote. Acceptance: 3 e2e scenarios all green (commit→catchup read-local consistent / incremental sync seq advance / leader down→LeaderDown→promote→new leader continues writing) + ReplaySink coverage tests + fm-cluster per-file offline regions 91-100%. 285 offline tests all green, clippy/fmt clean. Offline total regions 87.65% (down from M4 90.63%, due to new fm-cluster crate + engine integration expanding the regions denominator, while mlx-gated summarize/consolidate saga + engine_builder !stub branches are unreachable offline; the live metric still covers these branches, PRD acceptance caliber = live, M6 did not touch mlx code so live regions unchanged).
M5 partially completed (degraded positioning, non-blocking mainline): PRD §14 three parts — (a) store-fusion optional switch, (b) audit_memory_access → fusion-guard DLP gate, (c) perf baseline p99<50ms + concurrency. (c) landed: lightweight hand-written bench (crates/fm-engine/benches/retrieve_bench.rs, no criterion heavy dep), store-stub 10k memories + StubEmbedder dim=64, single retrieve p99=14.3ms (<50ms ✅), 10-concurrent p99=140ms (<200ms ✅). Baseline JSON saved to benches/baseline-2026-08-27.json. (a)(b) degraded: see M5 PRD deviation record. R8/§10.4 PII regex redaction landed beyond (c) (filling the prior zero-redaction vacuum): fm-engine/src/redact.rs five PII regex classes (phone/email/idcard/bankcard/ipv4, regex crate has no lookaround, order-sensitive replacement to avoid mis-eating), placeholder [REDACTED:type], idempotent. The commit/import write path redacts before embed+persist, so vectors/graph/retrieval all use redacted content. env FUSION_MEMORY_REDACT_PII=1 enables it (MemoryEngine::with_redact() + fm-server/fm-cli import path same-source env). 13 redaction tests green. Acceptance: perf bench two gates pass + 13 redaction tests green + 301 offline tests all green + live tests all green (bge-m3 + Qwen3.8-27B-4bit, entity extraction JSON 100%) + offline regions 90.82% / live regions 92.47% (both ≥90%) + clippy/fmt clean.
| Milestone | Scope | Status |
|---|---|---|
| M0 | workspace + core types + trait + CI | ✅ |
| M1 | store-stub backend + engine runnable (stub embedding) + CLI | ✅ |
| M2 | real embedding + entity extraction + graph + fused scoring + import | ✅ |
| M3 | service + PyO3 + consolidate + auth | ✅ |
| M4 | consumer integration (in-scope ✅ / outward ✅) | ✅ |
| M5 | PII redaction + perf baseline + store-fusion/guard degraded | ✅ (partial) |
| M6 | cluster sync leader-follower | ✅ |
Full milestone set M0–M6 (terminal state): landing architecture
~/fusion/fusion-memory-prd-plan-0826.md§14 defines only M0–M6, M6 is the final milestone, no M7+. §15 six open items all ✅ adjudicated; §17 audit corrections E1 (8 items)/E2 (10 items)/E3 (3 items) all landed or decided, audit closed with no residual. Subsequent work is only two types: (1) M5(b) awaiting upstreamfusion-guard#2to add PII classes before connecting the formal DLP gate; (2) PRD-external ops/perf/consumer evolution.
All 16 defects in audit/fusion-memory-audit-0827.md §8 fixed, landed in 9 batches by file cluster, each batch a cargo test checkpoint, 315 offline tests all green (baseline 301 → +14 new regression tests), clippy -D warnings + fmt clean.
P0 commercial blockers (5 items)
- H1 cross-store write has no transactional atomicity:
put_memorywrapped inconn.transaction()(memory_item + entity + memory_entity three INSERT types in same transaction, failure rollback leaves no half entity rows);commit_episodic_memoryreversedelete_vectorto clean already-written sled vectors onput_memoryfailure.fm-persist/src/store.rs,fm-engine/src/engine.rs. - H2 cluster replay not idempotent + LeaderDown false positive:
insert_vectormade idempotent (already on disk and not tombstoned → skiphnsw.insert, replay resends don't double-index; tombstone state clears tomb then re-inserts normally = resurrection path); replay errors classified (transient network/parse failure retry, permanent sink failure propagate).fm-store/src/stub.rs,fm-cluster/src/replay.rs. - H3 cluster TCP has no auth + 4GB frame OOM + plaintext:
read_frameaddsMAX_FRAME_LEN(16MB) cap to prevent OOM;handle_connvalidates cluster_token (compared during Hello handshake, empty token allowed on intranet); plaintext risk documented (intranet offline boundary, not public, PRD §16 already scoped).fm-cluster/src/protocol.rs,fm-cluster/src/transport.rs. - H4 consolidate TOCTOU + lost update: engine-level
tokio::sync::Mutex<()>(consolidate_lock) serializesconsolidate_memoriesagainst retrieve's touch_access write (snapshot→decision→write atomic);touch_access_batchdedup + single batchUPDATE ... WHERE id IN (...), relative toaccess_count=access_count+1prevents lost update.fm-engine/src/engine.rs,fm-persist/src/store.rs. - H5 PII redaction not enterprise-grade + lying comment: bankcard adds Luhn check + context boundary (avoid mis-eating order numbers/timestamps); expand PII classes (phone/email/idcard/bankcard/ipv4, order-sensitive replacement to avoid phone eating first 11 digits of bankcard);
redact.rs:58env comment corrected (reads env on every call, not at startup, and only on the builder/import non-hot path).fm-engine/src/redact.rs.
P1 must-fix (5 items)
- L1 graph_affinity always 0:
retrieve_contextextracts entities from query text when extractor present → passesquery_entity_idstoscore_candidate, graph_affinity connected (direct hit 1.0 / N-hop 0.5^h).fm-engine/src/engine.rs. - L2 touch_access multi-accumulation: see H4
touch_access_batch(same id multiple turn hits only +1, "retrieval session" count). - L3 reconcile one-directional: add store→SQLite reverse orphan scan (
StoreStub::list_vector_ids()enumerates non-tombstoned vector ids, not in SQLitevector_refset → orphan, recorded in report +delete_vector);physical_deleteexplicitly cascadesmemory_entity(not relying on FK pragma across connections).fm-store/src/stub.rs,fm-persist/src/store.rs,fm-engine/src/engine.rs. - L4 delete silently skips: bad
vector_ref(non-numeric/polluted) no longerunwrap_or(true)silent physical delete (would leave ghost vectors), changed to warn +append_reconcile("bad-vector-ref")+ skip physical delete (reconcile backstop cleans).fm-engine/src/engine.rs. - L5 slug collision: entity id =
ent-{slug}-{fnv1a_64(name)}(FNV-1a full-name hash guarantees uniqueness, slug is display-only). C/C++/C# etc. same slug, different entity ids all distinct.fm-engine/src/entity_extract.rs,fm-cli/src/import_studio.rs.
P2 perf (4 items)
- P1 single Mutex full serialization + poison panic amplification: keep single
Mutex<Connection>(SQLite WAL single writer, r2d2 connection pool is over-design for Rule 2), 24.expect("poisoned")→conn()/conn_mut()helpers returnPersistError::Poisonedpropagate, no longer panic amplifying single-point failure to global.fm-persist/src/store.rs,fm-persist/src/error.rs. - P2 vector serde_json text stored in sled (~3.7x waste): change to LE f32 raw bytes (4B/float, serde_json text 7-12B/float), zero-alloc deserialize.
fm-store/src/stub.rs. - P3 consolidate_merge O(S×KNN×N) catastrophe: KNN inner loop
list_all()full-table scan + string reverse lookup → buildvector_id → &MemoryItemindex once outside loop, inner O(1) lookup.fm-engine/src/engine.rs. - P4 CTE exponential fan-out + per-search single-point tombstone check: recursive CTE adds
LIMIT 256early termination (graph_affinity remote nodes 0.5^h exponential decay, truncation lossless);search_knntombstone check changed totombstone_set()single batch load into HashSet, replacing per-neighbor/per-fallback vector N sled point lookups.fm-persist/src/store.rs,fm-store/src/stub.rs.
P3 maintenance (3 items)
- M1 lying comment: physical_delete cascade comment (already explicitly DELETE memory_entity, comment matches impl) + redact.rs env comment (see H5).
fm-persist/src/store.rs,fm-engine/src/redact.rs. - M2 extract_and_attach swallows DB error:
get_memory().unwrap_or(None)turns SQLite error into "no such memory" (DB failure disguised as data missing) → explicit match, DB error warn + return (pending stays true for re-extract), no disguise.fm-engine/src/engine.rs. - M3 Runtime::new per Engine thread explosion: fm-py builds a tokio runtime per Python
Engine(N×worker threads) → process-levelOnceLock<Runtime>shared single runtime (2 workers), all PyEngine reuse.fm-py/src/lib.rs.
Fixed 3 open GitHub issues, added 2 RPCs + 1 UDS method:
- issue #2 — scope-level delete/count: added
delete_scope(batch tombstone by session_id, withconfirmguard, reuses delete's vector cleanup +append_wopaudit) andcount(full or per-session count). Backendfm-persistaddsdelete_by_session/list_by_session/count_by_session, engineMemoryEngine::delete_scope/count, trait adds defaultUnsupportedimpl (test stubs unchanged). HTTPPOST /v1/memory/{delete_scope,count}+ UDS methoddelete_scope/count. - issue #1/#4 —
memory.retrieve_contextcontract: fusion-event needs{trigger_id, query, top_k, node_id}→{context, memory_ids, cache_hit}. Added UDS methodmemory.retrieve_contextadapting existingRetrieveQuery, fusingFormattedContext.blocksinto the contract shape (context = turns joined by\n---\n, memory_ids = interaction_id dedup, cache_hit=false).
Acceptance: 325 offline tests all green (baseline 301 → +24 new, persist 3 + dispatch 5 + http 4 + trait/engine 12), clippy -D warnings + fmt clean, cargo check --workspace clean. CI blocked by GitHub account billing (recent account payments have failed, not a code issue, local fmt/clippy/check/test gate is the proxy metric).
Deep items in audit report audit/fusion-memory-audit-result-0827.md §1/§2/§3 (48 findings, landed in 8 batches) all fixed. This round focused on architecture-layer coupling, production-path zero-coverage, error-type semantics, complementing the v0.1.x behavior-defect fixes. 354 offline tests all green (baseline 325 → +29 new regression tests), clippy -D warnings + fmt clean.
Architecture-layer decoupling (§1.1/§1.4/§1.5)
- §1.1 connection pool breaks single-Mutex serialization:
PersistfromMutex<Connection>tor2d2::Pool<SqliteConnectionManager>(POOL_SIZE=8). WAL's native 1-writer N-reader concurrency was previously canceled by the single connection;PooledConnectionDeref→Connection,prepare_cached/transaction()call sites unchanged. AddedPersistError::Pool+From<r2d2::Error>, timeout/busy →MemoryError::Busyretryable.fm-persist/src/store.rs,fm-persist/src/error.rs,Cargo.toml(r2d2 0.8 / r2d2_sqlite 0.35, matching rusqlite 0.40 bundled). - §1.4 store backend trait-ification:
MemoryEngine.storefield fromArc<StoreStub>toArc<dyn FusionStoreEngine>(dynamic dispatch, not bound to a concrete backend).FusionStoreEnginetrait addslist_vector_ids(reconcile reverse scan store→SQLite orphans). store-fusion backend changed from empty shell tocompile_error!explicit block (until upstream fusion-store trait alignment).fm-store/src/trait_def.rs,fm-store/src/stub.rs,fm-store/src/fusion.rs,fm-engine/src/engine.rs. - §1.5 graph-layer storage abstraction: new
fm_graph::GraphStoretrait (onlyn_hop_reachable+list_entities_by_typetwo methods, minimal interface for the graph layer),impl GraphStore for Persist.graph_affinity/align_entity/score_candidatesignatures from&Persistto&dyn GraphStore, graph layer no longeruse fm_persist::Persist. New mock testmock_store_no_sqlite_neededproves the graph layer can be pure-memory unit-tested (noPersist::open_in_memory()+ SQL data fill needed).fm-graph/src/store.rs,fm-graph/src/affinity.rs,fm-graph/src/align.rs,fm-engine/src/scoring.rs.
Production-path gate (§1.6/§1.12)
- §1.6 CI live-compile gate: CI adds
live-compilejob, each PR compiles all mlx-live gated tests (--features mlx-live --no-run), verifyingMlxEmbedderbge-m3 / consolidate saga / real HTTP code paths type-check. Previously these production paths were compiled 0 times in the default CI build (live tests#![cfg(feature = "mlx-live")]+#[ignore]double gate), "325 green" had zero regression protection for live paths. Actual execution remains manual (needs fusion-mlx on Apple Silicon)..github/workflows/ci.yml. - §1.12 stub-vs-stub tautology: 91 stub tests (
StubEngine/DispatchStub/EchoEnginereturning magic constants) annotated as wiring tests (proving wire passes params, not behavior tests); realMemoryEnginebehavior coverage through HTTP/JSON-RPC real-stack roundtrip is borne bytests/offline_integration.rs+tests/consumer_scenarios.rs(stub engine + real stack, already run by default incargo test --workspace).fm-server/src/jsonrpc.rs.
Error-type semantics (§2.8)
- §2.8 error type finalize:
MemoryErroraddsPoisoned/Busy/NotFoundsemantic variants (old version all crushed intoSqlite(String), ops mistook it for sqlite error and ran VACUUM, real diagnosis hidden).PersistError::to_memorydistinguishes mapping: Poisoned→Poisoned (permanently non-retryable), SQLITE_BUSY/locked→Busy (transient retryable), Pool timeout→Busy.retryable()/is_not_found()helpers for caller decision.fm-core/src/error.rs,fm-persist/src/error.rs.
Full 8-batch record (Batch 0–7, by file cluster + checkpoint) in git history
fix/audit-p0-p3-layering-0828branch. Acceptance metric: 354 offline tests all green + clippy-D warnings+ fmt clean + live-compile gate compiles.
All 22 items (6 P0 + 10 P1 + 6 P2, no P3) in production-readiness audit report audit/fusion-memory-audit-result-product-0827.md §8 handled. All code-fixable items landed, 3 epic-scale architecture items explicitly deferred and documented as SLA/roadmap. 403 offline tests all green (baseline 354 → +49 new regression tests), clippy -D warnings + fmt clean, cargo check --workspace clean.
P0 commercial blockers (6 items all fixed)
- P0-1 process supervision: added
scripts/fusion-memory.service(systemd unit, Type=notify integrates healthz, Restart=on-failure + StartLimitBurst, journal logging). Companionstart.shdocuments deployment path (systemd managed / manual start.sh, choose one).scripts/fusion-memory.service. - P0-2 metrics endpoint:
GET /metricsreturns Prometheus text format (http_requests_total / http_errors_total / http_request_duration_seconds histogram + engine-layer embedder_in_flight / consolidate_running / store_pool_in_use). Public without Bearer (like healthz, for monitor scraping).crates/fm-server/src/metrics.rs,crates/fm-server/src/http.rs. - P0-3 HTTP body limit: axum
DefaultBodyLimit::max(8MB)on all routes (aligned with UDSMAX_LINE_BYTES), over-limit 413 Payload Too Large never reaches handler, prevents POST large-body memory-amplification DoS.crates/fm-server/src/http.rs. - P0-4 backup mechanism:
scripts/backup.sh(SQLite.backuponline hot backup + sled dir cp, timestamp archive, configurable retention window),fm-cli backupsubcommand calls same logic. Cron deployment documented.scripts/backup.sh,crates/fm-cli/src/backup.rs. - P0-5 CI billing-blocked: not a code issue (GitHub account
recent account payments have failedblocks Actions paid runs). Local gate (fmt/clippy/check/test) as proxy metric all green. Deferred until account billing restored, not code-fixable. - P0-6 deployment artifacts:
Dockerfile(multi-stage build, distroless runtime, non-root user, only exposes 11435).scripts/build-artifact.shpackages binary + config template.Dockerfile,scripts/build-artifact.sh.
P1 must-fix (10 items all fixed)
- P1-1 commit partial failure:
commit_episodic_memoryreturnsCommitOutcome{memory_ids, failed_turns}, single turn embed/insert/persist failure recorded asTurnFailurewithout interrupting other turns, client can sense failed turns and retry. Old version crushed all intoErrlosing the whole batch.crates/fm-engine/src/engine.rs,crates/fm-core/src/report.rs. - P1-2 consolidate half-merge compensation: merge writes
memory_itemsuccessfully butmerge_logfails →unmergeauto-rollback (reverse merge + clear merge_log row + warn), no half-merge ghost left.crates/fm-engine/src/engine.rs. - P1-3 tracing + audit log: full-engine
tracingstructured logging (commit/retrieve/consolidate each stage span + counts);audit_logtable records actor/action/target/detail, consolidate audit recordsactor="system".crates/fm-engine/src/engine.rs,crates/fm-persist/src/store.rs. - P1-4 PII log leak:
tracingfields redacted viaredact_text(memory content/params pass PII regex before logging), logs contain no raw PII.crates/fm-engine/src/engine.rs. - P1-5 UDS token auth: UDS connection-level token (
FUSION_MEMORY_UDS_TOKEN, connection first lineAUTH <token>handshake compare, empty token allowed locally), mismatch →-32004 unauthorizeddisconnect. Multi-tenant UDS auth.crates/fm-server/src/uds.rs. - P1-6 cluster bind gate: leader/follower startup validates bind address (non-127.0.0.1/intranet segment → refuse to start, prevent accidental public binding), PRD §16 offline boundary hard constraint.
crates/fm-cluster/src/transport.rs. - P1-7 scale-validation bench:
crates/fm-engine/benches/scale_bench.rs, 10k/100k/1M vector scale validation (FM_SCALEenv selects tier), measures seed throughput / rebuild_from_sled / single knn p99 / 10-concurrent retrieve p99 / sled disk usage. 100k baseline saved tobenches/baseline-scale-2026-08-28.json. Old version only 10k, scale unvalidated.crates/fm-engine/benches/scale_bench.rs. - P1-8 config file:
fm-serversupports TOML config (FM_CONFIGenv ordata_dir/fusion-memory.toml) + env override + secret files (FUSION_MEMORY_API_KEY_FILE/FUSION_MEMORY_UDS_TOKEN_FILE, avoid secrets in env/cmdline) + startupvalidate()fail-visible exit(1). Priority env > TOML > secret_file > default.crates/fm-server/src/config.rs,crates/fm-server/src/main.rs. v1.4.1 adds the[embed]section (mlx_url/embed_model/mlx_api_key/mlx_api_key_file, plus envFUSION_MEMORY_MLX_API_KEY_FILE) so the fusion-mlx embedding key no longer requires env (issue #23). - P1-9 connection pool get timeout: r2d2
connection_timeout(5s)explicit backstop (default 30s), pool-fullget()timeout returnsGetTimeout→MemoryError::Busyretryable, not infinite block preventing deadlock.crates/fm-persist/src/store.rs,crates/fm-persist/src/error.rs. - P1-10 StoreStub naming consistency:
store-stub→local-store(feature flag),StoreStub→LocalStore(type),stub.rs→local.rs(file). The sole implementor's naming de-pejorated (not stub, is long-term production backend).crates/fm-store/.
P2 post-release (6 items: 3 fixed + 3 explicitly deferred)
- P2-2 PII coverage expansion:
redact.rsadds IPv6 (abbreviated + full 8 segments) + international phone (E.164+\d{7,15}non-86 country code, ordered after China phone to avoid double redaction) patterns. Name/address regex high false-positive rate (locale-heavy), deferred to fusion-guard UDSguard.redact(awaiting upstream fusion-guard#2 to add PII classes, see M5 deviation record b).crates/fm-engine/src/redact.rs. - P2-3 summarize failure visibility:
consolidate_summarize's mlx call failure (None = network/non-2xx/parse) or empty content return, old version only warn silently swallowed → now recordsConsolidationFailure{stage:"summarize"}for client awareness.crates/fm-engine/src/engine.rs. - P2-4 API versioning: JSON-RPC
jsonrpc=="2.0"validation (non-2.0 →-32600 invalid_request, old version silently swallowed field); method version prefixv1.<method>routing (no prefix = latest = v1, backward compatible); newversionmethod +GET /v1/memory/versionendpoint returnsapi_versionfor client negotiation.crates/fm-server/src/jsonrpc.rs,crates/fm-server/src/http.rs. - P2-1 auto failover / split-brain protection — landed (v1.0.0 B-2, retired deferred status):
fm-cluster::electionlean self-contained election module (leader-lease + term + quorum + wop_log last_seq judgment, no openraft) replaces manual failover. Leader down → follower auto-campaigns and wins →epoch+++ writes role=Leader → restarts as leader (RTO seconds). Old leader revived is rejected by §1.8 StaleLeader fencing (prevents split-brain double-write). Manualfm cluster promotestill retained (when no election config). Split-brain protection: quorum majority write + epoch fencing + token auth (reuses H3). 16 new tests cover. See### v1.0.0 auto failover election. - P2-5 Persist god-object split into traits — explicitly deferred (architecture refactor epic):
Persistcurrently 30+ methods (Memory/Relation/Entity/Wop/Reconcile mixed). Adjudication deferred: ① splitting 5 traits (Memory/Relation/Entity/Wop/Reconcile) touches all engine call sites (~60 signature changes&Persist→&dyn MemoryStoreetc.) + fm-py PyO3 binding + fm-cluster ReplaySink, is a cross-crate architecture refactor epic, not this round's P0-P3 single-point fix; ② thefm-graph::GraphStoretrait (v0.2.0 §1.5 already split the graph-layer minimal interface) proves the trait-split pattern is viable, the Persist split follows the same method but at a different scale; ③ the currentPersistthough a god-object has clear internal partitioning (each responsibility's methods grouped + commented), not blocking commercial. Roadmap: dedicated PR for the split, with migration tests. - P2-6 dep migration sled→fjall / hnsw_rs alternative — explicitly deferred (under evaluation): sled 0.34 + hnsw_rs 0.3.4 maintenance-risk assessment. Adjudication deferred: ① sled author has pushed fjall (successor project, different API), migration is a local-store backend full rewrite + data format migration (on-disk vectors need reformat), not this round's scope; ② hnsw_rs alternatives (
hnsw/horalibs) need benchmark comparison of recall/latency, evaluation incomplete so no switch; ③ both deps currently functionally stable (100k scale bench validated, see P1-7), no known blocking bug. Roadmap: first bench-evaluate alternative libs' recall/latency, then decide migration priority; sled→fjall if done, with a data-migration script.
Acceptance metric: 403 offline tests all green (baseline 354 → +49 new regression tests, covering P0-2/3 metrics/body, P1-1/8/9/10 outcome/config/pool/rename, P2-2/3/4 PII-expansion/summarize-failure/API-version each 4-6 tests) + clippy
-D warnings+ fmt clean +cargo check --workspaceclean. 3 deferred items (P2-1/5/6) documented as SLA/roadmap, not code-fixable boundary.
- Kuzu DB → SQLite recursive CTE (adjudicated 2026-08-26): PRD §9.2 chose Kuzu DB for the embedded graph, but Kuzu has no Rust binding. Changed to SQLite recursive CTE (
relationtable +WITH RECURSIVEN-hop traversal), implemented infm-persist, consumed byfm-graph::graph_affinity. Functionally equivalent (N-hop reachability + direct hit), no extra server process needed.
PRD §14 M5 three parts, (a) store-fusion optional switch, (b) fusion-guard DLP gate degraded to self-contained PII regex redaction, (c) perf baseline landed.
- (a) store-fusion switch → originally "degraded not implemented", 2026-08-28 adapter landed (adjudicated 2026-08-27 → updated 2026-08-28): PRD §14/Tech Selection planned to reuse
fusion-store(HNSW) as zero-copy backend. Original "degraded not implemented" adjudication based on: ①fusion-store'sFusionStoreEngine(fs-core) and fm-store's same-named trait are two different APIs; ② constrained by "only modify this project's code". 2026-08-28 update: after upstream fusion-store landedfs-core, fm-store addsstore-fusionfeature +fusion.rsadapter (wraps fs-coreEngine, UFCS calls concrete methods not via fs-core trait, avoiding API misalignment), landed as an optional backend (default off, local-store remains default + always-on production backend). Both crates' same-named trait disambiguated by UFCS full-qualification. Zero-copy via ZeroCopyBuffer mmap→owned bridge (holding an mmap reference across calls is unsafe, so owned copy; true zero-copy needs the upper layer to hold the mmap handle alive, future optimization). 6 tests green. store-stub remains the default long-term production backend unchanged, store-fusion is an optional alternative. The perf gate still targets store-stub (default path), (c) unaffected. - (b) fusion-guard DLP gate → credential segment connected to upstream fg-redact, PII segment stays local (adjudicated 2026-08-27 → updated 2026-08-28): PRD R8/§10.4 "M5 connects guard for formal DLP gate". Review found fusion-guard already landed (
fg-redact::Redactorcovers credentials + PII, but PII behavior has defects: idcard wrongly eaten by credit_card / id_number has no validator and mis-eats long digits / +86 phone rejected by border validator). 2026-08-28 handling (user request "switch upstream fg-redact"): filed fusion-guard #10 upstream (requestredact_credentialscredential-subset API, skip PII) → PR fusion-guard#11 landedredact_credentials()+redact_with_patterns()+CREDENTIAL_PATTERNSconst (8 issue10_* tests). fusion-memoryredact.rsstage-1 consumesredact_credentials()(10 credential classes, adding AWS/JWT/PEM/bearer/conn_string/.env coverage previously missing); stage-2 PII remains fusion-memory-local (more accurate than fg-redact PII, tested). Integration point unchanged (with_redact()builder + commit/import write path). Future full UDSguard.redactDLP gate awaits upstream PII behavior fix. - (c) perf baseline → landed: see M5 summary. Both gates met, baseline JSON archived.
Handling of 7 blockers before commercial release. Class A 3 items constrained by cross-repo/account not code-fixable (tracking upstream issues), class B 2 epics + class C 2 items landed this round.
Class A — cross-repo/account blockers (not fixable in this repo, upstream issues filed and tracked)
- A-1 fusion-store zero-copy backend: PRD §14 planned to reuse fusion-store HNSW zero-copy. 2026-08-28 adapter built (
fm-store/src/fusion.rs, featurestore-fusion, wraps fs-coreEngine), landed as optional backend (default off, local-store remains default production backend). Still tracking fusion-store #3 (request VectorIndex exposeget_vector(id)+list_vector_ids()) + fusion-store #4 (request Engine impl) to complete the fs-core API surface. Integration point unchanged (FusionStoreEnginetrait already trait-ified, §1.4). - A-2 fusion-guard DLP gate: see M5 deviation record (b). 2026-08-28 credential segment connected upstream (fusion-guard #10/#11,
redact_credentials()consumed); PII segment stays fusion-memory-local (fg-redact PII behavior defects, by design). Full UDSguard.redactDLP gate awaits upstream PII fix. - A-3 GitHub Actions CI: account billing blocked (P0-5), not code. Local gate (fmt/clippy/check/test + fuzz) as proxy metric all green.
Class B — previously deferred epics, landed this round
- B-1 static encryption: see
### v1.0.0 static encryptionsection. FDE (FileVault/LUKS) as primary at-rest encryption (ops layer,deploy/README.mddocumented) + app-layer AES-256-GCM sensitive-field encryption (fm-persistencryption layer, defense-in-depth). Vectors not app-encrypted (FDE covers + upstream PII redaction). - B-2 auto failover: see
### v1.0.0 auto failover electionsection. Lean self-contained election (leader-lease + term + quorum voting, no openraft dependency — openraft only has alpha with no stable release, commercial risk), replaces manualfm cluster promote. Leader down → follower auto-campaigns, RTO from manual-intervention level down to seconds. Retires P2-1 deferred status.
Class C — clean fix, landed this round
- C-1 API 1.0 stability commitment: 11 crates 0.2.1→1.0.0, SemVer contract + wire-contract freeze (see status section v1.0.0 statement).
jsonrpc::API_VERSION=1+v1.prefix routing +jsonrpc=="2.0"validation in place. - C-2 fuzz + load stress: see
### v1.0.0 fuzz + load stresssection.cargo-fuzzJSON-RPC/HTTP parsing fuzz target + 100k vectors + high-concurrency stress.
Still awaiting upstream (not this round): fusion-store #3/#4 (fs-core API surface completion, adapter built consuming path dep), fusion-guard PII behavior fix (credential segment connected #10/#11, full UDS DLP gate awaits PII fix), GitHub account billing restore (CI).
v1.0.0 B/C batch acceptance: 425 offline tests all green (baseline 408 → +16 election unit + B-1 encryption 4 already in baseline + 1 B-2 e2e full-chain orbit), clippy -D warnings + fmt clean + cargo check --workspace clean. B-1 static encryption 4 tests (plaintext-compat/encrypt-roundtrip/wrong-key fail-open/no-cipher-reads-ciphertext) + B-2 election 16 unit tests (voting 4 criteria + quorum + campaign win/loss + lease expiry + live TCP vote listener + from_env boundary) + B-2 e2e 1 test (fm-server/tests/election_failover.rs, orbit full chain: leader down → campaign → quorum → epoch++/role file → detect Leader).
Layered strategy (Rule 7, single choice FDE as primary + app encryption as defense-in-depth, not averaging the two):
- FDE = primary at-rest encryption (ops layer): SQLite
memory.db+ sled vector store + cluster wop_log all land on FDE-encrypted volumes. macOS = FileVault (full disk); Linux = LUKS/dm-crypt (data volume). FDE is transparent to the application — hnsw_rs computes distance in RAM using plaintext, on-disk is ciphertext, KNN unaffected.deploy/README.mddocumented as deployment prerequisite + verification steps. - App-layer field encryption = defense-in-depth (code): the
fm-persistencryption layer AES-256-GCM encrypts SQLitememory.content+entity.textsensitive text columns, so even if a disk snapshot leaks (bypassing FDE) it is not plaintext. Key source:FUSION_MEMORY_ENC_KEY_FILE(0600 file, 32B raw key) orFUSION_MEMORY_ENC_PASSPHRASE(argon2 KDF derived). Read path transparently decrypts. - Vectors not app-encrypted: the hnsw_rs index needs plaintext to compute distance, app encryption would break KNN. Vectors covered by FDE + upstream PII redaction (redacted before commit, vectors derived from redacted content).
- Cluster transport: intranet loopback/private network, TLS not yet enforced (PRD §16 intranet offline); sensitive fields already app-encrypted, not plaintext in transit.
Key management: no key = plaintext mode (compat with old behavior, env not configured); key configured = encrypted mode. Key rotation = re-encrypt script (fm-cli to add later). Keys not in repo, not in logs.
Lean self-contained election (replaces manual failover, retires P2-1 deferral):
- Selection: lean self-contained election module (
fm-cluster::election, ~400 LOC), no openraft — openraft's latest is only0.10.0-alpha.34, no stable release, MSRV unknown, large dependency tree, commercial blocker (Rule 2 simplicity + Rule 7 single selection). Implements Raft essentials (not full Raft), fits the 100% offline constraint. - Algorithm: leader-lease (
heartbeat_secs × heartbeat_fails, reuses SyncConfig) + term-increment voting + quorum (floor(nodes/2)+1) + log-newness judgment (reuses wop_loglast_seq). - Campaign trigger: follower
run()detects LeaderDown (consecutive heartbeat failures) → transitions to candidate → self-increments term + votes for self → sendsVoteRequestto all peers (newFrameKind::VoteRequest/VoteResponse, reuses TCP wire frame) → gets quorum → wins. - Vote authorization criteria (Raft): ①
candidate.term ≥ own.term; ②candidate.last_seq ≥ own.last_seq; ③ not voted this term (or already voted for this candidate); ④ token matches (reuses H3 auth). Not met → reject. - Win → promote: candidate gets quorum →
epoch++(write_epoch_file, reuses §1.8 fencing) + writesrole=Leader(write_role_file) → orbit exits, supervisor restarts that node as leader. Old leader revived with lower epoch → followerStaleLeaderrejects sync (prevents split-brain double-write). - Priority: smaller node-list index has priority (deterministic, avoids randomness, same-term ties resolved).
- Membership: static, env
FUSION_MEMORY_CLUSTER_NODES=host:port,host:port,...(all nodes), own indexFUSION_MEMORY_CLUSTER_NODE_ID(0-based). Not configured → no election (standalone/manual mode compat, zero overhead). Membership change = change env + restart (not dynamic add/remove, Rule 2). - New deps: none (reuses transport TCP / wop_log / role epoch file / async_trait).
- M3 e2e scenario re-validation: commit→catchup consistent / incremental seq advance / leader down→auto election→new leader continues writing (original promote path changed to auto). 16 election unit tests cover (vote authorize/reject 4 criteria, quorum, campaign win/loss, lease expiry, live TCP vote listener, from_env boundary) + 1 e2e full-chain integration test
fm-server/tests/election_failover.rs: drives production entryspawn_cluster(role=Follower)+ real MemoryEngine + real in-process TCP, verifies leader down →follower_orbitcampaign → quorum → writes epoch++ + role=Leader file →detect_role_with_homereads Leader (fills orbit full-chain coverage, old claim only unit + manual promote). - Manual mode retained:
fm cluster promotestill usable (when no election config).fm cluster statusshows election state + epoch.
- fuzz:
cargo-fuzztargets — JSON-RPC request parsing (malformed JSON / overlong / bad UTF-8 / type confusion / deep nesting), HTTP body boundaries, entity extraction JSON parsing. Crash/panic treated as defect.crates/fm-server/fuzz/. - load stress: expanded
retrieve_bench— 100k vectors (baseline 10k × 10) + concurrency gradient (1/10/50/100), recording p50/p99/p999 + throughput. Commercial-grade load validation. Results saved tobenches/baseline-1.0.0-*.json.
Config governance Phase 3.3 (workspace architecture/fusion-config-opti-archi-1028.md §Phase 3). start.sh start verifies its own port-registry.yaml entry after the port-in-use check and before launching fm-server: the service's configured FUSION_MEMORY_HTTP_PORT must match the registered port: for fusion-memory, otherwise startup refuses (exit 1) — moving the CI check-ports.py failure forward to first boot. Semantics: missing registry file (FUSION_REGISTRY, default ~/fusion/architecture/port-registry.yaml) = warn + continue (standalone deployments without the workspace checkout); FUSION_SKIP_REGISTRY_CHECK=1 bypasses with a warning. Shell-only patch, no Rust code or API change. Acceptance verified against temp registry copies: unregistered port refuses (exit 1), registered port starts to Running, bypass and missing-file paths warn + start.
Config governance Phase 2.2 (workspace architecture/config-conventions.md §1). The issue's required-rename list for this repo was empty — every name already follows the FUSION_MEMORY_* core-repo convention. The single check-env finding ([legacy] start.sh) was a checker false positive: rule 2c matched by line-level substring, and compliant FUSION_MEMORY_MLX_API_KEY (the v1.4.1 API contract, issue #23) contains the substring MLX_API_KEY. Fixed in the workspace checker (scripts/check-env.py): rule 2c now extracts FUSION_* tokens and matches legacy names by exact prefix (alias-chain variants like FUSION_MLX_API_KEY_FILE still caught; the trailing-underscore FUSION_FUSION_ bug pattern stays substring as in rule 2a). Repo deliverable: consolidated "Environment variables" section (this README + README_CN) — full inventory with defaults, sources, and the env > TOML > secret-file > default priority chain. Acceptance: python scripts/check-env.py (workspace root) = 0 findings for this repo. Docs-only patch, no code change, no API change.
The v1.4.2 commit landed red: CI checks out fusion-memory alone, but fm-engine depended on fg-redact and fm-store (optional store-fusion) on fs-core via monorepo sibling relative paths — the workspace manifest failed to load before any build step ran. Both are now pinned git tag dependencies on the public upstream repos (dahai80/fusion-guard v0.2.0-rc.7, dahai80/fusion-store v0.2.0-rc.3), each the same commit as the local working tree (zero drift). Verified by cargo test --workspace in a fresh standalone clone with no sibling repos. Runtime stays 100% offline (HTTP only to 127.0.0.1); cargo fetches each git dep once at build time and caches it. Local upstream co-development: [patch."https://github.com/dahai80/fusion-guard"] / [patch."https://github.com/dahai80/fusion-store"] overrides.
Deterministic synthetic corpus + ground-truth queries prove the fusion score against plain cosine — and forced a retune on first measurement: raw-W(t) fusion scored worse than plain cosine (recall@5 0.4250 → 0.2285 full scale) because W(t) carries the type-dependent w0, so the decay term promoted Procedural/Semantic items regardless of relevance. The fusion decay term now uses normalized freshness (freshness_of(), w0 stripped, capped 1.0); α/β/γ weights and the θ_drop/θ_promote scheduling thresholds (raw W(t)) are unchanged. Full data + decision: docs/benchmarks/retrieval-quality-2026-10-25.md.
Full-scale ablation (400 topics / 5200 items / 400 queries, seed 0x5eed25abcd100001):
| arm | recall@5 | nDCG@10 | MRR |
|---|---|---|---|
| plain cosine | 0.4250 | 0.5797 | 0.7100 |
| cosine + decay (normalized freshness) | 0.6325 | 0.7891 | 0.9307 |
| cosine + graph affinity | 0.7190 | 0.8016 | 0.8603 |
| full fusion (α=0.5, β=0.3, γ=0.2) | 0.8270 | 0.9042 | 0.9629 |
| fusion, raw W(t) — pre-retune, deprecated | 0.2285 | 0.3166 | 0.6193 |
Temporal validity (fresh over stale duplicates): full fusion 40/40 queries vs plain cosine 18/40. CI gate (cargo test -p fm-engine --test retrieval_quality, <1s): fusion never below plain cosine, no component drags, seed-deterministic; full scale on demand via FM_RQ_SCALE=full.
- Core Rust (no GC pauses), SQLite WAL + SQLite recursive CTE graph traversal (replaces Kuzu, see deviation record), store-stub (hnsw_rs + sled, long-term production backend)
- Three-tier memory: Working → Short-Term → Long-Term Graph
- Ebbinghaus forgetting curve + entity-relation cognitive graph
- Turn-level storage: a single dialogue turn = one MemoryItem, retrieval aggregates by
interaction_idto restore the full Interaction - 100% offline (local + intranet cluster), no cloud API, HTTP only to 127.0.0.1
| crate | responsibility |
|---|---|
fm-core |
Core data structures + FusionMemoryEngine trait (zero business deps) |
fm-engine |
Engine impl: MemoryEngine + entity extraction + fused scoring + decay + Long promotion |
fm-similarity |
Cosine similarity + decay W(t) (forgetting curve + reinforcement cap) |
fm-graph |
Rule-priority entity alignment (A5) + alias dictionary + graph_affinity (N-hop) |
fm-store |
FusionStoreEngine trait + local-store backend (default) + store-fusion backend (optional feature, wraps fusion-store fs-core) |
fm-embed |
fusion-mlx bge-m3 embedding (LRU+semaphore) + StubEmbedder |
fm-persist |
SQLite WAL metadata schema + CRUD + relation table (recursive CTE graph traversal) |
fm-server |
UDS JSON-RPC + HTTP service |
fm-py |
PyO3 Python binding |
fm-cli |
CLI ops/import/query |
fm-cluster |
M6 cluster sync: leader/follower roles + wop_log replication + TCP transport + manual failover; v1.0.0 B-2 auto failover election (election module, leader-lease + term + quorum) |
cargo check --workspace # compile check
cargo test --workspace # all offline tests (478 cases, excludes fm-py cdylib; --features fm-store/store-fusion = 484)
cargo clippy --workspace --all-targets -- -D warnings # lint
cargo fmt --all --check # format check
# §13.2 perf baseline (store-stub 10k entries, StubEmbedder, no model):
# cargo bench -p fm-engine --bench retrieve_bench
# single retrieve p99<50ms + 10-concurrent p99<200ms, results to /tmp/fm-perf-baseline-*.json
# §13.2 live perf baseline (real fusion-mlx bge-m3 dim=1024, end-to-end retrieve latency, closes RC known-limitation #1):
# requires fusion-mlx running with bge-m3 loaded (standalone needs FUSION_ROUTE_WARN_ONLY=true + --api-key)
# cargo bench -p fm-engine --features mlx-live --bench retrieve_bench_live
# three paths: cold (unique query, real mlx) / cached (LRU hit, skip mlx) / concurrent x5 (real mlx)
# baseline JSON: crates/fm-engine/benches/baseline-live-bgem3-2026-08-28.json
# live data (Apple Silicon): cold p50=9.98ms/p99=10.73ms, cached p50=0.107ms/p99=0.126ms, concurrent x5 p50=44.6ms
# scale constrained by fusion-mlx rate-limit bug (#692: _serve_from_model_dir misses config, 60rpm always-on), small-scale real-latency reference; large-scale stress uses retrieve_bench (StubEmbedder)
# real-model integration tests (requires fusion-mlx running with bge-m3 + Qwen chat model, serial to avoid 429):
# ~/claude-home/fusion-mlx/start.sh start
# scripts/live-test.sh # full workspace live (serial)
# scripts/live-test.sh fm-engine # single crate
# chat model defaults to Qwen3.5-9B-4bit; if not cached, override via env:
# FUSION_MEMORY_CHAT_MODEL=Qwen3.8-27B-4bit scripts/live-test.sh # verified usable
# (Qwen3-0.6B entity extraction too weak, returns empty entities, not recommended for extract)Coverage (requires llvm-tools, system llvm or rustup component):
# Offline default (CI metric): excludes fm-py (PyO3 cdylib binding layer, acceptance via PyO3 roundtrip, not unit-test coverage).
# regions 90.82%. 429 cases all green (--features fm-store/store-fusion = 435).
# Note: run `cargo llvm-cov clean` first, stale profraw (including untriggered bench instrumented binaries) dilutes regions.
# engine.rs summarize/consolidate saga + engine_builder.rs !stub branches unreachable offline (via real mlx LLM/embedding),
# PRD acceptance metric = live (covers these branches).
LLVM_COV=/opt/homebrew/opt/llvm/bin/llvm-cov \
LLVM_PROFDATA=/opt/homebrew/opt/llvm/bin/llvm-profdata \
cargo llvm-cov --workspace --summary-only --exclude-from-report fm-py --ignore-filename-regex "src/main\.rs"
# Real-model integration (requires fusion-mlx running with bge-m3 + Qwen3.5-9B-4bit, serial to avoid 429):
# ~/claude-home/fusion-mlx/start.sh start
LLVM_COV=/opt/homebrew/opt/llvm/bin/llvm-cov \
LLVM_PROFDATA=/opt/homebrew/opt/llvm/bin/llvm-profdata \
cargo llvm-cov --workspace \
--features fm-embed/mlx-live --features fm-engine/mlx-live --features fm-server/mlx-live \
--summary-only --exclude-from-report fm-py -- --include-ignored --test-threads=1
# live regions 92.47% (covers !stub real-mlx branches summarize/consolidate saga + engine_builder). PRD acceptance uses this.Coverage metric: regions is the standard (industry standard + PRD has no functions hard target). functions % is falsely low due to trait monomorphization cross-binary duplicate 0-counting (
FusionStoreEngine for StoreStubinstantiated in each test binary, uninvoked instances count 0), not truly uncovered — stub.rs all methods have unit tests,tests/offline_integration.rsactually calls the trait path. regions is unaffected by this artifact.fm-py excluded: PyO3
extension-modulecdylib, macOS needs.cargo/config.toml's-undefined dynamic_lookupto lazily resolve Python symbols. Acceptance = PyO3 roundtrip (see below), not unit-test coverage, consistent with PRD §11.3.PyO3 roundtrip acceptance (GIL-safe, C2):
PYO3_PYTHON=/opt/homebrew/bin/python3.12 cargo build -p fm-py cp target/debug/libfusion_memory.dylib /tmp/fmpy/fusion_memory.so python3.12 roundtrip.py # commit→2 ids / retrieve→block / consolidate→report / GIL not frozenDuring commit, another thread's pure-Python counter keeps incrementing → proves
py.allow_threadsreleases the GIL, event loop not frozen.
Toolchain: edition 2021, MSRV 1.87. System rustc (Homebrew) compiles directly, no rustup needed.
Consolidated env contract (workspace architecture/config-conventions.md §1, config governance Phase 2.2, issue #29). All names are FUSION_MEMORY_* scoped (core-repo convention; no bare or cross-app names). Resolution priority: env > TOML (FM_CONFIG or <data_dir>/fusion-memory.toml) > secret file > default. Secrets should go through the *_FILE variants or the [embed] TOML section rather than env/command line.
Server (fm-server/src/config.rs):
| Env | Default | Purpose |
|---|---|---|
FM_HOME |
~/.fusion-memory |
data dir; sock path derives from it |
FM_CONFIG |
<data_dir>/fusion-memory.toml |
TOML config path |
FUSION_MEMORY_SOCK |
<data_dir>/fusion-memory.sock |
UDS socket path |
FUSION_MEMORY_HTTP_PORT |
11435 |
HTTP port (0 = HTTP off; opening HTTP requires api_key) |
FUSION_MEMORY_API_KEY / _FILE |
empty | HTTP bearer key (empty = no auth; file variant reads a 0600 secret file) |
FUSION_MEMORY_UDS_TOKEN / _FILE |
empty | UDS connection-level token (AUTH <token> handshake) |
FUSION_MEMORY_GATEWAY_ORIGIN_REQUIRED |
false |
require X-Fusion-Route gateway header on HTTP |
FUSION_MEMORY_DEFAULT_TENANT |
empty | tenant when caller provides none |
FUSION_MEMORY_MULTI_TENANT |
false |
enable fusion-identity JWT boundary verify |
FUSION_MEMORY_IDENTITY_URL |
http://127.0.0.1:11470 |
fusion-identity verify endpoint |
FUSION_MEMORY_IDENTITY_SERVICE_TOKEN / _FILE |
empty | service token for identity verify calls |
FUSION_MEMORY_DIM |
1024 |
embedding dimension (must be > 0) |
FUSION_MEMORY_STUB |
unset | =1 uses StubEmbedder (offline FNV-1a) instead of real bge-m3 |
FUSION_REGISTRY |
~/fusion/architecture/port-registry.yaml |
start.sh registry self-check source (issue #31); missing file = warn + continue |
FUSION_SKIP_REGISTRY_CHECK |
unset | =1 bypasses the start.sh registry self-check (warns) |
Embedding (fm-embed + fm-server [embed] TOML mirror, same env wins for both):
| Env | Default | Purpose |
|---|---|---|
FUSION_MLX_URL |
http://127.0.0.1:11434/v1 |
fusion-mlx OpenAI-compatible base URL |
FUSION_MEMORY_EMBED_MODEL |
bge-m3 |
embedding model name |
FUSION_MEMORY_MLX_API_KEY / _FILE |
empty | fusion-mlx API key (env wins over [embed] TOML) |
FUSION_MEMORY_MLX_CONCURRENCY |
2 |
concurrent embedding requests to mlx |
FUSION_MEMORY_CHAT_MODEL |
Qwen3.5-9B-4bit |
live-test entity-extraction chat model |
Engine / persistence:
| Env | Default | Purpose |
|---|---|---|
FUSION_MEMORY_REDACT_PII |
on (fail-closed) | =0/=false disables PII redaction on commit/import |
FUSION_MEMORY_ENC_KEY_FILE |
unset | AES-256-GCM key file (0600, 32B raw key) |
FUSION_MEMORY_ENC_PASSPHRASE |
unset | argon2id KDF passphrase (alternative key source) |
FUSION_MEMORY_WOP_RETENTION |
no prune | write-op log retention bound (caller-side pruning) |
Cluster (fm-cluster; all intranet, no cloud):
| Env | Default | Purpose |
|---|---|---|
FUSION_MEMORY_ROLE |
role file > standalone | inject leader/follower/standalone role |
FUSION_MEMORY_SYNC_PORT |
11436 |
TCP sync port |
FUSION_MEMORY_CLUSTER_BIND_ADDR |
127.0.0.1 |
sync listener bind address |
FUSION_MEMORY_CLUSTER_TOKEN |
unset | H3 handshake auth token |
FUSION_MEMORY_CLUSTER_ALLOW_NO_TOKEN |
false |
accept token-less peers (intranet trust) |
FUSION_MEMORY_CLUSTER_EPOCH |
epoch file > 0 | fencing epoch (rejects stale leaders) |
FUSION_MEMORY_CLUSTER_NODES |
unset | election membership host:port,... (unset = no election) |
FUSION_MEMORY_CLUSTER_NODE_ID |
unset | 0-based own index in CLUSTER_NODES |
FUSION_MEMORY_LEADER |
unset | follower mode trigger: leader address to sync from |
FUSION_MEMORY_HEARTBEAT_SECS |
5 |
follower ping interval |
FUSION_MEMORY_HEARTBEAT_FAILS |
3 |
consecutive failures = LeaderDown |
FUSION_MEMORY_FETCH_LIMIT |
256 |
max wops per sync pull |
Gate: python scripts/check-env.py (workspace root) — this repo has 0 findings.
commit Interaction ──turn-level split──> MemoryItem per turn
├── embed(text, dim) [M1: FNV-1a deterministic stub]
├── store.insert_vector(vec_id, vec) [hnsw_rs + sled]
└── persist.put_memory(item) [SQLite WAL]
retrieve query ──embed──> store.search_knn(top_k)
──hit interaction_id──> persist.list_by_interaction backfill all turns
──assemble ContextBlock──> token-budget truncate ──> FormattedContext
delete persist.get_memory → store.delete_vector (tombstone) + persist.tombstone
- vector_id = FNV-1a(ulid_string) → u64, stored in
MemoryItem.vector_ref - Aggregate restore: retrieval hits a turn, then queries persist for all turns by
interaction_id,AGG_MAX_TURNS=20 - Soft delete: store tombstone + persist tombstone;
compactphysically removes + rebuilds hnsw
commit turn ──MlxEmbedder.embed──> store.insert_vector(dim=1024)
└── persist.put_memory(entities_pending=true) ──> async extract_and_attach
└── MlxEntityExtractor(chat, injection-proof prompt) ──> strict JSON parse ──> entities writeback
retrieve query ──embed──> KNN ──> score_candidate = α·cosine + β·W(t) + γ·graph_affinity
α=0.5 β=0.3 γ=0.2; W(t)=W0·exp(-t/τ)·min(1+log2(1+count), CAP)
graph_affinity: direct hit=1.0, N-hop=0.5^h (hop≤2), else 0
consolidate W(t)<θ_drop(0.05) → tombstone reclaim; Short→Long promotion; entities_pending batch re-extract
import agent-studio memories ──map──> embed ──> ingest (scope/metadata → Project entity)
- Entity alignment rule-priority chain (A5, hit-and-stop): rule1 normalize+same-name-same-type(pri=3) → rule2 alias-dictionary canonical-name(pri=2) → rule3 existing name/alias(pri=1) → rule4 vector-threshold(pri=0) → new entity(pri=-1). Same name different type not mergeable.
- Injection-proof (§11.4): dialogue content wrapped in
<data>tags, prompt explicitly says ignore instructions inside the tag; parse failure → empty entities,entities_pendingstays true, content+vector still ingested. - Live acceptance:
FUSION_MEMORY_MLX_API_KEY=change-me cargo run -p fm-cli --example live_acceptance(requires fusion-mlx running bge-m3 + Qwen3.5).
cargo build --release -p fm-cli # produces target/release/fm
# Write a multi-turn interaction (JSON from stdin or --file)
echo '{"id":"ix-1","session_id":"s","turns":[{"turn_idx":0,"user_message":"hello rust","assistant_message":"hi","tool_calls":[]}],"timestamp":1,"metadata":{}}' \
| ./target/release/fm --home ~/.fusion-memory --dim 64 commit --session s
./target/release/fm --home ~/.fusion-memory query --text "hello rust" --top-k 5 --budget 4096
./target/release/fm --home ~/.fusion-memory stats
./target/release/fm --home ~/.fusion-memory delete --id <memory_id> --confirm
./target/release/fm --home ~/.fusion-memory doctor
# Import from fusion-agent-studio history memories (real bge-m3, dim=1024)
FUSION_MEMORY_MLX_API_KEY=change-me ./target/release/fm --home ~/.fusion-memory import
# Offline test import (--stub uses StubEmbedder, dim=64)
./target/release/fm --home ~/.fusion-memory import --stub --source /path/to/memory.db
# M3: forget/merge/summarize/reconcile saga (PRD §5.6)
./target/release/fm --home ~/.fusion-memory consolidate # trigger saga, reports dropped/promoted/merged/summarized/reextracted/reconciled
./target/release/fm --home ~/.fusion-memory merges # list merge_log (to find id for unmerge)
./target/release/fm --home ~/.fusion-memory unmerge --id 42 # undo merge: source reverse tombstone, delete merge_log
./target/release/fm --home ~/.fusion-memory reconcile # cross-DB reconciliation: tombstone physical delete, dangling vectors in report--home defaults to ~/.fusion-memory (or FM_HOME env), --dim defaults to 64 (stub); real embedding uses bge-m3 dim=1024, import without --stub auto-uses 1024.
import mapping: tier short_term→Short / long_term→Long / archive→skip; memory_type user→Semantic / feedback→Procedural / project→Episodic / reference→Semantic; scope graph:NAME or metadata.graph_id → Project entity.
cargo build --release -p fm-server # produces target/release/fm-server
# start.sh management (start/stop/restart/status/log/doctor)
./start.sh start # start fm-server (default real bge-m3; FUSION_MEMORY_STUB=1 offline)
./start.sh stop # graceful stop (SIGTERM)
./start.sh status # PID/port/sock/memory/healthz
./start.sh doctor # health check: binary/port/mlx connectivity/data dir
./start.sh log # tail logs
# env (see ServerConfig::from_env):
# FM_HOME (default ~/.fusion-memory)
# FUSION_MEMORY_HTTP_PORT (default 11435) / FUSION_MEMORY_API_KEY (HTTP required, B5)
# FUSION_MEMORY_STUB=1 (StubEmbedder offline, no mlx)UDS JSON-RPC (sock 0600, B6) + HTTP (axum enforced Bearer, B5, port 11435) concurrent. Without FUSION_MEMORY_API_KEY but HTTP port open → refuses to start HTTP (UDS only). Routes: POST /v1/memory/{commit,retrieve,consolidate,audit,delete,delete_scope,count}, GET /v1/memory/{id}, GET /healthz (public); delete/delete_scope require params.confirm=true. UDS method memory.retrieve_context (issue #1/#4 contract, {trigger_id,query,top_k,node_id} → {context,memory_ids,cache_hit}, reuses retrieve engine chain).
- 4-space indentation, no docstrings (
//!module docs + inline comments) tracinglogging,anyhow(application) +thiserror(library)- Fail visibly, no silent error swallowing