Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
107 commits
Select commit Hold shift + click to select a range
9d948ed
refactor(memory): update memory trait
senamakel Aug 29, 2026
5e7e263
test(namespace-store): update document store tests
senamakel Aug 29, 2026
92cc60c
chore(namespace-store): update initialization logic
senamakel Aug 29, 2026
96fdcd6
chore: update namespace store initialization
senamakel Aug 29, 2026
64f0a97
test: correct cross-session alias test name
senamakel Aug 29, 2026
8183f10
chore: update namespace store initialization
senamakel Aug 29, 2026
99ce5d6
chore: update namespace store initialization
senamakel Aug 29, 2026
b150898
fix(namespace): preserve logical namespace labels
senamakel Aug 29, 2026
4210ceb
fix(store): persist logical namespaces in document upserts
senamakel Aug 29, 2026
c9005fe
fix(store): use logical namespaces in memory summaries
senamakel Aug 29, 2026
d2e65f6
build(conformance): add tinymemory-bus dependency
senamakel Aug 29, 2026
3566142
chore(suite): import the namespace type
senamakel Aug 29, 2026
18ac14d
test(conformance): check section preservation in namespaces
senamakel Aug 29, 2026
bcb2a50
test(conformance): run namespace section preservation assertion
senamakel Aug 29, 2026
344b8b8
feat(conformance): expose namespace section assertion
senamakel Aug 29, 2026
7c51282
chore: update locked dependencies
senamakel Aug 29, 2026
e99122c
test(conformance): add scratch red check
senamakel Aug 29, 2026
d5b6e5e
chore(conformance): remove scratch namespace bug check
senamakel Aug 29, 2026
6ede5ba
test(reference): simulate namespace sanitization bug
senamakel Aug 29, 2026
fa0a677
fix(conformance): preserve namespace names in summaries
senamakel Aug 29, 2026
1ed5c9d
chore(conformance): remove unnecessary bus dependency
senamakel Aug 29, 2026
0d42110
fix(conformance): use the API namespace type
senamakel Aug 29, 2026
e676749
style(conformance): reorder namespace import
senamakel Aug 29, 2026
2ea2a76
chore(deps): remove tinymemory-bus dependency
senamakel Aug 29, 2026
5b32c82
docs(memory): document logical namespace storage separation
senamakel Aug 29, 2026
c1a3098
docs(memory): document sectioned write conformance
senamakel Aug 29, 2026
ca5db51
Merge remote-tracking branch 'upstream/main' into logical-namespace-r…
senamakel Aug 29, 2026
d535ffb
docs(memory): update section API documentation
senamakel Aug 29, 2026
0a76f4a
chore(safety): update safety module
senamakel Aug 29, 2026
55aea49
chore(core): update store safety module
senamakel Aug 29, 2026
587b6d0
chore: update namespace store documents
senamakel Aug 29, 2026
eac6895
chore: update namespace document store
senamakel Aug 29, 2026
bd1cc54
chore(namespace-store): update documents implementation
senamakel Aug 29, 2026
0b1059d
refactor(core): update memory store trait definitions
senamakel Aug 29, 2026
183d2c1
test(store): cover namespace summary normalization edge cases
senamakel Aug 29, 2026
f6934c8
test(core): update memory trait tests
senamakel Aug 29, 2026
f554b42
fix(safety): preserve namespace address equivalence
senamakel Aug 29, 2026
8d1ed03
test(store): verify reported namespaces remain addressable
senamakel Aug 29, 2026
eb9e987
style(tests): format namespace listing call consistently
senamakel Aug 29, 2026
628fcd7
test(memory): clarify PII namespace bracket substitution test
senamakel Aug 29, 2026
144e974
docs(specs): clarify namespace preservation assertion
senamakel Aug 29, 2026
0c89fe8
chore(core): update memory store trait
senamakel Aug 29, 2026
65680bc
chore: update memory store trait
senamakel Aug 29, 2026
46ae0cc
chore(core): update memory store trait
senamakel Aug 29, 2026
92c8e98
chore(core): update memory store trait
senamakel Aug 29, 2026
de53632
chore(core): update memory store trait
senamakel Aug 29, 2026
9b8c130
chore: update memory trait
senamakel Aug 29, 2026
74f332c
test(core): update memory trait tests
senamakel Aug 29, 2026
7e485b2
test(core): update memory trait tests
senamakel Aug 29, 2026
bdef1e7
style(core): format canonical namespace calls
senamakel Aug 29, 2026
bd99b18
docs(memory): update section API documentation
senamakel Aug 29, 2026
3802053
docs(memory): update memory section API
senamakel Aug 29, 2026
819f7ec
fix(store): restrict legacy namespace matches to exact addresses
senamakel Aug 29, 2026
4b982f9
refactor(store): reuse logical namespace filter
senamakel Aug 29, 2026
cbfa066
fix(safety): filter reads by logical namespace
senamakel Aug 29, 2026
dced0b3
refactor(store): organize safety filter import
senamakel Aug 29, 2026
bc9d25e
fix(namespace): filter scoped loads by logical namespace
senamakel Aug 29, 2026
85f6e9c
fix(namespace): filter recall by logical namespace
senamakel Aug 29, 2026
b7d10b2
test(memory): cover namespace alias isolation in recall and list
senamakel Aug 29, 2026
12263ff
fix(safety): include all legacy rows in namespace filtering
senamakel Aug 29, 2026
3f35013
fix(safety): constrain legacy rows to matching namespaces
senamakel Aug 29, 2026
3d56920
fix(namespace-query): load all documents within the namespace scope
senamakel Aug 29, 2026
eabc909
fix(namespace): filter queries by canonical logical namespace
senamakel Aug 29, 2026
897a448
docs(memory): clarify legacy namespace filtering
senamakel Aug 29, 2026
8b96112
docs: clarify memory namespace isolation behavior
senamakel Aug 29, 2026
6aa5094
fix(namespace-store): preserve logical namespace during address deriv…
senamakel Aug 29, 2026
785563a
fix(namespace): preserve logical namespace during queries
senamakel Aug 29, 2026
db5509d
refactor(query): pass namespace directly to scope loaders
senamakel Aug 29, 2026
b9e75c5
fix(store): require derived namespace address forms
senamakel Aug 29, 2026
018f553
fix(namespace-store): derive address forms for excluded queries
senamakel Aug 29, 2026
9ebf040
fix(namespace): preserve logical names in hybrid queries
senamakel Aug 29, 2026
da6491b
fix(core): normalize namespace forms for memory queries
senamakel Aug 29, 2026
ad37240
fix(retrieval): normalize namespace query addresses
senamakel Aug 29, 2026
fd6393a
fix(tinycortex): derive namespace query address forms together
senamakel Aug 29, 2026
38861fc
refactor(namespace): simplify namespace string conversion
senamakel Aug 29, 2026
576a1a9
chore(namespace): remove unused safety and namespace imports
senamakel Aug 29, 2026
1e82a18
test(namespace-store): update query tests for namespace parameter
senamakel Aug 29, 2026
08ca5cb
fix(namespace-store): pass namespace by value to event search
senamakel Aug 29, 2026
97b1ca7
style(namespace-store): format query code with rustfmt
senamakel Aug 29, 2026
d36aa6e
test(namespace-store): cover sectioned namespace context queries
senamakel Aug 29, 2026
8ea6f62
fix(namespace): sanitize namespace before querying
senamakel Aug 29, 2026
806ed02
fix(namespace): avoid double namespace sanitization
senamakel Aug 29, 2026
f33a97e
docs(core): fix namespace sanitization reference
senamakel Aug 29, 2026
7d0f14e
docs(memory): document namespace form derivation safeguards
senamakel Aug 29, 2026
f79650a
docs: document sectioned namespace query behavior
senamakel Aug 29, 2026
96990a3
fix(namespace-store): isolate query-less recall by logical namespace
senamakel Aug 29, 2026
0e63616
chore(store): restrict physical document loading to tests
senamakel Aug 29, 2026
e54593f
fix(core): scope namespace clearing across logical aliases
senamakel Aug 29, 2026
ac519e8
fix(memory): use physical namespaces for memory lookups
senamakel Aug 29, 2026
7d08251
chore: fix(store
senamakel Aug 29, 2026
1fc7528
chore(core): remove unused namespace filter import
senamakel Aug 29, 2026
89bf773
chore: files changed crates/tinymemory-core/src/store/namespace_store…
senamakel Aug 29, 2026
3048e73
fix(store): clear namespaces by physical address
senamakel Aug 29, 2026
18eba1f
chore: files changed crates/tinymemory-core/src/store/namespace_store…
senamakel Aug 29, 2026
2aa7dc8
fix(query): pass namespace by reference to event search
senamakel Aug 29, 2026
5a3c675
fix(namespace): use sanitized namespace for hybrid queries
senamakel Aug 29, 2026
faf9668
fix(namespace): align recent recall with scoped storage
senamakel Aug 29, 2026
8541f92
fix(retrieval): pass namespace directly to hit queries
senamakel Aug 29, 2026
5bb5d76
fix(tinycortex): query namespace hits with the original namespace
senamakel Aug 29, 2026
4d4084f
refactor(namespace): remove obsolete address forms helper
senamakel Aug 29, 2026
90e7521
chore(safety): remove obsolete logical namespace filter
senamakel Aug 29, 2026
f44a4ee
test(namespace-store): update query tests for simplified API
senamakel Aug 29, 2026
468a37d
test(namespace-store): update session exclusion query call
senamakel Aug 29, 2026
7d5fd4c
test(store): expect aliased namespaces to merge
senamakel Aug 29, 2026
dd392e7
docs(memory): clarify logical namespace behavior
senamakel Aug 29, 2026
dc2a0fc
docs(spec): clarify colliding namespace behavior
senamakel Aug 29, 2026
1875e7a
test: clarify double-sanitization regression rationale
senamakel Aug 29, 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
3 changes: 2 additions & 1 deletion crates/tinymemory-conformance/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ pub use reference::{InMemoryProvider, REFERENCE_DRIVER_ID};
pub use suite::{
assert_awkward_content_round_trips, assert_capability_audit, assert_export_cursor_terminates,
assert_export_import_round_trip, assert_forget_is_idempotent, assert_kv_round_trip,
assert_list_filters_narrow, assert_namespaces_are_isolated, assert_provider,
assert_list_filters_narrow, assert_namespaces_are_isolated,
assert_namespaces_preserve_their_section, assert_provider,
assert_recall_respects_limit_and_namespace, assert_store_get_round_trip,
assert_taint_is_preserved, assert_upsert_replaces_rather_than_duplicates,
};
Expand Down
66 changes: 66 additions & 0 deletions crates/tinymemory-conformance/src/suite/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ use std::sync::Arc;

use tinymemory_api::capabilities::Capability;
use tinymemory_api::error::MemoryError;
use tinymemory_api::namespace::Namespace;
use tinymemory_api::provider::{audit_provider, ExportRecord, MemoryProvider, SourceScope};
use tinymemory_api::recall::OwnedRecallOpts;
use tinymemory_api::types::{MemoryCategory, MemoryTaint};
Expand Down Expand Up @@ -61,6 +62,7 @@ pub async fn assert_provider(provider: Arc<dyn MemoryProvider>) {
assert_list_filters_narrow(p).await;
assert_taint_is_preserved(p).await;
assert_recall_respects_limit_and_namespace(p).await;
assert_namespaces_preserve_their_section(p).await;
assert_recall_respects_source_scope(p).await;
assert_export_import_round_trip(p).await;
assert_awkward_content_round_trips(p).await;
Expand Down Expand Up @@ -489,6 +491,70 @@ pub async fn assert_recall_respects_limit_and_namespace(provider: &dyn MemoryPro
cleanup(provider, &theirs, &["other"]).await;
}

/// `namespaces()` reports a sectioned namespace back under the same
/// [`tinymemory_api::namespace::MemorySection`] the caller wrote it in.
///
/// This is the regression the unified SQLite store's own storage-address
/// sanitiser taught us to check for: a driver whose on-disk address collapses
/// `:` to `_` (a real filesystem constraint) must still report the *logical*
/// namespace back through `namespaces()`, or `conversation:thread-8f21`
/// silently re-addresses out of the `conversation` section and every caller
/// enumerating a section's scopes sees nothing, even though the write itself
/// succeeded.
///
/// # Panics
///
/// Panics when no reported namespace parses to the same section as the one
/// that was written.
pub async fn assert_namespaces_preserve_their_section(provider: &dyn MemoryProvider) {
let who = provider.driver_id();
let namespace = format!("conversation:{}", ns(provider, "section-thread"));
let written = Namespace::parse(&namespace)
.unwrap_or_else(|e| panic!("{who}: test fixture `{namespace}` failed to parse: {e}"));

provider
.store(
&namespace,
"k",
"content",
MemoryCategory::Core,
None,
MemoryTaint::Internal,
)
.await
.unwrap_or_else(|e| panic!("{who}: store failed: {e}"));

let summaries = provider
.namespaces()
.await
.unwrap_or_else(|e| panic!("{who}: namespaces() failed: {e}"));

let matching_scope = summaries.iter().find_map(|summary| {
Namespace::parse(&summary.namespace)
.ok()
.filter(|parsed| parsed.scope() == written.scope())
});

match matching_scope {
Some(parsed) => assert_eq!(
parsed.section(),
written.section(),
"{who}: wrote `{namespace}` under section {:?}, but namespaces() reported \
its scope back under section {:?} instead — a driver must not silently \
re-address a sectioned namespace out of its section",
written.section(),
parsed.section(),
),
None => panic!(
"{who}: namespaces() did not report any namespace with scope `{}` after \
storing `{namespace}`; got {summaries:?}",
written.scope()
),
}

cleanup(provider, &namespace, &["k"]).await;
}

/// A present, empty source scope fails closed.
///
/// # Panics
Expand Down
94 changes: 77 additions & 17 deletions crates/tinymemory-core/src/store/memory_trait.rs
Original file line number Diff line number Diff line change
Expand Up @@ -300,8 +300,18 @@ impl UnifiedMemory {
// be `'static` and cannot borrow the store.

/// One `memory_docs` row as `get` selects it:
/// `(document_id, key, content, updated_at, category, taint, session_id)`.
type MemoryDocRow = (String, String, String, f64, String, String, Option<String>);
/// `(document_id, key, content, updated_at, category, taint, session_id,
/// logical_namespace)`.
type MemoryDocRow = (
String,
String,
String,
f64,
String,
String,
Option<String>,
Option<String>,
);

impl UnifiedMemory {
fn get_blocking(
Expand All @@ -315,9 +325,18 @@ impl UnifiedMemory {
// readers disagree about one record. The contract's round-trip
// assertion catches exactly that (`tinymemory_conformance`), and it was
// invisible until #18 §A3 let this store be bound as a driver at all.
//
// `logical_namespace` is selected too so the returned `MemoryEntry`
// reports the row's own logical name rather than the physical address
// this method happens to have been called with — see `list_blocking`'s
// doc comment for why that distinction matters. This is purely a
// labelling improvement: the row is still addressed by the physical
// `namespace` column alone (`WHERE namespace = ?1`), so two logical
// namespaces that sanitize to the same physical address are still
// one namespace here, same as before `logical_namespace` existed.
let row: Option<MemoryDocRow> = conn
.query_row(
"SELECT document_id, key, content, updated_at, category, taint, session_id
"SELECT document_id, key, content, updated_at, category, taint, session_id, logical_namespace
FROM memory_docs WHERE namespace = ?1 AND key = ?2 LIMIT 1",
params![ns, key],
|row| {
Expand All @@ -329,25 +348,41 @@ impl UnifiedMemory {
row.get(4)?,
row.get(5)?,
row.get(6)?,
row.get(7)?,
))
},
)
.optional()?;
Ok(row.map(
|(id, key, content, updated_at, category, taint_str, session_id)| MemoryEntry {
id,
key,
content,
namespace: Some(ns.to_string()),
category: memory_category_from_stored(&category),
timestamp: timestamp_to_rfc3339(updated_at),
session_id,
score: None,
taint: crate::MemoryTaint::from_db_str(&taint_str),
|(id, key, content, updated_at, category, taint_str, session_id, row_logical)| {
MemoryEntry {
id,
key,
content,
namespace: Some(row_logical.unwrap_or_else(|| ns.to_string())),
category: memory_category_from_stored(&category),
timestamp: timestamp_to_rfc3339(updated_at),
session_id,
score: None,
taint: crate::MemoryTaint::from_db_str(&taint_str),
}
},
))
}

/// List every row addressed to one physical namespace.
///
/// Addressed by the physical `namespace` column only (`WHERE namespace =
/// ?1`) — exactly as before `logical_namespace` existed. Two logical
/// namespaces that sanitize to the same physical address (`a:b_c` and
/// `a_b:c` both sanitize to `a_b_c`) are still one namespace for this
/// call, and `sanitize_namespace` has always collapsed them that way; this
/// is pre-existing behaviour, not something this column changes. What
/// `logical_namespace` adds is purely the label: each returned entry's
/// `namespace` is the row's *own* logical name (falling back to the
/// physical address for pre-migration NULL rows) instead of the raw
/// sanitized address, so a sectioned namespace still reports its `:`
/// spelling back to a caller enumerating it.
fn list_blocking(
conn: &Arc<Mutex<Connection>>,
ns: &str,
Expand All @@ -356,16 +391,17 @@ impl UnifiedMemory {
) -> anyhow::Result<Vec<MemoryEntry>> {
let conn = conn.lock();
let mut stmt = conn.prepare(
"SELECT document_id, key, content, category, session_id, updated_at, taint
"SELECT document_id, key, content, category, session_id, updated_at, taint, logical_namespace
FROM memory_docs WHERE namespace = ?1 ORDER BY updated_at DESC",
)?;
let rows = stmt.query_map(params![ns], |row| {
let stored_category: String = row.get(3)?;
let row_logical: Option<String> = row.get(7)?;
Ok(MemoryEntry {
id: row.get(0)?,
key: row.get(1)?,
content: row.get(2)?,
namespace: Some(ns.to_string()),
namespace: Some(row_logical.unwrap_or_else(|| ns.to_string())),
category: memory_category_from_stored(&stored_category),
session_id: row.get(4)?,
timestamp: timestamp_to_rfc3339(row.get(5)?),
Expand Down Expand Up @@ -402,11 +438,35 @@ impl UnifiedMemory {
conn: &Arc<Mutex<Connection>>,
) -> anyhow::Result<Vec<NamespaceSummary>> {
let conn = conn.lock();
// `COALESCE(logical_namespace, namespace)` is the entire backfill
// story, deliberately: rows written before the `logical_namespace`
// column existed have it NULL and fall back to exactly today's
// sanitized value. A sanitized `_` cannot be reconstructed into
// whatever delimiter it replaced (a scope may legitimately contain
// `_`), so guessing would silently mislabel unrelated namespaces —
// NULL rows simply keep reporting their sanitized address.
//
// `GROUP BY namespace` (the storage address), not the logical name:
// every OTHER operation on this store — `get`, `list`, `forget`,
// `recall`, `clear_namespace` — addresses a row by its physical
// `namespace` column alone, so two logical names that sanitize to the
// same address (`conversation:x` and `conversation_x` both sanitize
// to `conversation_x`) are already treated as one namespace
// everywhere else. Grouping summaries by the logical name instead
// would report two summaries with two partial counts for what every
// other call still treats, and returns, as a single merged
// namespace — `list` on either reported name would return BOTH
// aliases' rows, double the count either summary claims. Grouping by
// the address keeps one summary per physical namespace with an
// accurate count; `MIN(logical_namespace)` (aggregate `MIN` ignores
// `NULL`) just picks a single, deterministic logical representative
// to report it under, so a sectioned namespace still enumerates
// under its `:` spelling instead of the sanitized `_` form.
let mut stmt = conn.prepare(
"SELECT namespace, COUNT(*) AS n, MAX(updated_at) AS last
"SELECT COALESCE(MIN(logical_namespace), namespace) AS ns, COUNT(*) AS n, MAX(updated_at) AS last
FROM memory_docs
GROUP BY namespace
ORDER BY namespace",
ORDER BY ns",
)?;
let rows = stmt.query_map([], |row| {
let ns: String = row.get(0)?;
Expand Down
Loading