Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
81 commits
Select commit Hold shift + click to select a range
f2f8602
feat(people): add address book module with migration, resolver, score…
senamakel Aug 13, 2026
0eed0f6
fix(people): handle empty name in person lookup
senamakel Aug 13, 2026
716ab2d
chore: files changed core/Cargo.toml
senamakel Aug 13, 2026
207729f
fix(deps): enable default-features for cfg-if dependency
senamakel Aug 13, 2026
ac201f4
chore(core): update Cargo.toml dependencies
senamakel Aug 13, 2026
4c7b566
feat(api): add capability and version modules
senamakel Aug 13, 2026
ee9b51b
fix(provider): handle missing provider config in mod.rs
senamakel Aug 13, 2026
a90f19f
fix(api): handle null values in null.rs
senamakel Aug 13, 2026
1a68bed
feat(api): add capability to list all available API endpoints
senamakel Aug 13, 2026
560b709
fix(provider): handle missing driver in people endpoint
senamakel Aug 13, 2026
a3c055e
fix(provider): handle missing memory region in provider
senamakel Aug 13, 2026
8f27c04
chore(tinymemory-module): add Cargo.toml for new crate
senamakel Aug 13, 2026
ab7a170
fix(service): handle empty memory list in retrieval
senamakel Aug 13, 2026
50a06fb
chore(deps): update tinycortex submodule and Cargo.lock
senamakel Aug 13, 2026
b6c614c
feat(api): add capability-based access control to provider layer
senamakel Aug 13, 2026
1ef3246
fix(tree): handle empty cover set in retrieval
senamakel Aug 13, 2026
ffdef7a
fix(core): handle missing parent in tree retrieval
senamakel Aug 13, 2026
92b83d7
fix(provider): handle missing memory region in provider lookup
senamakel Aug 13, 2026
a67e84a
chore: files changed crates/tinymemory-module/src/service/mod.rs
senamakel Aug 13, 2026
02fe754
chore: files changed api/src/null.rs,api/src/provider/driver.rs,crate…
senamakel Aug 14, 2026
6cb77d6
chore: files changed api/src/null.rs,api/src/provider/mod.rs,api/src/…
senamakel Aug 14, 2026
d78cee3
chore: files changed core/src/tree/retrieval/mod.rs,core/src/tree/ret…
senamakel Aug 14, 2026
fe7502c
chore: files changed crates/tinymemory-module/src/provider.rs
senamakel Aug 14, 2026
68efd07
chore: files changed crates/tinymemory-module/src/service/mod.rs
senamakel Aug 14, 2026
8679f17
feat(api): add the retrieval trio and RankedPerson::interaction_count
senamakel Aug 14, 2026
85dfd0f
chore: files changed crates/tinymemory-module/src/service/mod.rs
senamakel Aug 14, 2026
8b4b982
refactor(api): move interaction_count onto PersonScore
senamakel Aug 14, 2026
da70c9c
chore: formatting after the facade removal
enamakel Aug 14, 2026
f193742
feat(chunks): add chunk_detail, a one-call inspection view
enamakel Aug 14, 2026
cfd1cb7
feat(api): add scored namespace recall with session exclusion
senamakel Aug 14, 2026
7025b2e
feat(profile): add the profile capability family
enamakel Aug 14, 2026
f42c9e3
fix(api): correct capability version handling for provider drivers
senamakel Aug 16, 2026
a29bfb2
fix(store): correct memory module service initialization order
senamakel Aug 16, 2026
d27892d
chore: reorder imports and reformat closures for consistency
senamakel Aug 16, 2026
882ce61
test(contract): update test expectations for new episodic capability
senamakel Aug 16, 2026
7cc51fb
refactor(provider): rename score variable and simplify interaction lo…
senamakel Aug 16, 2026
3e2765a
fix(clippy): satisfy -D warnings on the new profile and episodic surf…
senamakel Aug 16, 2026
acb2204
fix(store): return row id from episodic_insert to fix racy lookups
senamakel Aug 16, 2026
ecca506
feat(provider): implement MemoryEpisodic trait for ModuleMemoryProvider
senamakel Aug 16, 2026
eab2c62
chore(tinymemory-module): add MemoryEpisodic, ConversationSegment, an…
senamakel Aug 16, 2026
d65c639
fix(provider): return error for missing provider on module start
senamakel Aug 16, 2026
afd3a11
Merge remote-tracking branch 'origin/main' into memory-module-port
senamakel Aug 16, 2026
ad3fa94
chore(deps): pick up the people global-store test fix
senamakel Aug 16, 2026
2811c1f
test(host): pin the embedding signature to its persisted form
senamakel Aug 16, 2026
f68444b
chore(deps): point tinycortex at main
senamakel Aug 16, 2026
9b8541e
fix(service): handle empty memory list in memory service
senamakel Aug 16, 2026
1a1f26c
fix(service): handle empty memory list in memory listing endpoint
senamakel Aug 16, 2026
1fa4f28
fix(service): handle empty memory list in memory service
senamakel Aug 16, 2026
c6b5b96
fix(service): handle empty input in memory module
senamakel Aug 16, 2026
fbd328b
fix(scope): correct memory alignment for atomic operations
senamakel Aug 16, 2026
f92402d
fix(scope): correct memory alignment for atomic operations
senamakel Aug 16, 2026
e6a5249
fix(tinymemory-module): correct module initialization order in e2e test
senamakel Aug 16, 2026
9c71463
fix(tinymemory-module): correct module e2e test assertion order
senamakel Aug 16, 2026
7895c52
test(service): add test that every served method is declared in the m…
senamakel Aug 16, 2026
61c3fed
fix(provider): handle missing memory region in provider
senamakel Aug 16, 2026
8b3529b
fix(service): handle empty input in memory module
senamakel Aug 16, 2026
481382b
fix(service): handle empty memory list in memory service
senamakel Aug 16, 2026
378b4c9
fix(service): handle empty memory list in memory service
senamakel Aug 16, 2026
7230b02
fix(service): handle empty memory list in memory service
senamakel Aug 16, 2026
32fe268
fix(provider): handle missing memory region in provider lookup
senamakel Aug 16, 2026
deddcd3
fix(service): handle empty memory list in memory service
senamakel Aug 16, 2026
2532c22
fix(service): handle empty input in memory module
senamakel Aug 16, 2026
8b5afc2
fix(service): handle empty memory list in memory service
senamakel Aug 16, 2026
4932dd7
docs(provider): update family counts from thirteen to fifteen
senamakel Aug 16, 2026
5644db3
fix(retrieval): handle missing source file gracefully
senamakel Aug 16, 2026
dad2e40
fix(tree): handle missing source file in retrieval
senamakel Aug 16, 2026
8aecf1c
fix(tree): handle missing source file in retrieval
senamakel Aug 16, 2026
cddaeda
fix(provider): handle missing memory module gracefully
senamakel Aug 16, 2026
8808652
fix(tree): export SourceQuery from retrieval module
senamakel Aug 16, 2026
68ea87b
fix(embeddings): handle empty input in embedding request
senamakel Aug 16, 2026
fc5e6a0
fix(embeddings): handle empty input in embedding endpoint
senamakel Aug 16, 2026
eef28cf
fix(tree): handle missing parent in fetch error path
senamakel Aug 16, 2026
ad791ce
fix(tree): handle empty path segments in drill-down retrieval
senamakel Aug 16, 2026
aa0916d
fix(tree): handle empty drill-down path gracefully
senamakel Aug 16, 2026
d80a7cc
fix(tree): handle empty drill-down path gracefully
senamakel Aug 16, 2026
26ff17c
fix(api): handle missing provider in retrieval endpoint
senamakel Aug 16, 2026
d47d54a
feat(api): add scope parameter to null memory retrieval methods
senamakel Aug 16, 2026
db4324e
feat(provider): add scope parameter to drill-down and leaf retrieval
senamakel Aug 16, 2026
56b8630
feat(service): add source scope parameter to retrieve children and le…
senamakel Aug 16, 2026
3170829
fix(tree): handle missing source file in retrieval
senamakel Aug 16, 2026
dc3a725
fix(service): correct test assertion for memory limit enforcement
senamakel Aug 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

