Summary
An audit of the workspace as it stands (api/, src/, core/, adapters/*, crates/tinymemory-module) finds that the repository ships three parallel memory APIs, that tinymemory-core is coupled to TinyCortex in 97 of its 223 source files, that the driver-admission machinery the README describes is never invoked at runtime, and that no engine is feature-gated — so every consumer compiles SQLite, libgit2 (optionally), reqwest, axum, tinyagents and the whole TinyCortex engine whether it uses them or not.
This issue proposes one unified memory schema, a fully abstracted memory surface, the relocation of provider sync (Composio and friends) onto that surface, hard containment of everything TinyCortex-specific behind a feature-gated module, and the feature/E2E test matrix that would keep all of it honest.
No code changes are proposed here — this is the design and scope record.
Part 1 — Audit findings
1.1 Three memory APIs describe the same thing
| Surface |
Where |
Shape |
Who implements it |
tinymemory_api::traits::Memory |
api/src/traits.rs |
10 async fn, anyhow-typed |
TinycortexMemory, the three remote adapters |
tinymemory_api::provider::MemoryProvider |
api/src/provider/ |
13 capability families, MemoryError-typed |
NullMemoryProvider, MemoryTraitProvider (3 families), ModuleMemoryProvider (13 families) |
tinycortex::memory::Memory + tinycortex::memory::* |
vendor/tinycortex |
the engine's own trait and ~15 submodules |
UnifiedMemory, and 97 files in core/ that call it directly |
core/src/traits.rs re-exports the TinyCortex trait, not the TinyMemory one:
pub use tinycortex::memory::{
Memory, MemoryCategory, MemoryEntry, MemoryTaint, NamespaceSummary, RecallOpts,
};
So tinymemory_core::Memory and tinymemory_api::traits::Memory are two distinct Rust traits with identical method sets, and tinymemory_core::MemoryEntry and tinymemory_api::types::MemoryEntry are two distinct structs with identical fields. adapters/tinycortex/src/convert.rs exists solely to rebuild values field-by-field across that seam, and its own module docs say so:
The two contracts describe the same values and, today, describe them identically — tinymemory-api was moved out of tinycortex-api. They are nonetheless distinct Rust types in distinct crates, so a value has to be rebuilt to cross.
The file is careful and exhaustively destructures on purpose, which is the right mitigation — but the duplication it mitigates is the actual defect. Every new field is now a three-place edit (tinycortex-api, tinymemory-api, convert.rs) plus a conversion test.
1.2 The abstraction is bypassed by the code that matters
MemoryProvider advertises 13 families. Outside of NullMemoryProvider (the /dev/null reference) and ModuleMemoryProvider (which lives in an excluded, non-member crate), nothing implements the ten optional families:
adapters/tinycortex deliberately implements the mandatory three only, and documents why.
adapters/remote (Supermemory, Mem0, Cognee) implement the mandatory three only.
core/ implements the substance of trees, chunks, entities, graph, diff, goals, tool-memory, ingestion and sources — against tinycortex::memory::* directly, never through MemoryProvider.
Net effect: the contract is real for out-of-process module consumers and decorative for everyone in-tree. A second engine "binds in its place without the host learning anything new" (README) only for store/recall/export; every other feature of the product would silently vanish.
1.3 Driver admission is dead code
src/registry/ implements class reservation, config labels, and a fail-closed external-driver gate. Grepping the whole workspace for callers:
DriverRegistry::admit — referenced only in a doc comment in src/lib.rs.
MemoryHostConfig::memory_provider() — defined on the config seam, implemented by TestHostConfig and ModuleConfig, read by nobody.
The engine is chosen by calling create_memory_client_with_local_ai in core/src/store/factories.rs, which constructs TinyCortex unconditionally. Configuration cannot select an engine today.
1.4 Composio sync is welded to the engine
core/src/sync/composio/ is presented as the host-side sync layer, but the execution path is crate::tinycortex::run_composio_connection*:
core/src/sync/composio/mod.rs:164
core/src/sync/composio/providers/traits.rs:62
core/src/sync/composio/providers/gmail/provider.rs:155
core/src/sync/composio/providers/notion/provider.rs:282
core/src/sync/composio/providers/slack/provider.rs:236
Sync state (DailyBudget, SyncState, KV_NAMESPACE), the payload normalisers, and the source readers (core/src/sources/readers/*.rs) are all TinyCortex re-exports or thin Config adapters over TinyCortex readers. The same holds for core/src/sources/types.rs.
Consequence: a Mem0 or Supermemory deployment gets zero Composio sync, and the MemorySourceSink / MemoryIngest families — which exist precisely to express "pull upstream data into memory" — are unused by the sync layer that needs them.
1.5 Engine-specific concerns leak out of the engine module
core/src/tinycortex/ holds 11 files. TinyCortex-shaped concerns living outside it include:
core/src/people/ — resolver, store, and the macOS CNContactStore address-book reader (contacts feature). Contacts are an engine/product concern, not part of a memory contract.
core/src/persona-adjacent config (core/src/tinycortex/persona.rs is in the right place, but its dirs-based vault default leaks into the crate's dependency set).
core/src/diff/ — ops and stubs shaped around the TinyCortex git ledger.
core/src/tree/, core/src/store/chunks/, core/src/queue/ — 96 references to tinycortex::memory::tree, 67 to ::chunks, 29 to ::queue.
1.6 Nothing is feature-gated, so nothing is small
core/Cargo.toml has exactly three features: memory-git, test-support, contacts. There is no feature for an engine. Unconditional, non-optional dependencies include tinycortex (with obsidian, persona, sync on), tinycortex-api, tinyagents (with sqlite), rusqlite (bundled — compiles SQLite from source), reqwest (+rustls), axum, tokio (full), regex, walkdir, uuid, sha2, chrono, dirs, rand, url.
A host that wants Mem0 over HTTP still compiles bundled SQLite and the entire embedded engine. The facade crate (tinymemory) is dependency-light and fine; tinymemory-core is the problem, and it is what the module and every real consumer depend on.
api/Cargo.toml documents a forbidden-dependency guard as a comment with a suggested cargo tree invocation. It is not enforced anywhere in CI.
1.7 Test coverage is lopsided, and there is no cross-crate E2E
1,163 test attributes across 128 files, distributed:
| Crate |
#[test] / #[tokio::test] |
core |
879 |
api |
186 |
crates/tinymemory-module |
44 |
src (facade) |
33 |
adapters/tinycortex |
18 |
adapters/remote |
3 |
Structural gaps:
- No
tests/ directory at the workspace root, and none in api/, adapters/*, or the facade. AGENTS.md mandates tests/ for public-API integration tests; only core/tests/ exists, and it contains fixtures only, no test files.
- No
examples/ at the root (AGENTS.md requires runnable, CI-compiled examples and the README's cargo run --example basic does not exist). The only example is adapters/remote/examples/conformance.rs.
- Three tests total for the remote adapters — one happy-path round trip per engine, against a hand-rolled axum double. No error mapping, no pagination, no taint preservation, no
Unsupported behaviour.
- No conformance suite any driver can be run through.
audit_provider checks advertised-vs-reachable but nothing checks behavioural equivalence across engines.
- No feature-matrix CI. CI runs
--all-features and default only. memory-git off/on, contacts, and test-support are never exercised in isolated combinations.
- The
integration/remote-engines/ Docker harness is documented but is not run by any workflow.
docs/specs/ contains one spec (tinybus-module.md) and docs/plans/ is empty, contrary to AGENTS.md's spec-then-plan rule.
1.8 Smaller findings
rust_out and tmp — two 3.6 MB executables — are committed at the repository root.
worktrees/cognee-supermemory/ is present in the working tree with its own target/.
- MSRV is inconsistent:
1.96 for the facade and the module, 1.85 for api, core, and both adapters.
README.md documents a layout (src/registry/, src/mandatory/, adapters/, vendor/) that omits core/ entirely — the largest crate in the repository is undocumented in the README.
tinymemory-core's doc comment still frames the crate as "extracted from OpenHuman's src/openhuman/memory/", and default_openhuman_dir() hardcodes ~/.openhuman in an engine-neutral crate.
Part 2 — Proposed work
A. One schema
A1. Delete the duplicate value types. Pick tinymemory-api as the single source of truth for MemoryEntry, MemoryCategory, MemoryTaint, NamespaceSummary, RecallOpts, Capability, and the provider::types set. tinycortex-api either depends on tinymemory-api and re-exports, or is retired in favour of it. Either way adapters/tinycortex/src/convert.rs collapses to nothing.
A2. One Memory trait. tinymemory_api::traits::Memory is the storage trait; tinymemory_core::traits::Memory becomes a re-export of it, not of the engine's. UnifiedMemory implements the TinyMemory trait directly.
A3. MemoryProvider becomes the only in-tree memory surface. Everything in core/ that today calls tinycortex::memory::* goes through a &dyn MemoryProvider (or a family accessor). Where a family is missing a method the product needs, the fix is to widen the family — not to reach past it.
A4. Retire the anyhow / MemoryError split at the boundary: one error type across the contract, so "unsupported" is distinguishable from "failed" on every path, not just the provider one.
A5. Wire the registry. MemoryHostConfig::memory_provider() selects the driver; DriverRegistry::admit gates it; create_memory_* in core/src/store/factories.rs returns a bound Arc<dyn MemoryProvider> chosen by that decision instead of a hardcoded TinyCortex client. Until this lands, src/registry/ is untested-in-anger code.
B. Sync moves up, onto the memory API
B1. core/src/sync/ (Composio, workspace watcher, MCP) is rewritten against MemoryIngest, MemorySourceSink, MemoryDocuments and the KV surface. No crate::tinycortex::* call from any file under core/src/sync/.
B2. Sync state (DailyBudget, SyncState, budget accounting, the KV namespace) becomes engine-neutral, persisted through the provider's KV surface rather than re-exported from tinycortex::memory::sync.
B3. Payload normalisers (normalize::{github,linear,notion,gmail,slack}) are pure Value → Value transforms with no engine dependency. Move them back into a tinymemory-sync crate — or a sync module of core — so a non-TinyCortex engine gets Composio sync for free.
B4. core/src/sources/readers/* reader implementations likewise: a reader produces SourceItems and hands them to MemoryIngest. core/src/sources/types.rs stops being a TinyCortex re-export shim.
B5. Acceptance test for this section: Composio Gmail sync completes end to end against a driver that is not TinyCortex.
C. TinyCortex contained
C1. Everything engine-specific lives under core/src/tinycortex/ (or, better, moves wholesale into adapters/tinycortex), behind a tinycortex feature. Nothing outside that module names the tinycortex crate. Target: 0 of the current 97 files.
C2. Contacts / people (core/src/people/, including the macOS CNContactStore reader) move behind the engine module and stay gated by contacts, which in turn requires tinycortex. Same for persona, obsidian vault handling, and the git diff ledger (memory-git).
C3. adapters/tinycortex grows implementations of the optional families it can actually serve — tree, documents, ingest, entities, graph, diff, tool-memory, sources, maintenance — so MemoryTraitProvider's three-family ceiling stops being the reason core/ bypasses the contract. Much of ModuleMemoryProvider (crates/tinymemory-module/src/provider.rs, 1,197 lines) is exactly this code and should be lifted down into the adapter, leaving the module crate a thin bus transport.
C4. default_openhuman_dir() and the OpenHuman framing come out of tinymemory-core.
D. Feature-gated engines
D1. Engine features on the facade and on core:
[features]
default = []
tinycortex = ["dep:tinymemory-tinycortex", "dep:tinycortex"]
supermemory = ["dep:tinymemory-remote"]
mem0 = ["dep:tinymemory-remote"]
cognee = ["dep:tinymemory-remote"]
# capability add-ons, each requiring the engine that serves it
memory-git = ["tinycortex", "tinycortex/git-diff", "tinycortex/wiki-git"]
contacts = ["tinycortex", ...]
sync-composio = [] # engine-neutral once B lands
embeddings-local = ["dep:tinyagents"]
D2. Every heavy dependency in core/Cargo.toml becomes optional = true and is reachable only through a feature: rusqlite, tinyagents, axum, reqwest, walkdir, dirs, rand, regex. Default features build the contract, the registry, the mandatory composition, and nothing that links C.
D3. With --no-default-features, the crate must still compile and bind NullMemoryProvider.
D4. Enforce api/'s forbidden-dependency rule in CI with the forward-form cargo tree check its own manifest already spells out, rather than leaving it as a comment.
D5. Publish a size/timing budget in CI: dependency count and cold build time for --no-default-features, for --features mem0, and for --features tinycortex, so a regression is visible on the PR that causes it.
E. Test plan
E1. Driver conformance suite — a tinymemory-conformance crate exposing one function that takes an Arc<dyn MemoryProvider> and asserts the contract:
- mandatory three: store/get/forget/list/namespaces; recall ordering and limit; export pagination and import round-trip.
store_with_taint preserves ExternalSync (the failure this repo already calls out as security-relevant).
- upsert semantics on
(namespace, key).
- unicode, empty-string, and oversized keys/content.
- every unadvertised family returns
MemoryError::Unsupported, never Ok.
audit_provider reports no advertised-but-unreachable family.
Run it against: NullMemoryProvider, an in-memory reference driver, the TinyCortex adapter, and each of the three remote adapters (against the integration/remote-engines/ Docker harness, gated as live_*). This is what makes "swap the engine" a claim with evidence.
E2. Feature-matrix CI job — build + test each of:
--no-default-features · --features tinycortex · --features tinycortex,memory-git · --features tinycortex,contacts (macOS runner) · --features mem0 · --features supermemory · --features cognee · --features sync-composio · --all-features
Plus a cargo hack --feature-powerset --depth 2 check-only pass to catch feature-unification breakage.
E3. Workspace-level integration tests (tests/ at the root, public API only):
driver_selection.rs — config names an engine; the registry admits it; the bound provider's driver_id() matches; an unreserved external id is refused fail-closed.
capability_negotiation.rs — a host filters its surface from the cached capability set; a driver that lies fails the audit.
taint_end_to_end.rs — external content stored through the sync path arrives with ExternalSync at every engine.
null_provider.rs — the compiled-out configuration is usable and returns Unsupported rather than panicking.
E4. Sync E2E — with a mock Composio API and a mock provider API:
- Gmail / Slack / Notion / GitHub / Linear / ClickUp: fetch → normalise → ingest → recall the ingested content, asserted through
MemoryProvider only.
- incremental sync respects the persisted cursor.
- daily budget exhaustion stops the sync and records the reason.
- the same suite runs against two different drivers and asserts identical observable outcomes.
E5. Module E2E — keep the existing one-process-per-test loader job, and extend it to cover every family the module advertises (today the E2E is a single 569-line file), plus a shutdown/restart cycle.
E6. Property and failure-path tests:
- round-trip properties on the wire types (
api/src/wire.rs) and on export/import.
MemoryTaint::from_db_str fails closed on arbitrary input.
Capability::parse / as_str round-trip for all 13, with the serde strings pinned (partially covered today).
- backend error, timeout, and partial-page responses on every remote adapter — currently zero coverage.
E7. Docs and examples as tests — restore cargo run --example basic referenced by the README, add one example per engine, and keep cargo doc -D warnings green (already in CI).
E8. Coverage gate — AGENTS.md asks for 80% of meaningful library behaviour; nothing measures it. Add cargo llvm-cov reporting, with adapters/remote (3 tests) as the first target.
F. Housekeeping
- Remove the committed
rust_out and tmp binaries; add them to .gitignore.
- Remove
worktrees/ from the working tree, or ignore it.
- Align MSRV to one value across all six crates.
- Document
core/ in README.md.
- Write the spec for this change into
docs/specs/ and the ordered plan into docs/plans/, per AGENTS.md.
Suggested sequencing
Each step should land green on its own.
- F — housekeeping, so the diff of everything after it is readable.
- E1 + E3 — write the conformance suite and workspace integration tests against the current behaviour first. They are the safety net for everything below, and they will immediately document which families the TinyCortex adapter does not serve.
- A1 + A2 — collapse the duplicate types and traits; delete
convert.rs.
- C3 — lift the optional-family implementations from
crates/tinymemory-module into adapters/tinycortex.
- A3 + A4 + A5 — route
core/ through MemoryProvider; wire the registry; unify the error type.
- B — move sync up onto the memory API. Gated on 5.
- C1 + C2 + C4 — containment; verified by the "0 files reference tinycortex outside the engine module" check.
- D — feature-gate the engines and the heavy dependencies. This is only safe once 5–7 have removed the unconditional call sites.
- E2 + E4–E8 — the full test and CI matrix.
Acceptance criteria
Summary
An audit of the workspace as it stands (
api/,src/,core/,adapters/*,crates/tinymemory-module) finds that the repository ships three parallel memory APIs, thattinymemory-coreis coupled to TinyCortex in 97 of its 223 source files, that the driver-admission machinery the README describes is never invoked at runtime, and that no engine is feature-gated — so every consumer compiles SQLite, libgit2 (optionally), reqwest, axum, tinyagents and the whole TinyCortex engine whether it uses them or not.This issue proposes one unified memory schema, a fully abstracted memory surface, the relocation of provider sync (Composio and friends) onto that surface, hard containment of everything TinyCortex-specific behind a feature-gated module, and the feature/E2E test matrix that would keep all of it honest.
No code changes are proposed here — this is the design and scope record.
Part 1 — Audit findings
1.1 Three memory APIs describe the same thing
tinymemory_api::traits::Memoryapi/src/traits.rsasync fn,anyhow-typedTinycortexMemory, the three remote adapterstinymemory_api::provider::MemoryProviderapi/src/provider/MemoryError-typedNullMemoryProvider,MemoryTraitProvider(3 families),ModuleMemoryProvider(13 families)tinycortex::memory::Memory+tinycortex::memory::*vendor/tinycortexUnifiedMemory, and 97 files incore/that call it directlycore/src/traits.rsre-exports the TinyCortex trait, not the TinyMemory one:So
tinymemory_core::Memoryandtinymemory_api::traits::Memoryare two distinct Rust traits with identical method sets, andtinymemory_core::MemoryEntryandtinymemory_api::types::MemoryEntryare two distinct structs with identical fields.adapters/tinycortex/src/convert.rsexists solely to rebuild values field-by-field across that seam, and its own module docs say so:The file is careful and exhaustively destructures on purpose, which is the right mitigation — but the duplication it mitigates is the actual defect. Every new field is now a three-place edit (
tinycortex-api,tinymemory-api,convert.rs) plus a conversion test.1.2 The abstraction is bypassed by the code that matters
MemoryProvideradvertises 13 families. Outside ofNullMemoryProvider(the/dev/nullreference) andModuleMemoryProvider(which lives in an excluded, non-member crate), nothing implements the ten optional families:adapters/tinycortexdeliberately implements the mandatory three only, and documents why.adapters/remote(Supermemory, Mem0, Cognee) implement the mandatory three only.core/implements the substance of trees, chunks, entities, graph, diff, goals, tool-memory, ingestion and sources — againsttinycortex::memory::*directly, never throughMemoryProvider.Net effect: the contract is real for out-of-process module consumers and decorative for everyone in-tree. A second engine "binds in its place without the host learning anything new" (README) only for store/recall/export; every other feature of the product would silently vanish.
1.3 Driver admission is dead code
src/registry/implements class reservation, config labels, and a fail-closed external-driver gate. Grepping the whole workspace for callers:DriverRegistry::admit— referenced only in a doc comment insrc/lib.rs.MemoryHostConfig::memory_provider()— defined on the config seam, implemented byTestHostConfigandModuleConfig, read by nobody.The engine is chosen by calling
create_memory_client_with_local_aiincore/src/store/factories.rs, which constructs TinyCortex unconditionally. Configuration cannot select an engine today.1.4 Composio sync is welded to the engine
core/src/sync/composio/is presented as the host-side sync layer, but the execution path iscrate::tinycortex::run_composio_connection*:core/src/sync/composio/mod.rs:164core/src/sync/composio/providers/traits.rs:62core/src/sync/composio/providers/gmail/provider.rs:155core/src/sync/composio/providers/notion/provider.rs:282core/src/sync/composio/providers/slack/provider.rs:236Sync state (
DailyBudget,SyncState,KV_NAMESPACE), the payload normalisers, and the source readers (core/src/sources/readers/*.rs) are all TinyCortex re-exports or thinConfigadapters over TinyCortex readers. The same holds forcore/src/sources/types.rs.Consequence: a Mem0 or Supermemory deployment gets zero Composio sync, and the
MemorySourceSink/MemoryIngestfamilies — which exist precisely to express "pull upstream data into memory" — are unused by the sync layer that needs them.1.5 Engine-specific concerns leak out of the engine module
core/src/tinycortex/holds 11 files. TinyCortex-shaped concerns living outside it include:core/src/people/— resolver, store, and the macOSCNContactStoreaddress-book reader (contactsfeature). Contacts are an engine/product concern, not part of a memory contract.core/src/persona-adjacent config (core/src/tinycortex/persona.rsis in the right place, but itsdirs-based vault default leaks into the crate's dependency set).core/src/diff/— ops and stubs shaped around the TinyCortex git ledger.core/src/tree/,core/src/store/chunks/,core/src/queue/— 96 references totinycortex::memory::tree, 67 to::chunks, 29 to::queue.1.6 Nothing is feature-gated, so nothing is small
core/Cargo.tomlhas exactly three features:memory-git,test-support,contacts. There is no feature for an engine. Unconditional, non-optional dependencies includetinycortex(withobsidian,persona,syncon),tinycortex-api,tinyagents(withsqlite),rusqlite(bundled— compiles SQLite from source),reqwest(+rustls),axum,tokio(full),regex,walkdir,uuid,sha2,chrono,dirs,rand,url.A host that wants Mem0 over HTTP still compiles bundled SQLite and the entire embedded engine. The facade crate (
tinymemory) is dependency-light and fine;tinymemory-coreis the problem, and it is what the module and every real consumer depend on.api/Cargo.tomldocuments a forbidden-dependency guard as a comment with a suggestedcargo treeinvocation. It is not enforced anywhere in CI.1.7 Test coverage is lopsided, and there is no cross-crate E2E
1,163 test attributes across 128 files, distributed:
#[test]/#[tokio::test]coreapicrates/tinymemory-modulesrc(facade)adapters/tinycortexadapters/remoteStructural gaps:
tests/directory at the workspace root, and none inapi/,adapters/*, or the facade.AGENTS.mdmandatestests/for public-API integration tests; onlycore/tests/exists, and it contains fixtures only, no test files.examples/at the root (AGENTS.mdrequires runnable, CI-compiled examples and the README'scargo run --example basicdoes not exist). The only example isadapters/remote/examples/conformance.rs.Unsupportedbehaviour.audit_providerchecks advertised-vs-reachable but nothing checks behavioural equivalence across engines.--all-featuresand default only.memory-gitoff/on,contacts, andtest-supportare never exercised in isolated combinations.integration/remote-engines/Docker harness is documented but is not run by any workflow.docs/specs/contains one spec (tinybus-module.md) anddocs/plans/is empty, contrary toAGENTS.md's spec-then-plan rule.1.8 Smaller findings
rust_outandtmp— two 3.6 MB executables — are committed at the repository root.worktrees/cognee-supermemory/is present in the working tree with its owntarget/.1.96for the facade and the module,1.85forapi,core, and both adapters.README.mddocuments a layout (src/registry/,src/mandatory/,adapters/,vendor/) that omitscore/entirely — the largest crate in the repository is undocumented in the README.tinymemory-core's doc comment still frames the crate as "extracted from OpenHuman'ssrc/openhuman/memory/", anddefault_openhuman_dir()hardcodes~/.openhumanin an engine-neutral crate.Part 2 — Proposed work
A. One schema
A1. Delete the duplicate value types. Pick
tinymemory-apias the single source of truth forMemoryEntry,MemoryCategory,MemoryTaint,NamespaceSummary,RecallOpts,Capability, and theprovider::typesset.tinycortex-apieither depends ontinymemory-apiand re-exports, or is retired in favour of it. Either wayadapters/tinycortex/src/convert.rscollapses to nothing.A2. One
Memorytrait.tinymemory_api::traits::Memoryis the storage trait;tinymemory_core::traits::Memorybecomes a re-export of it, not of the engine's.UnifiedMemoryimplements the TinyMemory trait directly.A3.
MemoryProviderbecomes the only in-tree memory surface. Everything incore/that today callstinycortex::memory::*goes through a&dyn MemoryProvider(or a family accessor). Where a family is missing a method the product needs, the fix is to widen the family — not to reach past it.A4. Retire the
anyhow/MemoryErrorsplit at the boundary: one error type across the contract, so "unsupported" is distinguishable from "failed" on every path, not just the provider one.A5. Wire the registry.
MemoryHostConfig::memory_provider()selects the driver;DriverRegistry::admitgates it;create_memory_*incore/src/store/factories.rsreturns a boundArc<dyn MemoryProvider>chosen by that decision instead of a hardcoded TinyCortex client. Until this lands,src/registry/is untested-in-anger code.B. Sync moves up, onto the memory API
B1.
core/src/sync/(Composio, workspace watcher, MCP) is rewritten againstMemoryIngest,MemorySourceSink,MemoryDocumentsand the KV surface. Nocrate::tinycortex::*call from any file undercore/src/sync/.B2. Sync state (
DailyBudget,SyncState, budget accounting, the KV namespace) becomes engine-neutral, persisted through the provider's KV surface rather than re-exported fromtinycortex::memory::sync.B3. Payload normalisers (
normalize::{github,linear,notion,gmail,slack}) are pureValue → Valuetransforms with no engine dependency. Move them back into atinymemory-synccrate — or asyncmodule ofcore— so a non-TinyCortex engine gets Composio sync for free.B4.
core/src/sources/readers/*reader implementations likewise: a reader producesSourceItems and hands them toMemoryIngest.core/src/sources/types.rsstops being a TinyCortex re-export shim.B5. Acceptance test for this section: Composio Gmail sync completes end to end against a driver that is not TinyCortex.
C. TinyCortex contained
C1. Everything engine-specific lives under
core/src/tinycortex/(or, better, moves wholesale intoadapters/tinycortex), behind atinycortexfeature. Nothing outside that module names thetinycortexcrate. Target: 0 of the current 97 files.C2. Contacts / people (
core/src/people/, including the macOSCNContactStorereader) move behind the engine module and stay gated bycontacts, which in turn requirestinycortex. Same for persona, obsidian vault handling, and the git diff ledger (memory-git).C3.
adapters/tinycortexgrows implementations of the optional families it can actually serve — tree, documents, ingest, entities, graph, diff, tool-memory, sources, maintenance — soMemoryTraitProvider's three-family ceiling stops being the reasoncore/bypasses the contract. Much ofModuleMemoryProvider(crates/tinymemory-module/src/provider.rs, 1,197 lines) is exactly this code and should be lifted down into the adapter, leaving the module crate a thin bus transport.C4.
default_openhuman_dir()and the OpenHuman framing come out oftinymemory-core.D. Feature-gated engines
D1. Engine features on the facade and on
core:D2. Every heavy dependency in
core/Cargo.tomlbecomesoptional = trueand is reachable only through a feature:rusqlite,tinyagents,axum,reqwest,walkdir,dirs,rand,regex. Default features build the contract, the registry, the mandatory composition, and nothing that links C.D3. With
--no-default-features, the crate must still compile and bindNullMemoryProvider.D4. Enforce
api/'s forbidden-dependency rule in CI with the forward-formcargo treecheck its own manifest already spells out, rather than leaving it as a comment.D5. Publish a size/timing budget in CI: dependency count and cold build time for
--no-default-features, for--features mem0, and for--features tinycortex, so a regression is visible on the PR that causes it.E. Test plan
E1. Driver conformance suite — a
tinymemory-conformancecrate exposing one function that takes anArc<dyn MemoryProvider>and asserts the contract:store_with_taintpreservesExternalSync(the failure this repo already calls out as security-relevant).(namespace, key).MemoryError::Unsupported, neverOk.audit_providerreports no advertised-but-unreachable family.Run it against:
NullMemoryProvider, an in-memory reference driver, the TinyCortex adapter, and each of the three remote adapters (against theintegration/remote-engines/Docker harness, gated aslive_*). This is what makes "swap the engine" a claim with evidence.E2. Feature-matrix CI job — build + test each of:
--no-default-features·--features tinycortex·--features tinycortex,memory-git·--features tinycortex,contacts(macOS runner) ·--features mem0·--features supermemory·--features cognee·--features sync-composio·--all-featuresPlus a
cargo hack --feature-powerset --depth 2check-only pass to catch feature-unification breakage.E3. Workspace-level integration tests (
tests/at the root, public API only):driver_selection.rs— config names an engine; the registry admits it; the bound provider'sdriver_id()matches; an unreserved external id is refused fail-closed.capability_negotiation.rs— a host filters its surface from the cached capability set; a driver that lies fails the audit.taint_end_to_end.rs— external content stored through the sync path arrives withExternalSyncat every engine.null_provider.rs— the compiled-out configuration is usable and returnsUnsupportedrather than panicking.E4. Sync E2E — with a mock Composio API and a mock provider API:
MemoryProvideronly.E5. Module E2E — keep the existing one-process-per-test loader job, and extend it to cover every family the module advertises (today the E2E is a single 569-line file), plus a shutdown/restart cycle.
E6. Property and failure-path tests:
api/src/wire.rs) and on export/import.MemoryTaint::from_db_strfails closed on arbitrary input.Capability::parse/as_strround-trip for all 13, with the serde strings pinned (partially covered today).E7. Docs and examples as tests — restore
cargo run --example basicreferenced by the README, add one example per engine, and keepcargo doc -D warningsgreen (already in CI).E8. Coverage gate —
AGENTS.mdasks for 80% of meaningful library behaviour; nothing measures it. Addcargo llvm-covreporting, withadapters/remote(3 tests) as the first target.F. Housekeeping
rust_outandtmpbinaries; add them to.gitignore.worktrees/from the working tree, or ignore it.core/inREADME.md.docs/specs/and the ordered plan intodocs/plans/, perAGENTS.md.Suggested sequencing
Each step should land green on its own.
convert.rs.crates/tinymemory-moduleintoadapters/tinycortex.core/throughMemoryProvider; wire the registry; unify the error type.Acceptance criteria
grep -rl tinycortex core/src --include='*.rs'matches only files under the engine module (today: 97 files outside it).cargo build --no-default-featureslinks no SQLite, no libgit2, no reqwest, no async runtime beyond what the contract needs.cargo tree -p tinymemory-api -e normal,build --prefix none | grep -Ei 'rusqlite|libsqlite|git2|reqwest|regex|tokio'is empty, checked in CI.Memorytrait and one set of value types in the workspace;adapters/tinycortex/src/convert.rsis deleted.config.memory_provider()selects the engine andDriverRegistry::admitgates it, both covered by tests.