diff --git a/src/openhuman/agent/tools/remember_preference.rs b/src/openhuman/agent/tools/remember_preference.rs index 458ff118b4..4a5a213ff8 100644 --- a/src/openhuman/agent/tools/remember_preference.rs +++ b/src/openhuman/agent/tools/remember_preference.rs @@ -332,7 +332,10 @@ mod tests { // The read-back goes through the engine handle directly, so its entries // carry the engine's category type rather than the contract's. - use tinymemory_core::MemoryCategory as EngineMemoryCategory; + // The contract's type, not the engine's: `tinymemory-core` re-exports + // `tinymemory_api::traits::MemoryCategory` (#18 §A1). Named at its source + // so this test does not hold the engine crate in the build (#5560). + use tinymemory_api::types::MemoryCategory as EngineMemoryCategory; fn test_security() -> Arc { Arc::new(SecurityPolicy::default()) diff --git a/src/openhuman/memory/direct_engine_refs_tests.rs b/src/openhuman/memory/direct_engine_refs_tests.rs index dbec16859c..1f76550966 100644 --- a/src/openhuman/memory/direct_engine_refs_tests.rs +++ b/src/openhuman/memory/direct_engine_refs_tests.rs @@ -78,6 +78,73 @@ //! happens to live in the engine crate, and it should probably move to //! `tinymemory-api` rather than gain a bus method. //! +//! ## What the 2026-08-22 audit added to that list +//! +//! Draining `FacadeRevealed` turned 82 unexamined files into evidence, and it +//! widened the ask rather than narrowing it. Grouped by what blocks them, so +//! the upstream work can be sized per gap instead of per file: +//! +//! - **The engine handle itself** (~28 files) — `global::{init, client, +//! client_if_ready}`, `store::{UnifiedMemory, MemoryClient, MemoryClientRef}` +//! and `store::factories::create_memory`. These construct or hold the +//! in-process engine. Nothing routes here: the seam has no door onto a live +//! client, and it should not grow one — this is `memory::binding`'s job, and +//! the ask is that every caller take the binding's provider instead. +//! - **Chunk writes and transactions** (~21 files) — +//! `store::chunks::store::{with_connection, upsert_chunks, +//! upsert_staged_chunks_tx, get_or_init_connection}`, plus `store::{fts5, +//! segments, profile, events, content}`. `MemoryChunks` is a read family; +//! this is the write half, and `with_connection` hands out a SQLite handle, +//! which no engine-neutral contract can promise. **Moving these subsystems +//! behind the bus is the only shape that keeps a supermemory/mem0/cognee +//! driver implementable** — a contract with `with_connection` in it is a +//! SQLite contract wearing a trait. +//! - **Host policy reached through the engine crate** (~14 files) — +//! `store::safety::{sanitize_text, sanitize_json, has_likely_secret}`, +//! `util::redact::redact`, `source_scope::*`. This looked like the cheapest +//! item on the list and mostly is not, which is worth stating so the next +//! person does not re-derive it: `store::safety` is a **shim over +//! `crate::engine::backend::store::safety`**, so those scrubbers live in +//! tinycortex and relocating them is engine work rather than a contract +//! move. `source_scope` is a `tokio::task_local`, so hosting it in +//! `tinymemory-api` means adding tokio to a crate whose whole point is that +//! a caller can depend on it and compile almost nothing. Only +//! `util::redact` (136 lines over `sha2`) is the clean case, and it costs +//! `sha2` in the contract crate. +//! - **The re-embed queue** (~8 files) — `queue::{start, store, types, +//! ensure_reembed_backfill, requeue_failed_after_provider_change, +//! drain_until_idle, wake_workers, backfill_in_progress}`. No family. +//! - **Engine-shaped integration internals** (~11 files) — +//! `tinycortex::{memory_config_from, run_composio_connection, +//! load_composio_sync_state, HostSyncAdapter, CodingSession*}`. Named after +//! the engine, so no engine-neutral family can express them as they stand. +//! - **Engine-owned types** — `store::trees::types::TreeKind`, +//! `store::chunks::types::SourceKind`, `store::{NamespaceDocumentInput, +//! NamespaceRetrievalContext, GraphRelationRecord}`. A type import links the +//! crate exactly as a call does, so the shed needs these in +//! `tinymemory-api`. `MemoryCategory`/`MemoryEntry`/`MemoryTaint` already +//! are — `tinymemory_core::traits` re-exports them — so those call sites can +//! name the contract today. +//! +//! `rpc_models` was on this list and is **done**: all forty-five types were +//! named by this host and by nothing inside `tinymemory`, so they moved to +//! `memory::rpc_models` rather than into the contract. That is the shape to +//! look for first in what remains — a type the engine crate defines but only +//! the host uses does not need a contract to live in, it needs to come home. +//! `SourceKind` is emphatically **not** such a case (see the trap below). +//! - **Chat, ingest pipeline and preferences** (~12 files) — +//! `chat::{ChatProvider, build_chat_provider, test_override}`, +//! `ingest_pipeline::{ingest_chat, ingest_document_with_scope}`, +//! `preferences::{STANDING_PREFS_LIMIT, load_general_preferences, +//! recall_situational_preferences}`. +//! +//! The order that follows from this: relocate the pure helpers and types to +//! `tinymemory-api` (no bus surface, no release coupling), then move the +//! queue and chunk-write subsystems behind the module, and only then can the +//! handle-holding callers take the binding's provider and the crate leave the +//! build. Nothing here is a host-side routing pass, which is what the original +//! scope assumed. +//! //! # Known weaknesses, stated rather than hidden //! //! - **The lint sees text, not types.** A reference reached through a @@ -121,6 +188,19 @@ pub(crate) enum Verdict { /// for one of the three considered verdicts above. Draining it means /// re-classifying each entry as one of those three, not deleting the /// variant. + /// + /// **Drained 2026-08-22.** All 82 entries were audited into the three + /// considered verdicts; none turned out to be [`Verdict::SeamExpressible`], + /// which is the finding rather than a formality — every one of them is + /// blocked on a contract the module does not yet expose, so the remaining + /// #5560 work is upstream in `tinymemory` and not a routing pass here. The + /// variant is kept rather than removed for the reason it was added: if a + /// re-export facade grows back and hides engine users again, the label for + /// them already exists and already says what it means. + #[allow( + dead_code, + reason = "drained 2026-08-22; retained as the landing spot if a facade regrows" + )] FacadeRevealed, } @@ -144,388 +224,389 @@ const ALLOWED: &[(&str, Verdict, &str)] = &[ // inventory — is what surfaced them, and the count of real engine // dependencies did not grow by one. // - // They are `FacadeRevealed` rather than one of the three considered - // verdicts because they have not been audited individually. See the note on - // that variant. + // Audited individually on 2026-08-22 and re-classified out of + // `FacadeRevealed`; each entry now names the symbols it actually reaches + // for, so the verdict is checkable against the code rather than taken on + // trust. The audit's finding is that none of them is `SeamExpressible`. ( "src/bin/library_profile/scenarios/cold_phases.rs", - Verdict::FacadeRevealed, - "engine dependency predates this branch; the facade deletion made it visible", + Verdict::NeedsWiderSeam, + "holds or boots the in-process engine handle (store::MemoryClient); driver construction belongs to memory::binding and the seam has no door onto the live client", ), ( "src/bin/library_profile/scenarios/memory_ingest.rs", - Verdict::FacadeRevealed, - "engine dependency predates this branch; the facade deletion made it visible", + Verdict::NeedsWiderSeam, + "engine-internal ingest pipeline entry (ingest_pipeline::ingest_chat, queue::drain_until_idle); the ingest family covers documents and chat, not the scope-carrying pipeline variants", ), ( "src/core/memory_cli.rs", - Verdict::FacadeRevealed, - "engine dependency predates this branch; the facade deletion made it visible", + Verdict::NeedsWiderSeam, + "holds or boots the in-process engine handle (global::init, store::MemoryClientRef, store::NamespaceDocumentInput); driver construction belongs to memory::binding and the seam has no door onto the live client", ), ( "src/core/runtime/context.rs", - Verdict::FacadeRevealed, - "engine dependency predates this branch; the facade deletion made it visible", + Verdict::NeedsWiderSeam, + "holds or boots the in-process engine handle (global::init); driver construction belongs to memory::binding and the seam has no door onto the live client", ), ( "src/core/runtime/services.rs", - Verdict::FacadeRevealed, - "engine dependency predates this branch; the facade deletion made it visible", + Verdict::NeedsWiderSeam, + "the re-embed queue (queue::start) has no capability family", ), ( "src/lib.rs", - Verdict::FacadeRevealed, - "engine dependency predates this branch; the facade deletion made it visible", + Verdict::HostSide, + "crate-root re-export of the engine's store module under its historical path", ), ( "src/openhuman/agent/experience/ops.rs", - Verdict::FacadeRevealed, - "engine dependency predates this branch; the facade deletion made it visible", + Verdict::NeedsWiderSeam, + "holds or boots the in-process engine handle (global::init, global::client_if_ready, store::UnifiedMemory::new_with_memory_dir); driver construction belongs to memory::binding and the seam has no door onto the live client", ), ( "src/openhuman/agent/experience/store.rs", - Verdict::FacadeRevealed, - "engine dependency predates this branch; the facade deletion made it visible", + Verdict::NeedsWiderSeam, + "holds or boots the in-process engine handle (store::UnifiedMemory, store::safety::sanitize_text); driver construction belongs to memory::binding and the seam has no door onto the live client", ), ( "src/openhuman/agent/harness/archivist/hook_impl.rs", - Verdict::FacadeRevealed, - "engine dependency predates this branch; the facade deletion made it visible", + Verdict::NeedsWiderSeam, + "reaches engine storage below the contract (store::fts5, tinycortex::memory_config_from); MemoryChunks is read-only (list_chunks/get_chunk/chunk_detail/storage_kinds/chunk_embeddings) with no write or transaction door", ), ( "src/openhuman/agent/harness/archivist/lifecycle.rs", - Verdict::FacadeRevealed, - "engine dependency predates this branch; the facade deletion made it visible", + Verdict::NeedsWiderSeam, + "reaches engine storage below the contract (store::events, store::fts5::EpisodicEntry, store::profile, store::segments, chat::build_chat_provider); MemoryChunks is read-only (list_chunks/get_chunk/chunk_detail/storage_kinds/chunk_embeddings) with no write or transaction door", ), ( "src/openhuman/agent/harness/archivist/mod.rs", - Verdict::FacadeRevealed, - "engine dependency predates this branch; the facade deletion made it visible", + Verdict::NeedsWiderSeam, + "reaches engine storage below the contract (store::profile); MemoryChunks is read-only (list_chunks/get_chunk/chunk_detail/storage_kinds/chunk_embeddings) with no write or transaction door", ), ( "src/openhuman/agent/harness/archivist/recap.rs", - Verdict::FacadeRevealed, - "engine dependency predates this branch; the facade deletion made it visible", + Verdict::NeedsWiderSeam, + "reaches engine storage below the contract (store::fts5, store::segments::ConversationSegment, store::chunks::types::approx_token_count); MemoryChunks is read-only (list_chunks/get_chunk/chunk_detail/storage_kinds/chunk_embeddings) with no write or transaction door", ), ( "src/openhuman/agent/harness/archivist/test_constructors.rs", - Verdict::FacadeRevealed, - "engine dependency predates this branch; the facade deletion made it visible", + Verdict::NeedsWiderSeam, + "the engine-side chat provider seam (chat::ChatProvider); MemoryIngest has no provider-override door", ), ( "src/openhuman/agent/harness/archivist/tree_ingest.rs", - Verdict::FacadeRevealed, - "engine dependency predates this branch; the facade deletion made it visible", + Verdict::NeedsWiderSeam, + "reaches engine storage below the contract (store::fts5, store::segments::ConversationSegment, ingest_pipeline); MemoryChunks is read-only (list_chunks/get_chunk/chunk_detail/storage_kinds/chunk_embeddings) with no write or transaction door", ), ( "src/openhuman/agent/harness/archivist/types.rs", - Verdict::FacadeRevealed, - "engine dependency predates this branch; the facade deletion made it visible", + Verdict::NeedsWiderSeam, + "the engine-side chat provider seam (chat::ChatProvider); MemoryIngest has no provider-override door", ), ( "src/openhuman/agent/harness/artifact_offload/policy.rs", - Verdict::FacadeRevealed, - "engine dependency predates this branch; the facade deletion made it visible", + Verdict::NeedsWiderSeam, + "host policy reached through the engine crate (store::safety::sanitize_text); `store::safety` is itself a shim over `crate::engine::backend::store::safety`, so the scrubbers live in tinycortex — relocating them is engine work, not a contract move", ), ( "src/openhuman/agent/harness/session/builder/factory.rs", - Verdict::FacadeRevealed, - "engine dependency predates this branch; the facade deletion made it visible", + Verdict::NeedsWiderSeam, + "holds or boots the in-process engine handle (global::init); driver construction belongs to memory::binding and the seam has no door onto the live client", ), ( "src/openhuman/agent/harness/session/turn/context.rs", - Verdict::FacadeRevealed, - "engine dependency predates this branch; the facade deletion made it visible", + Verdict::NeedsWiderSeam, + "preferences namespace and loaders are engine-owned (preferences::STANDING_PREFS_LIMIT, preferences::load_general_preferences); no capability family covers them", ), ( "src/openhuman/agent/harness/session/turn/core.rs", - Verdict::FacadeRevealed, - "engine dependency predates this branch; the facade deletion made it visible", + Verdict::NeedsWiderSeam, + "preferences namespace and loaders are engine-owned (preferences::recall_situational_preferences); no capability family covers them", ), ( "src/openhuman/agent/harness/subagent_runner/ops/runner.rs", - Verdict::FacadeRevealed, - "engine dependency predates this branch; the facade deletion made it visible", + Verdict::NeedsWiderSeam, + "names engine-owned types (store::trees::types::TreeKind); relocating them to tinymemory-api is the ask, not a bus method", ), ( "src/openhuman/agent/harness/tool_result_artifacts/mod.rs", - Verdict::FacadeRevealed, - "engine dependency predates this branch; the facade deletion made it visible", + Verdict::NeedsWiderSeam, + "host policy reached through the engine crate (store::safety); `store::safety` is itself a shim over `crate::engine::backend::store::safety`, so the scrubbers live in tinycortex — relocating them is engine work, not a contract move", ), ( "src/openhuman/agent/learning/linkedin_enrichment.rs", - Verdict::FacadeRevealed, - "engine dependency predates this branch; the facade deletion made it visible", + Verdict::NeedsWiderSeam, + "holds or boots the in-process engine handle (store::MemoryClient, store::MemoryClientRef); driver construction belongs to memory::binding and the seam has no door onto the live client", ), ( "src/openhuman/agent/learning/startup.rs", - Verdict::FacadeRevealed, - "engine dependency predates this branch; the facade deletion made it visible", + Verdict::NeedsWiderSeam, + "holds or boots the in-process engine handle (global::client_if_ready, store::MemoryClient); driver construction belongs to memory::binding and the seam has no door onto the live client", ), ( "src/openhuman/agent/task_dispatcher/executor.rs", - Verdict::FacadeRevealed, - "engine dependency predates this branch; the facade deletion made it visible", + Verdict::NeedsWiderSeam, + "task-local host policy living in the engine crate (source_scope::with_source_scope); belongs in tinymemory-api, not a bus method", ), ( "src/openhuman/agent/tinyagents/host/agent_memory.rs", - Verdict::FacadeRevealed, - "engine dependency predates this branch; the facade deletion made it visible", + Verdict::NeedsWiderSeam, + "holds or boots the in-process engine handle (store::factories::create_memory, store::safety::sanitize_text); driver construction belongs to memory::binding and the seam has no door onto the live client", ), ( "src/openhuman/agent/tools/remember_preference.rs", - Verdict::FacadeRevealed, - "engine dependency predates this branch; the facade deletion made it visible", + Verdict::NeedsWiderSeam, + "holds or boots the in-process engine handle (store::UnifiedMemory); driver construction belongs to memory::binding and the seam has no door onto the live client", ), ( "src/openhuman/agent/tools/save_preference.rs", - Verdict::FacadeRevealed, - "engine dependency predates this branch; the facade deletion made it visible", + Verdict::NeedsWiderSeam, + "host policy reached through the engine crate (store::safety); `store::safety` is itself a shim over `crate::engine::backend::store::safety`, so the scrubbers live in tinycortex — relocating them is engine work, not a contract move", ), ( "src/openhuman/channels/controllers/ops/connect.rs", - Verdict::FacadeRevealed, - "engine dependency predates this branch; the facade deletion made it visible", + Verdict::NeedsWiderSeam, + "reaches engine storage below the contract (store::chunks::store, store::chunks::types::SourceKind); MemoryChunks is read-only (list_chunks/get_chunk/chunk_detail/storage_kinds/chunk_embeddings) with no write or transaction door", ), ( "src/openhuman/channels/runtime/startup.rs", - Verdict::FacadeRevealed, - "engine dependency predates this branch; the facade deletion made it visible", + Verdict::NeedsWiderSeam, + "holds or boots the in-process engine handle (store); driver construction belongs to memory::binding and the seam has no door onto the live client", ), ( "src/openhuman/channels/tests/memory.rs", - Verdict::FacadeRevealed, - "engine dependency predates this branch; the facade deletion made it visible", + Verdict::NeedsWiderSeam, + "holds or boots the in-process engine handle (store::UnifiedMemory); driver construction belongs to memory::binding and the seam has no door onto the live client", ), ( "src/openhuman/config/migration_helpers/core.rs", - Verdict::FacadeRevealed, - "engine dependency predates this branch; the facade deletion made it visible", + Verdict::NeedsWiderSeam, + "holds or boots the in-process engine handle (store); driver construction belongs to memory::binding and the seam has no door onto the live client", ), ( "src/openhuman/config/ops/model.rs", - Verdict::FacadeRevealed, - "engine dependency predates this branch; the facade deletion made it visible", + Verdict::NeedsWiderSeam, + "the re-embed queue (queue::ensure_reembed_backfill, queue::requeue_failed_after_provider_change) has no capability family", ), ( "src/openhuman/cron/scheduler.rs", - Verdict::FacadeRevealed, - "engine dependency predates this branch; the facade deletion made it visible", + Verdict::NeedsWiderSeam, + "task-local host policy living in the engine crate (source_scope::with_source_scope); belongs in tinymemory-api, not a bus method", ), ( "src/openhuman/desktop/app_state/ops.rs", - Verdict::FacadeRevealed, - "engine dependency predates this branch; the facade deletion made it visible", + Verdict::NeedsWiderSeam, + "holds or boots the in-process engine handle (global::init); driver construction belongs to memory::binding and the seam has no door onto the live client", ), ( "src/openhuman/flows/memory_tools.rs", - Verdict::FacadeRevealed, - "engine dependency predates this branch; the facade deletion made it visible", + Verdict::NeedsWiderSeam, + "holds or boots the in-process engine handle (store::UnifiedMemory, store::safety::has_likely_secret); driver construction belongs to memory::binding and the seam has no door onto the live client", ), ( "src/openhuman/flows/ops.rs", - Verdict::FacadeRevealed, - "engine dependency predates this branch; the facade deletion made it visible", + Verdict::NeedsWiderSeam, + "holds or boots the in-process engine handle (store::MemoryClientRef); driver construction belongs to memory::binding and the seam has no door onto the live client", ), ( "src/openhuman/flows/tinyflows/memory_adapter.rs", - Verdict::FacadeRevealed, - "engine dependency predates this branch; the facade deletion made it visible", + Verdict::NeedsWiderSeam, + "host policy reached through the engine crate (store::safety::has_likely_secret); `store::safety` is itself a shim over `crate::engine::backend::store::safety`, so the scrubbers live in tinycortex — relocating them is engine work, not a contract move", ), ( "src/openhuman/hosted/orchestration/effect_executor.rs", - Verdict::FacadeRevealed, - "engine dependency predates this branch; the facade deletion made it visible", + Verdict::NeedsWiderSeam, + "engine-internal ingest pipeline entry (ingest_pipeline::ingest_document_with_scope); the ingest family covers documents and chat, not the scope-carrying pipeline variants", ), ( "src/openhuman/inference/embeddings/rpc.rs", - Verdict::FacadeRevealed, - "engine dependency predates this branch; the facade deletion made it visible", + Verdict::NeedsWiderSeam, + "the re-embed queue (queue::ensure_reembed_backfill, queue::requeue_failed_after_provider_change) has no capability family", ), ( "src/openhuman/integrations/composio/ops/memory_cleanup.rs", - Verdict::FacadeRevealed, - "engine dependency predates this branch; the facade deletion made it visible", + Verdict::NeedsWiderSeam, + "reaches engine storage below the contract (store::chunks::store, store::chunks::types::SourceKind, tinycortex::HostSyncAdapter); MemoryChunks is read-only (list_chunks/get_chunk/chunk_detail/storage_kinds/chunk_embeddings) with no write or transaction door", ), ( "src/openhuman/integrations/composio/ops/mod.rs", - Verdict::FacadeRevealed, - "engine dependency predates this branch; the facade deletion made it visible", + Verdict::NeedsWiderSeam, + "holds or boots the in-process engine handle (store::MemoryClient); driver construction belongs to memory::binding and the seam has no door onto the live client", ), ( "src/openhuman/integrations/composio/ops/providers_ops.rs", - Verdict::FacadeRevealed, - "engine dependency predates this branch; the facade deletion made it visible", + Verdict::NeedsWiderSeam, + "engine internals (tinycortex::run_composio_connection); tinycortex-shaped, so no engine-neutral family can express it", ), ( "src/openhuman/integrations/composio/schemas.rs", - Verdict::FacadeRevealed, - "engine dependency predates this branch; the facade deletion made it visible", + Verdict::NeedsWiderSeam, + "holds or boots the in-process engine handle (global::client_if_ready); driver construction belongs to memory::binding and the seam has no door onto the live client", ), ( "src/openhuman/memory/guard/audit.rs", - Verdict::FacadeRevealed, - "engine dependency predates this branch; the facade deletion made it visible", + Verdict::NeedsWiderSeam, + "pure host-policy helper genuinely owned by the engine crate (util::redact::redact — 136 lines over `sha2` alone); relocatable to tinymemory-api, at the cost of adding `sha2` there, unlike its `store::safety` neighbours which only shim the engine's own scrubbers", ), ( "src/openhuman/memory/guard/policy.rs", - Verdict::FacadeRevealed, - "engine dependency predates this branch; the facade deletion made it visible", + Verdict::NeedsWiderSeam, + "task-local host policy living in the engine crate (source_scope::current_source_scope, store::safety::sanitize_json/sanitize_text); belongs in tinymemory-api, not a bus method", ), ( "src/openhuman/memory/ops/documents.rs", - Verdict::FacadeRevealed, - "engine dependency predates this branch; the facade deletion made it visible", + Verdict::NeedsWiderSeam, + "holds or boots the in-process engine handle (global::init, store::NamespaceRetrievalContext); driver construction belongs to memory::binding and the seam has no door onto the live client", ), ( "src/openhuman/memory/ops/guard.rs", - Verdict::FacadeRevealed, - "engine dependency predates this branch; the facade deletion made it visible", + Verdict::NeedsWiderSeam, + "holds or boots the in-process engine handle (global); driver construction belongs to memory::binding and the seam has no door onto the live client", ), ( "src/openhuman/memory/ops/helpers.rs", - Verdict::FacadeRevealed, - "engine dependency predates this branch; the facade deletion made it visible", + Verdict::NeedsWiderSeam, + "holds or boots the in-process engine handle (global::client, global::client_if_ready, global::init, store::GraphRelationRecord); driver construction belongs to memory::binding and the seam has no door onto the live client", ), ( "src/openhuman/memory/ops/learn.rs", - Verdict::FacadeRevealed, - "engine dependency predates this branch; the facade deletion made it visible", + Verdict::NeedsWiderSeam, + "holds or boots the in-process engine handle (global::client, store::NamespaceDocumentInput); driver construction belongs to memory::binding and the seam has no door onto the live client", ), ( "src/openhuman/memory/ops/sync.rs", - Verdict::FacadeRevealed, - "engine dependency predates this branch; the facade deletion made it visible", + Verdict::NeedsWiderSeam, + "two blockers: engine internals (tinycortex::run_composio_connection, sync_events), tinycortex-shaped so no engine-neutral family can express them; and the in-process engine handle (global::client), which belongs to memory::binding", ), ( "src/openhuman/memory/ops/test_support.rs", - Verdict::FacadeRevealed, - "engine dependency predates this branch; the facade deletion made it visible", + Verdict::NeedsWiderSeam, + "holds or boots the in-process engine handle (global::init); driver construction belongs to memory::binding and the seam has no door onto the live client", ), ( "src/openhuman/memory/read_rpc/admin.rs", - Verdict::FacadeRevealed, - "engine dependency predates this branch; the facade deletion made it visible", + Verdict::NeedsWiderSeam, + "reaches engine storage below the contract (queue::store, queue::types, queue::wake_workers, store::chunks::store); MemoryChunks is read-only (list_chunks/get_chunk/chunk_detail/storage_kinds/chunk_embeddings) with no write or transaction door", ), ( "src/openhuman/memory/read_rpc/chunks.rs", - Verdict::FacadeRevealed, - "engine dependency predates this branch; the facade deletion made it visible", + Verdict::NeedsWiderSeam, + "reaches engine storage below the contract (store::chunks::store::with_connection); MemoryChunks is read-only (list_chunks/get_chunk/chunk_detail/storage_kinds/chunk_embeddings) with no write or transaction door", ), ( "src/openhuman/memory/read_rpc/entities.rs", - Verdict::FacadeRevealed, - "engine dependency predates this branch; the facade deletion made it visible", + Verdict::NeedsWiderSeam, + "reaches engine storage below the contract (store::chunks::store::with_connection, util::redact::redact); MemoryChunks is read-only (list_chunks/get_chunk/chunk_detail/storage_kinds/chunk_embeddings) with no write or transaction door", ), ( "src/openhuman/memory/read_rpc/graph.rs", - Verdict::FacadeRevealed, - "engine dependency predates this branch; the facade deletion made it visible", + Verdict::NeedsWiderSeam, + "reaches engine storage below the contract (store::chunks::store::with_connection, util::redact::redact); MemoryChunks is read-only (list_chunks/get_chunk/chunk_detail/storage_kinds/chunk_embeddings) with no write or transaction door", ), ( "src/openhuman/memory/read_rpc/mod.rs", - Verdict::FacadeRevealed, - "engine dependency predates this branch; the facade deletion made it visible", + Verdict::NeedsWiderSeam, + "reaches engine storage below the contract (store::chunks::store::with_connection, store::chunks::types::SourceKind); MemoryChunks is read-only (list_chunks/get_chunk/chunk_detail/storage_kinds/chunk_embeddings) with no write or transaction door", ), ( "src/openhuman/memory/read_rpc/vault.rs", - Verdict::FacadeRevealed, - "engine dependency predates this branch; the facade deletion made it visible", + Verdict::NeedsWiderSeam, + "reaches engine storage below the contract (store::content::obsidian_registry, util::redact::redact); MemoryChunks is read-only (list_chunks/get_chunk/chunk_detail/storage_kinds/chunk_embeddings) with no write or transaction door", ), ( "src/openhuman/memory/sources/rpc.rs", - Verdict::FacadeRevealed, - "engine dependency predates this branch; the facade deletion made it visible", + Verdict::NeedsWiderSeam, + "engine internals (tinycortex::CodingSession* request/response/status types, tinycortex::SyncAuditEntry); tinycortex-shaped, so no engine-neutral family can express it", ), ( "src/openhuman/memory/sources/schemas.rs", - Verdict::FacadeRevealed, - "engine dependency predates this branch; the facade deletion made it visible", + Verdict::NeedsWiderSeam, + "engine internals (tinycortex::CodingSessionIngestRequest); tinycortex-shaped, so no engine-neutral family can express it", ), ( "src/openhuman/memory/store_golden.rs", - Verdict::FacadeRevealed, - "engine dependency predates this branch; the facade deletion made it visible", + Verdict::NeedsWiderSeam, + "two blockers: engine storage below the contract (store::chunks), where MemoryChunks is read-only (list_chunks/get_chunk/chunk_detail/storage_kinds/chunk_embeddings) with no write or transaction door; and the in-process engine handle (global::client, store::UnifiedMemory::new)", ), ( "src/openhuman/memory/sync/composio/bus.rs", - Verdict::FacadeRevealed, - "engine dependency predates this branch; the facade deletion made it visible", + Verdict::NeedsWiderSeam, + "engine internals (events::MemoryEvent/publish, observability::report_error_or_expected, tinycortex::run_composio_connection); tinycortex-shaped, so no engine-neutral family can express it", ), ( "src/openhuman/memory/sync/composio/providers/slack/rpc.rs", - Verdict::FacadeRevealed, - "engine dependency predates this branch; the facade deletion made it visible", + Verdict::NeedsWiderSeam, + "engine internals (tinycortex::load_composio_sync_state, tinycortex::run_composio_connection); tinycortex-shaped, so no engine-neutral family can express it", ), ( "src/openhuman/memory/sync/sync_status/rpc.rs", - Verdict::FacadeRevealed, - "engine dependency predates this branch; the facade deletion made it visible", + Verdict::NeedsWiderSeam, + "engine internals (tinycortex::memory_config_from); tinycortex-shaped, so no engine-neutral family can express it", ), ( "src/openhuman/memory/sync_events_bridge.rs", - Verdict::FacadeRevealed, - "engine dependency predates this branch; the facade deletion made it visible", + Verdict::NeedsWiderSeam, + "the re-embed queue (queue::ensure_reembed_backfill, sync_events) has no capability family", ), ( "src/openhuman/memory/tools/flavour.rs", - Verdict::FacadeRevealed, - "engine dependency predates this branch; the facade deletion made it visible", + Verdict::NeedsWiderSeam, + "engine internals (tinycortex::memory_config_from); tinycortex-shaped, so no engine-neutral family can express it", ), ( "src/openhuman/memory/tools/forget.rs", - Verdict::FacadeRevealed, - "engine dependency predates this branch; the facade deletion made it visible", + Verdict::NeedsWiderSeam, + "holds or boots the in-process engine handle (store::UnifiedMemory); driver construction belongs to memory::binding and the seam has no door onto the live client", ), ( "src/openhuman/memory/tools/recall.rs", - Verdict::FacadeRevealed, - "engine dependency predates this branch; the facade deletion made it visible", + Verdict::NeedsWiderSeam, + "holds or boots the in-process engine handle (store::UnifiedMemory); driver construction belongs to memory::binding and the seam has no door onto the live client", ), ( "src/openhuman/memory/tools/store.rs", - Verdict::FacadeRevealed, - "engine dependency predates this branch; the facade deletion made it visible", + Verdict::NeedsWiderSeam, + "holds or boots the in-process engine handle (store::UnifiedMemory, store::safety); driver construction belongs to memory::binding and the seam has no door onto the live client", ), ( "src/openhuman/memory/tree/retrieval/rpc.rs", - Verdict::FacadeRevealed, - "engine dependency predates this branch; the facade deletion made it visible", + Verdict::NeedsWiderSeam, + "reaches engine storage below the contract (store::chunks::store::upsert_chunks, upsert_staged_chunks_tx, with_connection); MemoryChunks is read-only (list_chunks/get_chunk/chunk_detail/storage_kinds/chunk_embeddings) with no write or transaction door", ), ( "src/openhuman/memory/tree/tree/rpc.rs", - Verdict::FacadeRevealed, - "engine dependency predates this branch; the facade deletion made it visible", + Verdict::NeedsWiderSeam, + "the re-embed queue (queue::store::requeue_failed, queue::backfill_in_progress, ingest_pipeline) has no capability family", ), ( "src/openhuman/platform/doctor/core.rs", - Verdict::FacadeRevealed, - "engine dependency predates this branch; the facade deletion made it visible", + Verdict::NeedsWiderSeam, + "reaches engine storage below the contract (store::chunks::store::with_connection, store::factories::effective_embedding_settings); MemoryChunks is read-only (list_chunks/get_chunk/chunk_detail/storage_kinds/chunk_embeddings) with no write or transaction door", ), ( "src/openhuman/security/approval/store.rs", - Verdict::FacadeRevealed, - "engine dependency predates this branch; the facade deletion made it visible", + Verdict::NeedsWiderSeam, + "host policy reached through the engine crate (store::safety::sanitize_text); `store::safety` is itself a shim over `crate::engine::backend::store::safety`, so the scrubbers live in tinycortex — relocating them is engine work, not a contract move", ), ( "src/openhuman/security/credentials/ops.rs", - Verdict::FacadeRevealed, - "engine dependency predates this branch; the facade deletion made it visible", + Verdict::NeedsWiderSeam, + "two blockers: the re-embed queue (queue::ensure_reembed_backfill), which has no capability family; and the in-process engine handle (global::init), which belongs to memory::binding", ), ( "src/openhuman/skills/runtime/run_machinery.rs", - Verdict::FacadeRevealed, - "engine dependency predates this branch; the facade deletion made it visible", + Verdict::NeedsWiderSeam, + "task-local host policy living in the engine crate (source_scope::current_source_scope, source_scope::with_source_scope); belongs in tinymemory-api, not a bus method", ), ( "src/openhuman/tools/registry/ops.rs", - Verdict::FacadeRevealed, - "engine dependency predates this branch; the facade deletion made it visible", + Verdict::NeedsWiderSeam, + "reaches engine storage below the contract (store::chunks::store); MemoryChunks is read-only (list_chunks/get_chunk/chunk_detail/storage_kinds/chunk_embeddings) with no write or transaction door", ), ( "src/openhuman/web_chat/run_task.rs", - Verdict::FacadeRevealed, - "engine dependency predates this branch; the facade deletion made it visible", + Verdict::NeedsWiderSeam, + "task-local host policy living in the engine crate (source_scope::with_source_scope); belongs in tinymemory-api, not a bus method", ), // ── Re-export shims: `pub use tinymemory_core::::*;` ──────────── // diff --git a/src/openhuman/memory/mod.rs b/src/openhuman/memory/mod.rs index ebe12bbe6a..86f06163c8 100644 --- a/src/openhuman/memory/mod.rs +++ b/src/openhuman/memory/mod.rs @@ -121,7 +121,13 @@ pub use tinymemory_core::ingestion::{ IngestionState, IngestionStatusSnapshot, MemoryIngestionConfig, MemoryIngestionRequest, MemoryIngestionResult, DEFAULT_MEMORY_EXTRACTION_MODEL, }; -pub use tinymemory_core::rpc_models::*; +// The host's own JSON-RPC request/response shapes. They lived in +// `tinymemory_core::rpc_models` and were re-exported here by a glob; nothing +// in `tinymemory` ever named one, so the engine crate was carrying this host's +// RPC surface (#5560). Same glob, same paths, same wire bytes — the definitions +// are simply ours now. See `rpc_models`'s module docs. +pub mod rpc_models; +pub use rpc_models::*; pub use tinymemory_core::traits::{ Memory, MemoryCategory, MemoryEntry, MemoryTaint, NamespaceSummary, RecallOpts, }; diff --git a/src/openhuman/memory/rpc_models.rs b/src/openhuman/memory/rpc_models.rs new file mode 100644 index 0000000000..ff3e994c7d --- /dev/null +++ b/src/openhuman/memory/rpc_models.rs @@ -0,0 +1,614 @@ +//! RPC data models for the OpenHuman memory system. +//! +//! This module defines the request and response structures used by the JSON-RPC +//! interface to interact with the memory system. These models ensure type-safe +//! communication between the frontend/client and the Rust backend. +//! +//! # Why these live here and not in the engine crate (#5560) +//! +//! They used to be `tinymemory_core::rpc_models`, re-exported into this module +//! by a glob. Nothing in `tinymemory` ever referenced them — not the engine, +//! not an adapter, not a test — while all forty-five types are named by this +//! host, so the engine crate was carrying one host's JSON-RPC surface and +//! every file that touched a request shape held the engine in the build for it. +//! +//! Moving them changes no bytes on the wire: the structs are verbatim, so the +//! serde shapes, field names and defaults are identical, and the re-export +//! below keeps every `memory::…` path resolving exactly as before. What it +//! changes is who owns them — a request shape this host defines, deserializes +//! and passes between its own functions (`memory_query_namespace`, +//! `thread_create_new`) was never the engine's to define. +//! +//! The mirror in `tinymemory-core` is dead on arrival and should be deleted +//! upstream; until it is, the two definitions cannot diverge in a way that +//! matters, because nothing reads the engine's copy. + +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; + +/// Standard error structure for API responses. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ApiError { + /// A machine-readable error code. + pub code: String, + /// A human-readable error message. + pub message: String, + /// Optional additional error details. + #[serde(skip_serializing_if = "Option::is_none")] + pub details: Option, +} + +/// Pagination metadata for list-based responses. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PaginationMeta { + /// Maximum number of items requested. + pub limit: usize, + /// Number of items skipped. + pub offset: usize, + /// Total number of items available in the backend. + pub count: usize, +} + +/// General metadata included in all API envelopes. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ApiMeta { + /// Unique identifier for the request. + pub request_id: String, + /// Time taken to process the request in seconds. + #[serde(skip_serializing_if = "Option::is_none")] + pub latency_seconds: Option, + /// Whether the response was served from a cache. + #[serde(skip_serializing_if = "Option::is_none")] + pub cached: Option, + /// Optional counts of various items (e.g., by category). + #[serde(skip_serializing_if = "Option::is_none")] + pub counts: Option>, + /// Optional pagination information. + #[serde(skip_serializing_if = "Option::is_none")] + pub pagination: Option, +} + +/// Generic envelope for all API responses. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ApiEnvelope { + /// The actual payload of the response. + #[serde(skip_serializing_if = "Option::is_none")] + pub data: Option, + /// Error information if the request failed. + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, + /// Metadata about the request and response. + pub meta: ApiMeta, +} + +/// An empty request body for methods that don't require parameters. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct EmptyRequest {} + +/// Request to create a new conversation thread. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct CreateConversationThreadRequest { + #[serde(default)] + pub labels: Option>, + #[serde(default)] + pub personality_id: Option, +} + +/// Request payload for `openhuman.memory_init`. +/// +/// `jwt_token` is accepted for backward compatibility but **not used** — memory +/// is local-only (SQLite). Remote/cloud memory sync is a future consideration. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct MemoryInitRequest { + /// Optional token, currently ignored as memory is local-only. + #[serde(default)] + pub jwt_token: Option, +} + +/// Response payload for `openhuman.memory_init`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MemoryInitResponse { + /// Whether the memory system was successfully initialized. + pub initialized: bool, + /// The root workspace directory. + pub workspace_dir: String, + /// The specific directory where memory data is stored. + pub memory_dir: String, +} + +/// Summary information for a workspace-backed conversation thread. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ConversationThreadSummary { + pub id: String, + pub title: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub chat_id: Option, + pub is_active: bool, + pub message_count: usize, + pub last_message_at: String, + pub created_at: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub parent_thread_id: Option, + #[serde(default)] + pub labels: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub personality_id: Option, +} + +/// A single persisted conversation message. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ConversationMessageRecord { + pub id: String, + pub content: String, + #[serde(rename = "type")] + pub message_type: String, + #[serde(default)] + pub extra_metadata: serde_json::Value, + pub sender: String, + pub created_at: String, +} + +/// Request to create or update a thread in workspace storage. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct UpsertConversationThreadRequest { + pub id: String, + pub title: String, + pub created_at: String, + #[serde(default)] + pub parent_thread_id: Option, + #[serde(default)] + pub labels: Option>, + #[serde(default)] + pub personality_id: Option, +} + +/// Request to update labels for a conversation thread. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct UpdateConversationThreadLabelsRequest { + pub thread_id: String, + pub labels: Vec, +} + +/// Request to set a user-specified title on a conversation thread. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct UpdateConversationThreadTitleRequest { + pub thread_id: String, + pub title: String, +} + +/// Response payload for thread list operations. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ConversationThreadsListResponse { + pub threads: Vec, + pub count: usize, +} + +/// Request to fetch messages for a specific thread. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ConversationMessagesRequest { + pub thread_id: String, +} + +/// Response payload for message list operations. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ConversationMessagesResponse { + pub messages: Vec, + pub count: usize, +} + +/// Request to append a message to a thread. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct AppendConversationMessageRequest { + pub thread_id: String, + pub message: ConversationMessageRecord, +} + +/// Request to generate or refresh a thread title after the first exchange. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct GenerateConversationThreadTitleRequest { + pub thread_id: String, + #[serde(default)] + pub assistant_message: Option, +} + +/// Request to patch a persisted message. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct UpdateConversationMessageRequest { + pub thread_id: String, + pub message_id: String, + #[serde(default)] + pub extra_metadata: Option, +} + +/// Request to delete a thread and its message log. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct DeleteConversationThreadRequest { + pub thread_id: String, + pub deleted_at: String, +} + +/// Response payload for single-thread deletion. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct DeleteConversationThreadResponse { + pub deleted: bool, +} + +/// Response payload for purging all workspace-backed conversations. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PurgeConversationThreadsResponse { + pub messages_deleted: usize, + pub agent_threads_deleted: usize, + pub agent_messages_deleted: usize, +} + +/// Request payload for `openhuman.list_documents`. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ListDocumentsRequest { + /// Optional namespace filter. + #[serde(default)] + pub namespace: Option, +} + +/// Summary information for a document in memory. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MemoryDocumentSummary { + /// Unique identifier for the document. + pub document_id: String, + /// Namespace the document belongs to. + pub namespace: String, + /// Lookup key for the document. + pub key: String, + /// Human-readable title. + pub title: String, + /// Type of the source (e.g., "file", "web", "note"). + pub source_type: String, + /// Ingestion priority. + pub priority: String, + /// Creation timestamp (Unix epoch). + pub created_at: f64, + /// Last update timestamp (Unix epoch). + pub updated_at: f64, +} + +/// Response payload for `openhuman.list_documents`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ListDocumentsResponse { + /// The namespace used for filtering. + #[serde(default)] + pub namespace: Option, + /// The list of document summaries. + pub documents: Vec, + /// Total number of documents found. + pub count: usize, +} + +/// Response payload for `openhuman.list_namespaces`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ListNamespacesResponse { + /// List of available namespace names. + pub namespaces: Vec, + /// Total number of namespaces. + pub count: usize, +} + +/// Request payload for `openhuman.delete_document`. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct DeleteDocumentRequest { + /// Namespace containing the document. + pub namespace: String, + /// ID of the document to delete. + pub document_id: String, +} + +/// Response payload for `openhuman.delete_document`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DeleteDocumentResponse { + /// Status message of the operation. + pub status: String, + /// Namespace of the document. + pub namespace: String, + /// ID of the deleted document. + pub document_id: String, + /// Whether the deletion was successful. + pub deleted: bool, +} + +/// Request payload for `openhuman.query_namespace`. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct QueryNamespaceRequest { + /// Namespace to query. + pub namespace: String, + /// Natural language query or search term. + pub query: String, + /// Whether to include reference citations in the response. + #[serde(default)] + pub include_references: Option, + /// Optional filter to specific document IDs. + #[serde(default)] + pub document_ids: Option>, + /// Maximum number of results to return. + #[serde(default)] + pub limit: Option, + /// Alias for limit, specifying max number of chunks. + #[serde(default)] + pub max_chunks: Option, +} + +impl QueryNamespaceRequest { + /// Resolves the effective limit from `max_chunks`, `limit`, or a default value. + pub fn resolved_limit(&self) -> u32 { + self.max_chunks.or(self.limit).unwrap_or(10) + } +} + +/// Response payload for `openhuman.query_namespace`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct QueryNamespaceResponse { + /// Retrieved context including entities, relations, and chunks. + #[serde(skip_serializing_if = "Option::is_none")] + pub context: Option, + /// A formatted message suitable for inclusion in an LLM prompt. + #[serde(skip_serializing_if = "Option::is_none")] + pub llm_context_message: Option, +} + +/// Request payload for `openhuman.recall_context`. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct RecallContextRequest { + /// Namespace to recall from. + pub namespace: String, + /// Whether to include references. + #[serde(default)] + pub include_references: Option, + /// Maximum number of results. + #[serde(default)] + pub limit: Option, + /// Maximum number of chunks. + #[serde(default)] + pub max_chunks: Option, +} + +impl RecallContextRequest { + /// Resolves the effective limit. + pub fn resolved_limit(&self) -> u32 { + self.max_chunks.or(self.limit).unwrap_or(10) + } +} + +/// Response payload for `openhuman.recall_context`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RecallContextResponse { + /// Retrieved context. + #[serde(skip_serializing_if = "Option::is_none")] + pub context: Option, + /// Formatted LLM message. + #[serde(skip_serializing_if = "Option::is_none")] + pub llm_context_message: Option, +} + +/// Request payload for `openhuman.recall_memories`. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct RecallMemoriesRequest { + /// Namespace to recall from. + pub namespace: String, + /// Minimum retention score (0.0 to 1.0). + #[serde(default)] + pub min_retention: Option, + /// Temporal filter (Unix epoch). + #[serde(default)] + pub as_of: Option, + /// Maximum results. + #[serde(default)] + pub limit: Option, + /// Alias for limit. + #[serde(default)] + pub max_chunks: Option, + /// Alias for limit (top K results). + #[serde(default)] + pub top_k: Option, +} + +impl RecallMemoriesRequest { + /// Resolves the effective limit checking `top_k`, `max_chunks`, and `limit`. + pub fn resolved_limit(&self) -> u32 { + self.top_k.or(self.max_chunks).or(self.limit).unwrap_or(10) + } +} + +/// Represents an entity retrieved from memory. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MemoryRetrievalEntity { + /// Unique identifier for the entity. + #[serde(skip_serializing_if = "Option::is_none")] + pub id: Option, + /// Name of the entity. + pub name: String, + /// Type of the entity (e.g., "Person", "Place"). + #[serde(skip_serializing_if = "Option::is_none")] + pub entity_type: Option, + /// Retrieval relevance score. + #[serde(skip_serializing_if = "Option::is_none")] + pub score: Option, + /// Additional arbitrary metadata. + #[serde(default)] + pub metadata: serde_json::Value, +} + +/// Represents a relationship between two entities. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MemoryRetrievalRelation { + /// The subject entity. + pub subject: String, + /// The relationship type (predicate). + pub predicate: String, + /// The object entity. + pub object: String, + /// Relevance score. + #[serde(skip_serializing_if = "Option::is_none")] + pub score: Option, + /// Number of times this relation was evidenced. + #[serde(skip_serializing_if = "Option::is_none")] + pub evidence_count: Option, + /// Additional metadata. + #[serde(default)] + pub metadata: serde_json::Value, +} + +/// Represents a text chunk retrieved from memory. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MemoryRetrievalChunk { + /// ID of the chunk. + #[serde(skip_serializing_if = "Option::is_none")] + pub chunk_id: Option, + /// ID of the parent document. + #[serde(skip_serializing_if = "Option::is_none")] + pub document_id: Option, + /// The text content of the chunk. + pub content: String, + /// Relevance score. + pub score: f64, + /// Additional metadata. + #[serde(default)] + pub metadata: serde_json::Value, + /// Creation timestamp. + #[serde(skip_serializing_if = "Option::is_none")] + pub created_at: Option, + /// Last update timestamp. + #[serde(skip_serializing_if = "Option::is_none")] + pub updated_at: Option, +} + +/// Container for all retrieved memory components. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MemoryRetrievalContext { + /// List of entities found. + pub entities: Vec, + /// List of relations between entities. + pub relations: Vec, + /// List of raw text chunks. + pub chunks: Vec, +} + +/// A specific item recalled from memory. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MemoryRecallItem { + /// Type of memory item (e.g., "fact", "observation"). + #[serde(rename = "type")] + pub kind: String, + /// Unique ID of the item. + pub id: String, + /// Text content of the memory. + pub content: String, + /// Relevance score. + pub score: f64, + /// Retention strength (0.0 to 1.0). + #[serde(skip_serializing_if = "Option::is_none")] + pub retention: Option, + /// Timestamp of last access. + #[serde(skip_serializing_if = "Option::is_none")] + pub last_accessed_at: Option, + /// Total number of times this memory was accessed. + #[serde(skip_serializing_if = "Option::is_none")] + pub access_count: Option, + /// How many days the memory has remained stable. + #[serde(skip_serializing_if = "Option::is_none")] + pub stability_days: Option, +} + +/// Response payload for `openhuman.recall_memories`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RecallMemoriesResponse { + /// List of recalled memory items. + pub memories: Vec, +} + +/// Request payload for `openhuman.list_memory_files`. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ListMemoryFilesRequest { + /// Directory path relative to the memory root. + #[serde(default = "default_memory_relative_dir")] + pub relative_dir: String, +} + +/// Response payload for `openhuman.list_memory_files`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ListMemoryFilesResponse { + /// The directory listed. + pub relative_dir: String, + /// List of filenames. + pub files: Vec, + /// Total count of files. + pub count: usize, +} + +/// Request payload for `openhuman.read_memory_file`. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ReadMemoryFileRequest { + /// Path to the file relative to the memory root. + pub relative_path: String, +} + +/// Response payload for `openhuman.read_memory_file`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ReadMemoryFileResponse { + /// The path of the file read. + pub relative_path: String, + /// Full content of the file. + pub content: String, +} + +/// Request payload for `openhuman.write_memory_file`. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct WriteMemoryFileRequest { + /// Path to write to relative to the memory root. + pub relative_path: String, + /// Content to write. + pub content: String, +} + +/// Response payload for `openhuman.write_memory_file`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WriteMemoryFileResponse { + /// The path of the file written. + pub relative_path: String, + /// Whether the write was successful. + pub written: bool, + /// Number of bytes written. + pub bytes_written: usize, +} + +/// Default directory for memory operations. Empty string means the memory +/// root itself (`/memory`); the file-based memory RPCs resolve all +/// relative paths under that directory. +pub(crate) fn default_memory_relative_dir() -> String { + String::new() +} + +#[cfg(test)] +#[path = "rpc_models_tests.rs"] +mod tests; diff --git a/src/openhuman/memory/rpc_models_tests.rs b/src/openhuman/memory/rpc_models_tests.rs new file mode 100644 index 0000000000..22207e2dc2 --- /dev/null +++ b/src/openhuman/memory/rpc_models_tests.rs @@ -0,0 +1,239 @@ +//! Unit tests for the memory RPC request/response models, covering +//! deserialization compatibility and limit-resolution helpers. + +use super::*; +use serde_json::json; + +#[test] +fn recall_memories_request_accepts_compatibility_noop_params() { + let request: RecallMemoriesRequest = serde_json::from_value(json!({ + "namespace": "team", + "top_k": 7, + "min_retention": 0.8, + "as_of": 1700000000.0 + })) + .expect("compatibility params should deserialize"); + + assert_eq!(request.namespace, "team"); + assert_eq!(request.top_k, Some(7)); + assert_eq!(request.min_retention, Some(0.8)); + assert_eq!(request.as_of, Some(1_700_000_000.0)); +} + +#[test] +fn recall_memories_request_limit_resolution_ignores_compatibility_noop_params() { + let request: RecallMemoriesRequest = serde_json::from_value(json!({ + "namespace": "team", + "limit": 3, + "min_retention": 0.5, + "as_of": 1700000000.0 + })) + .expect("request should deserialize"); + + assert_eq!(request.resolved_limit(), 3); +} + +// ── resolved_limit priorities ───────────────────────────────── + +#[test] +fn recall_memories_resolved_limit_prefers_top_k_over_max_chunks_and_limit() { + let req = RecallMemoriesRequest { + namespace: "n".into(), + min_retention: None, + as_of: None, + limit: Some(5), + max_chunks: Some(7), + top_k: Some(9), + }; + assert_eq!(req.resolved_limit(), 9); +} + +#[test] +fn recall_memories_resolved_limit_falls_back_to_max_chunks_then_limit_then_default() { + let without_top_k = RecallMemoriesRequest { + namespace: "n".into(), + min_retention: None, + as_of: None, + limit: Some(5), + max_chunks: Some(7), + top_k: None, + }; + assert_eq!(without_top_k.resolved_limit(), 7); + + let limit_only = RecallMemoriesRequest { + namespace: "n".into(), + min_retention: None, + as_of: None, + limit: Some(5), + max_chunks: None, + top_k: None, + }; + assert_eq!(limit_only.resolved_limit(), 5); + + let none = RecallMemoriesRequest { + namespace: "n".into(), + min_retention: None, + as_of: None, + limit: None, + max_chunks: None, + top_k: None, + }; + assert_eq!(none.resolved_limit(), 10); +} + +#[test] +fn query_namespace_resolved_limit_prefers_max_chunks_then_limit_then_default() { + let req = QueryNamespaceRequest { + namespace: "n".into(), + query: "q".into(), + include_references: None, + document_ids: None, + limit: Some(3), + max_chunks: Some(9), + }; + assert_eq!(req.resolved_limit(), 9); + + let req_limit_only = QueryNamespaceRequest { + namespace: "n".into(), + query: "q".into(), + include_references: None, + document_ids: None, + limit: Some(3), + max_chunks: None, + }; + assert_eq!(req_limit_only.resolved_limit(), 3); + + let req_none = QueryNamespaceRequest { + namespace: "n".into(), + query: "q".into(), + include_references: None, + document_ids: None, + limit: None, + max_chunks: None, + }; + assert_eq!(req_none.resolved_limit(), 10); +} + +#[test] +fn recall_context_resolved_limit_prefers_max_chunks_then_limit_then_default() { + let req = RecallContextRequest { + namespace: "n".into(), + include_references: None, + limit: Some(3), + max_chunks: Some(9), + }; + assert_eq!(req.resolved_limit(), 9); + + let req_limit_only = RecallContextRequest { + namespace: "n".into(), + include_references: None, + limit: Some(3), + max_chunks: None, + }; + assert_eq!(req_limit_only.resolved_limit(), 3); + + let req_none = RecallContextRequest { + namespace: "n".into(), + include_references: None, + limit: None, + max_chunks: None, + }; + assert_eq!(req_none.resolved_limit(), 10); +} + +// ── deny_unknown_fields enforcement ─────────────────────────── + +#[test] +fn query_namespace_request_rejects_unknown_fields() { + let err = serde_json::from_value::(json!({ + "namespace": "n", + "query": "q", + "bogus": 1 + })) + .unwrap_err(); + assert!(err.to_string().contains("bogus")); +} + +#[test] +fn recall_context_request_rejects_unknown_fields() { + let err = serde_json::from_value::(json!({ + "namespace": "n", + "bogus": true + })) + .unwrap_err(); + assert!(err.to_string().contains("bogus")); +} + +#[test] +fn empty_request_rejects_any_field() { + let err = serde_json::from_value::(json!({"x": 1})).unwrap_err(); + assert!(err.to_string().contains("x")); + serde_json::from_value::(json!({})).unwrap(); +} + +// ── MemoryInitRequest tolerates backwards-compatible jwt_token ──── + +#[test] +fn memory_init_request_jwt_token_is_optional_and_ignored() { + let without: MemoryInitRequest = serde_json::from_value(json!({})).unwrap(); + assert_eq!(without.jwt_token, None); + let with: MemoryInitRequest = serde_json::from_value(json!({"jwt_token": "abc"})).unwrap(); + assert_eq!(with.jwt_token.as_deref(), Some("abc")); +} + +// ── ApiError / ApiMeta / ApiEnvelope round-trip ────────────── + +#[test] +fn api_error_round_trips_with_optional_details() { + let err = ApiError { + code: "E".into(), + message: "boom".into(), + details: Some(json!({"why": "reason"})), + }; + let s = serde_json::to_string(&err).unwrap(); + let back: ApiError = serde_json::from_str(&s).unwrap(); + assert_eq!(back.code, "E"); + assert_eq!(back.message, "boom"); + assert!(back.details.is_some()); +} + +#[test] +fn api_error_without_details_omits_field_when_serialized() { + let err = ApiError { + code: "E".into(), + message: "boom".into(), + details: None, + }; + let s = serde_json::to_string(&err).unwrap(); + assert!(!s.contains("details"), "got: {s}"); +} + +#[test] +fn api_envelope_round_trip_preserves_data_and_meta() { + let env = ApiEnvelope:: { + data: Some(42), + error: None, + meta: ApiMeta { + request_id: "r1".into(), + latency_seconds: Some(0.5), + cached: Some(false), + counts: None, + pagination: Some(PaginationMeta { + limit: 10, + offset: 0, + count: 1, + }), + }, + }; + let s = serde_json::to_string(&env).unwrap(); + let back: ApiEnvelope = serde_json::from_str(&s).unwrap(); + assert_eq!(back.data, Some(42)); + assert!(back.error.is_none()); + assert_eq!(back.meta.pagination.unwrap().count, 1); +} + +#[test] +fn default_memory_relative_dir_is_memory() { + // Empty string == the memory root itself (`/memory`). + assert_eq!(super::default_memory_relative_dir(), ""); +} diff --git a/src/openhuman/memory/store_golden.rs b/src/openhuman/memory/store_golden.rs index f3f1a9c226..47e1d82788 100644 --- a/src/openhuman/memory/store_golden.rs +++ b/src/openhuman/memory/store_golden.rs @@ -48,7 +48,7 @@ use crate::openhuman::memory::ops::{ doc_list, doc_put, graph_query, graph_upsert, kv_get, memory_query_namespace, GraphQueryParams, GraphUpsertParams, KvGetDeleteParams, KvSetParams, NamespaceOnlyParams, PutDocParams, }; -use tinymemory_core::rpc_models::QueryNamespaceRequest; +use crate::openhuman::memory::rpc_models::QueryNamespaceRequest; use tinymemory_core::store::chunks; use tinymemory_core::store::chunks::types::{Chunk, Metadata, SourceKind, SourceRef}; use tinymemory_core::store::namespace_store::{events, fts5, profile, segments}; diff --git a/src/openhuman/memory/tools/store.rs b/src/openhuman/memory/tools/store.rs index 58edc65ad8..ccad6085cf 100644 --- a/src/openhuman/memory/tools/store.rs +++ b/src/openhuman/memory/tools/store.rs @@ -146,9 +146,14 @@ mod tests { use tempfile::TempDir; use tinymemory_core::store::UnifiedMemory; - // The read-back below goes through the engine handle directly, so its - // entries carry the *engine's* category type, not the contract's. - use tinymemory_core::MemoryCategory as EngineMemoryCategory; + // The read-back below goes through the engine handle directly, but the + // category it hands back is the CONTRACT's type: `tinymemory-core` merely + // re-exports `tinymemory_api::traits::MemoryCategory` (issue #18 §A1 moved + // the memory value types onto the contract precisely so a second engine + // could be bound without translating). Naming the contract here rather + // than the engine keeps that true at the import as well as the type, and + // is one fewer reference holding the crate in the build (#5560). + use tinymemory_api::types::MemoryCategory as EngineMemoryCategory; fn test_security() -> Arc { Arc::new(SecurityPolicy::default()) diff --git a/tests/memory_golden_parity_e2e.rs b/tests/memory_golden_parity_e2e.rs index f1b235a308..66474d0372 100644 --- a/tests/memory_golden_parity_e2e.rs +++ b/tests/memory_golden_parity_e2e.rs @@ -70,7 +70,7 @@ use openhuman_core::openhuman::memory::ops::{ doc_put, kv_get, kv_set, memory_recall_context, memory_recall_memories, KvGetDeleteParams, KvSetParams, PutDocParams, }; -use tinymemory_core::rpc_models::{RecallContextRequest, RecallMemoriesRequest}; +use openhuman_core::openhuman::memory::rpc_models::{RecallContextRequest, RecallMemoriesRequest}; use tinymemory_core::tinycortex::memory_config_from; // ── Env isolation (mirrors memory_roundtrip_e2e) ───────────────────────────── diff --git a/tests/memory_roundtrip_e2e.rs b/tests/memory_roundtrip_e2e.rs index b176456b12..6c7499fce7 100644 --- a/tests/memory_roundtrip_e2e.rs +++ b/tests/memory_roundtrip_e2e.rs @@ -19,7 +19,7 @@ use openhuman_core::openhuman::memory::ops::{ clear_namespace, doc_put, memory_recall_context, memory_recall_memories, ClearNamespaceParams, PutDocParams, }; -use tinymemory_core::rpc_models::{RecallContextRequest, RecallMemoriesRequest}; +use openhuman_core::openhuman::memory::rpc_models::{RecallContextRequest, RecallMemoriesRequest}; // ── Env isolation ────────────────────────────────────────────────────