33 changes: 31 additions & 2 deletions api/src/capabilities.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ use crate::error::MemoryError;

/// One capability family a memory driver may advertise.
///
/// The variants are exactly the thirteen families of the memory contract. Each
/// The variants are exactly the sixteen families of the memory contract. Each
/// maps to a trait family in the contract, a group of RPC methods, and a group
/// of agent tools; a driver that does not advertise a family simply has that
/// surface absent.
Expand Down Expand Up @@ -85,6 +85,17 @@ pub enum Capability {
Maintenance,
/// Export and import of the whole store as a stream. **Mandatory.**
Portability,
/// Contacts, handle resolution, and closeness scoring.
People,
/// Direct read access to the stored chunk tier.
Chunks,
/// Deterministic retrieval primitives: graph walk, time-window cover,
/// entity-index search.
Retrieval,
/// Learned facets about the user.
Profile,
/// The turn-by-turn conversation record and its segment lifecycle.
Episodic,
}

impl Capability {
Expand All @@ -93,7 +104,7 @@ impl Capability {
/// Declaration order is also bit order in [`Capabilities`] and iteration
/// order in its serialized form, so this slice is the single ordering
/// authority for the whole module.
pub const ALL: [Capability; 13] = [
pub const ALL: [Capability; 18] = [
Capability::Core,
Capability::Recall,
Capability::Ingest,
Expand All @@ -107,6 +118,14 @@ impl Capability {
Capability::Sources,
Capability::Maintenance,
Capability::Portability,
// Appended, never inserted: declaration order is bit order in
// `Capabilities`, so moving an existing variant would silently change
// what an already-persisted or already-transmitted bitset means.
Capability::People,
Capability::Chunks,
Capability::Retrieval,
Capability::Profile,
Capability::Episodic,
];

/// The families a driver must advertise to be bindable at all.
Expand Down Expand Up @@ -145,6 +164,11 @@ impl Capability {
Self::Sources => "sources",
Self::Maintenance => "maintenance",
Self::Portability => "portability",
Self::People => "people",
Self::Chunks => "chunks",
Self::Retrieval => "retrieval",
Self::Profile => "profile",
Self::Episodic => "episodic",
}
}

Expand Down Expand Up @@ -187,6 +211,11 @@ impl Capability {
Self::Sources => 10,
Self::Maintenance => 11,
Self::Portability => 12,
Self::People => 13,
Self::Chunks => 14,
Self::Retrieval => 15,
Self::Profile => 16,
Self::Episodic => 17,
}
}

Expand Down
15 changes: 10 additions & 5 deletions api/src/capabilities_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//!
//! Three properties are load-bearing and each has its own test:
//!
//! 1. the enum has exactly the thirteen contract families and no more;
//! 1. the enum has exactly the sixteen contract families and no more;
//! 2. the serialized form is stable snake_case **strings**, never discriminant
//! integers — a driver deployed against an older build must keep advertising
//! the same set after a variant is inserted mid-enum;
Expand All @@ -13,9 +13,9 @@ use super::*;
use serde_json::json;

#[test]
fn capability_has_exactly_the_thirteen_contract_families() {
assert_eq!(Capability::ALL.len(), 13);
assert_eq!(Capability::all().len(), 13);
fn capability_has_exactly_the_eighteen_contract_families() {
assert_eq!(Capability::ALL.len(), 18);
assert_eq!(Capability::all().len(), 18);

let names: Vec<&str> = Capability::ALL.iter().map(|c| c.as_str()).collect();
assert_eq!(
Expand All @@ -34,6 +34,11 @@ fn capability_has_exactly_the_thirteen_contract_families() {
"sources",
"maintenance",
"portability",
"people",
"chunks",
"retrieval",
"profile",
"episodic",
]
);
}
Expand Down Expand Up @@ -141,7 +146,7 @@ fn capabilities_empty_contains_nothing() {
}

#[test]
fn capabilities_bit_width_has_room_well_beyond_the_current_thirteen_families() {
fn capabilities_bit_width_has_room_well_beyond_the_current_sixteen_families() {
// A `u16` bitset (the original representation) has exactly 16 bit
// positions, leaving room for only 3 more families before a family's
// `1 << index` bit-shift overflows. Pin the wider `u64` representation so
Expand Down
103 changes: 103 additions & 0 deletions api/src/host/embeddings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,114 @@ use async_trait::async_trait;
/// provider. Drift between the two silently splits one embedding space into
/// two, and every vector written on the wrong side of the split becomes
/// unsearchable without a re-embed.
/// # Delimiters in a component
///
/// A component containing `;`, `=` or `%` is percent-encoded, because without
/// that the format is ambiguous: `("a;model=b", "c")` and `("a", "b;model=c")`
/// are different embedding spaces that would otherwise produce one identical
/// key, and vectors from both would then be compared as though they came from
/// the same model.
///
/// Encoding only those three characters is what keeps this from being a
/// migration. Every provider and model identifier actually in use is
/// alphanumeric plus `-`, `_`, `.`, `/` or `:`, and each of those passes
/// through untouched — so every signature already on disk still formats to the
/// same bytes. Only a name that could have collided changes, and such a name
/// has never been written.
#[must_use]
pub fn format_embedding_signature(name: &str, model_id: &str, dims: usize) -> String {
let name = escape_component(name);
let model_id = escape_component(model_id);
format!("provider={name};model={model_id};dims={dims}")
}

/// Percent-encode the three characters that carry structure in a signature.
///
/// `%` goes first and must: encoding it afterwards would re-encode the `%` this
/// function just introduced, and `a;b` would arrive as `a%3Bb` from one path
/// and `a%253Bb` from another.
fn escape_component(value: &str) -> String {
if !value.contains(['%', ';', '=']) {
// The overwhelmingly common path, and the one that guarantees existing
// keys are untouched: no allocation beyond the copy, no rewriting.
return value.to_string();
}
value
.replace('%', "%25")
.replace(';', "%3B")
.replace('=', "%3D")
}

#[cfg(test)]
mod embedding_signature_tests {
use super::format_embedding_signature;

/// The signature format is a **persisted key**, pinned to literal values.
///
/// Written against golden strings rather than against another copy of the
/// function on purpose: the host used to hold a byte-identical duplicate of
/// this file and the two silently diverged once already. A guard that
/// compares two implementations stops protecting anything the moment one of
/// them goes away — which is exactly what happened when the duplicate was
/// removed. Literals outlive that.
///
/// Every vector on disk is keyed by one of these strings, so a change here
/// is a migration, never an edit.
#[test]
fn signature_format_is_pinned_to_its_persisted_form() {
assert_eq!(
format_embedding_signature("ollama", "nomic-embed-text", 768),
"provider=ollama;model=nomic-embed-text;dims=768"
);
assert_eq!(
format_embedding_signature("none", "none", 0),
"provider=none;model=none;dims=0"
);
}

/// Two distinct embedding spaces must never share one signature.
///
/// Without escaping these two collide exactly: both format to
/// `provider=a;model=b;model=c;dims=3`. A collision here is not a cosmetic
/// problem — the signature is what decides which vectors are comparable, so
/// two models' vectors would be scored against each other as though they
/// came from one space.
#[test]
fn delimiter_characters_cannot_make_distinct_spaces_collide() {
let first = format_embedding_signature("a;model=b", "c", 3);
let second = format_embedding_signature("a", "b;model=c", 3);
assert_ne!(first, second);
}

/// Escaping `%` last would make the encoding itself ambiguous.
#[test]
fn an_already_percent_encoded_name_does_not_collide_with_a_literal_one() {
assert_ne!(
format_embedding_signature("a%3Bb", "m", 3),
format_embedding_signature("a;b", "m", 3)
);
}

/// The escaping is not a migration: every identifier shaped like the ones
/// actually in use formats to the same bytes it always did.
#[test]
fn identifiers_in_real_use_are_untouched_by_the_escaping() {
for (provider, model) in [
("ollama", "nomic-embed-text"),
("openai", "text-embedding-3-small"),
("huggingface", "sentence-transformers/all-MiniLM-L6-v2"),
("local", "bge_base.en-v1.5"),
("backend", "tinyhumans:default"),
] {
assert_eq!(
format_embedding_signature(provider, model, 768),
format!("provider={provider};model={model};dims=768"),
"{provider}/{model} must not be rewritten — it is a persisted key"
);
}
}
}

/// Converts text into numerical vectors.
#[async_trait]
pub trait EmbeddingProvider: Send + Sync {
Expand Down
4 changes: 2 additions & 2 deletions api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,10 @@
//! - [`recall`]: the borrowed [`recall::RecallOpts`] and owned, serde-derived
//! [`recall::OwnedRecallOpts`] recall filters (both re-exported from
//! [`types`]).
//! - [`capabilities`]: the thirteen [`capabilities::Capability`] families and
//! - [`capabilities`]: the sixteen [`capabilities::Capability`] families and
//! the [`capabilities::Capabilities`] set negotiated at bind time.
//! - [`provider`]: the driver contract — [`provider::MemoryProvider`] plus the
//! thirteen capability family traits and the value types they need.
//! sixteen capability family traits and the value types they need.
//! - [`null`]: [`null::NullMemoryProvider`], the reference driver a
//! compiled-out or unconfigured memory subsystem binds to.
//! - [`health`]: [`health::MemoryHealth`], the liveness state a driver reports.
Expand Down
Loading