From 9836f679a8c9c01fcda8b658c090dc8d0c27c162 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Sun, 9 Aug 2026 11:50:28 +0300 Subject: [PATCH 01/20] style(core): apply cargo fmt to the unformatted merge base Co-authored-by: Medulla --- src/core/bus.rs | 18 +-- src/core/jsonrpc.rs | 56 ++++---- src/core/mod.rs | 4 +- src/openhuman/agent/artifacts/store_tests.rs | 4 +- src/openhuman/agent/bus.rs | 28 ++-- .../agent/harness/session/runtime_tests.rs | 17 ++- .../agent/harness/session/turn/tools.rs | 4 +- .../agent/learning/extract/signature.rs | 4 +- .../orchestration/run_ledger_finalize.rs | 2 +- .../run_ledger_finalize_tests.rs | 2 +- src/openhuman/agent/tinyagents/tools.rs | 24 ++-- src/openhuman/agent/triage/escalation.rs | 2 +- src/openhuman/agent/triage/evaluator.rs | 11 +- src/openhuman/agent/triage/events.rs | 19 +-- src/openhuman/channels/bus.rs | 2 +- src/openhuman/channels/host/adapters.rs | 2 +- src/openhuman/channels/proactive.rs | 8 +- .../providers/telegram/approval_surface.rs | 2 +- .../telegram/approval_surface_tests.rs | 2 +- .../channels/providers/telegram/bus.rs | 2 +- .../channels/providers/telegram/bus_tests.rs | 2 +- src/openhuman/channels/routes_tests.rs | 2 +- .../channels/runtime/dispatch/processor.rs | 40 +++--- .../channels/runtime/test_support.rs | 124 +++++++++--------- src/openhuman/channels/tests/health.rs | 9 +- .../channels/tests/runtime_dispatch.rs | 3 +- src/openhuman/config/ops/agent.rs | 8 +- src/openhuman/cron/bus.rs | 2 +- src/openhuman/cron/scheduler_tests.rs | 6 +- src/openhuman/desktop/notifications/bus.rs | 8 +- src/openhuman/flows/bus.rs | 2 +- src/openhuman/flows/ops.rs | 24 ++-- src/openhuman/flows/ops_tests.rs | 8 +- .../inference/provider/factory_tests.rs | 12 +- .../provider/openhuman_backend_model.rs | 18 ++- .../inference/provider/ops/http_error.rs | 16 +-- src/openhuman/inference/provider/ops_tests.rs | 7 +- .../integrations/composio/ops/direct_mode.rs | 20 ++- .../integrations/task_sources/bus.rs | 2 +- src/openhuman/meet/backend_bot/calendar.rs | 4 +- src/openhuman/memory/conversations/bus.rs | 6 +- src/openhuman/memory/diff/ops.rs | 26 ++-- src/openhuman/memory/global.rs | 5 +- src/openhuman/memory/ops/sync.rs | 23 ++-- src/openhuman/memory/sync/composio/bus.rs | 12 +- src/openhuman/memory/sync_events.rs | 30 +++-- .../memory/sync_pipeline_e2e_tests.rs | 4 +- src/openhuman/memory/tinycortex/sync.rs | 18 ++- src/openhuman/memory/tree/tree_runtime/bus.rs | 2 +- src/openhuman/security/approval/gate.rs | 5 +- src/openhuman/security/credentials/bus.rs | 2 +- .../security/credentials/session_support.rs | 13 +- src/openhuman/security/devices/bus.rs | 4 +- src/openhuman/security/egress/emit_tests.rs | 8 +- .../security/keyring_consent/policy.rs | 19 +-- src/openhuman/skills/bus.rs | 4 +- src/openhuman/skills/ops_create.rs | 4 +- src/openhuman/skills/webhooks/bus.rs | 2 +- src/openhuman/voice/bus.rs | 4 +- src/openhuman/web_chat/event_bus.rs | 2 +- tests/agent_harness_e2e.rs | 4 +- tests/calendar_grounding_e2e.rs | 2 +- ...io_list_tools_stack_overflow_regression.rs | 2 +- .../config_auth_app_state_connectivity_e2e.rs | 2 +- tests/json_rpc_e2e.rs | 39 +++--- tests/monitor_agent_e2e.rs | 2 +- tests/subconscious_conversation_e2e.rs | 33 ++--- tests/subconscious_fullstack_e2e.rs | 2 +- tests/subconscious_triggers_e2e.rs | 4 +- 69 files changed, 391 insertions(+), 422 deletions(-) diff --git a/src/core/bus.rs b/src/core/bus.rs index cd3057eb41..d03e9c47fa 100644 --- a/src/core/bus.rs +++ b/src/core/bus.rs @@ -80,13 +80,9 @@ pub fn manifest() -> PeerManifest { .expect("the events interface constant is valid"); PeerManifest::new("openhuman") .version( - Version::parse(env!("CARGO_PKG_VERSION")) - .unwrap_or_else(|_| Version::new(0, 0, 0)), + Version::parse(env!("CARGO_PKG_VERSION")).unwrap_or_else(|_| Version::new(0, 0, 0)), ) - .provides(InterfaceVersion::provided( - interface, - EVENTS_VERSION, - )) + .provides(InterfaceVersion::provided(interface, EVENTS_VERSION)) .consumes(InterfaceVersion::consumed( EVENTS_INTERFACE .try_into() @@ -183,8 +179,14 @@ mod tests { fn the_manifest_declares_the_catalog_in_both_directions() { let manifest = manifest(); let interface = EVENTS_INTERFACE.try_into().unwrap(); - assert!(manifest.provided(&interface).is_some(), "openhuman publishes"); - assert!(manifest.consumed(&interface).is_some(), "openhuman subscribes"); + assert!( + manifest.provided(&interface).is_some(), + "openhuman publishes" + ); + assert!( + manifest.consumed(&interface).is_some(), + "openhuman subscribes" + ); } #[tokio::test] diff --git a/src/core/jsonrpc.rs b/src/core/jsonrpc.rs index bdd32e22dc..665775a631 100644 --- a/src/core/jsonrpc.rs +++ b/src/core/jsonrpc.rs @@ -298,12 +298,10 @@ pub async fn invoke_method(state: AppState, method: &str, params: Value) -> Resu // `scheduler_gate::set_signed_out(false)`. Duplicating that check // here would pull a domain concern into the transport layer and would // add an extra config-load round-trip on every 401. - crate::core::bus::BUS.publish( - crate::core::events::DomainEvent::SessionExpired { - source: format!("jsonrpc.invoke_method:{method}"), - reason: sanitized_reason, - }, - ); + crate::core::bus::BUS.publish(crate::core::events::DomainEvent::SessionExpired { + source: format!("jsonrpc.invoke_method:{method}"), + reason: sanitized_reason, + }); } else if is_unconfirmed_unauthorized_error(msg) { log::info!( "[jsonrpc] unconfirmed unauthorized error for method='{}' (not session expiry) — leaving session intact: {}", @@ -1663,25 +1661,23 @@ async fn domain_events_handler(headers: axum::http::HeaderMap) -> Response { .await .map(|event| (Ok::<_, std::convert::Infallible>(event), rx)) }) - .filter_map( - |item| -> Option> { - let event = match item { - Ok(ev) => ev, - Err(_) => return None, - }; - let domain = event.domain().to_string(); - let event_name = event.variant_name(); - let agent = event.agent_hint().unwrap_or("").to_string(); - let data = json!({ - "domain": domain, - "event": event_name, - "agent": agent, - "timestamp": chrono::Utc::now().format("%H:%M:%S").to_string(), - }); - let data_str = serde_json::to_string(&data).ok()?; - Some(Ok(Event::default().event(domain).data(data_str))) - }, - ); + .filter_map(|item| -> Option> { + let event = match item { + Ok(ev) => ev, + Err(_) => return None, + }; + let domain = event.domain().to_string(); + let event_name = event.variant_name(); + let agent = event.agent_hint().unwrap_or("").to_string(); + let data = json!({ + "domain": domain, + "event": event_name, + "agent": agent, + "timestamp": chrono::Utc::now().format("%H:%M:%S").to_string(), + }); + let data_str = serde_json::to_string(&data).ok()?; + Some(Ok(Event::default().event(domain).data(data_str))) + }); let config_stream = futures::stream::once(async move { Ok::<_, std::convert::Infallible>(config_event) }); @@ -2660,12 +2656,10 @@ pub async fn bootstrap_core_runtime( Prompt-class external-effect tool calls run unprompted", host_kind.tag() ); - crate::core::bus::BUS.publish( - crate::core::events::DomainEvent::ApprovalGateDisabled { - host: host_kind.tag().to_string(), - reason: "env-override".to_string(), - }, - ); + crate::core::bus::BUS.publish(crate::core::events::DomainEvent::ApprovalGateDisabled { + host: host_kind.tag().to_string(), + reason: "env-override".to_string(), + }); } // Artifact surface bridges DomainEvent::ArtifactReady/Failed onto the web // channel ("Files in this chat" panel + ArtifactCard updates). This is diff --git a/src/core/mod.rs b/src/core/mod.rs index f74fa67983..baff34bf02 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -9,12 +9,12 @@ use serde::Serialize; pub mod agent_cli; pub mod all; pub mod auth; +pub mod bus; +pub mod bus_testing; pub mod cli; pub mod cli_capability; pub mod dispatch; pub mod event_bind_tokens; -pub mod bus; -pub mod bus_testing; pub mod events; // Ungated compile-time marker for the `http-server` gate (#5048) — the desktop // shell asserts `HTTP_SERVER_COMPILED_IN` so a listener-less core fails the diff --git a/src/openhuman/agent/artifacts/store_tests.rs b/src/openhuman/agent/artifacts/store_tests.rs index 55b565c1bf..26d4bbb43a 100644 --- a/src/openhuman/agent/artifacts/store_tests.rs +++ b/src/openhuman/agent/artifacts/store_tests.rs @@ -196,10 +196,10 @@ async fn validate_artifact_id_rejects_slashes() { use crate::core::bus::BUS; use crate::core::events::DomainEvent; -use tinybus::EventHandler; -use tinybus::SubscriptionHandle; use async_trait::async_trait; use std::sync::{Arc, Mutex as StdMutex}; +use tinybus::EventHandler; +use tinybus::SubscriptionHandle; #[derive(Clone)] struct PendingCollector { diff --git a/src/openhuman/agent/bus.rs b/src/openhuman/agent/bus.rs index 4980db30e5..6c2465afcc 100644 --- a/src/openhuman/agent/bus.rs +++ b/src/openhuman/agent/bus.rs @@ -373,9 +373,8 @@ async fn handle_agent_run_turn_on_large_stack( /// allowing any part of the system to request an agentic turn without /// depending directly on the agent harness. pub fn register_agent_handlers() { - BUS.native().register::( - AGENT_RUN_TURN_METHOD, - |req| { + BUS.native() + .register::(AGENT_RUN_TURN_METHOD, |req| { #[cfg(test)] { handle_agent_run_turn_on_large_stack(req) @@ -384,8 +383,7 @@ pub fn register_agent_handlers() { { handle_agent_run_turn(req) } - }, - ); + }); tracing::debug!("[agent::bus] registered native handler `{AGENT_RUN_TURN_METHOD}`"); } @@ -434,20 +432,16 @@ pub fn register_agent_handlers() { /// } /// ``` #[cfg(test)] -pub async fn mock_agent_run_turn( - handler: F, -) -> crate::core::bus_testing::MockBusGuard +pub async fn mock_agent_run_turn(handler: F) -> crate::core::bus_testing::MockBusGuard where F: Fn(AgentTurnRequest) -> Fut + Send + Sync + 'static, Fut: std::future::Future> + Send + 'static, { - crate::core::bus_testing::mock_bus_stub::< - AgentTurnRequest, - AgentTurnResponse, - F, - Fut, - _, - >(AGENT_RUN_TURN_METHOD, handler, || register_agent_handlers()) + crate::core::bus_testing::mock_bus_stub::( + AGENT_RUN_TURN_METHOD, + handler, + || register_agent_handlers(), + ) .await } @@ -461,9 +455,7 @@ where /// handler with a stub, use [`mock_agent_run_turn`] instead. #[cfg(test)] pub async fn use_real_agent_handler() -> tokio::sync::MutexGuard<'static, ()> { - let guard = crate::core::bus_testing::BUS_HANDLER_LOCK - .lock() - .await; + let guard = crate::core::bus_testing::BUS_HANDLER_LOCK.lock().await; register_agent_handlers(); guard } diff --git a/src/openhuman/agent/harness/session/runtime_tests.rs b/src/openhuman/agent/harness/session/runtime_tests.rs index d3dd0c0ae4..f0773579c8 100644 --- a/src/openhuman/agent/harness/session/runtime_tests.rs +++ b/src/openhuman/agent/harness/session/runtime_tests.rs @@ -258,13 +258,16 @@ async fn run_single_publishes_completed_and_error_events() { crate::core::bus::init().await.expect("bus init"); let events = Arc::new(AsyncMutex::new(Vec::::new())); let events_handler = Arc::clone(&events); - let _handle = crate::core::bus::BUS.get().unwrap().on("runtime-events-test", move |event| { - let events = Arc::clone(&events_handler); - let cloned = event.clone(); - Box::pin(async move { - events.lock().await.push(cloned); - }) - }); + let _handle = crate::core::bus::BUS + .get() + .unwrap() + .on("runtime-events-test", move |event| { + let events = Arc::clone(&events_handler); + let cloned = event.clone(); + Box::pin(async move { + events.lock().await.push(cloned); + }) + }); let ok_provider: Arc> = Arc::new(StaticModel { response: Mutex::new(Some(Ok(ChatResponse { diff --git a/src/openhuman/agent/harness/session/turn/tools.rs b/src/openhuman/agent/harness/session/turn/tools.rs index fd325370a7..ef219726f9 100644 --- a/src/openhuman/agent/harness/session/turn/tools.rs +++ b/src/openhuman/agent/harness/session/turn/tools.rs @@ -165,9 +165,7 @@ impl Agent { let mut closed = false; loop { match rx.try_recv() { - Ok(crate::core::events::DomainEvent::ComposioIntegrationsChanged { - toolkits, - }) => { + Ok(crate::core::events::DomainEvent::ComposioIntegrationsChanged { toolkits }) => { saw_signal = true; log::info!( "[agent_loop] received composio integrations changed event (active_toolkits={:?})", diff --git a/src/openhuman/agent/learning/extract/signature.rs b/src/openhuman/agent/learning/extract/signature.rs index 73d9831248..2277386457 100644 --- a/src/openhuman/agent/learning/extract/signature.rs +++ b/src/openhuman/agent/learning/extract/signature.rs @@ -24,11 +24,11 @@ use async_trait::async_trait; use crate::core::bus::BUS; use crate::core::events::DomainEvent; -use tinybus::EventHandler; -use tinybus::SubscriptionHandle; use crate::openhuman::agent::learning::candidate::{ self, Buffer, CueFamily, EvidenceRef, FacetClass, LearningCandidate, }; +use tinybus::EventHandler; +use tinybus::SubscriptionHandle; // ── Constants ──────────────────────────────────────────────────────────────── diff --git a/src/openhuman/agent/orchestration/run_ledger_finalize.rs b/src/openhuman/agent/orchestration/run_ledger_finalize.rs index 334496c22c..2a85be89cc 100644 --- a/src/openhuman/agent/orchestration/run_ledger_finalize.rs +++ b/src/openhuman/agent/orchestration/run_ledger_finalize.rs @@ -31,9 +31,9 @@ use async_trait::async_trait; use crate::core::bus::BUS; use crate::core::events::DomainEvent; -use tinybus::EventHandler; use crate::openhuman::config::Config; use tinyagents::session::run_ledger::{transition_agent_run_status, AgentRunStatus}; +use tinybus::EventHandler; const LOG_PREFIX: &str = "[run_ledger][finalize]"; diff --git a/src/openhuman/agent/orchestration/run_ledger_finalize_tests.rs b/src/openhuman/agent/orchestration/run_ledger_finalize_tests.rs index 16eec16dba..fe961da5a2 100644 --- a/src/openhuman/agent/orchestration/run_ledger_finalize_tests.rs +++ b/src/openhuman/agent/orchestration/run_ledger_finalize_tests.rs @@ -5,10 +5,10 @@ use super::*; use serde_json::json; use tempfile::TempDir; -use tinybus::EventHandler; use tinyagents::session::run_ledger::{ get_agent_run, upsert_agent_run, AgentRunKind, AgentRunStatus, AgentRunUpsert, }; +use tinybus::EventHandler; fn test_config(dir: &TempDir) -> Config { let mut config = Config::default(); diff --git a/src/openhuman/agent/tinyagents/tools.rs b/src/openhuman/agent/tinyagents/tools.rs index bae233ad29..cb795d701f 100644 --- a/src/openhuman/agent/tinyagents/tools.rs +++ b/src/openhuman/agent/tinyagents/tools.rs @@ -210,12 +210,10 @@ pub(crate) async fn execute_openhuman_tool( // the `TaToolResult` below. let started = std::time::Instant::now(); let tool_name = call.name.clone(); - crate::core::bus::BUS.publish( - crate::core::events::DomainEvent::ToolExecutionStarted { - tool_name: tool_name.clone(), - session_id: TINYAGENTS_TOOL_SESSION.to_string(), - }, - ); + crate::core::bus::BUS.publish(crate::core::events::DomainEvent::ToolExecutionStarted { + tool_name: tool_name.clone(), + session_id: TINYAGENTS_TOOL_SESSION.to_string(), + }); // Approval (HITL) now runs in `ApprovalSecurityMiddleware` // (`tinyagents/middleware.rs`, a `wrap_tool` middleware) so a denial @@ -304,14 +302,12 @@ pub(crate) async fn execute_openhuman_tool( // Terminal per-tool telemetry (#4467, item 5): success is derived from the // rendered result's error channel so a tool-reported error surfaces as a // failed completion, mirroring the node-runtime bridge. - crate::core::bus::BUS.publish( - crate::core::events::DomainEvent::ToolExecutionCompleted { - tool_name, - session_id: TINYAGENTS_TOOL_SESSION.to_string(), - success: result.error.is_none(), - elapsed_ms, - }, - ); + crate::core::bus::BUS.publish(crate::core::events::DomainEvent::ToolExecutionCompleted { + tool_name, + session_id: TINYAGENTS_TOOL_SESSION.to_string(), + success: result.error.is_none(), + elapsed_ms, + }); result } diff --git a/src/openhuman/agent/triage/escalation.rs b/src/openhuman/agent/triage/escalation.rs index dca3faf97e..ef4234c432 100644 --- a/src/openhuman/agent/triage/escalation.rs +++ b/src/openhuman/agent/triage/escalation.rs @@ -385,7 +385,7 @@ async fn gate_linked_card_terminal(envelope: &TriggerEnvelope, decision: &str) { mod tests { use super::*; use crate::core::bus::BUS; -use crate::core::events::DomainEvent; + use crate::core::events::DomainEvent; use crate::openhuman::agent::harness::definition::AgentDefinitionRegistry; use serde_json::json; use tokio::time::{sleep, timeout, Duration}; diff --git a/src/openhuman/agent/triage/evaluator.rs b/src/openhuman/agent/triage/evaluator.rs index 255cd52796..43d9210928 100644 --- a/src/openhuman/agent/triage/evaluator.rs +++ b/src/openhuman/agent/triage/evaluator.rs @@ -35,7 +35,6 @@ use std::time::{Duration, Instant}; use anyhow::{anyhow, Context}; use crate::core::bus::BUS; -use tinybus::NativeRequestError; use crate::openhuman::agent::bus::{AgentTurnRequest, AgentTurnResponse, AGENT_RUN_TURN_METHOD}; use crate::openhuman::agent::harness::definition::{AgentDefinition, PromptSource}; use crate::openhuman::agent::harness::AgentDefinitionRegistry; @@ -46,6 +45,7 @@ use crate::openhuman::cron::scheduler_gate::LlmPermit; use crate::openhuman::inference::provider::error_classify::{ is_rate_limited, is_upstream_unhealthy, parse_retry_after_ms, }; +use tinybus::NativeRequestError; use super::decision::{parse_triage_decision, ParseError, TriageDecision}; use super::envelope::TriggerEnvelope; @@ -508,11 +508,10 @@ async fn try_arm( }, }; - let response = match BUS.native().request::( - AGENT_RUN_TURN_METHOD, - request, - ) - .await + let response = match BUS + .native() + .request::(AGENT_RUN_TURN_METHOD, request) + .await { Ok(r) => r, Err(err) => { diff --git a/src/openhuman/agent/triage/events.rs b/src/openhuman/agent/triage/events.rs index 5de795b0dc..fbfb0cf32f 100644 --- a/src/openhuman/agent/triage/events.rs +++ b/src/openhuman/agent/triage/events.rs @@ -109,7 +109,7 @@ pub fn publish_failed(envelope: &TriggerEnvelope, reason: &str) { mod tests { use super::*; use crate::core::bus::BUS; -use crate::core::events::DomainEvent; + use crate::core::events::DomainEvent; use crate::openhuman::agent::triage::TriggerEnvelope; use serde_json::json; use std::sync::Arc; @@ -121,13 +121,16 @@ use crate::core::events::DomainEvent; crate::core::bus::init().await.expect("bus init"); let seen = Arc::new(Mutex::new(Vec::::new())); let seen_handler = Arc::clone(&seen); - let _handle = crate::core::bus::BUS.get().unwrap().on("triage-events-test", move |event| { - let seen = Arc::clone(&seen_handler); - let cloned = event.clone(); - Box::pin(async move { - seen.lock().await.push(cloned); - }) - }); + let _handle = crate::core::bus::BUS + .get() + .unwrap() + .on("triage-events-test", move |event| { + let seen = Arc::clone(&seen_handler); + let cloned = event.clone(); + Box::pin(async move { + seen.lock().await.push(cloned); + }) + }); let envelope = TriggerEnvelope::from_composio( "gmail", diff --git a/src/openhuman/channels/bus.rs b/src/openhuman/channels/bus.rs index 65d390492a..a3756b9fa3 100644 --- a/src/openhuman/channels/bus.rs +++ b/src/openhuman/channels/bus.rs @@ -5,9 +5,9 @@ //! channel provider and sends the reply back through the REST API. use crate::core::events::DomainEvent; -use tinybus::EventHandler; use async_trait::async_trait; use serde_json::{json, Value}; +use tinybus::EventHandler; /// Subscribes to `ChannelInboundMessage` events and runs the agent loop, /// sending replies back to the originating channel via the backend REST API. diff --git a/src/openhuman/channels/host/adapters.rs b/src/openhuman/channels/host/adapters.rs index 381018ed50..4b5b7f5600 100644 --- a/src/openhuman/channels/host/adapters.rs +++ b/src/openhuman/channels/host/adapters.rs @@ -357,7 +357,7 @@ impl EventSink for OpenHumanEventSink { } "channel" => { use crate::core::bus::BUS; -use crate::core::events::DomainEvent; + use crate::core::events::DomainEvent; let event = match kind { "reaction_received" => DomainEvent::ChannelReactionReceived { channel: json_str(&payload, "channel"), diff --git a/src/openhuman/channels/proactive.rs b/src/openhuman/channels/proactive.rs index 62854e1225..b13b95fe44 100644 --- a/src/openhuman/channels/proactive.rs +++ b/src/openhuman/channels/proactive.rs @@ -20,13 +20,13 @@ //! exist. use crate::core::events::DomainEvent; -use tinybus::EventHandler; use crate::core::socketio::WebChannelEvent; use crate::openhuman::channels::{Channel, ChannelSendExt, SendMessage}; use crate::openhuman::web_chat::publish_web_channel_event; use async_trait::async_trait; use std::collections::HashMap; use std::sync::{Arc, RwLock}; +use tinybus::EventHandler; #[cfg(not(test))] fn proactive_approval_gate() -> Option> { @@ -46,9 +46,9 @@ pub fn register_web_only_proactive_subscriber() { use std::sync::Once; static REGISTERED: Once = Once::new(); REGISTERED.call_once(|| { - if let Some(handle) = crate::core::bus::BUS.subscribe(Arc::new( - ProactiveMessageSubscriber::web_only(), - )) { + if let Some(handle) = + crate::core::bus::BUS.subscribe(Arc::new(ProactiveMessageSubscriber::web_only())) + { std::mem::forget(handle); tracing::debug!("[proactive] web-only subscriber registered"); } else { diff --git a/src/openhuman/channels/providers/telegram/approval_surface.rs b/src/openhuman/channels/providers/telegram/approval_surface.rs index e0539bcb99..053f7cfd1d 100644 --- a/src/openhuman/channels/providers/telegram/approval_surface.rs +++ b/src/openhuman/channels/providers/telegram/approval_surface.rs @@ -36,12 +36,12 @@ //! [`parse_approval_reply`]: crate::openhuman::security::approval::parse_approval_reply use crate::core::events::DomainEvent; -use tinybus::EventHandler; use crate::openhuman::channels::traits::{ChannelSendExt, SendMessage}; use crate::openhuman::channels::Channel; use async_trait::async_trait; use std::collections::HashMap; use std::sync::{Arc, Mutex}; +use tinybus::EventHandler; const LOG_PREFIX: &str = "[telegram-approval]"; diff --git a/src/openhuman/channels/providers/telegram/approval_surface_tests.rs b/src/openhuman/channels/providers/telegram/approval_surface_tests.rs index 81e211fcb0..2a2cc9cf4d 100644 --- a/src/openhuman/channels/providers/telegram/approval_surface_tests.rs +++ b/src/openhuman/channels/providers/telegram/approval_surface_tests.rs @@ -1,13 +1,13 @@ //! Tests for the Telegram approval-surface subscriber. use super::*; -use tinybus::EventHandler; use crate::openhuman::channels::traits::{ChannelMessage, SendMessage}; use crate::openhuman::channels::Channel; use async_trait::async_trait; use std::collections::HashMap; use std::path::PathBuf; use std::sync::Mutex as StdMutex; +use tinybus::EventHandler; /// Mock channel that records every outbound `send()` call. Used in place /// of the real `TelegramChannel` so tests can assert what the subscriber diff --git a/src/openhuman/channels/providers/telegram/bus.rs b/src/openhuman/channels/providers/telegram/bus.rs index bde2fc6b45..d2b767fc14 100644 --- a/src/openhuman/channels/providers/telegram/bus.rs +++ b/src/openhuman/channels/providers/telegram/bus.rs @@ -1,10 +1,10 @@ //! Event-bus subscriber for Telegram remote-control lifecycle signals. use crate::core::events::DomainEvent; -use tinybus::EventHandler; use crate::openhuman::channels::providers::telegram::session_store::with_store; use async_trait::async_trait; use std::path::PathBuf; +use tinybus::EventHandler; const LOG_PREFIX: &str = "[telegram-remote]"; diff --git a/src/openhuman/channels/providers/telegram/bus_tests.rs b/src/openhuman/channels/providers/telegram/bus_tests.rs index 32b774f6e9..e2802b3923 100644 --- a/src/openhuman/channels/providers/telegram/bus_tests.rs +++ b/src/openhuman/channels/providers/telegram/bus_tests.rs @@ -1,7 +1,7 @@ use super::bus::TelegramRemoteSubscriber; use crate::core::events::DomainEvent; -use tinybus::EventHandler; use tempfile::tempdir; +use tinybus::EventHandler; #[tokio::test] async fn subscriber_marks_busy_on_received_and_clears_on_processed() { diff --git a/src/openhuman/channels/routes_tests.rs b/src/openhuman/channels/routes_tests.rs index a66e322def..288202e980 100644 --- a/src/openhuman/channels/routes_tests.rs +++ b/src/openhuman/channels/routes_tests.rs @@ -1,6 +1,5 @@ use super::*; use crate::core::events::DomainEvent; -use tinybus::EventHandler; use crate::openhuman::agent::messages::ChatMessage; use crate::openhuman::channels::context::{ ChannelRuntimeContext, RouteSelectionMap, TurnModelSourceCacheMap, @@ -13,6 +12,7 @@ use async_trait::async_trait; use std::collections::HashMap; use std::path::PathBuf; use std::sync::{Arc, Mutex}; +use tinybus::EventHandler; struct DummyMemory; diff --git a/src/openhuman/channels/runtime/dispatch/processor.rs b/src/openhuman/channels/runtime/dispatch/processor.rs index 84ec82a75e..370b490976 100644 --- a/src/openhuman/channels/runtime/dispatch/processor.rs +++ b/src/openhuman/channels/runtime/dispatch/processor.rs @@ -12,7 +12,6 @@ use crate::core::bus::BUS; use crate::core::events::DomainEvent; -use tinybus::NativeRequestError; use crate::openhuman::agent::bus::{AgentTurnRequest, AgentTurnResponse, AGENT_RUN_TURN_METHOD}; use crate::openhuman::agent::messages::ChatMessage; use crate::openhuman::agent::progress::AgentProgress; @@ -30,6 +29,7 @@ use crate::openhuman::inference::provider; use crate::openhuman::util::truncate_with_ellipsis; use std::sync::Arc; use std::time::{Duration, Instant}; +use tinybus::NativeRequestError; use tokio_util::sync::CancellationToken; use super::helpers::{ @@ -519,26 +519,24 @@ pub(crate) async fn process_channel_runtime_message( "[channels::dispatch] dispatching {AGENT_RUN_TURN_METHOD} via native bus" ); let agent_call = async { - BUS.native().request::( - AGENT_RUN_TURN_METHOD, - turn_request, - ) - .await - .map_err(|err| match err { - // Unwrap handler-returned errors so the underlying - // message (e.g. "Agent exceeded maximum tool iterations") - // flows through without being wrapped in bus-transport - // layer prose. The error-formatting path downstream - // treats this `anyhow::Error` the same way it did before - // the bus migration. - NativeRequestError::HandlerFailed { message, .. } => { - anyhow::anyhow!(message) - } - // Bus-level errors (UnregisteredHandler / TypeMismatch / - // NotInitialized) surface with their full Display so - // startup wiring bugs are immediately obvious in logs. - other => anyhow::anyhow!("[agent.run_turn dispatch] {other}"), - }) + BUS.native() + .request::(AGENT_RUN_TURN_METHOD, turn_request) + .await + .map_err(|err| match err { + // Unwrap handler-returned errors so the underlying + // message (e.g. "Agent exceeded maximum tool iterations") + // flows through without being wrapped in bus-transport + // layer prose. The error-formatting path downstream + // treats this `anyhow::Error` the same way it did before + // the bus migration. + NativeRequestError::HandlerFailed { message, .. } => { + anyhow::anyhow!(message) + } + // Bus-level errors (UnregisteredHandler / TypeMismatch / + // NotInitialized) surface with their full Display so + // startup wiring bugs are immediately obvious in logs. + other => anyhow::anyhow!("[agent.run_turn dispatch] {other}"), + }) }; // Sub-issue 2 of #3098: scope the agent turn in an `ApprovalChatContext` // for channels that have a registered approval surface — currently diff --git a/src/openhuman/channels/runtime/test_support.rs b/src/openhuman/channels/runtime/test_support.rs index 05279307a5..568179f04a 100644 --- a/src/openhuman/channels/runtime/test_support.rs +++ b/src/openhuman/channels/runtime/test_support.rs @@ -298,9 +298,7 @@ fn memory_entry(input: TestMemoryEntry) -> MemoryEntry { /// `start_channels`) so concurrent registrations cannot race in the same /// process. pub async fn lock_agent_handler() -> tokio::sync::MutexGuard<'static, ()> { - crate::core::bus_testing::BUS_HANDLER_LOCK - .lock() - .await + crate::core::bus_testing::BUS_HANDLER_LOCK.lock().await } pub async fn run_dispatch_harness(options: DispatchHarnessOptions) -> DispatchHarnessObservation { @@ -312,7 +310,10 @@ pub async fn run_dispatch_harness(options: DispatchHarnessOptions) -> DispatchHa let _harness_guard = lock_agent_handler().await; crate::core::bus::init().await.expect("bus init"); - let mut event_rx = crate::core::bus::BUS.get().expect("bus initialised").receiver(); + let mut event_rx = crate::core::bus::BUS + .get() + .expect("bus initialised") + .receiver(); let _ = crate::openhuman::agent::harness::definition::AgentDefinitionRegistry::init_global_builtins( ); @@ -329,70 +330,71 @@ pub async fn run_dispatch_harness(options: DispatchHarnessOptions) -> DispatchHa let handler_error = options.handler_error.clone(); let handler_delay = Duration::from_millis(options.handler_delay_ms); - BUS.native().register::(AGENT_RUN_TURN_METHOD, { - let handler_roles = Arc::clone(&handler_roles); - let handler_text = Arc::clone(&handler_text); - let handler_provider = Arc::clone(&handler_provider); - let handler_channel = Arc::clone(&handler_channel); - let handler_progress = Arc::clone(&handler_progress); - move |req| { + BUS.native() + .register::(AGENT_RUN_TURN_METHOD, { let handler_roles = Arc::clone(&handler_roles); let handler_text = Arc::clone(&handler_text); let handler_provider = Arc::clone(&handler_provider); let handler_channel = Arc::clone(&handler_channel); let handler_progress = Arc::clone(&handler_progress); - let response_text = response_text.clone(); - let handler_error = handler_error.clone(); - async move { - *handler_roles.lock().expect("roles lock") = - req.history.iter().map(|msg| msg.role.clone()).collect(); - *handler_text.lock().expect("text lock") = req - .history - .iter() - .map(|msg| msg.content.as_str()) - .collect::>() - .join("\n---\n"); - *handler_provider.lock().expect("provider lock") = req.provider_name; - *handler_channel.lock().expect("channel lock") = req.channel_name; - - if let Some(tx) = req.on_progress { - handler_progress.fetch_add(1, Ordering::SeqCst); - let _ = tx.send(AgentProgress::TurnStarted).await; - let _ = tx - .send(AgentProgress::ThinkingDelta { - delta: "thinking".to_string(), - iteration: 1, - }) - .await; - let _ = tx - .send(AgentProgress::TextDelta { - delta: "partial ".to_string(), - iteration: 1, - }) - .await; - let _ = tx - .send(AgentProgress::ToolCallStarted { - call_id: "call-1".to_string(), - tool_name: "harness_tool".to_string(), - arguments: serde_json::json!({}), - iteration: 1, - display_label: None, - display_detail: None, - }) - .await; - } - - if !handler_delay.is_zero() { - tokio::time::sleep(handler_delay).await; - } - - match handler_error { - Some(message) => Err(message), - None => Ok(AgentTurnResponse::new(response_text)), + move |req| { + let handler_roles = Arc::clone(&handler_roles); + let handler_text = Arc::clone(&handler_text); + let handler_provider = Arc::clone(&handler_provider); + let handler_channel = Arc::clone(&handler_channel); + let handler_progress = Arc::clone(&handler_progress); + let response_text = response_text.clone(); + let handler_error = handler_error.clone(); + async move { + *handler_roles.lock().expect("roles lock") = + req.history.iter().map(|msg| msg.role.clone()).collect(); + *handler_text.lock().expect("text lock") = req + .history + .iter() + .map(|msg| msg.content.as_str()) + .collect::>() + .join("\n---\n"); + *handler_provider.lock().expect("provider lock") = req.provider_name; + *handler_channel.lock().expect("channel lock") = req.channel_name; + + if let Some(tx) = req.on_progress { + handler_progress.fetch_add(1, Ordering::SeqCst); + let _ = tx.send(AgentProgress::TurnStarted).await; + let _ = tx + .send(AgentProgress::ThinkingDelta { + delta: "thinking".to_string(), + iteration: 1, + }) + .await; + let _ = tx + .send(AgentProgress::TextDelta { + delta: "partial ".to_string(), + iteration: 1, + }) + .await; + let _ = tx + .send(AgentProgress::ToolCallStarted { + call_id: "call-1".to_string(), + tool_name: "harness_tool".to_string(), + arguments: serde_json::json!({}), + iteration: 1, + display_label: None, + display_detail: None, + }) + .await; + } + + if !handler_delay.is_zero() { + tokio::time::sleep(handler_delay).await; + } + + match handler_error { + Some(message) => Err(message), + None => Ok(AgentTurnResponse::new(response_text)), + } } } - } - }); + }); let state = Arc::new(HarnessState::default()); let channel_impl = Arc::new(HarnessChannel { diff --git a/src/openhuman/channels/tests/health.rs b/src/openhuman/channels/tests/health.rs index fd24281ee5..48d5142aab 100644 --- a/src/openhuman/channels/tests/health.rs +++ b/src/openhuman/channels/tests/health.rs @@ -42,10 +42,11 @@ async fn supervised_listener_marks_error_and_restarts_on_failures() { // The global health subscriber may have been registered by another test // runtime; keep a fresh subscriber alive for this test's runtime too. crate::core::bus::init().await.expect("bus init"); - let _health_handle = crate::core::bus::BUS.subscribe(Arc::new( - crate::openhuman::platform::health::bus::HealthSubscriber, - )) - .expect("event bus should be initialized for channel health test"); + let _health_handle = crate::core::bus::BUS + .subscribe(Arc::new( + crate::openhuman::platform::health::bus::HealthSubscriber, + )) + .expect("event bus should be initialized for channel health test"); tokio::task::yield_now().await; let handle = spawn_supervised_listener(channel, tx, 1, 1); diff --git a/src/openhuman/channels/tests/runtime_dispatch.rs b/src/openhuman/channels/tests/runtime_dispatch.rs index 11cb708f04..cb607c967d 100644 --- a/src/openhuman/channels/tests/runtime_dispatch.rs +++ b/src/openhuman/channels/tests/runtime_dispatch.rs @@ -352,7 +352,8 @@ async fn dispatch_routes_through_agent_run_turn_bus_handler() { #[tokio::test] async fn channel_processed_event_records_resolved_agent_route() { crate::core::bus::init().await.expect("bus init"); - let mut events = crate::core::bus::BUS.get() + let mut events = crate::core::bus::BUS + .get() .expect("event bus should be initialized") .receiver(); diff --git a/src/openhuman/config/ops/agent.rs b/src/openhuman/config/ops/agent.rs index f89078f740..b14db1631d 100644 --- a/src/openhuman/config/ops/agent.rs +++ b/src/openhuman/config/ops/agent.rs @@ -133,9 +133,7 @@ pub async fn apply_autonomy_settings( config.save().await.map_err(|e| e.to_string())?; crate::openhuman::security::live_policy::reload_from(&config.autonomy); - crate::core::bus::BUS.publish( - crate::core::events::DomainEvent::AutonomyConfigChanged, - ); + crate::core::bus::BUS.publish(crate::core::events::DomainEvent::AutonomyConfigChanged); let snapshot = snapshot_config_json(config)?; Ok(RpcOutcome::new( @@ -472,9 +470,7 @@ pub async fn apply_agent_paths_settings( config.save().await.map_err(|e| e.to_string())?; crate::openhuman::security::live_policy::set_action_dir(config.action_dir.clone()); - crate::core::bus::BUS.publish( - crate::core::events::DomainEvent::AgentPathsChanged, - ); + crate::core::bus::BUS.publish(crate::core::events::DomainEvent::AgentPathsChanged); log::debug!( "[config][agent_paths] action_dir now '{}' (source={})", diff --git a/src/openhuman/cron/bus.rs b/src/openhuman/cron/bus.rs index 90bd5447d0..1d62e24558 100644 --- a/src/openhuman/cron/bus.rs +++ b/src/openhuman/cron/bus.rs @@ -7,10 +7,10 @@ //! channel construction out of the scheduler. use crate::core::events::DomainEvent; -use tinybus::EventHandler; use async_trait::async_trait; use std::collections::HashMap; use std::sync::Arc; +use tinybus::EventHandler; use tinychannels::{Channel, SendMessage}; /// Subscribes to `CronDeliveryRequested` events and dispatches diff --git a/src/openhuman/cron/scheduler_tests.rs b/src/openhuman/cron/scheduler_tests.rs index 365298af89..ff71f5e427 100644 --- a/src/openhuman/cron/scheduler_tests.rs +++ b/src/openhuman/cron/scheduler_tests.rs @@ -1201,8 +1201,8 @@ async fn deliver_if_configured_skips_non_announce_mode() { #[tokio::test] async fn deliver_if_configured_publishes_event_for_announce_mode() { use crate::core::events::DomainEvent; -use tinybus::EventHandler; use std::sync::atomic::{AtomicUsize, Ordering}; + use tinybus::EventHandler; // Create an isolated bus for this test. let bus = crate::core::bus_testing::isolated_bus().await; @@ -1741,10 +1741,10 @@ fn classify_agent_anyhow_does_not_leak_when_downcast_succeeds() { #[tokio::test] async fn scheduler_tick_once_publishes_health_recovery_signal_on_empty_queue() { use crate::core::bus::BUS; -use crate::core::events::DomainEvent; -use tinybus::EventHandler; + use crate::core::events::DomainEvent; use async_trait::async_trait; use std::sync::Mutex as StdMutex; + use tinybus::EventHandler; #[derive(Default)] struct HealthEventCollector { diff --git a/src/openhuman/desktop/notifications/bus.rs b/src/openhuman/desktop/notifications/bus.rs index 026688a5ac..f8ec81efd9 100644 --- a/src/openhuman/desktop/notifications/bus.rs +++ b/src/openhuman/desktop/notifications/bus.rs @@ -14,9 +14,9 @@ use std::time::{SystemTime, UNIX_EPOCH}; use tokio::sync::broadcast; use crate::core::events::DomainEvent; -use tinybus::EventHandler; use crate::openhuman::config::Config; use async_trait::async_trait; +use tinybus::EventHandler; use super::types::{CoreNotificationCategory, CoreNotificationEvent}; @@ -253,9 +253,9 @@ impl EventHandler for NotificationBridgeSubscriber { /// but the caller (`register_domain_subscribers`) is Once-guarded. pub fn register_notification_bridge_subscriber(config: Config) { use std::sync::Arc; - if let Some(handle) = crate::core::bus::BUS.subscribe(Arc::new( - NotificationBridgeSubscriber::new(config), - )) { + if let Some(handle) = + crate::core::bus::BUS.subscribe(Arc::new(NotificationBridgeSubscriber::new(config))) + { // SAFETY: intentional leak; handle's Drop would cancel the subscriber. std::mem::forget(handle); log::info!("{LOG_PREFIX} notification bridge subscriber registered"); diff --git a/src/openhuman/flows/bus.rs b/src/openhuman/flows/bus.rs index 6e0068be11..39d58f3f57 100644 --- a/src/openhuman/flows/bus.rs +++ b/src/openhuman/flows/bus.rs @@ -11,7 +11,6 @@ //! dispatch on enable/disable. use crate::core::events::DomainEvent; -use tinybus::EventHandler; use crate::openhuman::config::Config; use crate::openhuman::flows::store; use crate::openhuman::flows::{flow_namespace, Flow, FlowRun}; @@ -20,6 +19,7 @@ use async_trait::async_trait; use serde_json::Value; use std::collections::{HashMap, HashSet}; use std::sync::{Arc, LazyLock, Mutex}; +use tinybus::EventHandler; use tinyflows::model::{NodeKind, TriggerKind}; use tinyflows::nodes::control_flow::dedup as dedup_node; diff --git a/src/openhuman/flows/ops.rs b/src/openhuman/flows/ops.rs index 5198eadb61..27b913acd7 100644 --- a/src/openhuman/flows/ops.rs +++ b/src/openhuman/flows/ops.rs @@ -5731,13 +5731,11 @@ pub async fn sweep_expired_parked_runs(config: &Config) -> usize { flow_id, "[flows] TTL sweep: publishing FlowRunFinished for expired parked run" ); - crate::core::bus::BUS.publish( - crate::core::events::DomainEvent::FlowRunFinished { - flow_id: flow_id.to_string(), - run_id: run_id.to_string(), - status: "cancelled".to_string(), - }, - ); + crate::core::bus::BUS.publish(crate::core::events::DomainEvent::FlowRunFinished { + flow_id: flow_id.to_string(), + run_id: run_id.to_string(), + status: "cancelled".to_string(), + }); drop_checkpoint(config, run_id).await; } if !swept.is_empty() { @@ -5806,13 +5804,11 @@ pub async fn sweep_orphaned_running_runs_on_boot(config: &Config) -> usize { if let Err(e) = store::record_run(config, &flow_id, "interrupted") { tracing::warn!(target: "flows", run_id = %run_id, flow_id = %flow_id, error = %e, "[flows] boot sweep: failed to update flow summary for reconciled run"); } - crate::core::bus::BUS.publish( - crate::core::events::DomainEvent::FlowRunFinished { - flow_id: flow_id.clone(), - run_id: run_id.clone(), - status: "interrupted".to_string(), - }, - ); + crate::core::bus::BUS.publish(crate::core::events::DomainEvent::FlowRunFinished { + flow_id: flow_id.clone(), + run_id: run_id.clone(), + status: "interrupted".to_string(), + }); drop_checkpoint(config, &run_id).await; tracing::info!(target: "flows", run_id = %run_id, flow_id = %flow_id, "[flows] boot sweep: reconciled orphaned running run to 'interrupted'"); } diff --git a/src/openhuman/flows/ops_tests.rs b/src/openhuman/flows/ops_tests.rs index 10b60c78f8..affba8812a 100644 --- a/src/openhuman/flows/ops_tests.rs +++ b/src/openhuman/flows/ops_tests.rs @@ -2955,10 +2955,10 @@ async fn flows_run_does_not_notify_when_run_completes_without_pending_approvals( #[tokio::test] async fn flows_run_publishes_flow_run_started_with_flow_and_run_id() { use crate::core::bus::BUS; -use crate::core::events::DomainEvent; -use tinybus::EventHandler; + use crate::core::events::DomainEvent; use async_trait::async_trait; use std::sync::Mutex as StdMutex; + use tinybus::EventHandler; #[derive(Default)] struct Collector { @@ -3043,10 +3043,10 @@ use tinybus::EventHandler; #[tokio::test] async fn flows_run_finished_event_skips_pending_approval_and_fires_once_on_resume() { use crate::core::bus::BUS; -use crate::core::events::DomainEvent; -use tinybus::EventHandler; + use crate::core::events::DomainEvent; use async_trait::async_trait; use std::sync::Mutex as StdMutex; + use tinybus::EventHandler; #[derive(Default)] struct Collector { diff --git a/src/openhuman/inference/provider/factory_tests.rs b/src/openhuman/inference/provider/factory_tests.rs index c2ed3a606b..010c828963 100644 --- a/src/openhuman/inference/provider/factory_tests.rs +++ b/src/openhuman/inference/provider/factory_tests.rs @@ -1092,7 +1092,7 @@ fn configured_openhuman_jwt_slug_routes_to_managed_chat_model() { #[tokio::test] async fn openhuman_jwt_slug_discloses_pinned_model() { use crate::core::bus::BUS; -use crate::core::events::DomainEvent; + use crate::core::events::DomainEvent; use crate::openhuman::security::egress::{EgressDescriptor, EgressReason}; use std::time::Duration; @@ -1141,7 +1141,7 @@ use crate::core::events::DomainEvent; #[tokio::test] async fn native_claude_turn_routes_disclose_pinned_models() { use crate::core::bus::BUS; -use crate::core::events::DomainEvent; + use crate::core::events::DomainEvent; use crate::openhuman::security::egress::EgressDescriptor; use std::time::Duration; @@ -1327,7 +1327,7 @@ fn crate_native_chat_model_factory_preserves_invalid_route_diagnostics() { #[tokio::test] async fn from_string_external_provider_emits_egress_realpath() { use crate::core::bus::BUS; -use crate::core::events::DomainEvent; + use crate::core::events::DomainEvent; use crate::openhuman::security::egress::EgressReason; crate::core::bus::init().await.expect("bus init"); @@ -1350,7 +1350,7 @@ use crate::core::events::DomainEvent; } Some(_) => continue, None => panic!("the bus closed before the expected event arrived"), -} + } } }) .await; @@ -1369,7 +1369,7 @@ use crate::core::events::DomainEvent; #[tokio::test] async fn create_chat_model_managed_emits_exactly_one_egress_realpath() { use crate::core::bus::BUS; -use crate::core::events::DomainEvent; + use crate::core::events::DomainEvent; use crate::openhuman::security::egress::{EgressDescriptor, EgressReason}; use std::time::Duration; @@ -1422,7 +1422,7 @@ use crate::core::events::DomainEvent; #[tokio::test] async fn create_chat_model_local_runtime_does_not_emit_egress_realpath() { use crate::core::bus::BUS; -use crate::core::events::DomainEvent; + use crate::core::events::DomainEvent; use crate::openhuman::security::egress::EgressDescriptor; use std::time::Duration; diff --git a/src/openhuman/inference/provider/openhuman_backend_model.rs b/src/openhuman/inference/provider/openhuman_backend_model.rs index ef88611191..18502f02e0 100644 --- a/src/openhuman/inference/provider/openhuman_backend_model.rs +++ b/src/openhuman/inference/provider/openhuman_backend_model.rs @@ -390,16 +390,14 @@ fn maybe_publish_session_expired(err: &TinyAgentsError, operation: &str) { if pe.provider.as_str() == "OpenHuman" && matches!(pe.status, Some(401 | 403)) { let reason = crate::openhuman::inference::provider::ops::sanitize_api_error(&pe.message); - crate::core::bus::BUS.publish( - crate::core::events::DomainEvent::SessionExpired { - source: format!( - "openhuman_backend_model.{}({})", - operation, - pe.status.unwrap_or(0) - ), - reason, - }, - ); + crate::core::bus::BUS.publish(crate::core::events::DomainEvent::SessionExpired { + source: format!( + "openhuman_backend_model.{}({})", + operation, + pe.status.unwrap_or(0) + ), + reason, + }); } } } diff --git a/src/openhuman/inference/provider/ops/http_error.rs b/src/openhuman/inference/provider/ops/http_error.rs index 166410948b..c407ed59ac 100644 --- a/src/openhuman/inference/provider/ops/http_error.rs +++ b/src/openhuman/inference/provider/ops/http_error.rs @@ -837,15 +837,13 @@ pub fn log_byo_provider_auth_failure( // re-flooding the notification center the way the raw error flooded Sentry. let status_code = status.as_u16(); if crate::openhuman::inference::auth_error_registry::record(provider, status_code) { - crate::core::bus::BUS.publish( - crate::core::events::DomainEvent::ProviderApiKeyRejected { - provider: provider.to_string(), - message: crate::openhuman::inference::auth_error_registry::auth_error_message( - provider, - status_code, - ), - }, - ); + crate::core::bus::BUS.publish(crate::core::events::DomainEvent::ProviderApiKeyRejected { + provider: provider.to_string(), + message: crate::openhuman::inference::auth_error_registry::auth_error_message( + provider, + status_code, + ), + }); } } diff --git a/src/openhuman/inference/provider/ops_tests.rs b/src/openhuman/inference/provider/ops_tests.rs index f3714a6bde..6b69ee55b3 100644 --- a/src/openhuman/inference/provider/ops_tests.rs +++ b/src/openhuman/inference/provider/ops_tests.rs @@ -1332,10 +1332,13 @@ async fn api_error_monthly_quota_returns_message_via_demoted_branch() { #[tokio::test] async fn publish_backend_session_expired_emits_sanitized_session_expired() { use crate::core::bus::BUS; -use crate::core::events::DomainEvent; + use crate::core::events::DomainEvent; crate::core::bus::init().await.expect("bus init"); - let mut rx = crate::core::bus::BUS.get().expect("event bus initialized").receiver(); + let mut rx = crate::core::bus::BUS + .get() + .expect("event bus initialized") + .receiver(); // `TEST_MARKER_A` makes this event distinguishable from the sibling // `chat_completions_backend_401_*` test's event on the shared global diff --git a/src/openhuman/integrations/composio/ops/direct_mode.rs b/src/openhuman/integrations/composio/ops/direct_mode.rs index 029d0079ce..7e8b0ac71c 100644 --- a/src/openhuman/integrations/composio/ops/direct_mode.rs +++ b/src/openhuman/integrations/composio/ops/direct_mode.rs @@ -111,12 +111,10 @@ pub async fn composio_set_api_key( config.composio.mode.clone() }; - crate::core::bus::BUS.publish( - crate::core::events::DomainEvent::ComposioConfigChanged { - mode: effective_mode.clone(), - api_key_set: true, - }, - ); + crate::core::bus::BUS.publish(crate::core::events::DomainEvent::ComposioConfigChanged { + mode: effective_mode.clone(), + api_key_set: true, + }); tracing::debug!( mode = %effective_mode, "[composio-cache] published ComposioConfigChanged after set_api_key" @@ -149,12 +147,10 @@ pub async fn composio_clear_api_key(config: &Config) -> OpResult { let _ = CONVERSATION_PERSISTENCE_HANDLE.set(handle); } diff --git a/src/openhuman/memory/diff/ops.rs b/src/openhuman/memory/diff/ops.rs index 02c0c1d024..3236e38c5b 100644 --- a/src/openhuman/memory/diff/ops.rs +++ b/src/openhuman/memory/diff/ops.rs @@ -59,15 +59,13 @@ pub async fn take_snapshot( "[memory_diff] snapshot taken" ); - crate::core::bus::BUS.publish( - crate::core::events::DomainEvent::MemoryDiffSnapshotTaken { - snapshot_id: snapshot.id.clone(), - source_id: source.id.clone(), - source_kind: source.kind.as_str().to_string(), - item_count: snapshot.item_count as usize, - trigger: snapshot.trigger.as_str().to_string(), - }, - ); + crate::core::bus::BUS.publish(crate::core::events::DomainEvent::MemoryDiffSnapshotTaken { + snapshot_id: snapshot.id.clone(), + source_id: source.id.clone(), + source_kind: source.kind.as_str().to_string(), + item_count: snapshot.item_count as usize, + trigger: snapshot.trigger.as_str().to_string(), + }); Ok(snapshot) } @@ -230,12 +228,10 @@ pub async fn mark_read(config: &Config, source_ids: Option>) -> Resu "[memory_diff] mark_read committed read markers" ); - crate::core::bus::BUS.publish( - crate::core::events::DomainEvent::MemoryDiffMarkedRead { - source_ids: target_ids, - snapshot_ids, - }, - ); + crate::core::bus::BUS.publish(crate::core::events::DomainEvent::MemoryDiffMarkedRead { + source_ids: target_ids, + snapshot_ids, + }); Ok(marked) } diff --git a/src/openhuman/memory/global.rs b/src/openhuman/memory/global.rs index e5c415cec1..b94d35ff7d 100644 --- a/src/openhuman/memory/global.rs +++ b/src/openhuman/memory/global.rs @@ -221,10 +221,7 @@ fn cached_client(workspace_dir: &Path) -> Result, String /// A racing caller may have inserted first; theirs wins, so the "one ingestion /// worker per workspace" property holds even when two paths construct /// concurrently. Callers must use the returned handle, not the one they passed. -fn cache_client( - workspace_dir: &Path, - client: &MemoryClientRef, -) -> Result { +fn cache_client(workspace_dir: &Path, client: &MemoryClientRef) -> Result { let mut guard = WORKSPACE_CLIENTS .get_or_init(Default::default) .write() diff --git a/src/openhuman/memory/ops/sync.rs b/src/openhuman/memory/ops/sync.rs index 0dcf900d29..3dc05a638e 100644 --- a/src/openhuman/memory/ops/sync.rs +++ b/src/openhuman/memory/ops/sync.rs @@ -61,11 +61,9 @@ pub async fn memory_sync_channel( ) -> Result, String> { // `channel_id` is a user/context identifier — keep it out of normal logs. tracing::info!("[memory.sync] memory_sync_channel: entry"); - crate::core::bus::BUS.publish( - crate::core::events::DomainEvent::MemorySyncRequested { - channel_id: Some(params.channel_id.clone()), - }, - ); + crate::core::bus::BUS.publish(crate::core::events::DomainEvent::MemorySyncRequested { + channel_id: Some(params.channel_id.clone()), + }); emit_sync_stage( MemorySyncTrigger::Manual, MemorySyncStage::Requested, @@ -97,9 +95,8 @@ pub async fn memory_sync_channel( /// ingestion subscribers. pub async fn memory_sync_all() -> Result, String> { tracing::info!("[memory.sync] memory_sync_all: entry"); - crate::core::bus::BUS.publish( - crate::core::events::DomainEvent::MemorySyncRequested { channel_id: None }, - ); + crate::core::bus::BUS + .publish(crate::core::events::DomainEvent::MemorySyncRequested { channel_id: None }); emit_sync_stage( MemorySyncTrigger::Manual, MemorySyncStage::Requested, @@ -243,8 +240,8 @@ mod tests { use tokio::time::{timeout, Duration}; use crate::core::bus::BUS; -use crate::core::events::DomainEvent; -use tinybus::EventHandler; + use crate::core::events::DomainEvent; + use tinybus::EventHandler; fn test_mutex() -> &'static std::sync::Mutex<()> { static LOCK: OnceLock> = OnceLock::new(); @@ -320,7 +317,8 @@ use tinybus::EventHandler; .unwrap_or_else(|poisoned| poisoned.into_inner()); let _ = crate::core::bus::init().await; let (tx, mut rx) = mpsc::unbounded_channel(); - let _subscription = BUS.subscribe(Arc::new(ChannelCapture { tx })) + let _subscription = BUS + .subscribe(Arc::new(ChannelCapture { tx })) .expect("global bus should be initialized"); let outcome = memory_sync_channel(SyncChannelParams { @@ -348,7 +346,8 @@ use tinybus::EventHandler; .unwrap_or_else(|poisoned| poisoned.into_inner()); let _ = crate::core::bus::init().await; let (tx, mut rx) = mpsc::unbounded_channel(); - let _subscription = BUS.subscribe(Arc::new(ChannelCapture { tx })) + let _subscription = BUS + .subscribe(Arc::new(ChannelCapture { tx })) .expect("global bus should be initialized"); let outcome = memory_sync_all().await.expect("memory_sync_all"); diff --git a/src/openhuman/memory/sync/composio/bus.rs b/src/openhuman/memory/sync/composio/bus.rs index 7cf2dbd8a2..b845b4d278 100644 --- a/src/openhuman/memory/sync/composio/bus.rs +++ b/src/openhuman/memory/sync/composio/bus.rs @@ -53,12 +53,12 @@ use async_trait::async_trait; use crate::core::bus::BUS; use crate::core::events::DomainEvent; -use tinybus::EventHandler; -use tinybus::SubscriptionHandle; use crate::openhuman::agent::triage::{apply_decision, run_triage, TriageOutcome, TriggerEnvelope}; use crate::openhuman::config::rpc as config_rpc; use crate::openhuman::config::schema::COMPOSIO_MODE_DIRECT; use crate::openhuman::integrations::composio::trigger_history; +use tinybus::EventHandler; +use tinybus::SubscriptionHandle; use super::providers::{get_provider, ProviderContext}; use crate::openhuman::integrations::composio::client::ComposioClient; @@ -894,11 +894,9 @@ impl EventHandler for ComposioConfigChangedSubscriber { .collect(); toolkits.sort(); toolkits.dedup(); - crate::core::bus::BUS.publish( - DomainEvent::ComposioIntegrationsChanged { - toolkits: toolkits.clone(), - }, - ); + crate::core::bus::BUS.publish(DomainEvent::ComposioIntegrationsChanged { + toolkits: toolkits.clone(), + }); tracing::debug!( active_toolkits = ?toolkits, "[composio-cache] config changed eager warm complete; published integrations changed" diff --git a/src/openhuman/memory/sync_events.rs b/src/openhuman/memory/sync_events.rs index f80fc5749f..c0e05a6b68 100644 --- a/src/openhuman/memory/sync_events.rs +++ b/src/openhuman/memory/sync_events.rs @@ -307,8 +307,9 @@ mod tests { crate::core::bus::init().await.expect("bus init"); let collector = StageCollector::default(); - let _subscription = - BUS.subscribe(Arc::new(collector.clone())).expect("event bus initialized"); + let _subscription = BUS + .subscribe(Arc::new(collector.clone())) + .expect("event bus initialized"); let bridge = MemorySyncStageBridge; bridge @@ -346,8 +347,9 @@ mod tests { crate::core::bus::init().await.expect("bus init"); let collector = StageCollector::default(); - let _subscription = - BUS.subscribe(Arc::new(collector.clone())).expect("event bus initialized"); + let _subscription = BUS + .subscribe(Arc::new(collector.clone())) + .expect("event bus initialized"); let bridge = MemorySyncStageBridge; bridge @@ -437,8 +439,9 @@ mod tests { crate::core::bus::init().await.expect("bus init"); let collector = StageCollector::default(); - let _subscription = - BUS.subscribe(Arc::new(collector.clone())).expect("event bus initialized"); + let _subscription = BUS + .subscribe(Arc::new(collector.clone())) + .expect("event bus initialized"); let bridge = MemorySyncStageBridge; bridge @@ -485,8 +488,9 @@ mod tests { crate::core::bus::init().await.expect("bus init"); let collector = StageCollector::default(); - let _subscription = - BUS.subscribe(Arc::new(collector.clone())).expect("event bus initialized"); + let _subscription = BUS + .subscribe(Arc::new(collector.clone())) + .expect("event bus initialized"); let bridge = MemorySyncStageBridge; // Non-memory-source sync (e.g. Slack channel sync) should have source_id=None @@ -532,8 +536,9 @@ mod tests { crate::core::bus::init().await.expect("bus init"); let collector = StageCollector::default(); - let _subscription = - BUS.subscribe(Arc::new(collector.clone())).expect("event bus initialized"); + let _subscription = BUS + .subscribe(Arc::new(collector.clone())) + .expect("event bus initialized"); let bridge = MemorySyncStageBridge; bridge @@ -585,8 +590,9 @@ mod tests { crate::core::bus::init().await.expect("bus init"); let collector = StageCollector::default(); - let _subscription = - BUS.subscribe(Arc::new(collector.clone())).expect("event bus initialized"); + let _subscription = BUS + .subscribe(Arc::new(collector.clone())) + .expect("event bus initialized"); let bridge = MemorySyncStageBridge; // Non-memory-source ingestion (plain document_id, no mem_src prefix) diff --git a/src/openhuman/memory/sync_pipeline_e2e_tests.rs b/src/openhuman/memory/sync_pipeline_e2e_tests.rs index 12fd67d16d..667a13f4f8 100644 --- a/src/openhuman/memory/sync_pipeline_e2e_tests.rs +++ b/src/openhuman/memory/sync_pipeline_e2e_tests.rs @@ -22,8 +22,6 @@ use tempfile::TempDir; use crate::core::bus::BUS; use crate::core::events::DomainEvent; -use tinybus::EventHandler; -use tinybus::SubscriptionHandle; use crate::openhuman::config::Config; use crate::openhuman::memory::ingest_pipeline::ingest_chat; use crate::openhuman::memory::queue::{ @@ -36,6 +34,8 @@ use crate::openhuman::memory::store::trees::{store as tree_store, types::TreeKin use crate::openhuman::memory::sync_events::{emit_sync_stage, MemorySyncStage, MemorySyncTrigger}; use crate::openhuman::memory::tree::retrieval::{query_source, search_entities}; use crate::openhuman::memory::tree::score::store::lookup_entity; +use tinybus::EventHandler; +use tinybus::SubscriptionHandle; use tinycortex::memory::ingest::canonicalize::chat::{ChatBatch, ChatMessage}; // ── helpers ───────────────────────────────────────────────────────────── diff --git a/src/openhuman/memory/tinycortex/sync.rs b/src/openhuman/memory/tinycortex/sync.rs index 41b8003d6e..ea53db5ad9 100644 --- a/src/openhuman/memory/tinycortex/sync.rs +++ b/src/openhuman/memory/tinycortex/sync.rs @@ -674,16 +674,14 @@ impl SyncStateStore for HostSyncAdapter { #[async_trait] impl SyncEventSink for HostSyncAdapter { async fn emit(&self, event: SyncEvent) -> anyhow::Result<()> { - crate::core::bus::BUS.publish( - crate::core::events::DomainEvent::MemorySyncStageChanged { - trigger: "tinycortex".into(), - stage: stage_name(event.stage).into(), - provider: Some(event.toolkit), - connection_id: event.connection_id, - detail: event.message, - source_id: Some(event.source_id), - }, - ); + crate::core::bus::BUS.publish(crate::core::events::DomainEvent::MemorySyncStageChanged { + trigger: "tinycortex".into(), + stage: stage_name(event.stage).into(), + provider: Some(event.toolkit), + connection_id: event.connection_id, + detail: event.message, + source_id: Some(event.source_id), + }); Ok(()) } } diff --git a/src/openhuman/memory/tree/tree_runtime/bus.rs b/src/openhuman/memory/tree/tree_runtime/bus.rs index 09870fdcb8..e51c071bc5 100644 --- a/src/openhuman/memory/tree/tree_runtime/bus.rs +++ b/src/openhuman/memory/tree/tree_runtime/bus.rs @@ -4,8 +4,8 @@ //! Future subscribers can react to these events for cross-module workflows. use crate::core::events::DomainEvent; -use tinybus::EventHandler; use async_trait::async_trait; +use tinybus::EventHandler; /// Subscribes to tree summarizer events and logs activity. pub struct TreeSummarizerEventSubscriber; diff --git a/src/openhuman/security/approval/gate.rs b/src/openhuman/security/approval/gate.rs index b7eba0884e..f99321ab1d 100644 --- a/src/openhuman/security/approval/gate.rs +++ b/src/openhuman/security/approval/gate.rs @@ -3296,7 +3296,8 @@ mod tests { // (bridged to a broadcast Socket.IO event by `core::socketio`) and // the `flow-gate-approval` CoreNotification with its three actions. crate::core::bus::init().await.expect("bus init"); - let mut event_rx = crate::core::bus::BUS.get() + let mut event_rx = crate::core::bus::BUS + .get() .expect("event bus initialized above") .receiver(); let mut notif_rx = @@ -3368,7 +3369,7 @@ mod tests { }) if flow_id == expected_flow_id => return (request_id, run_id, tool_name), Some(_) => continue, None => panic!("the bus closed before the expected event arrived"), -} + } } } diff --git a/src/openhuman/security/credentials/bus.rs b/src/openhuman/security/credentials/bus.rs index 4d51eca32c..3852a60c2a 100644 --- a/src/openhuman/security/credentials/bus.rs +++ b/src/openhuman/security/credentials/bus.rs @@ -21,9 +21,9 @@ //! cron-driven LLM calls after session expiry). use crate::core::events::DomainEvent; -use tinybus::EventHandler; use crate::openhuman::cron::scheduler_gate; use async_trait::async_trait; +use tinybus::EventHandler; /// Subscribes to [`DomainEvent::SessionExpired`] and runs the canonical /// session-teardown. Singleton — register once at startup. diff --git a/src/openhuman/security/credentials/session_support.rs b/src/openhuman/security/credentials/session_support.rs index 5c26e4b4fb..fd3114f40f 100644 --- a/src/openhuman/security/credentials/session_support.rs +++ b/src/openhuman/security/credentials/session_support.rs @@ -219,14 +219,11 @@ pub fn require_live_session_token(config: &Config) -> Result { operation = "require_live_session_token", "[credentials] app-session token expired locally — publishing SessionExpired before any backend call" ); - crate::core::bus::BUS.publish( - crate::core::events::DomainEvent::SessionExpired { - source: "credentials.local_expiry_precheck".to_string(), - reason: - "backend session token expired locally — re-authentication required" - .to_string(), - }, - ); + crate::core::bus::BUS.publish(crate::core::events::DomainEvent::SessionExpired { + source: "credentials.local_expiry_precheck".to_string(), + reason: "backend session token expired locally — re-authentication required" + .to_string(), + }); } Err( "SESSION_EXPIRED: backend session token expired locally — re-authentication required" diff --git a/src/openhuman/security/devices/bus.rs b/src/openhuman/security/devices/bus.rs index 9132a2338d..0bf8684fb5 100644 --- a/src/openhuman/security/devices/bus.rs +++ b/src/openhuman/security/devices/bus.rs @@ -11,8 +11,6 @@ use std::sync::{Arc, OnceLock}; use crate::core::bus::BUS; use crate::core::events::DomainEvent; -use tinybus::EventHandler; -use tinybus::SubscriptionHandle; use crate::openhuman::security::devices::crypto::{ base64url_decode, base64url_encode, derive_session_keys, TunnelCipher, TunnelRole, }; @@ -24,6 +22,8 @@ use crate::openhuman::security::devices::tunnel_client::emit_frame; use async_trait::async_trait; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; +use tinybus::EventHandler; +use tinybus::SubscriptionHandle; use x25519_dalek::{PublicKey, StaticSecret}; static DEVICE_TUNNEL_HANDLE: OnceLock = OnceLock::new(); diff --git a/src/openhuman/security/egress/emit_tests.rs b/src/openhuman/security/egress/emit_tests.rs index 79411d3379..9d1963f882 100644 --- a/src/openhuman/security/egress/emit_tests.rs +++ b/src/openhuman/security/egress/emit_tests.rs @@ -25,7 +25,7 @@ async fn find_pending( }) if descriptor.service == marker => return (descriptor, thread_id, client_id), Some(_) => continue, None => panic!("the bus closed before the expected event arrived"), -} + } } } @@ -73,7 +73,7 @@ async fn local_transfer_does_not_publish() { } Some(_) => continue, None => panic!("the bus closed before the expected event arrived"), -} + } } } @@ -139,7 +139,7 @@ async fn dedup_turn_scope_collapses_repeat_destination() { } Some(_) => continue, None => panic!("the bus closed before the expected event arrived"), -} + } } assert_eq!( dup_count, 1, @@ -178,7 +178,7 @@ async fn dedup_absent_outside_scope_publishes_each_time() { } Some(_) => continue, None => panic!("the bus closed before the expected event arrived"), -} + } } assert_eq!( count, 2, diff --git a/src/openhuman/security/keyring_consent/policy.rs b/src/openhuman/security/keyring_consent/policy.rs index 7575d0ce61..a2a2d9a28d 100644 --- a/src/openhuman/security/keyring_consent/policy.rs +++ b/src/openhuman/security/keyring_consent/policy.rs @@ -73,9 +73,8 @@ pub fn check_secret_access() -> PolicyDecision { debug!("{LOG_PREFIX} check_secret_access: keyring unavailable, no consent recorded"); if !CONSENT_EVENT_PUBLISHED.swap(true, Ordering::SeqCst) { info!("{LOG_PREFIX} publishing KeyringConsentRequired event"); - crate::core::bus::BUS.publish( - crate::core::events::DomainEvent::KeyringConsentRequired, - ); + crate::core::bus::BUS + .publish(crate::core::events::DomainEvent::KeyringConsentRequired); } PolicyDecision::ConsentRequired } @@ -173,21 +172,17 @@ pub fn notify_master_key_unavailable(reason: &str) { warn!("{LOG_PREFIX} master key unavailable: {reason}"); if !CONSENT_EVENT_PUBLISHED.swap(true, Ordering::SeqCst) { info!("{LOG_PREFIX} publishing KeyringConsentRequired event (master key unavailable)"); - crate::core::bus::BUS.publish( - crate::core::events::DomainEvent::KeyringConsentRequired, - ); + crate::core::bus::BUS.publish(crate::core::events::DomainEvent::KeyringConsentRequired); } } /// Publish a decrypt-failure event for frontend notification. pub fn notify_decrypt_failure(field_name: &str, reason: &str) { warn!("{LOG_PREFIX} decrypt failure field={field_name} reason={reason}"); - crate::core::bus::BUS.publish( - crate::core::events::DomainEvent::KeyringDecryptFailed { - field_name: field_name.to_string(), - reason: reason.to_string(), - }, - ); + crate::core::bus::BUS.publish(crate::core::events::DomainEvent::KeyringDecryptFailed { + field_name: field_name.to_string(), + reason: reason.to_string(), + }); } fn classify_failure_reason(backend_name: &str) -> KeyringFailureReason { diff --git a/src/openhuman/skills/bus.rs b/src/openhuman/skills/bus.rs index 2ca4cb1714..dfe58b4699 100644 --- a/src/openhuman/skills/bus.rs +++ b/src/openhuman/skills/bus.rs @@ -13,11 +13,11 @@ use crate::core::bus::BUS; use crate::core::events::DomainEvent; -use tinybus::EventHandler; -use tinybus::SubscriptionHandle; use crate::openhuman::skills::Workflow; use async_trait::async_trait; use std::sync::{Arc, OnceLock}; +use tinybus::EventHandler; +use tinybus::SubscriptionHandle; // ── Trigger pattern ─────────────────────────────────────────────────────────── diff --git a/src/openhuman/skills/ops_create.rs b/src/openhuman/skills/ops_create.rs index 3b49a0e542..05ab5bc52b 100644 --- a/src/openhuman/skills/ops_create.rs +++ b/src/openhuman/skills/ops_create.rs @@ -691,7 +691,9 @@ mod render_skill_toml_tests { use tinybus::TryRecvError; crate::core::bus::init().await.expect("bus init"); - let mut rx = crate::core::bus::BUS.get().expect("event bus should be initialized") + let mut rx = crate::core::bus::BUS + .get() + .expect("event bus should be initialized") .receiver(); let home = tempfile::TempDir::new().expect("temp home"); diff --git a/src/openhuman/skills/webhooks/bus.rs b/src/openhuman/skills/webhooks/bus.rs index ea173659eb..75a1c33b08 100644 --- a/src/openhuman/skills/webhooks/bus.rs +++ b/src/openhuman/skills/webhooks/bus.rs @@ -7,13 +7,13 @@ use crate::core::bus::BUS; use crate::core::events::DomainEvent; -use tinybus::EventHandler; use crate::openhuman::platform::socket::global_socket_manager; use crate::openhuman::skills::webhooks::WebhookResponseData; use async_trait::async_trait; use serde_json::json; use std::collections::HashMap; use std::time::Instant; +use tinybus::EventHandler; /// Base64-encode a string (for webhook response bodies). fn base64_encode(input: &str) -> String { diff --git a/src/openhuman/voice/bus.rs b/src/openhuman/voice/bus.rs index 6eddbd95be..2262e88b00 100644 --- a/src/openhuman/voice/bus.rs +++ b/src/openhuman/voice/bus.rs @@ -27,10 +27,10 @@ pub fn publish_ptt_transcript_committed( mod tests { use super::*; use crate::core::bus::BUS; -use crate::core::events::DomainEvent; -use tinybus::EventHandler; + use crate::core::events::DomainEvent; use async_trait::async_trait; use std::sync::Arc; + use tinybus::EventHandler; use tokio::sync::Mutex as AsyncMutex; #[derive(Default)] diff --git a/src/openhuman/web_chat/event_bus.rs b/src/openhuman/web_chat/event_bus.rs index 9c5cf984bb..0b77205968 100644 --- a/src/openhuman/web_chat/event_bus.rs +++ b/src/openhuman/web_chat/event_bus.rs @@ -4,9 +4,9 @@ use std::sync::{Arc, OnceLock}; use tokio::sync::broadcast; use crate::core::events::DomainEvent; +use crate::core::socketio::WebChannelEvent; use tinybus::EventHandler; use tinybus::SubscriptionHandle; -use crate::core::socketio::WebChannelEvent; static EVENT_BUS: Lazy> = Lazy::new(|| { let (tx, _rx) = broadcast::channel(512); diff --git a/tests/agent_harness_e2e.rs b/tests/agent_harness_e2e.rs index 770b088266..5e04b20649 100644 --- a/tests/agent_harness_e2e.rs +++ b/tests/agent_harness_e2e.rs @@ -1313,9 +1313,7 @@ async fn ensure_approval_gate() { // `build_core_http_router` does NOT call `bootstrap_core_runtime`, so the // bus is not initialized by boot_stack. Standing it up is async now — it // connects to a broker — which is why this helper is too. Idempotent. - openhuman_core::core::bus::init() - .await - .expect("bus init"); + openhuman_core::core::bus::init().await.expect("bus init"); let mut cfg: openhuman_core::openhuman::config::Config = toml::from_str( r#"api_url = "http://127.0.0.1:1" diff --git a/tests/calendar_grounding_e2e.rs b/tests/calendar_grounding_e2e.rs index 0eb305ef57..22989798a1 100644 --- a/tests/calendar_grounding_e2e.rs +++ b/tests/calendar_grounding_e2e.rs @@ -56,7 +56,7 @@ impl ChatModel<()> for MockCalendarModel { raw: None, resolved_model: None, continue_turn: None, - served_from_cache: false, + served_from_cache: false, }) } else { // End the loop diff --git a/tests/composio_list_tools_stack_overflow_regression.rs b/tests/composio_list_tools_stack_overflow_regression.rs index b52fc1eb33..578ae72a28 100644 --- a/tests/composio_list_tools_stack_overflow_regression.rs +++ b/tests/composio_list_tools_stack_overflow_regression.rs @@ -226,7 +226,7 @@ impl ChatModel<()> for StubModel { raw: None, resolved_model: None, continue_turn: None, - served_from_cache: false, + served_from_cache: false, }) } else { Ok(ModelResponse::assistant("done")) diff --git a/tests/config_auth_app_state_connectivity_e2e.rs b/tests/config_auth_app_state_connectivity_e2e.rs index 73b2575ae3..1724ba556d 100644 --- a/tests/config_auth_app_state_connectivity_e2e.rs +++ b/tests/config_auth_app_state_connectivity_e2e.rs @@ -26,7 +26,6 @@ use openhuman_core::api::config::{ }; use openhuman_core::core::auth::{init_rpc_token, CORE_TOKEN_ENV_VAR}; use openhuman_core::core::events::DomainEvent; -use tinybus::EventHandler; use openhuman_core::core::jsonrpc::build_core_http_router; use openhuman_core::openhuman::config::schema::{ generate_provider_id, generate_voice_provider_id, is_slug_reserved, is_voice_slug_reserved, @@ -69,6 +68,7 @@ use openhuman_core::openhuman::security::credentials::{ list_provider_credentials_by_prefix, normalize_provider, rpc_store_composio_api_key, store_composio_api_key, AuthService, APP_SESSION_PROVIDER, COMPOSIO_DIRECT_PROVIDER, }; +use tinybus::EventHandler; const TEST_RPC_TOKEN: &str = "worker-a-domain-e2e-token"; diff --git a/tests/json_rpc_e2e.rs b/tests/json_rpc_e2e.rs index a24d3b2d07..b8d9065602 100644 --- a/tests/json_rpc_e2e.rs +++ b/tests/json_rpc_e2e.rs @@ -9548,24 +9548,27 @@ async fn whatsapp_data_agent_tools_e2e_1341() { } // Stand in for the shell store: register canned native handlers. - BUS.native().register::, _, _>( - methods::LIST_CHATS, - |_req| async move { Ok(vec![sample_chat("alice@c.us"), sample_chat("team@g.us")]) }, - ); - BUS.native().register::, _, _>( - methods::LIST_MESSAGES, - |_req| async move { Ok(vec![sample_msg("Send the umbrella report by Friday")]) }, - ); - BUS.native().register::, _, _>( - methods::SEARCH_MESSAGES, - |req| async move { - if req.query.to_lowercase().contains("umbrella") { - Ok(vec![sample_msg("Send the umbrella report by Friday")]) - } else { - Ok(vec![]) - } - }, - ); + BUS.native() + .register::, _, _>( + methods::LIST_CHATS, + |_req| async move { Ok(vec![sample_chat("alice@c.us"), sample_chat("team@g.us")]) }, + ); + BUS.native() + .register::, _, _>( + methods::LIST_MESSAGES, + |_req| async move { Ok(vec![sample_msg("Send the umbrella report by Friday")]) }, + ); + BUS.native() + .register::, _, _>( + methods::SEARCH_MESSAGES, + |req| async move { + if req.query.to_lowercase().contains("umbrella") { + Ok(vec![sample_msg("Send the umbrella report by Friday")]) + } else { + Ok(vec![]) + } + }, + ); fn parse_tool_output(result: openhuman_core::openhuman::skills::types::ToolResult) -> Value { assert!(!result.is_error, "tool returned error: {result:?}"); diff --git a/tests/monitor_agent_e2e.rs b/tests/monitor_agent_e2e.rs index d264817fa0..b5d4a5f15c 100644 --- a/tests/monitor_agent_e2e.rs +++ b/tests/monitor_agent_e2e.rs @@ -207,7 +207,7 @@ fn tool_response(id: &str, name: &str, arguments: serde_json::Value) -> ModelRes raw: None, resolved_model: None, continue_turn: None, - served_from_cache: false, + served_from_cache: false, } } diff --git a/tests/subconscious_conversation_e2e.rs b/tests/subconscious_conversation_e2e.rs index aafcaef4d1..f1b6b7dfda 100644 --- a/tests/subconscious_conversation_e2e.rs +++ b/tests/subconscious_conversation_e2e.rs @@ -237,9 +237,7 @@ struct Harness { impl Harness { async fn new(config: OrchestratorConfig) -> Self { - openhuman_core::core::bus::init() - .await - .expect("bus init"); + openhuman_core::core::bus::init().await.expect("bus init"); let transcript = Transcript::new(); let emit: Emitter = Arc::new(StdMutex::new(VecDeque::new())); let tmp = tempfile::tempdir().expect("tempdir"); @@ -258,20 +256,23 @@ impl Harness { // Capture proactive (subconscious → human) deliveries off the bus. let notifications = Arc::new(StdMutex::new(Vec::::new())); let sink = Arc::clone(¬ifications); - let sub = openhuman_core::core::bus::BUS.get().expect("bus").on("conv-e2e-notify", move |event| { - let sink = Arc::clone(&sink); - let event = event.clone(); - Box::pin(async move { - if let DomainEvent::ProactiveMessageRequested { - source, message, .. - } = &event - { - if source == "subconscious" { - sink.lock().unwrap().push(message.clone()); + let sub = openhuman_core::core::bus::BUS.get().expect("bus").on( + "conv-e2e-notify", + move |event| { + let sink = Arc::clone(&sink); + let event = event.clone(); + Box::pin(async move { + if let DomainEvent::ProactiveMessageRequested { + source, message, .. + } = &event + { + if source == "subconscious" { + sink.lock().unwrap().push(message.clone()); + } } - } - }) - }); + }) + }, + ); let loop_handle = Arc::clone(&orch); let task = tokio::spawn(async move { loop_handle.run_loop().await }); diff --git a/tests/subconscious_fullstack_e2e.rs b/tests/subconscious_fullstack_e2e.rs index 73ef0d431c..e54a71f017 100644 --- a/tests/subconscious_fullstack_e2e.rs +++ b/tests/subconscious_fullstack_e2e.rs @@ -123,7 +123,7 @@ impl MockLlm { raw: None, resolved_model: None, continue_turn: None, - served_from_cache: false, + served_from_cache: false, }; } else { "Mock orchestrator handled the promoted trigger.".to_string() diff --git a/tests/subconscious_triggers_e2e.rs b/tests/subconscious_triggers_e2e.rs index 733e35136e..45729eb7c9 100644 --- a/tests/subconscious_triggers_e2e.rs +++ b/tests/subconscious_triggers_e2e.rs @@ -486,7 +486,9 @@ async fn scenario_notify_user_delivers_and_persists() { let captured: Arc>> = Arc::new(StdMutex::new(Vec::new())); let sink = Arc::clone(&captured); - let _sub = openhuman_core::core::bus::BUS.get().expect("bus initialized") + let _sub = openhuman_core::core::bus::BUS + .get() + .expect("bus initialized") .on("e2e-notify-capture", move |event| { let sink = Arc::clone(&sink); let event = event.clone(); From f14819bc649fb894856f1050c43477d117a85983 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Sun, 9 Aug 2026 11:50:39 +0300 Subject: [PATCH 02/20] fix(memory): finish the core::event_bus to core::bus migration in guard and binding Co-authored-by: Medulla --- src/openhuman/memory/binding.rs | 4 +- src/openhuman/memory/guard/audit.rs | 61 +++++++++++++++++++- src/openhuman/memory/guard/provider_tests.rs | 42 +++++--------- 3 files changed, 75 insertions(+), 32 deletions(-) diff --git a/src/openhuman/memory/binding.rs b/src/openhuman/memory/binding.rs index 474631e004..74d64429c5 100644 --- a/src/openhuman/memory/binding.rs +++ b/src/openhuman/memory/binding.rs @@ -395,8 +395,8 @@ fn build(workspace_dir: &Path, cfg: &MemorySubsystemConfig) -> MemoryBinding { ); // Sync, and a no-op when the bus is not yet initialized, so this is // safe to call pre-boot with no `#[cfg(test)]` guard. - crate::core::event_bus::publish_global( - crate::core::event_bus::DomainEvent::MemoryDriverBindFailed { + crate::core::bus::BUS.publish( + crate::core::events::DomainEvent::MemoryDriverBindFailed { configured_driver: fallback.configured_driver.clone(), bound_driver: NULL_DRIVER_ID.to_string(), reason: fallback.reason.clone(), diff --git a/src/openhuman/memory/guard/audit.rs b/src/openhuman/memory/guard/audit.rs index 92c105bc23..eb8c5e3795 100644 --- a/src/openhuman/memory/guard/audit.rs +++ b/src/openhuman/memory/guard/audit.rs @@ -30,7 +30,8 @@ use tinycortex_api::capabilities::Capability; -use crate::core::event_bus::{publish_global, DomainEvent}; +use crate::core::bus::BUS; +use crate::core::events::DomainEvent; use crate::openhuman::memory::util::redact::redact; use super::policy::GuardPolicy; @@ -91,7 +92,7 @@ pub fn trace_budget(policy: &GuardPolicy, method: &str, dropped: usize, trimmed_ /// /// Called from [`GuardPolicy::denied`](super::GuardPolicy::denied), so every /// deny path audits by construction rather than by each call site remembering -/// to. `publish_global` is synchronous and a no-op before the bus is +/// to. `BUS.publish` is synchronous and a no-op before the bus is /// initialised, so this is safe pre-boot with no `#[cfg(test)]` guard — the /// same property `binding::build` relies on. pub fn publish_guard_denied(policy: &GuardPolicy, method: &str, reason: &str) { @@ -100,13 +101,67 @@ pub fn publish_guard_denied(policy: &GuardPolicy, method: &str, reason: &str) { policy.driver_id(), policy.class(), ); - publish_global(DomainEvent::MemoryGuardDenied { + #[cfg(test)] + recorder::record(policy.driver_id(), method, reason); + BUS.publish(DomainEvent::MemoryGuardDenied { driver_id: policy.driver_id().to_string(), method: method.to_string(), reason: reason.to_string(), }); } +/// Test-only observation seam for [`publish_guard_denied`]. +/// +/// The global [`BUS`] is a no-op until [`crate::core::bus::init`] runs, and +/// standing one up per test attaches a broker to a runtime that is then torn +/// down — see the runtime-affinity note on `core::bus`. So the deny path also +/// appends here, and a test reads the entries for *its own* driver id rather +/// than assuming it is alone in the process. +#[cfg(test)] +pub(crate) mod recorder { + use std::sync::Mutex; + + /// `(driver_id, method, reason)` for every refusal this test process saw. + static DENIED: Mutex> = Mutex::new(Vec::new()); + + pub(crate) fn record(driver_id: &str, method: &str, reason: &str) { + if let Ok(mut log) = DENIED.lock() { + log.push(( + driver_id.to_string(), + method.to_string(), + reason.to_string(), + )); + } + } + + /// How many refusals have been recorded so far, process-wide. + /// + /// Take this before driving the code under test and pass it to + /// [`denied_for_since`] — sibling tests run in parallel, reuse the same + /// driver ids, and must not see each other's rows. + pub(crate) fn watermark() -> usize { + DENIED.lock().map(|log| log.len()).unwrap_or(0) + } + + /// Refusals for `driver_id` recorded at or after `watermark`. + /// Non-draining: the log is shared, so nothing may consume from it. + pub(crate) fn denied_for_since( + watermark: usize, + driver_id: &str, + ) -> Vec<(String, String, String)> { + DENIED + .lock() + .map(|log| { + log.iter() + .skip(watermark) + .filter(|(id, _, _)| id == driver_id) + .cloned() + .collect() + }) + .unwrap_or_default() + } +} + /// A caller-supplied identifier in a form that is safe to log: an 8-hex-char /// digest, never the value. Use for keys, node ids, and source ids when a log /// line genuinely needs to correlate two calls. diff --git a/src/openhuman/memory/guard/provider_tests.rs b/src/openhuman/memory/guard/provider_tests.rs index f716454630..10ee14c2c0 100644 --- a/src/openhuman/memory/guard/provider_tests.rs +++ b/src/openhuman/memory/guard/provider_tests.rs @@ -13,9 +13,9 @@ use tinycortex_api::provider::{ use tinycortex_api::recall::OwnedRecallOpts; use tinycortex_api::types::{MemoryCategory, MemoryTaint}; -use crate::core::event_bus::{init_global, DomainEvent, DEFAULT_CAPACITY}; use crate::core::subsystem::DriverClass; use crate::openhuman::config::schema::MemoryHooksConfig; +use crate::openhuman::memory::guard::audit::recorder; use crate::openhuman::memory::guard::policy::TRUSTED; use crate::openhuman::memory::guard::test_support::{ embedded_policy, entry, export_record, external_policy, guarded, guarded_with, @@ -195,7 +195,7 @@ async fn guard_does_not_budget_trim_an_export() { #[tokio::test] async fn guard_publishes_memory_guard_denied_on_refusal() { - let mut rx = init_global(DEFAULT_CAPACITY).raw_receiver(); + let watermark = recorder::watermark(); let (driver, guard) = guarded(external_policy("untrusted")); let err = guard .store( @@ -211,19 +211,11 @@ async fn guard_publishes_memory_guard_denied_on_refusal() { assert!(err.to_string().contains("memory guard: ")); assert_eq!(driver.call_count(), 0, "the driver must never be reached"); - let mut seen = None; - while let Ok(event) = rx.try_recv() { - if let DomainEvent::MemoryGuardDenied { - driver_id, - method, - reason, - } = event - { - seen = Some((driver_id, method, reason)); - break; - } - } - let (driver_id, method, reason) = seen.expect("a MemoryGuardDenied event"); + let denied = recorder::denied_for_since(watermark, "supermemory"); + let (driver_id, method, reason) = denied + .first() + .cloned() + .expect("a MemoryGuardDenied audit record"); assert_eq!(driver_id, "supermemory"); assert_eq!(method, "core.store"); assert!(!reason.contains("hello"), "must never carry content"); @@ -231,7 +223,7 @@ async fn guard_publishes_memory_guard_denied_on_refusal() { #[tokio::test] async fn guard_publishes_nothing_on_the_success_path() { - let mut rx = init_global(DEFAULT_CAPACITY).raw_receiver(); + let watermark = recorder::watermark(); let (_driver, guard) = guarded(embedded_policy()); guard .store( @@ -249,16 +241,12 @@ async fn guard_publishes_nothing_on_the_success_path() { .await .expect("recall"); - // Sibling tests share the process-global bus and run in parallel, so - // filter to *this* guard's driver id rather than asserting the channel is + // Sibling tests share the process-wide audit log and run in parallel, so + // filter to *this* guard's driver id rather than asserting the log is // empty — `guard_publishes_memory_guard_denied_on_refusal` legitimately - // publishes one (for `supermemory`) at the same time. - while let Ok(event) = rx.try_recv() { - if let DomainEvent::MemoryGuardDenied { driver_id, .. } = &event { - assert_ne!( - driver_id, "recording", - "a guarded read/write must not publish on success" - ); - } - } + // records one (for `supermemory`) at the same time. + assert!( + recorder::denied_for_since(watermark, "recording").is_empty(), + "a guarded read/write must not publish on success" + ); } From 9eeb3d55122797ee7c0f930f711e4187abccff93 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Sun, 9 Aug 2026 11:50:40 +0300 Subject: [PATCH 03/20] refactor(memory): confine raw profile SQLite behind a typed ProfileStore Co-authored-by: Medulla --- docs/specs/memory-guard-allowlist.md | 67 +++++--- src/openhuman/agent/learning/README.md | 2 +- src/openhuman/agent/learning/cache.rs | 35 ++-- src/openhuman/agent/learning/cache_tests.rs | 4 +- .../agent/learning/profile_md_renderer.rs | 8 +- .../agent/learning/prompt_sections.rs | 8 +- .../agent/learning/prompt_sections_tests.rs | 4 +- src/openhuman/agent/learning/schemas.rs | 8 +- .../agent/learning/stability_detector.rs | 4 +- src/openhuman/agent/learning/startup.rs | 8 +- src/openhuman/agent/learning/tools.rs | 2 +- .../memory/bypass_allowlist_tests.rs | 60 ++++--- src/openhuman/memory/guard/mod.rs | 27 ++- src/openhuman/memory/store/client.rs | 61 ++++--- src/openhuman/memory/store/client_tests.rs | 55 ++++++ src/openhuman/memory/store/mod.rs | 2 + src/openhuman/memory/store/profile_store.rs | 159 ++++++++++++++++++ .../memory/store/profile_store_tests.rs | 120 +++++++++++++ .../memory/sync/composio/providers/profile.rs | 61 ++----- 19 files changed, 535 insertions(+), 160 deletions(-) create mode 100644 src/openhuman/memory/store/profile_store.rs create mode 100644 src/openhuman/memory/store/profile_store_tests.rs diff --git a/docs/specs/memory-guard-allowlist.md b/docs/specs/memory-guard-allowlist.md index c778572608..d213f7d188 100644 --- a/docs/specs/memory-guard-allowlist.md +++ b/docs/specs/memory-guard-allowlist.md @@ -17,7 +17,7 @@ dead-string rot the ratchet exists to prevent. ## Scope -The lint scans `src/` for twelve patterns, keyed on `(file, pattern)` so the +The lint scans `src/` for thirteen patterns, keyed on `(file, pattern)` so the failure message names the needle that tripped: | Pattern | What it hands out | @@ -25,7 +25,8 @@ failure message names the needle that tripped: | `active_memory_client(` | `MemoryClientRef` | | `global::client_if_ready(` / `global::client(` | `MemoryClientRef` | | `.memory_handle(` | raw `Arc` | -| `.profile_conn(` | raw `Arc>` | +| `.profile_conn(` | raw `Arc>` (one in-family site) | +| `.profile_store(` | a typed `ProfileStore` — confined, but still unguarded | | `.get_document(` | `pub(crate)` read-one escape hatch | | `EmbeddedMemoryProvider::new(` / `NullMemoryProvider::new(` | a driver, built outside `binding::for_workspace` | | `MemoryClient::from_workspace_dir(` | a second engine on the same store | @@ -96,22 +97,42 @@ changes anything here. | `core/cli_capability.rs` (`binding::for_workspace(`) | The CLI's capability gate (`kernel.md` §3.3's one exception to "degradation is absence"). Reads the driver id and advertised capability set only — the same two values `memory.provider_status` already returns over RPC — and never reaches memory content. No CLI subcommand except `run`/`serve` builds a `CoreContext`, so `CoreContext::memory()` resolves to nothing and there is no guard to route through. `core/memory_cli.rs` calls `bound_memory_driver_for` rather than binding itself. | | `core/subsystems_cli.rs` | The `openhuman subsystems` slot table. Delegates to `memory_subsystem_status` (which itself resolves the binding in `memory/ops/provider.rs`, already allowlisted above), so `subsystems_cli.rs` never touches `binding::for_workspace(` directly — the CLI's command arms go through `bound_memory_driver_for`. | -### B. Unguardable raw SQLite — `profile_conn()`, out of scope for M4 - -No decorator can wrap an `Arc>`. These reach the -profile / facet tables beneath all seven policy steps. **This is why "the guard -is the only path" is not yet a true invariant.** - -| Path | Sites | -| --- | --- | -| `memory/sync/composio/providers/profile.rs` | 5 | -| `agent/learning/schemas.rs` | 3 | -| `agent/learning/tools.rs` | 1 | -| `agent/learning/startup.rs` | 2 | -| `memory/store/client_tests.rs` | 2 (test) | - -The brief named only the first two files. The other two were found by grep and -are recorded here so M4c starts from the real set. +### B. Profile / facet access — confined, still unguarded + +`MemoryClient::profile_conn()` used to hand a raw +`Arc>` to three domains outside the memory family, +two of which wrote SQL inline at the call site. It is now +`pub(in crate::openhuman::memory)` with a single caller — `profile_store()`, +which wraps it in a typed `ProfileStore` (`memory/store/profile_store.rs`). Every +SQL statement against `user_profile` is inside the memory family, and the +compiler enforces that; `client_tests.rs::profile_conn_is_confined_to_the_memory_family` +restates the rule in a form that names the offending file. + +**That is confinement, not policy.** The contract has no profile/facet +capability family, so these reads and writes still run beneath all seven steps — +no tier check, no source scope, no taint, no redaction, no budget, no audit +event. **This is why "the guard is the only path" is still not a true +invariant.** Closing it needs a fourteenth family in `tinycortex_api`, or a +host-side half-measure where `ProfileStore` consults `GuardPolicy` directly — +which would make a `readonly` tier start rejecting learning-cache rebuilds and +composio identity persistence, a behaviour change with its own blast radius. + +The `.profile_store(` needle exists so the count does not vanish by rename: the +number of unguarded profile call sites did not drop, only their shape changed. + +| Path | Pattern | Sites | +| --- | --- | --- | +| `memory/store/client.rs` | `.profile_conn(` | 1 (the wrap site) | +| `memory/sync/composio/providers/profile.rs` | `.profile_store(` | 4 | +| `agent/learning/schemas.rs` | `.profile_store(` | 3 | +| `agent/learning/tools.rs` | `.profile_store(` | 1 | +| `agent/learning/startup.rs` | `.profile_store(` | 2 | + +A second write path into `user_profile` is **not** covered by either needle: +`agent/harness/archivist/lifecycle.rs` calls `profile::profile_upsert` on a +connection injected at construction. It has no production construction site +today (only `archivist_tests.rs` and `test_constructors.rs` build one), so it is +inert — but it is a fresh unlinted write path the moment anyone wires it up. ### C. Needs a concrete engine type the contract does not expose @@ -146,13 +167,15 @@ module). ## Honest scorecard Four of the twenty-eight `active_memory_client()` call sites now route through -the guard. Eleven non-test `profile_conn()` sites and twelve non-test -`memory_handle()` sites still hand out raw handles. The defensible claim for M4 -is therefore: +the guard. Raw `profile_conn()` no longer leaves the memory family — but the ten +profile/facet call sites it fed are still unguarded, now through a typed +`ProfileStore`, and twelve non-test `memory_handle()` sites still hand out raw +handles. The defensible claim is therefore: > Every memory RPC handler whose contract twin is a literal delegation now > routes through the guard, and every remaining bypass is enumerated here with > a reason and pinned by a drift guard. "Impossible to skip by construction" is **not** true until `memory_handle()` -and `profile_conn()` are gone. +is gone and the profile/facet tables have a capability family to be guarded +against. diff --git a/src/openhuman/agent/learning/README.md b/src/openhuman/agent/learning/README.md index a0b8cce346..795d8edacf 100644 --- a/src/openhuman/agent/learning/README.md +++ b/src/openhuman/agent/learning/README.md @@ -68,7 +68,7 @@ Namespace `learning` (wired into `src/core/all.rs`; 11 controllers). Methods: | `learning.forget_facet` | Mark `Dropped` + `user_state = Forgotten` (blocks re-promotion). | | `learning.reset_cache` | Delete all `Auto` rows, preserve `Pinned`. | -All handlers go through the memory client's `profile_conn()` and a `FacetCache`; `linkedin_enrichment` / `save_profile` load config via `config::rpc::load_config_with_timeout`. +All handlers go through the memory client's `profile_store()` and a `FacetCache`; `linkedin_enrichment` / `save_profile` load config via `config::rpc::load_config_with_timeout`. ## Agent tools diff --git a/src/openhuman/agent/learning/cache.rs b/src/openhuman/agent/learning/cache.rs index 06ba93b4a7..0169accc8c 100644 --- a/src/openhuman/agent/learning/cache.rs +++ b/src/openhuman/agent/learning/cache.rs @@ -4,36 +4,33 @@ //! The stability detector uses this to persist the result of each rebuild cycle. //! Prompt sections use [`FacetCache::list_active`] to read the ambient cache. -use parking_lot::Mutex; -use rusqlite::Connection; -use std::sync::Arc; - use crate::openhuman::agent::learning::candidate::FacetClass; -use crate::openhuman::memory::store::profile::{self, ProfileFacet, UserState}; +use crate::openhuman::memory::store::profile::{ProfileFacet, UserState}; +use crate::openhuman::memory::store::ProfileStore; /// Thin wrapper around the `user_profile` table. /// -/// All methods delegate to the standalone helpers in -/// `memory_store::namespace_store::profile`. This type exists so callers -/// (stability detector, prompt sections, RPCs) share a single typed -/// entry-point that can be constructed from any `Arc>`. +/// A learning-side newtype over [`ProfileStore`], which owns the SQL. This +/// type exists because the class↔key vocabulary below (`FacetClass`) is agent +/// domain knowledge that must not move into the memory family; everything +/// else forwards straight to the store. pub struct FacetCache { - conn: Arc>, + store: ProfileStore, } impl FacetCache { - pub fn new(conn: Arc>) -> Self { - Self { conn } + pub fn new(store: ProfileStore) -> Self { + Self { store } } /// List all facets with `state = 'active'`, ordered by stability descending. pub fn list_active(&self) -> anyhow::Result> { - profile::profile_select_active(&self.conn) + self.store.list_active() } /// List all facets (all states), ordered by stability descending. pub fn list_all(&self) -> anyhow::Result> { - profile::profile_select_all(&self.conn) + self.store.list_all() } /// List active facets belonging to a specific class. @@ -50,31 +47,31 @@ impl FacetCache { /// Fetch a single facet by its full key (e.g. `"style/verbosity"`). pub fn get(&self, key: &str) -> anyhow::Result> { - profile::profile_get_by_key(&self.conn, key) + self.store.get(key) } /// Upsert a fully-formed facet row (rebuild path). pub fn upsert(&self, facet: &ProfileFacet) -> anyhow::Result<()> { - profile::profile_upsert_full(&self.conn, facet) + self.store.upsert_full(facet) } /// Override the `user_state` of a facet. /// /// Returns `Ok(true)` if a row was found and updated. pub fn set_user_state(&self, key: &str, user_state: UserState) -> anyhow::Result { - profile::profile_set_user_state(&self.conn, key, user_state) + self.store.set_user_state(key, user_state) } /// Delete a facet by key. Returns `true` if a row was removed. pub fn delete(&self, key: &str) -> anyhow::Result { - profile::profile_delete_by_key(&self.conn, key) + self.store.delete(key) } /// Delete all `Dropped`-state facets whose stability is below `threshold`. /// /// Pinned facets are never deleted. Returns the number of rows removed. pub fn drop_below_threshold(&self, threshold: f64) -> anyhow::Result { - profile::profile_delete_below_threshold(&self.conn, threshold) + self.store.drop_below_threshold(threshold) } } diff --git a/src/openhuman/agent/learning/cache_tests.rs b/src/openhuman/agent/learning/cache_tests.rs index 1301dfb3d6..04df23c020 100644 --- a/src/openhuman/agent/learning/cache_tests.rs +++ b/src/openhuman/agent/learning/cache_tests.rs @@ -13,7 +13,9 @@ use crate::openhuman::memory::store::profile::{ fn make_cache() -> FacetCache { let conn = Connection::open_in_memory().unwrap(); conn.execute_batch(PROFILE_INIT_SQL).unwrap(); - FacetCache::new(Arc::new(Mutex::new(conn))) + FacetCache::new(crate::openhuman::memory::store::ProfileStore::for_tests( + Arc::new(Mutex::new(conn)), + )) } fn stub_facet(id: &str, key: &str, value: &str, state: FacetState, stability: f64) -> ProfileFacet { diff --git a/src/openhuman/agent/learning/profile_md_renderer.rs b/src/openhuman/agent/learning/profile_md_renderer.rs index 57cf2699a4..67a3b0518e 100644 --- a/src/openhuman/agent/learning/profile_md_renderer.rs +++ b/src/openhuman/agent/learning/profile_md_renderer.rs @@ -41,11 +41,11 @@ use async_trait::async_trait; use crate::core::bus::BUS; use crate::core::events::DomainEvent; -use tinybus::EventHandler; -use tinybus::SubscriptionHandle; use crate::openhuman::agent::learning::cache::FacetCache; use crate::openhuman::integrations::composio::providers::profile_md::replace_managed_block; use crate::openhuman::memory::store::profile::UserState; +use tinybus::EventHandler; +use tinybus::SubscriptionHandle; // ── Class → block metadata ──────────────────────────────────────────────────── @@ -227,7 +227,9 @@ mod tests { use tempfile::TempDir; fn make_cache(conn: Arc>) -> Arc { - Arc::new(FacetCache::new(conn)) + Arc::new(FacetCache::new( + crate::openhuman::memory::store::ProfileStore::for_tests(conn), + )) } fn insert_facet( diff --git a/src/openhuman/agent/learning/prompt_sections.rs b/src/openhuman/agent/learning/prompt_sections.rs index e6d01d03da..725fa8d3dd 100644 --- a/src/openhuman/agent/learning/prompt_sections.rs +++ b/src/openhuman/agent/learning/prompt_sections.rs @@ -389,7 +389,9 @@ mod tests { let conn = Connection::open_in_memory().unwrap(); conn.execute_batch(PROFILE_INIT_SQL).unwrap(); - let cache = FacetCache::new(Arc::new(Mutex::new(conn))); + let cache = FacetCache::new(crate::openhuman::memory::store::ProfileStore::for_tests( + Arc::new(Mutex::new(conn)), + )); let make_facet = |id: &str, key: &str, value: &str, stab: f64| ProfileFacet { facet_id: id.into(), @@ -467,7 +469,9 @@ mod tests { let conn = Connection::open_in_memory().unwrap(); conn.execute_batch(PROFILE_INIT_SQL).unwrap(); - let cache = FacetCache::new(Arc::new(Mutex::new(conn))); + let cache = FacetCache::new(crate::openhuman::memory::store::ProfileStore::for_tests( + Arc::new(Mutex::new(conn)), + )); let result = load_learned_from_cache(&cache); assert!(result.is_empty()); diff --git a/src/openhuman/agent/learning/prompt_sections_tests.rs b/src/openhuman/agent/learning/prompt_sections_tests.rs index 17018d10ed..8aaf578400 100644 --- a/src/openhuman/agent/learning/prompt_sections_tests.rs +++ b/src/openhuman/agent/learning/prompt_sections_tests.rs @@ -15,7 +15,9 @@ use crate::openhuman::memory::store::profile::{ fn open_cache() -> FacetCache { let conn = Connection::open_in_memory().unwrap(); conn.execute_batch(PROFILE_INIT_SQL).unwrap(); - FacetCache::new(Arc::new(Mutex::new(conn))) + FacetCache::new(crate::openhuman::memory::store::ProfileStore::for_tests( + Arc::new(Mutex::new(conn)), + )) } fn make_active(id: &str, key: &str, value: &str, stability: f64) -> ProfileFacet { diff --git a/src/openhuman/agent/learning/schemas.rs b/src/openhuman/agent/learning/schemas.rs index 0c7ebb289f..354e91edea 100644 --- a/src/openhuman/agent/learning/schemas.rs +++ b/src/openhuman/agent/learning/schemas.rs @@ -660,8 +660,7 @@ fn handle_rebuild_cache(_params: Map) -> ControllerFuture { let client = crate::openhuman::memory::global::client_if_ready() .ok_or_else(|| "memory client not ready".to_string())?; - let conn = client.profile_conn(); - let cache = FacetCache::new(conn); + let cache = FacetCache::new(client.profile_store()); let detector = StabilityDetector::new(cache); let now = SystemTime::now() @@ -698,8 +697,7 @@ fn handle_cache_stats(_params: Map) -> ControllerFuture { let client = crate::openhuman::memory::global::client_if_ready() .ok_or_else(|| "memory client not ready".to_string())?; - let conn = client.profile_conn(); - let cache = FacetCache::new(conn); + let cache = FacetCache::new(client.profile_store()); let all_facets = cache .list_all() @@ -760,7 +758,7 @@ fn get_cache() -> Result StabilityDetector { let conn = Connection::open_in_memory().unwrap(); conn.execute_batch(PROFILE_INIT_SQL).unwrap(); - let cache = FacetCache::new(Arc::new(Mutex::new(conn))); + let cache = FacetCache::new(crate::openhuman::memory::store::ProfileStore::for_tests( + Arc::new(Mutex::new(conn)), + )); // Use a private buffer so tests don't interfere with the global singleton. let buffer: &'static Buffer = Box::leak(Box::new(Buffer::new(256))); StabilityDetector { cache, buffer } diff --git a/src/openhuman/agent/learning/startup.rs b/src/openhuman/agent/learning/startup.rs index 4c76bfcdbc..c5ad88361b 100644 --- a/src/openhuman/agent/learning/startup.rs +++ b/src/openhuman/agent/learning/startup.rs @@ -28,9 +28,9 @@ use std::path::Path; use std::sync::OnceLock; -use tinybus::SubscriptionHandle; use crate::openhuman::memory::global::client_if_ready; use crate::openhuman::memory::store::MemoryClientRef; +use tinybus::SubscriptionHandle; static EMAIL_SIG_HANDLE: OnceLock> = OnceLock::new(); @@ -118,7 +118,7 @@ fn register_with_client( use crate::openhuman::agent::learning::scheduler::register_event_trigger; use crate::openhuman::agent::learning::StabilityDetector; use std::sync::Arc; - let cache = FacetCache::new(client.profile_conn()); + let cache = FacetCache::new(client.profile_store()); let detector = Arc::new(StabilityDetector::new(cache)); // Also spawn the periodic rebuild loop (30-minute cadence). let (shutdown_tx, shutdown_rx) = tokio::sync::watch::channel(false); @@ -148,7 +148,7 @@ fn register_with_client( use crate::openhuman::agent::learning::cache::FacetCache; use crate::openhuman::agent::learning::ProfileMdRenderer; use std::sync::Arc; - let cache = Arc::new(FacetCache::new(client.profile_conn())); + let cache = Arc::new(FacetCache::new(client.profile_store())); let renderer = Arc::new(ProfileMdRenderer::new(cache, workspace_dir.to_path_buf())); let handle = ProfileMdRenderer::subscribe(renderer); if handle.is_some() { @@ -167,7 +167,6 @@ fn register_with_client( mod tests { use super::*; use crate::core::events::DomainEvent; -use tinybus::EventBus; use crate::openhuman::agent::learning::candidate::Buffer; use crate::openhuman::agent::learning::extract::signature::{ parse_signature, register_email_signature_subscriber_on, @@ -176,6 +175,7 @@ use tinybus::EventBus; use std::sync::Arc; use std::time::Duration; use tempfile::TempDir; + use tinybus::EventBus; /// Build a real `MemoryClient` against a fresh temp workspace. The temp dir /// is returned so callers keep it alive for the client's lifetime. diff --git a/src/openhuman/agent/learning/tools.rs b/src/openhuman/agent/learning/tools.rs index 0afa14aeb9..106ebad87b 100644 --- a/src/openhuman/agent/learning/tools.rs +++ b/src/openhuman/agent/learning/tools.rs @@ -30,7 +30,7 @@ use crate::openhuman::tools::traits::{PermissionLevel, Tool, ToolResult}; fn get_cache() -> anyhow::Result { let client = crate::openhuman::memory::global::client_if_ready() .ok_or_else(|| anyhow::anyhow!("memory client not ready"))?; - Ok(FacetCache::new(client.profile_conn())) + Ok(FacetCache::new(client.profile_store())) } /// Compose the full facet key from a class string + key suffix. diff --git a/src/openhuman/memory/bypass_allowlist_tests.rs b/src/openhuman/memory/bypass_allowlist_tests.rs index e60c8bb286..96806b2dff 100644 --- a/src/openhuman/memory/bypass_allowlist_tests.rs +++ b/src/openhuman/memory/bypass_allowlist_tests.rs @@ -7,12 +7,16 @@ //! pretend otherwise. `MemoryClient` still hands out raw, undecoratable //! handles, all `pub(crate)` and all with live production callers: //! -//! - `profile_conn()` (`memory/store/client.rs`) — an -//! `Arc>`. **No decorator can wrap a raw SQLite -//! connection**, so the eleven non-test call sites beneath -//! `agent/learning/*` and `memory/sync/composio/providers/profile.rs` reach -//! the profile/facet tables under all of the guard's policy steps. Closing -//! this is explicitly out of scope for M4. +//! - `profile_store()` (`memory/store/client.rs`) — a typed `ProfileStore` +//! over the profile/facet tables. `profile_conn()`, the raw +//! `Arc>` it used to hand out, is now +//! `pub(in crate::openhuman::memory)` with a single in-family caller, so +//! every SQL statement against `user_profile` lives inside the memory +//! family and the compiler keeps it there. **This did not put the profile +//! tables under the guard**: the contract has no profile/facet capability +//! family, so these reads and writes still run beneath all seven policy +//! steps. The `.profile_store(` needle exists to keep that fact counted +//! rather than renamed away. //! - `memory_handle()` (`memory/store/client.rs`) — a raw `Arc`. //! The contract has no `Arc` door, so consumers that must satisfy //! a foreign trait (tinyflows, the agent-experience store) still take it. @@ -89,6 +93,10 @@ const BYPASS_PATTERNS: &[(&str, &str)] = &[ ".profile_conn(", "raw rusqlite connection — undecoratable by construction", ), + ( + ".profile_store(", + "typed profile store — the profile/facet tables have no capability family, so these reads and writes still skip the guard's seven steps", + ), ( ".memory_handle(", "raw Arc — bypasses the MemoryClient API surface", @@ -179,16 +187,16 @@ const ALLOWED: &[(&str, &str, &str)] = &[ ".memory_handle(", "session builder needs Arc; no contract door for it", ), - // ── Unguardable raw SQLite (profile_conn) — the known hole ── + // ── Unguarded (but no longer raw) profile/facet access ── ( "src/openhuman/agent/learning/schemas.rs", - ".profile_conn(", - "raw SQLite profile/facet reads; undecoratable, out of scope for M4", + ".profile_store(", + "typed profile/facet reads; the contract has no profile family, so still unguarded", ), ( "src/openhuman/agent/learning/schemas.rs", "global::client_if_ready(", - "resolved only to reach profile_conn() on the line below", + "resolved only to reach profile_store() on the line below", ), ( "src/openhuman/agent/learning/startup.rs", @@ -197,18 +205,18 @@ const ALLOWED: &[(&str, &str, &str)] = &[ ), ( "src/openhuman/agent/learning/startup.rs", - ".profile_conn(", - "raw SQLite facet bootstrap; undecoratable, out of scope for M4", + ".profile_store(", + "typed facet bootstrap; the contract has no profile family, so still unguarded", ), ( "src/openhuman/agent/learning/tools.rs", - ".profile_conn(", - "raw SQLite facet read from an agent tool; undecoratable, out of scope for M4", + ".profile_store(", + "typed facet read from an agent tool; the contract has no profile family", ), ( "src/openhuman/agent/learning/tools.rs", "global::client_if_ready(", - "resolved only to reach profile_conn() on the line below", + "resolved only to reach profile_store() on the line below", ), // ── Flows: foreign trait shapes and a test-override seam ── ( @@ -354,16 +362,21 @@ const ALLOWED: &[(&str, &str, &str)] = &[ "active_memory_client(", "tool_rule_put/get/*_json/*_for_prompt have no contract equivalent", ), - // ── Composio memory sync: profile_conn + &MemoryClientRef ── ( - "src/openhuman/memory/sync/composio/providers/profile.rs", + "src/openhuman/memory/store/client.rs", ".profile_conn(", - "raw SQLite profile writes; undecoratable, out of scope for M4", + "sole in-family call; wraps the raw handle in ProfileStore. profile_conn is pub(in crate::openhuman::memory), so the compiler — not this lint — is the primary enforcement", + ), + // ── Composio memory sync: profile_store + &MemoryClientRef ── + ( + "src/openhuman/memory/sync/composio/providers/profile.rs", + ".profile_store(", + "typed profile writes; the contract has no profile family, so still unguarded", ), ( "src/openhuman/memory/sync/composio/providers/profile.rs", "global::client_if_ready(", - "resolved only to reach profile_conn()", + "resolved only to reach profile_store()", ), ( "src/openhuman/memory/sync/composio/providers/types.rs", @@ -487,9 +500,10 @@ fn render(pairs: impl IntoIterator) -> String { /// A parser that silently found nothing would turn every other test here into a /// rubber stamp, so refuse to pass vacuously. /// -/// The literal pinned below is the densest known bypass in the tree: five -/// `profile_conn()` calls reaching raw SQLite. If the scanner ever stops seeing -/// it, the scanner is broken — fix it, do not relax this assertion. +/// The literal pinned below is `profile_store()`'s own construction site — a +/// call inside the module that defines the method, so it is the most stable +/// pair available. If the scanner ever stops seeing it, the scanner is broken — +/// fix it, do not relax this assertion. #[test] fn bypass_scanner_finds_the_known_bypasses() { let found = scan(); @@ -499,7 +513,7 @@ fn bypass_scanner_finds_the_known_bypasses() { module would pass vacuously. Fix the scanner, not the assertion." ); let canary = ( - "src/openhuman/memory/sync/composio/providers/profile.rs".to_string(), + "src/openhuman/memory/store/client.rs".to_string(), ".profile_conn(".to_string(), ); assert!( diff --git a/src/openhuman/memory/guard/mod.rs b/src/openhuman/memory/guard/mod.rs index 40ff0aea8d..ff5b36e8da 100644 --- a/src/openhuman/memory/guard/mod.rs +++ b/src/openhuman/memory/guard/mod.rs @@ -58,16 +58,31 @@ //! //! ## Honesty clause: "the guard is the only path" is NOT yet true //! -//! [`MemoryClient::profile_conn`](crate::openhuman::memory::store::MemoryClient::profile_conn) -//! hands out a raw `Arc>`. No decorator can wrap a -//! SQLite connection, so those callers reach the profile/facet tables beneath -//! every one of the seven steps above. It is explicitly out of scope for M4a -//! and must be closed before the invariant may be claimed. Current production -//! callers: +//! `MemoryClient::profile_conn` no longer leaves the memory family: it is +//! `pub(in crate::openhuman::memory)` with one caller, +//! [`MemoryClient::profile_store`](crate::openhuman::memory::store::MemoryClient::profile_store), +//! which wraps it in a typed +//! [`ProfileStore`](crate::openhuman::memory::store::ProfileStore). Every SQL +//! statement against `user_profile` is now inside the family, and the compiler +//! enforces that. +//! +//! **That is confinement, not policy.** The contract has no profile/facet +//! capability family, so `ProfileStore`'s reads and writes still run beneath +//! every one of the seven steps above — no tier check, no source scope, no +//! taint, no redaction, no budget, no audit. Closing *that* needs a fourteenth +//! family in `tinycortex_api`, or a host-side half-measure where `ProfileStore` +//! consults [`policy::GuardPolicy`] directly (which would make a `readonly` +//! tier start rejecting learning-cache rebuilds — a behaviour change, not a +//! refactor). Current unguarded profile callers: //! //! - `memory/sync/composio/providers/profile.rs` //! - `agent/learning/{tools,startup,schemas}.rs` //! +//! A second, independent write path into `user_profile` exists and is *not* +//! covered by the `.profile_store(` needle: `agent/harness/archivist/lifecycle.rs` +//! calls `profile::profile_upsert` on a connection injected at construction. +//! It has no production construction site today. +//! //! `MemoryClient::memory_handle()` is already `pub(crate)`; do not widen it. pub mod audit; diff --git a/src/openhuman/memory/store/client.rs b/src/openhuman/memory/store/client.rs index 0b46502615..f663bc164c 100644 --- a/src/openhuman/memory/store/client.rs +++ b/src/openhuman/memory/store/client.rs @@ -55,19 +55,32 @@ pub struct MemoryClient { } impl MemoryClient { - /// Returns a handle to the underlying SQLite connection for direct - /// profile-facet writes via - /// [`crate::openhuman::memory::store::namespace_store::profile::profile_upsert`]. + /// Returns a handle to the underlying SQLite connection backing the + /// profile/facet tables. /// - /// Intentionally `pub(crate)` — external consumers should use the - /// higher-level `MemoryClient` API; this escape hatch exists so - /// in-crate subsystems (composio providers, archivist, learning - /// hooks) can write structured profile facets without an additional - /// round-trip through the ingestion queue. - pub(crate) fn profile_conn(&self) -> std::sync::Arc> { + /// Narrowed from `pub(crate)` to `pub(in crate::openhuman::memory)`: a raw + /// `Arc>` cannot be wrapped by any decorator, so no + /// caller outside the memory family may hold one. [`Self::profile_store`] + /// is the only door out, and every SQL statement against `user_profile` + /// now lives inside this family. + pub(in crate::openhuman::memory) fn profile_conn( + &self, + ) -> std::sync::Arc> { std::sync::Arc::clone(&self.inner.conn) } + /// Typed access to the profile/facet tables. + /// + /// **Not guarded.** The profile tables have no capability family in the + /// thirteen-family `tinycortex_api` contract, so these reads and writes + /// still run beneath [`crate::openhuman::memory::guard::MemoryGuard`]'s + /// seven steps. What this buys is confinement, not policy: the SQL is in + /// the memory family and the compiler keeps it there. + pub(crate) fn profile_store(&self) -> crate::openhuman::memory::store::ProfileStore { + tracing::debug!("[memory::profile_store] handing out typed profile store"); + crate::openhuman::memory::store::ProfileStore::from_conn(self.profile_conn()) + } + /// Returns an `Arc` handle backed by the same /// [`UnifiedMemory`] this client wraps. Used by sub-systems that /// want to build on top of the `Memory` trait (e.g. the @@ -191,14 +204,12 @@ impl MemoryClient { let queue_depth = state.snapshot().queue_depth; state.mark_running(&placeholder_id, &title, &namespace); - crate::core::bus::BUS.publish( - crate::core::events::DomainEvent::MemoryIngestionStarted { - document_id: placeholder_id.clone(), - title, - namespace: namespace.clone(), - queue_depth, - }, - ); + crate::core::bus::BUS.publish(crate::core::events::DomainEvent::MemoryIngestionStarted { + document_id: placeholder_id.clone(), + title, + namespace: namespace.clone(), + queue_depth, + }); let started = std::time::Instant::now(); let outcome = self.inner.ingest_document(request).await; @@ -214,15 +225,13 @@ impl MemoryClient { success, chrono::Utc::now().timestamp_millis(), ); - crate::core::bus::BUS.publish( - crate::core::events::DomainEvent::MemoryIngestionCompleted { - document_id: placeholder_id, - namespace, - success, - elapsed_ms, - queue_depth: state.snapshot().queue_depth, - }, - ); + crate::core::bus::BUS.publish(crate::core::events::DomainEvent::MemoryIngestionCompleted { + document_id: placeholder_id, + namespace, + success, + elapsed_ms, + queue_depth: state.snapshot().queue_depth, + }); outcome } diff --git a/src/openhuman/memory/store/client_tests.rs b/src/openhuman/memory/store/client_tests.rs index 4696d3003a..f9917b35c4 100644 --- a/src/openhuman/memory/store/client_tests.rs +++ b/src/openhuman/memory/store/client_tests.rs @@ -291,6 +291,61 @@ async fn profile_conn_returns_arc_shared_connection() { assert!(Arc::ptr_eq(&a, &b)); } +/// `profile_conn()` hands out a raw `Arc>` that no decorator +/// can wrap. It is `pub(in crate::openhuman::memory)`, so the compiler already +/// refuses a call from outside the family — this test states the rule in a form +/// that *names the offending file*, because a visibility error at a call site +/// reads as "private method", not as "you are reaching around the guard". +/// +/// Before the typed-store change this reported +/// `agent/learning/{schemas,startup,tools}.rs` (six call sites). +#[test] +fn profile_conn_is_confined_to_the_memory_family() { + fn rs_files_under(dir: &std::path::Path, out: &mut Vec) { + let Ok(entries) = std::fs::read_dir(dir) else { + return; + }; + for entry in entries.flatten() { + let path = entry.path(); + if path.is_dir() { + rs_files_under(&path, out); + } else if path.extension().is_some_and(|e| e == "rs") { + out.push(path); + } + } + } + + let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src"); + let family = root.join("openhuman").join("memory"); + let mut files = Vec::new(); + rs_files_under(&root, &mut files); + + let mut outside = Vec::new(); + for path in files { + if path.starts_with(&family) { + continue; + } + let Ok(text) = std::fs::read_to_string(&path) else { + continue; + }; + for line in text.lines() { + if line.trim_start().starts_with("//") { + continue; + } + if line.contains(".profile_conn(") { + outside.push(path.display().to_string()); + break; + } + } + } + assert!( + outside.is_empty(), + "raw profile connections reached from outside the memory family: {outside:?}\n\ + Use `MemoryClient::profile_store()`; every SQL statement against \ + user_profile belongs inside `crate::openhuman::memory`." + ); +} + #[tokio::test] async fn put_doc_full_pipeline_completes() { // Exercise the full `put_doc` path (vs `put_doc_light`) — the diff --git a/src/openhuman/memory/store/mod.rs b/src/openhuman/memory/store/mod.rs index 73fa06fbbf..c11b2df2cf 100644 --- a/src/openhuman/memory/store/mod.rs +++ b/src/openhuman/memory/store/mod.rs @@ -23,6 +23,7 @@ pub mod entities; pub mod kinds; pub mod kv; pub mod namespace_store; +pub mod profile_store; pub mod retrieval; pub mod safety; pub mod tools; @@ -47,6 +48,7 @@ pub use namespace_store::fts5; pub use namespace_store::profile; pub use namespace_store::segments; pub use namespace_store::UnifiedMemory; +pub use profile_store::ProfileStore; pub use types::{ GraphRelationRecord, MemoryItemKind, MemoryKvRecord, NamespaceDocumentInput, NamespaceMemoryHit, NamespaceQueryResult, NamespaceRetrievalContext, RetrievalScoreBreakdown, diff --git a/src/openhuman/memory/store/profile_store.rs b/src/openhuman/memory/store/profile_store.rs new file mode 100644 index 0000000000..ebcdc0ba2d --- /dev/null +++ b/src/openhuman/memory/store/profile_store.rs @@ -0,0 +1,159 @@ +//! `ProfileStore` — the only typed door onto the `user_profile` table. +//! +//! Before this type existed, `MemoryClient::profile_conn()` handed a raw +//! `Arc>` to three domains outside the memory +//! family (`agent/learning/*`, `memory/sync/composio/providers/profile.rs`), +//! two of which wrote SQL inline at the call site. Every SQL statement against +//! profile/facet rows now lives either here or in +//! [`super::namespace_store::profile`], both inside `crate::openhuman::memory`; +//! callers outside the family hold this handle and never a `Connection`. +//! +//! **This is not a guard win.** The profile/facet tables have no capability +//! family in the `tinycortex_api` contract, so reads and writes through this +//! type still run beneath [`crate::openhuman::memory::guard::MemoryGuard`]'s +//! seven policy steps: no tier check, no source-scope predicate, no taint +//! stamping, no redaction, no budget, no audit event. What changed is the shape +//! of the door — raw SQLite reachable from three domains became one typed store +//! whose confinement the compiler enforces. + +use parking_lot::Mutex; +use rusqlite::{params, Connection}; +use std::sync::Arc; + +use super::namespace_store::profile::{self, FacetType, ProfileFacet, UserState}; + +/// Typed access to the `user_profile` table. +/// +/// Cheap to clone — it is an `Arc` over the same connection `MemoryClient` +/// owns, so clones share one lock. +#[derive(Clone)] +pub struct ProfileStore { + conn: Arc>, +} + +impl ProfileStore { + /// The single production construction site is + /// [`super::MemoryClient::profile_store`]. + pub(in crate::openhuman::memory) fn from_conn(conn: Arc>) -> Self { + Self { conn } + } + + /// Test-only: build a store over a caller-owned in-memory database. + /// + /// Not a hole — the caller already holds the `Connection`; this hands out + /// nothing a `MemoryClient` owns, and it is absent from a release build. + #[cfg(test)] + pub(crate) fn for_tests(conn: Arc>) -> Self { + Self { conn } + } + + // ── Facet-cache surface ─────────────────────────────────────────────── + + /// List all facets with `state = 'active'`, ordered by stability descending. + pub fn list_active(&self) -> anyhow::Result> { + profile::profile_select_active(&self.conn) + } + + /// List all facets (all states), ordered by stability descending. + pub fn list_all(&self) -> anyhow::Result> { + profile::profile_select_all(&self.conn) + } + + /// Fetch a single facet by its full key (e.g. `"style/verbosity"`). + pub fn get(&self, key: &str) -> anyhow::Result> { + profile::profile_get_by_key(&self.conn, key) + } + + /// Upsert a fully-formed facet row (rebuild path). + pub fn upsert_full(&self, facet: &ProfileFacet) -> anyhow::Result<()> { + profile::profile_upsert_full(&self.conn, facet) + } + + /// Override the `user_state` of a facet. `Ok(true)` if a row was updated. + pub fn set_user_state(&self, key: &str, user_state: UserState) -> anyhow::Result { + profile::profile_set_user_state(&self.conn, key, user_state) + } + + /// Delete a facet by key. Returns `true` if a row was removed. + pub fn delete(&self, key: &str) -> anyhow::Result { + profile::profile_delete_by_key(&self.conn, key) + } + + /// Delete all `Dropped`-state facets whose stability is below `threshold`. + pub fn drop_below_threshold(&self, threshold: f64) -> anyhow::Result { + profile::profile_delete_below_threshold(&self.conn, threshold) + } + + // ── Provider-identity surface ───────────────────────────────────────── + + /// Confidence-aware upsert of one provider-sourced facet row. + #[allow(clippy::too_many_arguments)] + pub fn upsert_provider_facet( + &self, + facet_id: &str, + facet_type: &FacetType, + key: &str, + value: &str, + confidence: f64, + segment_id: Option<&str>, + now: f64, + ) -> anyhow::Result<()> { + profile::profile_upsert( + &self.conn, facet_id, facet_type, key, value, confidence, segment_id, now, + ) + } + + /// Load every facet of `facet_type`, ordered by evidence count descending. + pub fn facets_by_type(&self, facet_type: &FacetType) -> anyhow::Result> { + profile::profile_facets_by_type(&self.conn, facet_type) + } + + /// True if any [`FacetType::Workflow`] (`"skill"`) row's key matches + /// `key_pattern` (a SQL `LIKE` pattern) with exactly `canonical_value`. + /// + /// Encapsulates the two hand-rolled `SELECT 1 … LIKE` queries the composio + /// provider used to write inline. Deliberately infallible: the callers are + /// "is this row the user?" predicates whose only sane answer on a database + /// error is "no", which is what the raw `.is_ok()` gave before. + pub fn skill_identity_matches(&self, key_pattern: &str, canonical_value: &str) -> bool { + let conn = self.conn.lock(); + let matched = conn + .query_row( + "SELECT 1 FROM user_profile + WHERE facet_type = ?1 + AND key LIKE ?2 + AND value = ?3 + LIMIT 1", + params![FacetType::Workflow.as_str(), key_pattern, canonical_value], + |_| Ok(()), + ) + .is_ok(); + // Facet values are user PII (emails, phone numbers, handles) — log the + // pattern and the verdict, never the value. + tracing::debug!( + pattern = %key_pattern, + matched, + "[memory::profile_store] skill_identity_matches" + ); + matched + } + + /// Delete exactly one row by `facet_id`. `Ok(true)` if a row was removed. + pub fn delete_by_facet_id(&self, facet_id: &str) -> anyhow::Result { + let conn = self.conn.lock(); + let removed = conn.execute( + "DELETE FROM user_profile WHERE facet_id = ?1", + params![facet_id], + )?; + tracing::debug!( + facet_id = %facet_id, + removed, + "[memory::profile_store] delete_by_facet_id" + ); + Ok(removed > 0) + } +} + +#[cfg(test)] +#[path = "profile_store_tests.rs"] +mod tests; diff --git a/src/openhuman/memory/store/profile_store_tests.rs b/src/openhuman/memory/store/profile_store_tests.rs new file mode 100644 index 0000000000..567cb141f2 --- /dev/null +++ b/src/openhuman/memory/store/profile_store_tests.rs @@ -0,0 +1,120 @@ +//! Tests for [`ProfileStore`]. +//! +//! The two interesting methods are the ones that replaced hand-rolled SQL in +//! `memory/sync/composio/providers/profile.rs`. A subtly wrong reimplementation +//! of `skill_identity_matches` makes the entity matcher stop recognising the +//! user, which degrades silently rather than erroring — so the oracle here is +//! the literal SQL that was replaced, executed against the same connection, +//! rather than my reading of it. + +use super::*; +use crate::openhuman::memory::store::profile::PROFILE_INIT_SQL; + +fn seeded_store() -> ProfileStore { + let conn = Connection::open_in_memory().unwrap(); + conn.execute_batch(PROFILE_INIT_SQL).unwrap(); + let store = ProfileStore::for_tests(Arc::new(Mutex::new(conn))); + + let rows = [ + ( + "skill-gmail-default-email", + "skill:gmail:default:email", + "user@example.com", + ), + ( + "skill-slack-c123-handle", + "skill:slack:c123:handle", + "userhandle", + ), + ( + "skill-slack-c123-email", + "skill:slack:c123:email", + "work@example.com", + ), + ]; + for (facet_id, key, value) in rows { + store + .upsert_provider_facet( + facet_id, + &FacetType::Workflow, + key, + value, + 0.9, + None, + 1000.0, + ) + .unwrap(); + } + store +} + +/// The exact query string from the pre-refactor +/// `is_self_identity` / `is_self_identity_any_toolkit`, run here so the +/// assertion compares against the code that was replaced. +fn legacy_like_query(store: &ProfileStore, key_pattern: &str, canonical: &str) -> bool { + let conn = store.conn.lock(); + conn.query_row( + "SELECT 1 FROM user_profile + WHERE facet_type = 'skill' + AND key LIKE ?1 + AND value = ?2 + LIMIT 1", + params![key_pattern, canonical], + |_| Ok(()), + ) + .is_ok() +} + +#[test] +fn skill_identity_matches_agrees_with_the_legacy_like_query() { + let store = seeded_store(); + let cases = [ + ("skill:gmail:%:email", "user@example.com"), // exact toolkit hit + ("skill:slack:%:email", "user@example.com"), // wrong toolkit + ("skill:%:%:email", "user@example.com"), // cross-toolkit hit + ("skill:%:%:email", "other@example.com"), // value miss + ("skill:%:%:phone", "user@example.com"), // kind miss + ("skill:gmail:%:handle", ""), // empty value + ("skill:slack:%:handle", "userhandle"), // second toolkit hit + ]; + for (pattern, value) in cases { + let legacy = legacy_like_query(&store, pattern, value); + assert_eq!( + store.skill_identity_matches(pattern, value), + legacy, + "divergence for pattern={pattern:?} value={value:?}" + ); + } + // Non-vacuity: at least one case must actually be a hit, or the loop above + // would pass with a method that always returns false. + assert!(store.skill_identity_matches("skill:%:%:email", "user@example.com")); +} + +#[test] +fn delete_by_facet_id_removes_exactly_one_row() { + let store = seeded_store(); + assert_eq!(store.facets_by_type(&FacetType::Workflow).unwrap().len(), 3); + + assert!(store.delete_by_facet_id("skill-slack-c123-email").unwrap()); + + let survivors = store.facets_by_type(&FacetType::Workflow).unwrap(); + let ids: Vec<&str> = survivors.iter().map(|f| f.facet_id.as_str()).collect(); + assert_eq!(survivors.len(), 2, "deleted more than one row: {ids:?}"); + assert!(ids.contains(&"skill-gmail-default-email"), "{ids:?}"); + assert!(ids.contains(&"skill-slack-c123-handle"), "{ids:?}"); + + assert!( + !store.delete_by_facet_id("skill-does-not-exist").unwrap(), + "deleting an unknown facet_id must report false" + ); +} + +#[test] +fn facet_cache_surface_round_trips_through_the_store() { + let store = seeded_store(); + let facet = store.get("skill:gmail:default:email").unwrap(); + assert_eq!(facet.map(|f| f.value).as_deref(), Some("user@example.com")); + assert_eq!(store.list_all().unwrap().len(), 3); + assert!(store.delete("skill:gmail:default:email").unwrap()); + assert_eq!(store.list_all().unwrap().len(), 2); +} diff --git a/src/openhuman/memory/sync/composio/providers/profile.rs b/src/openhuman/memory/sync/composio/providers/profile.rs index 5fe0768847..06d7e2d880 100644 --- a/src/openhuman/memory/sync/composio/providers/profile.rs +++ b/src/openhuman/memory/sync/composio/providers/profile.rs @@ -21,8 +21,7 @@ use super::ProviderUserProfile; use crate::openhuman::agent::learning::candidate::{ self as learning_candidate, CueFamily, EvidenceRef, FacetClass, LearningCandidate, }; -use crate::openhuman::memory::store::profile::{self, FacetType}; -use rusqlite::params; +use crate::openhuman::memory::store::profile::FacetType; use serde_json::Value; use std::collections::BTreeMap; @@ -136,7 +135,7 @@ pub fn persist_provider_profile(profile: &ProviderUserProfile) -> usize { ); return 0; }; - let conn = client.profile_conn(); + let store = client.profile_store(); let now = now_secs(); let toolkit = normalize_token(&profile.toolkit); @@ -154,8 +153,7 @@ pub fn persist_provider_profile(profile: &ProviderUserProfile) -> usize { let key = format!("skill:{toolkit}:{identifier}:{}", kind.as_str()); let facet_id = format!("skill-{toolkit}-{identifier}-{}", kind.as_str()); - if let Err(e) = profile::profile_upsert( - &conn, + if let Err(e) = store.upsert_provider_facet( &facet_id, &FacetType::Workflow, &key, @@ -284,8 +282,7 @@ pub fn load_connected_identities() -> Vec { tracing::debug!("[composio:profile] load_connected_identities: memory client not ready"); return Vec::new(); }; - let conn = client.profile_conn(); - let facets = match profile::profile_facets_by_type(&conn, &FacetType::Workflow) { + let facets = match client.profile_store().facets_by_type(&FacetType::Workflow) { Ok(f) => f, Err(error) => { tracing::warn!( @@ -338,20 +335,10 @@ pub fn is_self_identity(toolkit: &str, kind: IdentityKind, raw_value: &str) -> b let Some(client) = crate::openhuman::memory::global::client_if_ready() else { return false; }; - let conn = client.profile_conn(); - let conn = conn.lock(); - let key_pattern = format!("skill:{}:%:{}", normalize_token(toolkit), kind.as_str()); - conn.query_row( - "SELECT 1 FROM user_profile - WHERE facet_type = 'skill' - AND key LIKE ?1 - AND value = ?2 - LIMIT 1", - params![key_pattern, canonical], - |_| Ok(()), - ) - .is_ok() + client + .profile_store() + .skill_identity_matches(&key_pattern, &canonical) } /// Cross-toolkit variant — matches against every connected provider's @@ -368,20 +355,10 @@ pub fn is_self_identity_any_toolkit(kind: IdentityKind, raw_value: &str) -> bool let Some(client) = crate::openhuman::memory::global::client_if_ready() else { return false; }; - let conn = client.profile_conn(); - let conn = conn.lock(); - let key_pattern = format!("skill:%:%:{}", kind.as_str()); - conn.query_row( - "SELECT 1 FROM user_profile - WHERE facet_type = 'skill' - AND key LIKE ?1 - AND value = ?2 - LIMIT 1", - params![key_pattern, canonical], - |_| Ok(()), - ) - .is_ok() + client + .profile_store() + .skill_identity_matches(&key_pattern, &canonical) } /// Render a compact section for prompt injection. Skips `user_id` (not @@ -451,8 +428,8 @@ pub fn delete_connected_identity_facets(source: &str, identifier: &str) -> usize ); return 0; }; - let conn = client.profile_conn(); - let Ok(facets) = profile::profile_facets_by_type(&conn, &FacetType::Workflow) else { + let store = client.profile_store(); + let Ok(facets) = store.facets_by_type(&FacetType::Workflow) else { return 0; }; let mut deleted = 0usize; @@ -461,15 +438,9 @@ pub fn delete_connected_identity_facets(source: &str, identifier: &str) -> usize continue; }; if s == source && i == identifier { - let conn_guard = conn.lock(); - if conn_guard - .execute( - "DELETE FROM user_profile WHERE facet_id = ?1", - params![facet.facet_id], - ) - .unwrap_or(0) - > 0 - { + // Same swallow as before: a disconnect must not fail because one + // row was already gone. + if store.delete_by_facet_id(&facet.facet_id).unwrap_or(false) { deleted += 1; } } @@ -538,7 +509,7 @@ fn now_secs() -> f64 { #[cfg(test)] mod tests { use super::*; - use crate::openhuman::memory::store::profile::{profile_load_all, PROFILE_INIT_SQL}; + use crate::openhuman::memory::store::profile::{self, profile_load_all, PROFILE_INIT_SQL}; use parking_lot::Mutex; use rusqlite::Connection; use serde_json::json; From f6eb3e00167941fa2afdcb7af59c887c55f66322 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Sun, 9 Aug 2026 12:13:35 +0300 Subject: [PATCH 04/20] refactor(memory): route the tool_memory agent tools through the guard Co-authored-by: Medulla --- docs/specs/memory-guard-allowlist.md | 17 +- .../memory/bypass_allowlist_tests.rs | 21 -- src/openhuman/memory/ops/tool_memory.rs | 5 +- .../memory/tool_memory/tools/list.rs | 28 ++- src/openhuman/memory/tool_memory/tools/put.rs | 180 ++++++++++++++++-- 5 files changed, 207 insertions(+), 44 deletions(-) diff --git a/docs/specs/memory-guard-allowlist.md b/docs/specs/memory-guard-allowlist.md index d213f7d188..8506ec02e5 100644 --- a/docs/specs/memory-guard-allowlist.md +++ b/docs/specs/memory-guard-allowlist.md @@ -63,6 +63,17 @@ store: | `tool_memory::tool_rule_list` | `MemoryToolMemory::tool_rules` | `tool_memory_store(memory).list_rules(tool)` | | `tool_memory::tool_rule_delete` | `MemoryToolMemory::delete_tool_rule` | `tool_memory_store(memory).delete_rule(tool, id)` | +Two **agent tools** followed the same route: + +| Tool | Contract method | Note | +| --- | --- | --- | +| `memory_tools_list` | `MemoryToolMemory::tool_rules` | 1:1 — same rules, same order, same serialization. | +| `memory_tools_put` | `MemoryToolMemory::put_tool_rule` + `tool_rules` | The contract method returns unit while the tool answers with the *stored* rule, so the write is followed by a read-back on the id `ToolMemoryRule::new` generated before the write. Exact, not lossy: there is no server-assigned identity, and `tool_memory_namespace` normalises the caller's raw `tool_name` the same way the write did. A concurrent delete in that window errors rather than fabricating a rule. | + +`memory_tools_put` therefore now refuses under the `readonly` autonomy tier +with `"memory guard: "`-prefixed text, and store-level validation errors arrive +as `MemoryError::Invalid` rather than as a raw string. Both are intended. + **Three deltas ride along, and they are the point of the milestone, not accidents:** @@ -142,7 +153,6 @@ inert — but it is a fresh unlinted write path the moment anyone wires it up. | `agent/harness/session/builder/factory.rs` | `.memory_handle()` → `Arc`. | | `flows/tinyflows/memory_adapter.rs` | Returns `Arc` to satisfy a tinyflows engine trait. The contract has no `Arc` door. | | `flows/bus.rs` | `resolve_memory() -> Option>`, and carries a `#[cfg(test)] memory_override` seam a guard would bypass. | -| `memory/tool_memory/tools/list.rs`, `tools/put.rs` | Agent tools building `ToolMemoryStore` from `memory_handle()`. Re-pointable in principle via `as_tool_memory()` — **deferred to M5**, which filters the tool surface by capability and would collide with a re-point made now. | | `memory/ops/tool_memory.rs` (`open_store`) | Still needed by the four handlers left on the client. Shrank; did not disappear. | ### D. No contract method exists, or the wire shape would change @@ -166,8 +176,9 @@ module). ## Honest scorecard -Four of the twenty-eight `active_memory_client()` call sites now route through -the guard. Raw `profile_conn()` no longer leaves the memory family — but the ten +Six of the twenty-eight `active_memory_client()` call sites now route through +the guard — four RPC handlers plus the `memory_tools_list` / `memory_tools_put` +agent tools. Raw `profile_conn()` no longer leaves the memory family — but the ten profile/facet call sites it fed are still unguarded, now through a typed `ProfileStore`, and twelve non-test `memory_handle()` sites still hand out raw handles. The defensible claim is therefore: diff --git a/src/openhuman/memory/bypass_allowlist_tests.rs b/src/openhuman/memory/bypass_allowlist_tests.rs index 96806b2dff..4ad5c50211 100644 --- a/src/openhuman/memory/bypass_allowlist_tests.rs +++ b/src/openhuman/memory/bypass_allowlist_tests.rs @@ -399,27 +399,6 @@ const ALLOWED: &[(&str, &str, &str)] = &[ "global::client_if_ready(", "the TinyCortex engine seam; it sits beneath the contract, not above it", ), - // ── Agent tools — deferred to M5's capability filter ── - ( - "src/openhuman/memory/tool_memory/tools/list.rs", - ".memory_handle(", - "builds ToolMemoryStore; re-pointable via as_tool_memory(), deferred to M5", - ), - ( - "src/openhuman/memory/tool_memory/tools/list.rs", - "active_memory_client(", - "same tool; M5 filters the tool surface by capability and would collide", - ), - ( - "src/openhuman/memory/tool_memory/tools/put.rs", - ".memory_handle(", - "builds ToolMemoryStore; re-pointable via as_tool_memory(), deferred to M5", - ), - ( - "src/openhuman/memory/tool_memory/tools/put.rs", - "active_memory_client(", - "same tool; M5 filters the tool surface by capability and would collide", - ), ]; /// True for source files the lint deliberately does not scan. diff --git a/src/openhuman/memory/ops/tool_memory.rs b/src/openhuman/memory/ops/tool_memory.rs index a87845872e..eab5145b38 100644 --- a/src/openhuman/memory/ops/tool_memory.rs +++ b/src/openhuman/memory/ops/tool_memory.rs @@ -116,7 +116,10 @@ pub async fn tool_rule_get( /// A driver that does not advertise `Capability::ToolMemory` returns `None` /// from `as_tool_memory()`; the embedded driver always advertises it, so this /// is reachable only under a null / fallback binding. -const NO_TOOL_MEMORY: &str = "memory driver does not support the tool_memory family"; +/// +/// Shared with the `memory_tools_list` / `memory_tools_put` agent tools, which +/// route through the same family. +pub(crate) const NO_TOOL_MEMORY: &str = "memory driver does not support the tool_memory family"; /// List every tool-scoped rule for a tool. /// diff --git a/src/openhuman/memory/tool_memory/tools/list.rs b/src/openhuman/memory/tool_memory/tools/list.rs index ef3b996f57..65cda588a3 100644 --- a/src/openhuman/memory/tool_memory/tools/list.rs +++ b/src/openhuman/memory/tool_memory/tools/list.rs @@ -1,11 +1,21 @@ //! `memory_tools_list` — list every stored rule for a given tool. +//! +//! Routed through [`MemoryGuard`](crate::openhuman::memory::guard::MemoryGuard) +//! rather than a raw `ToolMemoryStore`. `MemoryToolMemory::tool_rules` on the +//! embedded driver is literally `tool_memory_store(self.memory()).list_rules(…)`, +//! and the wire type matches by identity, not conversion: +//! `memory::tool_memory::ToolMemoryRule` **is** +//! `tinycortex_api::tool_memory::ToolMemoryRule`. So the re-point is exact — +//! same rules, same order, same serialization — with `Capability::ToolMemory` +//! admitted first. use async_trait::async_trait; use serde::Deserialize; use serde_json::json; +use tinycortex_api::provider::MemoryProvider; -use crate::openhuman::memory::ops::helpers::active_memory_client; -use crate::openhuman::memory::tool_memory::tool_memory_store; +use crate::openhuman::memory::ops::guard::active_memory_guard; +use crate::openhuman::memory::ops::tool_memory::NO_TOOL_MEMORY; use crate::openhuman::tools::traits::{Tool, ToolResult}; pub struct MemoryToolsListTool; @@ -45,14 +55,20 @@ impl Tool for MemoryToolsListTool { let parsed: Args = serde_json::from_value(args) .map_err(|e| anyhow::anyhow!("invalid arguments for memory_tools_list: {e}"))?; log::debug!("[tool][memory_tools] list tool_name={}", parsed.tool_name); - let client = active_memory_client() + let guard = active_memory_guard() .await .map_err(|e| anyhow::anyhow!("memory_tools_list: {e}"))?; - let store = tool_memory_store(client.memory_handle()); - let rules = store - .list_rules(&parsed.tool_name) + let rules = guard + .as_tool_memory() + .ok_or_else(|| anyhow::anyhow!("memory_tools_list: {NO_TOOL_MEMORY}"))? + .tool_rules(&parsed.tool_name) .await .map_err(|e| anyhow::anyhow!("memory_tools_list: {e}"))?; + log::debug!( + "[tool][memory_tools] list via guard tool_name={} rules={}", + parsed.tool_name, + rules.len() + ); let json = serde_json::to_string(&rules)?; Ok(ToolResult::success(json)) } diff --git a/src/openhuman/memory/tool_memory/tools/put.rs b/src/openhuman/memory/tool_memory/tools/put.rs index a807c58b26..267b525447 100644 --- a/src/openhuman/memory/tool_memory/tools/put.rs +++ b/src/openhuman/memory/tool_memory/tools/put.rs @@ -1,13 +1,34 @@ //! `memory_tools_put` — upsert a tool-scoped memory rule. +//! +//! Routed through [`MemoryGuard`](crate::openhuman::memory::guard::MemoryGuard). +//! `MemoryToolMemory::put_tool_rule` delegates to the same +//! `ToolMemoryStore::put_rule` this tool used to build by hand, with one +//! asymmetry: the contract method returns unit while the store returns the +//! *stored* rule (trim/lower-cased `tool_name`, `created_at` preserved on +//! upsert, `updated_at` refreshed) — which is what this tool answers with. The +//! asymmetry is recovered exactly by reading the rule back: +//! `ToolMemoryRule::new` always generates the id before the write, so there is +//! no server-assigned identity to lose, and `tool_memory_namespace` applies the +//! same `trim().to_lowercase()` the write normalised into, so reading back with +//! the caller's raw `tool_name` hits the same namespace. +//! +//! A concurrent delete between the write and the read-back yields no rule. That +//! answers with an error, never a fabricated rule — absence, not a lie. +//! +//! **Behaviour change, deliberate:** the write now takes +//! `SecurityPolicy::enforce_write_tier`, so the tool is refused under the +//! `readonly` autonomy tier with `"memory guard: "`-prefixed text, and +//! store-level validation errors arrive as `MemoryError::Invalid` rather than as +//! a raw string. use async_trait::async_trait; use serde::Deserialize; use serde_json::json; +use tinycortex_api::provider::MemoryProvider; -use crate::openhuman::memory::ops::helpers::active_memory_client; -use crate::openhuman::memory::tool_memory::{ - tool_memory_store, ToolMemoryPriority, ToolMemoryRule, ToolMemorySource, -}; +use crate::openhuman::memory::ops::guard::active_memory_guard; +use crate::openhuman::memory::ops::tool_memory::NO_TOOL_MEMORY; +use crate::openhuman::memory::tool_memory::{ToolMemoryPriority, ToolMemoryRule, ToolMemorySource}; use crate::openhuman::tools::traits::{Tool, ToolResult}; pub struct MemoryToolsPutTool; @@ -79,10 +100,12 @@ impl Tool for MemoryToolsPutTool { parsed.priority, parsed.tags.len() ); - let client = active_memory_client() + let guard = active_memory_guard() .await .map_err(|e| anyhow::anyhow!("memory_tools_put: {e}"))?; - let store = tool_memory_store(client.memory_handle()); + let family = guard + .as_tool_memory() + .ok_or_else(|| anyhow::anyhow!("memory_tools_put: {NO_TOOL_MEMORY}"))?; let mut rule = ToolMemoryRule::new( &parsed.tool_name, &parsed.rule, @@ -90,10 +113,29 @@ impl Tool for MemoryToolsPutTool { ToolMemorySource::UserExplicit, ); rule.tags = parsed.tags; - let stored = store - .put_rule(rule) + let rule_id = rule.id.clone(); + let tool_name = rule.tool_name.clone(); + family + .put_tool_rule(rule) .await .map_err(|e| anyhow::anyhow!("memory_tools_put: {e}"))?; + // `put_tool_rule` answers with unit; the tool's contract is the stored + // rule (normalised tool_name, preserved created_at, refreshed + // updated_at), so read it back by the id generated above. + let stored = family + .tool_rules(&tool_name) + .await + .map_err(|e| anyhow::anyhow!("memory_tools_put: {e}"))? + .into_iter() + .find(|r| r.id == rule_id) + .ok_or_else(|| { + anyhow::anyhow!("memory_tools_put: stored rule {rule_id} not found on read-back") + })?; + log::debug!( + "[tool][memory_tools] put via guard tool_name={} id={} read_back=ok", + stored.tool_name, + stored.id + ); let json = serde_json::to_string(&stored)?; Ok(ToolResult::success(json)) } @@ -107,9 +149,27 @@ mod tests { use tempfile::TempDir; use crate::openhuman::config::{Config, TEST_ENV_LOCK}; - use crate::openhuman::memory::tool_memory::tool_memory_store; + use crate::openhuman::memory::guard::policy::GUARD_DENIED_PREFIX; + use crate::openhuman::security::live_policy; + use crate::openhuman::security::policy::{AutonomyLevel, SecurityPolicy}; use crate::openhuman::tools::traits::Tool; use serde_json::json; + use std::sync::Arc; + + /// Install `autonomy` as the live policy for this test thread only. Same + /// shape `memory/guard/policy_tests.rs` uses; `#[tokio::test]`'s + /// current-thread runtime keeps the future on the installing thread. + fn scoped_tier(autonomy: AutonomyLevel) -> live_policy::TestPolicyGuard { + let dir = std::env::temp_dir(); + live_policy::install_scoped( + Arc::new(SecurityPolicy { + autonomy, + ..SecurityPolicy::default() + }), + dir.clone(), + dir, + ) + } struct WorkspaceEnvGuard { _lock: std::sync::MutexGuard<'static, ()>, @@ -237,11 +297,15 @@ mod tests { assert_eq!(parsed["tags"], json!(["safety", "shell"])); assert!(parsed["id"].as_str().is_some()); - let client = crate::openhuman::memory::ops::helpers::active_memory_client() + let guard = crate::openhuman::memory::ops::guard::active_memory_guard() + .await + .expect("active memory guard"); + let rules = guard + .as_tool_memory() + .expect("embedded driver advertises the tool_memory family") + .tool_rules("bash") .await - .expect("active memory client"); - let store = tool_memory_store(client.memory_handle()); - let rules = store.list_rules("bash").await.expect("list stored rules"); + .expect("list stored rules"); let stored = rules .iter() .find(|rule| rule.rule == "Always dry-run dangerous commands first") @@ -273,4 +337,94 @@ mod tests { serde_json::from_str(&result.text()).expect("tool result should be json"); assert_eq!(parsed["priority"], "normal"); } + + /// The behavioural discriminator for the re-point: before it, the tool + /// wrote through an undecorated `MemoryClientRef` and no tier check ran, so + /// a `readonly` agent could still pin rules. Through the guard, + /// `admit_write` calls `enforce_write_tier` first. + #[tokio::test] + async fn execute_is_refused_under_the_readonly_tier() { + let _serial = crate::openhuman::memory::ops::GLOBAL_MEMORY_TEST_LOCK + .lock() + .await; + let tmp = TempDir::new().expect("tempdir"); + let (_workspace, _cfg) = isolated_config(&tmp).await; + let _tier = scoped_tier(AutonomyLevel::ReadOnly); + let tool = MemoryToolsPutTool; + let err = tool + .execute(json!({ + "tool_name": "bash", + "rule": "readonly agents must not pin rules" + })) + .await + .expect_err("the readonly tier must refuse a tool-memory write"); + let message = err.to_string(); + assert!( + message.contains(GUARD_DENIED_PREFIX), + "refusal must be attributable to the guard: {message}" + ); + } + + /// The paired positive case: the same call under `full` succeeds, so the + /// test above is proving the tier gate rather than a broken write path. + #[tokio::test] + async fn execute_succeeds_under_the_full_tier() { + let _serial = crate::openhuman::memory::ops::GLOBAL_MEMORY_TEST_LOCK + .lock() + .await; + let tmp = TempDir::new().expect("tempdir"); + let (_workspace, _cfg) = isolated_config(&tmp).await; + let _tier = scoped_tier(AutonomyLevel::Full); + let tool = MemoryToolsPutTool; + let result = tool + .execute(json!({ + "tool_name": "bash", + "rule": "full-tier agents may pin rules" + })) + .await + .expect("the full tier must admit a tool-memory write"); + assert!(!result.is_error); + } + + /// `memory_tools_put` and `memory_tools_list` must observe each other now + /// that both resolve through the guard rather than through their own + /// `ToolMemoryStore` handles. + #[tokio::test] + async fn guarded_put_and_guarded_list_share_the_store() { + let _serial = crate::openhuman::memory::ops::GLOBAL_MEMORY_TEST_LOCK + .lock() + .await; + let tmp = TempDir::new().expect("tempdir"); + let (_workspace, _cfg) = isolated_config(&tmp).await; + let put = MemoryToolsPutTool; + let stored = put + .execute(json!({ + "tool_name": "web_search", + "rule": "prefer primary sources", + "priority": "critical" + })) + .await + .expect("put should succeed"); + let stored: serde_json::Value = + serde_json::from_str(&stored.text()).expect("put result should be json"); + let stored_id = stored["id"].as_str().expect("stored id").to_string(); + + let list = super::super::list::MemoryToolsListTool; + let listed = list + .execute(json!({ "tool_name": "web_search" })) + .await + .expect("list should succeed"); + let listed: serde_json::Value = + serde_json::from_str(&listed.text()).expect("list result should be json"); + let ids: Vec<&str> = listed + .as_array() + .expect("list returns an array") + .iter() + .filter_map(|r| r["id"].as_str()) + .collect(); + assert!( + ids.contains(&stored_id.as_str()), + "the guarded list must observe the guarded put: {ids:?}" + ); + } } From faa54cc21a81b06bb522ecc6964d415818c0719d Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Sun, 9 Aug 2026 12:17:51 +0300 Subject: [PATCH 05/20] refactor(memory): drop the memory_diff types re-export shim Delete src/openhuman/memory/diff/types.rs, which was a 19-line pub-use of tinycortex::memory::diff. Importers now name the crate module directly. Pure re-point: type identity is unchanged. Co-authored-by: Medulla --- src/openhuman/memory/diff/mod.rs | 11 ++++++----- src/openhuman/memory/diff/ops.rs | 2 +- src/openhuman/memory/diff/rpc.rs | 2 +- src/openhuman/memory/diff/tools.rs | 2 +- src/openhuman/memory/diff/types.rs | 19 ------------------- src/openhuman/memory/driver/embedded/diff.rs | 4 ++-- src/openhuman/subconscious/profiles/memory.rs | 8 ++++---- .../subconscious/profiles/memory_tests.rs | 2 +- 8 files changed, 16 insertions(+), 34 deletions(-) delete mode 100644 src/openhuman/memory/diff/types.rs diff --git a/src/openhuman/memory/diff/mod.rs b/src/openhuman/memory/diff/mod.rs index b2ba908d3b..06190d5dc2 100644 --- a/src/openhuman/memory/diff/mod.rs +++ b/src/openhuman/memory/diff/mod.rs @@ -17,8 +17,10 @@ //! `tinycortex::memory::diff::DiffEngine` (a byte-identical port over the same //! `/memory_diff/repo` git layout). This module is a thin host shim: //! [`ops`] async-wraps the engine, [`source`] supplies the chunk-store item -//! seam (`DiffEngine`'s `SnapshotItemSource`), [`types`] re-exports the crate -//! wire types, and [`rpc`]/[`schemas`]/[`tools`] keep the RPC + agent surface. +//! seam (`DiffEngine`'s `SnapshotItemSource`), and [`rpc`]/[`schemas`]/[`tools`] +//! keep the RPC + agent surface. The wire types are the crate's, named directly +//! (`tinycortex::memory::diff::types`) rather than through a host re-export +//! module. //! //! Features: //! - Per-source snapshots (auto after sync, or manual via RPC) @@ -31,14 +33,13 @@ pub mod rpc; pub mod schemas; pub mod source; pub mod tools; -pub mod types; pub use schemas::{ all_controller_schemas as all_memory_diff_controller_schemas, all_registered_controllers as all_memory_diff_registered_controllers, }; -pub use tools::MemoryDiffTool; -pub use types::{ +pub use tinycortex::memory::diff::types::{ ChangeKind, Checkpoint, CrossSourceDiff, DiffResult, DiffSummary, ItemChange, Snapshot, SnapshotTrigger, }; +pub use tools::MemoryDiffTool; diff --git a/src/openhuman/memory/diff/ops.rs b/src/openhuman/memory/diff/ops.rs index 3236e38c5b..9e9ed5aebc 100644 --- a/src/openhuman/memory/diff/ops.rs +++ b/src/openhuman/memory/diff/ops.rs @@ -16,7 +16,7 @@ use crate::openhuman::memory::sources::types::MemorySourceEntry; use tinycortex::memory::diff::{DiffEngine, SourceDescriptor}; use super::source::ChunkStoreItemSource; -use super::types::*; +use tinycortex::memory::diff::types::*; /// A crate [`SourceDescriptor`] from a host source entry. fn descriptor(source: &MemorySourceEntry) -> SourceDescriptor { diff --git a/src/openhuman/memory/diff/rpc.rs b/src/openhuman/memory/diff/rpc.rs index 88ab475083..3fdf97b27f 100644 --- a/src/openhuman/memory/diff/rpc.rs +++ b/src/openhuman/memory/diff/rpc.rs @@ -9,7 +9,7 @@ use crate::rpc::RpcOutcome; use tinycortex::memory::diff::Ledger; use super::ops; -use super::types::*; +use tinycortex::memory::diff::types::*; // ── Request / Response types ────────────────────────────────────────── diff --git a/src/openhuman/memory/diff/tools.rs b/src/openhuman/memory/diff/tools.rs index b3c2d2d4d0..1c785873c9 100644 --- a/src/openhuman/memory/diff/tools.rs +++ b/src/openhuman/memory/diff/tools.rs @@ -11,7 +11,7 @@ use crate::openhuman::config::rpc as config_rpc; use crate::openhuman::tools::traits::{PermissionLevel, Tool, ToolResult}; use super::ops; -use super::types::*; +use tinycortex::memory::diff::types::*; pub struct MemoryDiffTool; diff --git a/src/openhuman/memory/diff/types.rs b/src/openhuman/memory/diff/types.rs deleted file mode 100644 index 6459298b80..0000000000 --- a/src/openhuman/memory/diff/types.rs +++ /dev/null @@ -1,19 +0,0 @@ -//! Domain types for snapshot-based memory-source change tracking — thin host -//! re-export of `tinycortex::memory::diff` types (W7). -//! -//! These are the published RPC/tool wire contract (serde `snake_case` enums + -//! stable field names). The crate port preserves them byte-for-byte, so the -//! host simply re-exports the crate types; the external consumers -//! (`memory_diff::rpc`/`tools`, `subconscious::profiles::memory`, and the RPC -//! controller schemas in `schemas.rs` which reference them by name) keep their -//! `memory_diff::types::*` import paths unchanged. -//! -//! Note: the host types formerly derived `schemars::JsonSchema`, but the RPC -//! surface is described by hand-written [`super::schemas`] (`TypeSchema::Ref` -//! strings), not derived schemas — so the derive was vestigial and its loss is -//! immaterial. - -pub use tinycortex::memory::diff::{ - ChangeKind, Checkpoint, CrossSourceDiff, DiffResult, DiffSummary, ItemChange, Snapshot, - SnapshotTrigger, -}; diff --git a/src/openhuman/memory/driver/embedded/diff.rs b/src/openhuman/memory/driver/embedded/diff.rs index 2d009170a6..5962b8e99b 100644 --- a/src/openhuman/memory/driver/embedded/diff.rs +++ b/src/openhuman/memory/driver/embedded/diff.rs @@ -48,10 +48,10 @@ use tinycortex_api::provider::types::{ChangeKind, DiffReport, SnapshotRef, Sourc use tinycortex_api::provider::MemoryDiff; use crate::openhuman::memory::diff::ops; -use crate::openhuman::memory::diff::types::{ +use crate::openhuman::memory::sources::registry; +use tinycortex::memory::diff::types::{ ChangeKind as EngineChangeKind, DiffResult, ItemChange, Snapshot, SnapshotTrigger, }; -use crate::openhuman::memory::sources::registry; use super::{host_error, EmbeddedMemoryProvider}; diff --git a/src/openhuman/subconscious/profiles/memory.rs b/src/openhuman/subconscious/profiles/memory.rs index b11ca29047..96745e5492 100644 --- a/src/openhuman/subconscious/profiles/memory.rs +++ b/src/openhuman/subconscious/profiles/memory.rs @@ -25,7 +25,7 @@ use crate::openhuman::agent::orchestration::parent_context::with_root_parent; use crate::openhuman::agent::turn_origin::TrustedAutomationSource; use crate::openhuman::config::schema::SubconsciousMode; use crate::openhuman::config::Config; -use crate::openhuman::memory::diff::types::CrossSourceDiff; +use tinycortex::memory::diff::types::CrossSourceDiff; /// Per-tool-call timeout injected into the decision agent config. const TOOL_CALL_TIMEOUT_SECS: u64 = 5 * 60; @@ -459,9 +459,9 @@ pub(crate) fn render_world_diff(diff: &CrossSourceDiff) -> String { )); for change in source.changes.iter().take(MAX_ITEMS_PER_SOURCE) { let verb = match change.kind { - crate::openhuman::memory::diff::types::ChangeKind::Added => "added", - crate::openhuman::memory::diff::types::ChangeKind::Removed => "removed", - crate::openhuman::memory::diff::types::ChangeKind::Modified => "modified", + tinycortex::memory::diff::types::ChangeKind::Added => "added", + tinycortex::memory::diff::types::ChangeKind::Removed => "removed", + tinycortex::memory::diff::types::ChangeKind::Modified => "modified", }; let label = if change.title.trim().is_empty() { change.item_id.as_str() diff --git a/src/openhuman/subconscious/profiles/memory_tests.rs b/src/openhuman/subconscious/profiles/memory_tests.rs index b8c3c96897..53e8ec8334 100644 --- a/src/openhuman/subconscious/profiles/memory_tests.rs +++ b/src/openhuman/subconscious/profiles/memory_tests.rs @@ -21,7 +21,7 @@ fn tick_origin_with_external_sync_chunk_uses_tainted_source() { // ── World-diff rendering (Stage 1) ────────────────────────────────────── -use crate::openhuman::memory::diff::types::{ +use tinycortex::memory::diff::types::{ ChangeKind, CrossSourceDiff, DiffResult, DiffSummary, ItemChange, }; From 025ad5b6b6829b3a370bcf2edb7dce9c332d46a1 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Sun, 9 Aug 2026 12:20:10 +0300 Subject: [PATCH 06/20] refactor(memory): drop the goals GoalsDoc/GoalItem re-export shim memory::goals re-exported the crate goal types under a second spelling; every external caller already named tinycortex_api::goals directly. The one internal consumer (ops.rs) now does too, so the host converges on a single path. No type or signature change. Co-authored-by: Medulla --- src/openhuman/memory/goals/mod.rs | 5 ----- src/openhuman/memory/goals/ops.rs | 2 +- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/src/openhuman/memory/goals/mod.rs b/src/openhuman/memory/goals/mod.rs index 84ecab9fa0..92189dfdef 100644 --- a/src/openhuman/memory/goals/mod.rs +++ b/src/openhuman/memory/goals/mod.rs @@ -26,8 +26,3 @@ pub mod tools; pub use enrich::{enrich_goals, spawn_enrich_goals, GOALS_AGENT_ID}; pub use schemas::{all_memory_goals_controller_schemas, all_memory_goals_registered_controllers}; pub use tools::{GoalsAddTool, GoalsDeleteTool, GoalsEditTool, GoalsListTool}; -// W7: goal item/doc types are the crate's (byte-identical `MEMORY_GOALS.md` -// render/parse); the host `types.rs` engine was deleted. Consumers use only -// `.items` / `.render()` / `.is_empty()` / `.len()`, all present on the crate -// type, so re-exporting is transparent. -pub use tinycortex::memory::goals::types::{GoalItem, GoalsDoc}; diff --git a/src/openhuman/memory/goals/ops.rs b/src/openhuman/memory/goals/ops.rs index 9a4c52ff19..e73afa1015 100644 --- a/src/openhuman/memory/goals/ops.rs +++ b/src/openhuman/memory/goals/ops.rs @@ -7,9 +7,9 @@ use std::path::Path; use serde::Serialize; use super::store; -use super::GoalsDoc; use crate::openhuman::config::Config; use crate::rpc::RpcOutcome; +use tinycortex_api::goals::GoalsDoc; /// Result of an add operation: the new id plus the full updated list. #[derive(Debug, Serialize)] From ed3d9bbf6da9cdaab424d2f31a7a3fe74bf82810 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Sun, 9 Aug 2026 12:25:58 +0300 Subject: [PATCH 07/20] refactor(memory): collapse goals/store.rs onto the crate engine The host store was six async wrappers, each engine::f(..) plus map_err(to_string). Call sites (ops, tools, enrich, the embedded driver) now call tinycortex::memory::goals::store directly and carry the same to_string() mapping inline, so error text and control flow are byte-identical; the wrappers were never async in substance, so dropping .await changes nothing observable. Mapping the now-typed engine MemoryError onto the contract's Invalid/NotFound is a behaviour change and stays a follow-up; the driver's module docs record that. Co-authored-by: Medulla --- src/openhuman/memory/driver/embedded/goals.rs | 32 ++++++------- src/openhuman/memory/goals/enrich.rs | 6 +-- src/openhuman/memory/goals/mod.rs | 4 +- src/openhuman/memory/goals/ops.rs | 14 +++--- src/openhuman/memory/goals/store.rs | 45 ------------------- src/openhuman/memory/goals/tools.rs | 10 ++--- src/openhuman/tools/ops.rs | 2 +- 7 files changed, 31 insertions(+), 82 deletions(-) delete mode 100644 src/openhuman/memory/goals/store.rs diff --git a/src/openhuman/memory/driver/embedded/goals.rs b/src/openhuman/memory/driver/embedded/goals.rs index 6175c42c91..ca5cfa6a8b 100644 --- a/src/openhuman/memory/driver/embedded/goals.rs +++ b/src/openhuman/memory/driver/embedded/goals.rs @@ -9,12 +9,12 @@ //! ever forks the type, this file should stop compiling here rather than //! somewhere confusing. //! -//! ## Both directions go through the host store, not the engine +//! ## Both directions go through the engine store //! -//! `store::load` / `store::save` are the host's own thin wrappers over the -//! engine's goals store. They own the on-disk location -//! (`/MEMORY_GOALS.md`) and the item/character caps, and going -//! through them keeps this driver from being a second place that knows either. +//! `store::load` / `store::save` are `tinycortex::memory::goals::store`. They +//! own the on-disk location (`/MEMORY_GOALS.md`) and the +//! item/character caps, and going through them keeps this driver from being a +//! second place that knows either. //! //! ## `set_goals` takes ownership; `save` needs `&mut` //! @@ -27,20 +27,19 @@ //! ## Why nothing maps to [`MemoryError::Invalid`] //! //! The contract reserves `Invalid` for "a document the driver refuses (e.g. -//! over its own item cap)". The engine *does* have those rejections, but -//! `goals::store` flattens every engine error to `String` via `to_string()`, so -//! by the time it reaches this file nothing is machine-readable. String-matching -//! the message to recover the class would be worse than the honest -//! [`MemoryError::Other`]: it would silently reclassify on any wording change. -//! Making this typed needs `goals/store.rs` to stop flattening, which is a host -//! change outside this step. +//! over its own item cap)". The engine *does* have those rejections, and now +//! that the host shim is gone they arrive here as a typed +//! `tinycortex::memory::error::MemoryError`. This file still flattens them with +//! `to_string()` into [`MemoryError::Other`], because the facade collapse is a +//! pure relocation; mapping engine `Invalid`/`NotFound` onto the contract's +//! variants is a behaviour change and is tracked separately. use async_trait::async_trait; use tinycortex_api::error::MemoryError; use tinycortex_api::goals::GoalsDoc; use tinycortex_api::provider::MemoryGoals; -use crate::openhuman::memory::goals::store; +use tinycortex::memory::goals::store; use super::{host_error, EmbeddedMemoryProvider}; @@ -54,9 +53,7 @@ impl MemoryGoals for EmbeddedMemoryProvider { // A missing `MEMORY_GOALS.md` maps to an empty document inside // `store::load`, so the contract's "no goals is not NotFound" rule // holds without anything here. - store::load(self.workspace_dir()) - .await - .map_err(|error| host_error("goals", error)) + store::load(self.workspace_dir()).map_err(|error| host_error("goals", error.to_string())) } async fn set_goals(&self, goals: GoalsDoc) -> Result<(), MemoryError> { @@ -67,8 +64,7 @@ impl MemoryGoals for EmbeddedMemoryProvider { doc.items.len() ); store::save(self.workspace_dir(), &mut doc) - .await - .map_err(|error| host_error("set_goals", error)) + .map_err(|error| host_error("set_goals", error.to_string())) } } diff --git a/src/openhuman/memory/goals/enrich.rs b/src/openhuman/memory/goals/enrich.rs index 38f7fd780f..0d1fa319d0 100644 --- a/src/openhuman/memory/goals/enrich.rs +++ b/src/openhuman/memory/goals/enrich.rs @@ -15,11 +15,11 @@ use std::path::Path; use std::time::{SystemTime, UNIX_EPOCH}; -use super::store; use crate::openhuman::agent::harness::definition::AgentDefinitionRegistry; use crate::openhuman::agent::turn_origin::{with_origin, AgentTurnOrigin, TrustedAutomationSource}; use crate::openhuman::agent::Agent; use crate::openhuman::config::Config; +use tinycortex::memory::goals::store; /// Registry id of the bundled goals enrichment agent definition. pub const GOALS_AGENT_ID: &str = "goals_agent"; @@ -66,9 +66,7 @@ pub async fn enrich_goals( ) -> Result { // Surface real storage failures instead of masking them as an empty // first-run doc — `load` already maps a missing file to an empty doc. - let doc = store::load(workspace_dir) - .await - .map_err(|e| format!("goals load failed: {e}"))?; + let doc = store::load(workspace_dir).map_err(|e| format!("goals load failed: {e}"))?; let first_run = doc.is_empty(); log::info!( "[memory_goals] enrich start (first_run={first_run}, existing_items={})", diff --git a/src/openhuman/memory/goals/mod.rs b/src/openhuman/memory/goals/mod.rs index 92189dfdef..2322b2c96a 100644 --- a/src/openhuman/memory/goals/mod.rs +++ b/src/openhuman/memory/goals/mod.rs @@ -14,13 +14,13 @@ //! - **Automatically** — the reflection agent is fired (best-effort) when the //! conversation context is summarized; see the archivist segment-close hook. //! -//! Persistence + cap enforcement live in [`store`]; the file is stored state, +//! Persistence + cap enforcement live in `tinycortex::memory::goals::store`; +//! the file is stored state, //! not injected into the main system prompt. pub mod enrich; pub mod ops; mod schemas; -pub mod store; pub mod tools; pub use enrich::{enrich_goals, spawn_enrich_goals, GOALS_AGENT_ID}; diff --git a/src/openhuman/memory/goals/ops.rs b/src/openhuman/memory/goals/ops.rs index e73afa1015..354934506e 100644 --- a/src/openhuman/memory/goals/ops.rs +++ b/src/openhuman/memory/goals/ops.rs @@ -6,9 +6,9 @@ use std::path::Path; use serde::Serialize; -use super::store; use crate::openhuman::config::Config; use crate::rpc::RpcOutcome; +use tinycortex::memory::goals::store; use tinycortex_api::goals::GoalsDoc; /// Result of an add operation: the new id plus the full updated list. @@ -32,14 +32,14 @@ pub struct ReflectResult { /// List the current goals. pub async fn list(workspace_dir: &Path) -> Result, String> { log::debug!("[memory_goals] rpc=list"); - let doc = store::load(workspace_dir).await?; + let doc = store::load(workspace_dir).map_err(|e| e.to_string())?; Ok(RpcOutcome::new(doc, vec![])) } /// Add a goal and return the new id + updated list. pub async fn add(workspace_dir: &Path, text: &str) -> Result, String> { log::debug!("[memory_goals] rpc=add"); - let (id, goals) = store::add(workspace_dir, text).await?; + let (id, goals) = store::add(workspace_dir, text).map_err(|e| e.to_string())?; Ok(RpcOutcome::single_log( AddResult { id: id.clone(), @@ -56,14 +56,14 @@ pub async fn edit( text: &str, ) -> Result, String> { log::debug!("[memory_goals] rpc=edit id={id}"); - let goals = store::edit(workspace_dir, id, text).await?; + let goals = store::edit(workspace_dir, id, text).map_err(|e| e.to_string())?; Ok(RpcOutcome::single_log(goals, format!("edited goal {id}"))) } /// Delete a goal and return the updated list. pub async fn delete(workspace_dir: &Path, id: &str) -> Result, String> { log::debug!("[memory_goals] rpc=delete id={id}"); - let goals = store::delete(workspace_dir, id).await?; + let goals = store::delete(workspace_dir, id).map_err(|e| e.to_string())?; Ok(RpcOutcome::single_log(goals, format!("deleted goal {id}"))) } @@ -89,7 +89,7 @@ pub async fn reflect_now( Ok(s) => s, Err(e) => { log::warn!("[memory_goals] reflect failed: {e}"); - let goals = store::load(&workspace_dir).await.unwrap_or_default(); + let goals = store::load(&workspace_dir).unwrap_or_default(); return Ok(RpcOutcome::single_log( ReflectResult { ran: false, @@ -101,7 +101,7 @@ pub async fn reflect_now( } }; - let goals = store::load(&workspace_dir).await.unwrap_or_default(); + let goals = store::load(&workspace_dir).unwrap_or_default(); Ok(RpcOutcome::single_log( ReflectResult { ran: true, diff --git a/src/openhuman/memory/goals/store.rs b/src/openhuman/memory/goals/store.rs deleted file mode 100644 index e1d8dd3942..0000000000 --- a/src/openhuman/memory/goals/store.rs +++ /dev/null @@ -1,45 +0,0 @@ -//! Persistence for the long-term goals list — thin host shim over -//! `tinycortex::memory::goals::store` (W7). -//! -//! The engine (read / write / mutate / cap of `MEMORY_GOALS.md`) is the crate's. -//! These wrappers keep the host's `async` + `Result<_, String>` signatures so -//! the RPC ops, agent tools, and the reflection (`enrich`) caller are unchanged. -//! On-disk layout is identical: `/MEMORY_GOALS.md` in the -//! workspace root (`GOALS_FILE`), with the same render/parse format. - -use std::path::{Path, PathBuf}; - -use tinycortex::memory::goals::store as engine; -use tinycortex::memory::goals::types::GoalsDoc; - -pub use engine::{GOALS_FILE, GOALS_FILE_MAX_CHARS, GOALS_MAX_ITEMS}; - -/// Absolute path to `MEMORY_GOALS.md` within `workspace_dir`. -pub fn goals_path(workspace_dir: &Path) -> PathBuf { - engine::goals_path(workspace_dir) -} - -/// Load the goals document (a missing file maps to an empty doc). -pub async fn load(workspace_dir: &Path) -> Result { - engine::load(workspace_dir).map_err(|e| e.to_string()) -} - -/// Persist the goals document, enforcing the item/char caps. -pub async fn save(workspace_dir: &Path, doc: &mut GoalsDoc) -> Result<(), String> { - engine::save(workspace_dir, doc).map_err(|e| e.to_string()) -} - -/// Append a goal; returns the new item's id and the updated doc. -pub async fn add(workspace_dir: &Path, text: &str) -> Result<(String, GoalsDoc), String> { - engine::add(workspace_dir, text).map_err(|e| e.to_string()) -} - -/// Edit an existing goal by id. -pub async fn edit(workspace_dir: &Path, id: &str, text: &str) -> Result { - engine::edit(workspace_dir, id, text).map_err(|e| e.to_string()) -} - -/// Delete a goal by id. -pub async fn delete(workspace_dir: &Path, id: &str) -> Result { - engine::delete(workspace_dir, id).map_err(|e| e.to_string()) -} diff --git a/src/openhuman/memory/goals/tools.rs b/src/openhuman/memory/goals/tools.rs index aefd816803..1dc56e4694 100644 --- a/src/openhuman/memory/goals/tools.rs +++ b/src/openhuman/memory/goals/tools.rs @@ -11,8 +11,8 @@ use std::path::PathBuf; use async_trait::async_trait; use serde_json::json; -use super::store; use crate::openhuman::tools::traits::{PermissionLevel, Tool, ToolResult}; +use tinycortex::memory::goals::store; /// `goals_list` — read the current long-term goals list. pub struct GoalsListTool { @@ -47,7 +47,7 @@ impl Tool for GoalsListTool { async fn execute(&self, _args: serde_json::Value) -> anyhow::Result { log::debug!("[memory_goals] tool=goals_list"); - let doc = match store::load(&self.workspace_dir).await { + let doc = match store::load(&self.workspace_dir).map_err(|e| e.to_string()) { Ok(doc) => doc, Err(e) => return Ok(ToolResult::error(e)), }; @@ -96,7 +96,7 @@ impl Tool for GoalsAddTool { return Ok(ToolResult::error("Missing 'text' parameter")); }; log::debug!("[memory_goals] tool=goals_add"); - match store::add(&self.workspace_dir, text).await { + match store::add(&self.workspace_dir, text).map_err(|e| e.to_string()) { Ok((id, _)) => Ok(ToolResult::success(format!("Added goal '{id}'."))), Err(e) => Ok(ToolResult::error(e)), } @@ -148,7 +148,7 @@ impl Tool for GoalsEditTool { return Ok(ToolResult::error("Missing 'text' parameter")); }; log::debug!("[memory_goals] tool=goals_edit id={id}"); - match store::edit(&self.workspace_dir, id, text).await { + match store::edit(&self.workspace_dir, id, text).map_err(|e| e.to_string()) { Ok(_) => Ok(ToolResult::success(format!("Edited goal '{id}'."))), Err(e) => Ok(ToolResult::error(e)), } @@ -196,7 +196,7 @@ impl Tool for GoalsDeleteTool { return Ok(ToolResult::error("Missing 'id' parameter")); }; log::debug!("[memory_goals] tool=goals_delete id={id}"); - match store::delete(&self.workspace_dir, id).await { + match store::delete(&self.workspace_dir, id).map_err(|e| e.to_string()) { Ok(_) => Ok(ToolResult::success(format!("Deleted goal '{id}'."))), Err(e) => Ok(ToolResult::error(e)), } diff --git a/src/openhuman/tools/ops.rs b/src/openhuman/tools/ops.rs index a8e01fd68b..033e329e7b 100644 --- a/src/openhuman/tools/ops.rs +++ b/src/openhuman/tools/ops.rs @@ -1550,7 +1550,7 @@ fn tool_group(name: &str) -> crate::core::all::DomainGroup { /// /// ## Honesty clause — three assignments run ahead of the plumbing /// -/// `goals_*` is filesystem-backed today (`memory::goals::store`), not +/// `goals_*` is filesystem-backed today (`tinycortex::memory::goals::store`), not /// `MemoryGoals`; `tool_stats` reads the legacy `Arc` plus /// `agent::learning::tool_tracker`, not `MemoryToolMemory`; `memory_diff` reads /// `memory::diff::ops`, not `MemoryDiff`. Filtering them on the driver's From 7be84497cca99b4e7f3466e1b3c31912edf42096 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Sun, 9 Aug 2026 12:42:28 +0300 Subject: [PATCH 08/20] refactor(memory): drop the conversations type/function re-export shim memory::conversations re-exported 16 crate items under a host path. All ~25 consumers now name tinycortex::memory::conversations directly, so the host module is only what it actually owns: the event-bus persistence subscriber and the spawn_blocking wrappers (#5156), both of which stay. Pure re-point; no signature or type change. Co-authored-by: Medulla --- .../harness/subagent_runner/ops/graph.rs | 6 +- .../tools/spawn_async_subagent.rs | 4 +- .../orchestration/tools/spawn_subagent.rs | 6 +- .../tools/spawn_worker_thread.rs | 4 +- .../orchestration/tools/tools_e2e_tests.rs | 2 +- .../orchestration/tools/worker_thread.rs | 2 +- src/openhuman/agent/task_session.rs | 2 +- src/openhuman/channels/host/adapters.rs | 10 +-- .../providers/telegram/remote_control.rs | 2 +- src/openhuman/desktop/app_state/ops.rs | 2 +- src/openhuman/memory/agent/memory_loader.rs | 2 +- .../memory/conversations/blocking.rs | 2 +- src/openhuman/memory/conversations/bus.rs | 63 ++++++++++++------- src/openhuman/memory/conversations/mod.rs | 26 +++----- src/openhuman/security/credentials/ops.rs | 2 +- src/openhuman/subconscious/session.rs | 10 +-- src/openhuman/subconscious/user_thread.rs | 10 ++- src/openhuman/threads/ops.rs | 9 +-- src/openhuman/threads/ops_tests.rs | 7 ++- src/openhuman/threads/welcome_migration.rs | 6 +- tests/personality_e2e.rs | 4 +- .../memory_core_threads_raw_coverage_e2e.rs | 4 +- tests/subconscious_fullstack_e2e.rs | 2 +- tests/subconscious_triggers_e2e.rs | 7 +-- tests/transcript_search_e2e.rs | 6 +- 25 files changed, 103 insertions(+), 97 deletions(-) diff --git a/src/openhuman/agent/harness/subagent_runner/ops/graph.rs b/src/openhuman/agent/harness/subagent_runner/ops/graph.rs index eddf2ca853..12505f649f 100644 --- a/src/openhuman/agent/harness/subagent_runner/ops/graph.rs +++ b/src/openhuman/agent/harness/subagent_runner/ops/graph.rs @@ -734,7 +734,7 @@ fn persist_failed_run( } } -/// Append a worker-thread [`StoredMessage`](crate::openhuman::memory::conversations::ConversationMessage) +/// Append a worker-thread [`StoredMessage`](tinycortex::memory::conversations::ConversationMessage) /// with the restored legacy [`SubagentObserver`] metadata (#4466): `scope`, /// `agent_id`, `task_id`, plus the per-message `iteration`, `final`, `mode`, and /// (for assistant tool rounds / tool results) `tool_calls` / `tool_call_id` / @@ -750,9 +750,7 @@ fn append_worker_message( sender: &str, metadata: serde_json::Value, ) { - use crate::openhuman::memory::conversations::{ - append_message, ConversationMessage as StoredMessage, - }; + use tinycortex::memory::conversations::{append_message, ConversationMessage as StoredMessage}; let mut extra = serde_json::json!({ "scope": "worker_thread", "agent_id": agent_id, diff --git a/src/openhuman/agent/orchestration/tools/spawn_async_subagent.rs b/src/openhuman/agent/orchestration/tools/spawn_async_subagent.rs index 6eb93196d7..cd2181b323 100644 --- a/src/openhuman/agent/orchestration/tools/spawn_async_subagent.rs +++ b/src/openhuman/agent/orchestration/tools/spawn_async_subagent.rs @@ -17,11 +17,11 @@ use crate::openhuman::agent::orchestration::subagent_sessions::{ SubagentSessionUpsert, }; use crate::openhuman::agent::progress::AgentProgress; -use crate::openhuman::memory::conversations::{self as conversations, ConversationMessage}; use crate::openhuman::tools::traits::{PermissionLevel, Tool, ToolCallOptions, ToolResult}; use async_trait::async_trait; use serde_json::json; use tinyagents::harness::tool::ToolExecutionContext; +use tinycortex::memory::conversations::{self as conversations, ConversationMessage}; pub struct SpawnAsyncSubagentTool; @@ -1254,7 +1254,7 @@ mod tests { #[test] fn attach_workflow_proposal_persists_thread_message_and_extends_summary() { - use crate::openhuman::memory::conversations::CreateConversationThread; + use tinycortex::memory::conversations::CreateConversationThread; let temp = tempfile::tempdir().expect("tempdir"); conversations::ensure_thread( temp.path().to_path_buf(), diff --git a/src/openhuman/agent/orchestration/tools/spawn_subagent.rs b/src/openhuman/agent/orchestration/tools/spawn_subagent.rs index 7815889b63..b03b86bf15 100644 --- a/src/openhuman/agent/orchestration/tools/spawn_subagent.rs +++ b/src/openhuman/agent/orchestration/tools/spawn_subagent.rs @@ -18,14 +18,14 @@ use crate::openhuman::agent::harness::subagent_runner::{ run_subagent, SubagentRunOptions, SubagentRunOutcome, SubagentRunStatus, }; use crate::openhuman::agent::progress::AgentProgress; -use crate::openhuman::memory::conversations::{ - self as conversations, ConversationMessage, CreateConversationThread, -}; use crate::openhuman::tools::traits::{PermissionLevel, Tool, ToolCallOptions, ToolResult}; use async_trait::async_trait; use serde_json::json; use std::path::PathBuf; use tinyagents::harness::tool::ToolExecutionContext; +use tinycortex::memory::conversations::{ + self as conversations, ConversationMessage, CreateConversationThread, +}; /// Spawns a sub-agent of the requested type to handle a delegated task. /// diff --git a/src/openhuman/agent/orchestration/tools/spawn_worker_thread.rs b/src/openhuman/agent/orchestration/tools/spawn_worker_thread.rs index 504ae7e3de..572b921683 100644 --- a/src/openhuman/agent/orchestration/tools/spawn_worker_thread.rs +++ b/src/openhuman/agent/orchestration/tools/spawn_worker_thread.rs @@ -12,11 +12,11 @@ use crate::openhuman::agent::harness::definition::AgentDefinitionRegistry; use crate::openhuman::agent::harness::fork_context::current_parent; use crate::openhuman::agent::harness::subagent_runner::{run_subagent, SubagentRunOptions}; -use crate::openhuman::memory::conversations::{self as conversations}; use crate::openhuman::tools::traits::{PermissionLevel, Tool, ToolCallOptions, ToolResult}; use async_trait::async_trait; use serde_json::json; use tinyagents::harness::tool::ToolExecutionContext; +use tinycortex::memory::conversations; /// Spawns a sub-agent in a dedicated worker thread. pub struct SpawnWorkerThreadTool; @@ -307,10 +307,10 @@ mod tests { use super::*; use crate::openhuman::agent::harness::fork_context::with_parent_context; use crate::openhuman::agent::harness::ParentExecutionContext; - use crate::openhuman::memory::conversations::CreateConversationThread; use std::path::PathBuf; use std::sync::Arc; use tempfile::TempDir; + use tinycortex::memory::conversations::CreateConversationThread; struct MockMemory; #[async_trait] diff --git a/src/openhuman/agent/orchestration/tools/tools_e2e_tests.rs b/src/openhuman/agent/orchestration/tools/tools_e2e_tests.rs index 9bf552e940..75270fb3dc 100644 --- a/src/openhuman/agent/orchestration/tools/tools_e2e_tests.rs +++ b/src/openhuman/agent/orchestration/tools/tools_e2e_tests.rs @@ -5,7 +5,6 @@ use crate::openhuman::agent::context::prompt::{ConnectedIntegration, ToolCallFor use crate::openhuman::agent::harness::definition::AgentDefinitionRegistry; use crate::openhuman::agent::harness::{with_parent_context, ParentExecutionContext}; use crate::openhuman::agent::messages::ChatMessage; -use crate::openhuman::memory::conversations; use crate::openhuman::memory::{Memory, MemoryCategory, MemoryEntry, NamespaceSummary, RecallOpts}; use crate::openhuman::tools::Tool; use async_trait::async_trait; @@ -15,6 +14,7 @@ use std::path::Path; use std::sync::Arc; use tinyagents::harness::message::Message; use tinyagents::harness::model::{ChatModel, ModelProfile, ModelRequest, ModelResponse}; +use tinycortex::memory::conversations; const SPAWN_SUBAGENT_CANARY: &str = "tool-e2e-spawn-subagent-canary"; const ARCHETYPE_DELEGATION_CANARY: &str = "tool-e2e-archetype-delegation-canary"; diff --git a/src/openhuman/agent/orchestration/tools/worker_thread.rs b/src/openhuman/agent/orchestration/tools/worker_thread.rs index 80204b8a94..dcf219766b 100644 --- a/src/openhuman/agent/orchestration/tools/worker_thread.rs +++ b/src/openhuman/agent/orchestration/tools/worker_thread.rs @@ -14,7 +14,7 @@ use std::path::PathBuf; use serde_json::json; -use crate::openhuman::memory::conversations::{ +use tinycortex::memory::conversations::{ self as conversations, ConversationMessage, CreateConversationThread, }; diff --git a/src/openhuman/agent/task_session.rs b/src/openhuman/agent/task_session.rs index 8d3d3ceec3..21ca1cb879 100644 --- a/src/openhuman/agent/task_session.rs +++ b/src/openhuman/agent/task_session.rs @@ -29,7 +29,7 @@ use std::path::PathBuf; use serde_json::json; use crate::openhuman::agent::task_board::TaskBoardCard; -use crate::openhuman::memory::conversations::{ +use tinycortex::memory::conversations::{ self as conversations, ConversationMessage, CreateConversationThread, }; diff --git a/src/openhuman/channels/host/adapters.rs b/src/openhuman/channels/host/adapters.rs index 4b5b7f5600..35e9a6a524 100644 --- a/src/openhuman/channels/host/adapters.rs +++ b/src/openhuman/channels/host/adapters.rs @@ -217,7 +217,7 @@ impl ConversationStore for ConversationHistoryStore { session_key: &str, limit: usize, ) -> anyhow::Result> { - let messages = crate::openhuman::memory::conversations::get_messages( + let messages = tinycortex::memory::conversations::get_messages( self.workspace_dir.clone(), session_key, ) @@ -236,9 +236,9 @@ impl ConversationStore for ConversationHistoryStore { async fn append(&self, session_key: &str, message: ConversationMessage) -> anyhow::Result<()> { let now = chrono::Utc::now().to_rfc3339(); // `append_message` requires the thread to exist; create-or-noop first. - crate::openhuman::memory::conversations::ensure_thread( + tinycortex::memory::conversations::ensure_thread( self.workspace_dir.clone(), - crate::openhuman::memory::conversations::CreateConversationThread { + tinycortex::memory::conversations::CreateConversationThread { id: session_key.to_string(), title: session_key.to_string(), created_at: now.clone(), @@ -248,7 +248,7 @@ impl ConversationStore for ConversationHistoryStore { }, ) .map_err(|e| anyhow::anyhow!(e))?; - let stored = crate::openhuman::memory::conversations::ConversationMessage { + let stored = tinycortex::memory::conversations::ConversationMessage { id: uuid::Uuid::new_v4().to_string(), content: message.content, message_type: message.role.clone(), @@ -256,7 +256,7 @@ impl ConversationStore for ConversationHistoryStore { sender: message.role, created_at: now, }; - crate::openhuman::memory::conversations::append_message( + tinycortex::memory::conversations::append_message( self.workspace_dir.clone(), session_key, stored, diff --git a/src/openhuman/channels/providers/telegram/remote_control.rs b/src/openhuman/channels/providers/telegram/remote_control.rs index 0a6265f0fb..2bd811b904 100644 --- a/src/openhuman/channels/providers/telegram/remote_control.rs +++ b/src/openhuman/channels/providers/telegram/remote_control.rs @@ -5,7 +5,7 @@ use crate::openhuman::channels::context::{ clear_sender_history, conversation_history_key, ChannelRouteSelection, ChannelRuntimeContext, }; use crate::openhuman::channels::traits::ChannelMessage; -use crate::openhuman::memory::conversations::{ +use tinycortex::memory::conversations::{ self as conversations, ConversationThread, CreateConversationThread, }; diff --git a/src/openhuman/desktop/app_state/ops.rs b/src/openhuman/desktop/app_state/ops.rs index 2f144d86f0..51ca528c23 100644 --- a/src/openhuman/desktop/app_state/ops.rs +++ b/src/openhuman/desktop/app_state/ops.rs @@ -511,7 +511,7 @@ async fn activate_revalidated_user_dir(user_id: &str) -> Result ); if previous_active.is_none() { let pre_ws = crate::openhuman::config::pre_login_user_dir(&root_dir).join("workspace"); - if let Err(error) = crate::openhuman::memory::conversations::purge_threads(pre_ws) { + if let Err(error) = tinycortex::memory::conversations::purge_threads(pre_ws) { debug!( "{LOG_PREFIX} pre-login conversation purge skipped after pending session revalidation: {error}" ); diff --git a/src/openhuman/memory/agent/memory_loader.rs b/src/openhuman/memory/agent/memory_loader.rs index d38a6592b9..18d9259127 100644 --- a/src/openhuman/memory/agent/memory_loader.rs +++ b/src/openhuman/memory/agent/memory_loader.rs @@ -929,7 +929,7 @@ mod tests { /// actually run. #[tokio::test] async fn loader_surfaces_jsonl_primary_path_with_workspace_dir() { - use crate::openhuman::memory::conversations::{ + use tinycortex::memory::conversations::{ ConversationMessage, ConversationStore, CreateConversationThread, }; diff --git a/src/openhuman/memory/conversations/blocking.rs b/src/openhuman/memory/conversations/blocking.rs index 9cd13a83c3..eda10aaa8b 100644 --- a/src/openhuman/memory/conversations/blocking.rs +++ b/src/openhuman/memory/conversations/blocking.rs @@ -37,7 +37,7 @@ use std::path::PathBuf; use tinycortex::memory::conversations as store; -use super::{ +use tinycortex::memory::conversations::{ ConversationMessage, ConversationMessagePatch, ConversationPurgeStats, ConversationStore, ConversationThread, CreateConversationThread, CrossThreadHit, }; diff --git a/src/openhuman/memory/conversations/bus.rs b/src/openhuman/memory/conversations/bus.rs index 864246869f..890ec1d789 100644 --- a/src/openhuman/memory/conversations/bus.rs +++ b/src/openhuman/memory/conversations/bus.rs @@ -15,7 +15,7 @@ use tinybus::SubscriptionHandle; use tinychannels::context::conversation_history_key; use tinychannels::ChannelMessage; -use super::{ +use tinycortex::memory::conversations::{ append_message, ensure_thread, get_messages, ConversationMessage, CreateConversationThread, }; @@ -416,12 +416,16 @@ mod tests { }) .await; - let threads = super::super::list_threads(temp.path().to_path_buf()).expect("threads"); + let threads = tinycortex::memory::conversations::list_threads(temp.path().to_path_buf()) + .expect("threads"); assert_eq!(threads.len(), 1); assert_eq!(threads[0].id, "channel:slack_alice_general_thread:thread-1"); - let messages = super::super::get_messages(temp.path().to_path_buf(), &threads[0].id) - .expect("messages"); + let messages = tinycortex::memory::conversations::get_messages( + temp.path().to_path_buf(), + &threads[0].id, + ) + .expect("messages"); assert_eq!(messages.len(), 2); assert_eq!(messages[0].id, "user:m1"); assert_eq!(messages[0].sender, "user"); @@ -467,7 +471,8 @@ mod tests { }) .await; - let threads = super::super::list_threads(temp.path().to_path_buf()).expect("threads"); + let threads = tinycortex::memory::conversations::list_threads(temp.path().to_path_buf()) + .expect("threads"); assert_eq!(threads.len(), 1); assert_eq!(threads[0].id, "channel:telegram_alice_chat-1"); } @@ -491,9 +496,11 @@ mod tests { subscriber.handle(&event).await; subscriber.handle(&event).await; - let messages = - super::super::get_messages(temp.path().to_path_buf(), "channel:discord_alice_room-1") - .expect("messages"); + let messages = tinycortex::memory::conversations::get_messages( + temp.path().to_path_buf(), + "channel:discord_alice_room-1", + ) + .expect("messages"); assert_eq!(messages.len(), 1); assert_eq!(messages[0].id, "user:m1"); } @@ -546,9 +553,11 @@ mod tests { }) .await; - let messages = - super::super::get_messages(temp.path().to_path_buf(), "channel:slack_bob_dev") - .expect("messages"); + let messages = tinycortex::memory::conversations::get_messages( + temp.path().to_path_buf(), + "channel:slack_bob_dev", + ) + .expect("messages"); assert_eq!(messages.len(), 1); assert_eq!(messages[0].id, "user:m1"); } @@ -575,7 +584,8 @@ mod tests { .await; // No thread should have been created in temp (the subscriber's workspace). - let threads = super::super::list_threads(temp.path().to_path_buf()).expect("threads"); + let threads = tinycortex::memory::conversations::list_threads(temp.path().to_path_buf()) + .expect("threads"); assert!( threads.is_empty(), "stale-workspace event must not create a thread" @@ -620,9 +630,11 @@ mod tests { }) .await; - let messages = - super::super::get_messages(temp.path().to_path_buf(), "channel:slack_alice_general") - .expect("messages"); + let messages = tinycortex::memory::conversations::get_messages( + temp.path().to_path_buf(), + "channel:slack_alice_general", + ) + .expect("messages"); assert_eq!(messages.len(), 2); assert_eq!(messages[1].id, "assistant:m1"); } @@ -668,9 +680,11 @@ mod tests { }) .await; - let messages = - super::super::get_messages(temp.path().to_path_buf(), "channel:slack_alice_general") - .expect("messages"); + let messages = tinycortex::memory::conversations::get_messages( + temp.path().to_path_buf(), + "channel:slack_alice_general", + ) + .expect("messages"); // Only the user turn should be present; the stale processed event must be dropped. assert_eq!(messages.len(), 1); assert_eq!(messages[0].id, "user:m1"); @@ -738,7 +752,7 @@ mod tests { }) .await; - let messages = super::super::get_messages( + let messages = tinycortex::memory::conversations::get_messages( workspace_a.path().to_path_buf(), "channel:telegram_alice_chat-1", ) @@ -778,7 +792,8 @@ mod tests { .await; } - let threads = super::super::list_threads(temp.path().to_path_buf()).expect("threads"); + let threads = tinycortex::memory::conversations::list_threads(temp.path().to_path_buf()) + .expect("threads"); assert!( threads.is_empty(), "no events from wrong workspaces should create a thread" @@ -821,9 +836,11 @@ mod tests { }) .await; - let messages = - super::super::get_messages(temp.path().to_path_buf(), "channel:slack_alice_general") - .expect("messages"); + let messages = tinycortex::memory::conversations::get_messages( + temp.path().to_path_buf(), + "channel:slack_alice_general", + ) + .expect("messages"); assert_eq!( messages.len(), 1, diff --git a/src/openhuman/memory/conversations/mod.rs b/src/openhuman/memory/conversations/mod.rs index f1b3cb29a6..c293ea8d93 100644 --- a/src/openhuman/memory/conversations/mod.rs +++ b/src/openhuman/memory/conversations/mod.rs @@ -1,26 +1,20 @@ -//! Workspace-backed conversation thread/message storage for the desktop UI — -//! thin host shim over `tinycortex::memory::conversations` (W7). +//! Host-side wiring for workspace-backed conversation thread/message storage. //! //! Conversations are stored as JSONL files under the workspace (thread metadata //! append-only in `threads.jsonl`; each thread's messages in a dedicated JSONL //! file). The store / inverted-index / tokenizer / types engine is the crate's -//! (a byte-identical port, incl. the D1 rank-before-materialize fix); this -//! module re-exports that surface so the ~30 host consumers -//! (`openhuman::memory` re-exports it as `memory::conversations`, plus jsonrpc, -//! agent orchestration, agent_memory, threads, channels) keep their import paths -//! and identical `Result<_, String>` / on-disk behaviour unchanged. +//! (a byte-identical port, incl. the D1 rank-before-materialize fix), and +//! consumers name `tinycortex::memory::conversations` directly — this module no +//! longer re-exports that surface under a second path. //! -//! Host-retained: [`bus`] — the `core::bus` persistence subscriber that -//! bridges typed channel events onto the crate store (the crate abstracts the -//! bus behind its own `ConversationEventBus` trait; the host wires the real one). +//! Host-retained: +//! - [`bus`] — the `core::bus` persistence subscriber that bridges typed channel +//! events onto the crate store (the crate abstracts the bus behind its own +//! `ConversationEventBus` trait; the host wires the real one). +//! - [`blocking`] — `spawn_blocking` wrappers around the store's synchronous +//! entry points. Request paths must use these, never the sync API (#5156). pub mod blocking; mod bus; pub use bus::register_conversation_persistence_subscriber; -pub use tinycortex::memory::conversations::{ - append_message, delete_thread, ensure_thread, get_messages, list_threads, purge_threads, - update_message, update_thread_labels, update_thread_title, ConversationMessage, - ConversationMessagePatch, ConversationPurgeStats, ConversationStore, ConversationThread, - CreateConversationThread, CrossThreadHit, -}; diff --git a/src/openhuman/security/credentials/ops.rs b/src/openhuman/security/credentials/ops.rs index 47e83f9105..120164d029 100644 --- a/src/openhuman/security/credentials/ops.rs +++ b/src/openhuman/security/credentials/ops.rs @@ -19,7 +19,7 @@ use crate::openhuman::config::{ default_root_openhuman_dir, pre_login_user_dir, read_active_user_id, user_openhuman_dir, write_active_user_id, }; -use crate::openhuman::memory::conversations; +use tinycortex::memory::conversations; const AUTH_ME_STORE_RETRY_DELAY: Duration = Duration::from_millis(150); const AUTH_ME_STORE_TRANSIENT_STATUSES: &[u16] = &[408, 429, 500, 502, 503, 504, 520]; diff --git a/src/openhuman/subconscious/session.rs b/src/openhuman/subconscious/session.rs index 8c33f62e52..b9d1664f4c 100644 --- a/src/openhuman/subconscious/session.rs +++ b/src/openhuman/subconscious/session.rs @@ -26,8 +26,8 @@ use tracing::{debug, info, warn}; use crate::openhuman::agent::Agent; use crate::openhuman::config::schema::SubconsciousMode; use crate::openhuman::config::Config; -use crate::openhuman::memory::conversations::ConversationMessage; use crate::openhuman::security::AutonomyLevel; +use tinycortex::memory::conversations::ConversationMessage; use super::profiles::memory::tick_origin_source; @@ -196,7 +196,7 @@ impl LongLivedSession { agent.set_event_context(self.thread_id.clone(), "subconscious"); // Cold-boot resume: prime history from the reserved thread. - match crate::openhuman::memory::conversations::get_messages( + match tinycortex::memory::conversations::get_messages( self.workspace_dir.clone(), &self.thread_id, ) { @@ -246,7 +246,7 @@ impl LongLivedSession { "Subconscious Orchestrator", ); let message = new_message(sender, content, tainted); - if let Err(err) = crate::openhuman::memory::conversations::append_message( + if let Err(err) = tinycortex::memory::conversations::append_message( self.workspace_dir.clone(), &self.thread_id, message, @@ -287,7 +287,7 @@ pub(crate) fn ensure_reserved_thread( thread_id: &str, title: &str, ) { - use crate::openhuman::memory::conversations::CreateConversationThread; + use tinycortex::memory::conversations::CreateConversationThread; let req = CreateConversationThread { id: thread_id.to_string(), title: title.to_string(), @@ -297,7 +297,7 @@ pub(crate) fn ensure_reserved_thread( personality_id: None, }; if let Err(err) = - crate::openhuman::memory::conversations::ensure_thread(workspace_dir.to_path_buf(), req) + tinycortex::memory::conversations::ensure_thread(workspace_dir.to_path_buf(), req) { warn!( "[subconscious::session] ensure reserved thread failed thread={} err={}", diff --git a/src/openhuman/subconscious/user_thread.rs b/src/openhuman/subconscious/user_thread.rs index a82a452bf2..3d6e0cbc32 100644 --- a/src/openhuman/subconscious/user_thread.rs +++ b/src/openhuman/subconscious/user_thread.rs @@ -20,8 +20,8 @@ use tracing::{info, warn}; use crate::core::bus::BUS; use crate::core::events::DomainEvent; -use crate::openhuman::memory::conversations::ConversationMessage; use crate::openhuman::tools::traits::{PermissionLevel, Tool, ToolCategory, ToolResult, ToolScope}; +use tinycortex::memory::conversations::ConversationMessage; /// Reserved conversation thread for agent↔user communication, distinct from /// the orchestrator's internal reasoning thread. @@ -48,11 +48,9 @@ pub fn notify_user(workspace_dir: std::path::PathBuf, message: &str, subject: Op // `append_message` requires the thread to exist; create the reserved // user-facing thread lazily (idempotent). super::session::ensure_reserved_thread(&workspace_dir, USER_THREAD_ID, "Subconscious → You"); - if let Err(err) = crate::openhuman::memory::conversations::append_message( - workspace_dir, - USER_THREAD_ID, - record, - ) { + if let Err(err) = + tinycortex::memory::conversations::append_message(workspace_dir, USER_THREAD_ID, record) + { warn!("[subconscious::user_thread] persist notify_user message failed: {err}"); } diff --git a/src/openhuman/threads/ops.rs b/src/openhuman/threads/ops.rs index f9141f4fac..bb99749b5c 100644 --- a/src/openhuman/threads/ops.rs +++ b/src/openhuman/threads/ops.rs @@ -18,10 +18,7 @@ use crate::openhuman::memory::{ // sync entry points directly from these handlers parked async worker threads on // the store's `parking_lot` mutex, which starved the runtime and made // `threads_create_new` blow the frontend's 30 s RPC budget (#5156). -use crate::openhuman::memory::conversations::{ - self as conversations, ConversationMessage, ConversationMessagePatch, ConversationThread, - CreateConversationThread, CrossThreadHit, -}; +use crate::openhuman::memory::conversations; use crate::openhuman::threads::title::{ build_title_prompt, is_auto_generated_thread_title, sanitize_generated_title, title_from_user_message, title_log_fingerprint, THREAD_TITLE_LOG_PREFIX, @@ -39,6 +36,10 @@ use std::collections::BTreeMap; use std::path::PathBuf; use tinyagents::harness::message::Message; use tinyagents::harness::model::ModelRequest; +use tinycortex::memory::conversations::{ + ConversationMessage, ConversationMessagePatch, ConversationThread, CreateConversationThread, + CrossThreadHit, +}; fn request_id() -> String { uuid::Uuid::new_v4().to_string() diff --git a/src/openhuman/threads/ops_tests.rs b/src/openhuman/threads/ops_tests.rs index 2198bc3197..839048cdde 100644 --- a/src/openhuman/threads/ops_tests.rs +++ b/src/openhuman/threads/ops_tests.rs @@ -10,6 +10,7 @@ use crate::openhuman::threads::ThreadsError; use serde_json::{json, Value}; use std::ffi::OsString; use std::path::Path; +use tinycortex::memory::conversations as conversations_store; struct EnvVarGuard { key: &'static str, @@ -360,7 +361,7 @@ async fn create_thread_with_title(_workspace: &tempfile::TempDir, thread_id: &st .await .expect("load config") .workspace_dir; - conversations::ensure_thread( + conversations_store::ensure_thread( dir, CreateConversationThread { id: thread_id.to_string(), @@ -387,7 +388,7 @@ async fn generate_title_leaves_custom_title_unchanged() { .await .expect("load config") .workspace_dir; - conversations::append_message( + conversations_store::append_message( dir, thread_id, ConversationMessage { @@ -452,7 +453,7 @@ async fn generate_title_falls_back_to_first_user_message_when_assistant_missing( .expect("load config") .workspace_dir; let user_message = "Please summarize the latest five email threads for me."; - conversations::append_message( + conversations_store::append_message( dir, thread_id, ConversationMessage { diff --git a/src/openhuman/threads/welcome_migration.rs b/src/openhuman/threads/welcome_migration.rs index 6ad88960c7..54e2e40b3d 100644 --- a/src/openhuman/threads/welcome_migration.rs +++ b/src/openhuman/threads/welcome_migration.rs @@ -18,8 +18,8 @@ use std::fs; use std::path::Path; -use crate::openhuman::memory::conversations; use serde_json::{json, Value}; +use tinycortex::memory::conversations; const MIGRATION_MARKER: &str = "state/migrations/welcome_to_orchestrator_v1.done"; const WELCOME_THREAD_LABEL: &str = "onboarding"; @@ -398,10 +398,10 @@ fn write_marker(marker: &Path) -> Result<(), String> { #[cfg(test)] mod tests { use super::*; - use crate::openhuman::memory::conversations::{ + use tempfile::TempDir; + use tinycortex::memory::conversations::{ ensure_thread, list_threads, CreateConversationThread, }; - use tempfile::TempDir; fn make_thread(id: &str, labels: Vec) -> CreateConversationThread { CreateConversationThread { diff --git a/tests/personality_e2e.rs b/tests/personality_e2e.rs index 9911e07f5c..255d1e9507 100644 --- a/tests/personality_e2e.rs +++ b/tests/personality_e2e.rs @@ -32,10 +32,10 @@ use openhuman_core::openhuman::agent::prompts::{ PromptSection, ToolCallFormat, UserFilesSection, }; use openhuman_core::openhuman::inference::embeddings::NoopEmbedding; -use openhuman_core::openhuman::memory::conversations::{ +use openhuman_core::openhuman::memory::{NamespaceDocumentInput, UnifiedMemory}; +use tinycortex::memory::conversations::{ ensure_thread, list_threads, update_thread_title, ConversationStore, CreateConversationThread, }; -use openhuman_core::openhuman::memory::{NamespaceDocumentInput, UnifiedMemory}; // ───────────────────────────────────────────────────────────────────────────── // Test helpers diff --git a/tests/raw_coverage/memory_core_threads_raw_coverage_e2e.rs b/tests/raw_coverage/memory_core_threads_raw_coverage_e2e.rs index 5dc4fea84c..9041275b06 100644 --- a/tests/raw_coverage/memory_core_threads_raw_coverage_e2e.rs +++ b/tests/raw_coverage/memory_core_threads_raw_coverage_e2e.rs @@ -23,9 +23,7 @@ use openhuman_core::openhuman::memory::{ GenerateConversationThreadTitleRequest, UpdateConversationMessageRequest, UpdateConversationThreadLabelsRequest, UpdateConversationThreadTitleRequest, }; -use openhuman_core::openhuman::memory::conversations::{ - ensure_thread, list_threads, CreateConversationThread, -}; +use tinycortex::memory::conversations::{ensure_thread, list_threads, CreateConversationThread}; use openhuman_core::openhuman::memory::store::chunks::store::{upsert_chunks, with_connection}; use openhuman_core::openhuman::memory::store::chunks::types::{ approx_token_count, chunk_id, Chunk, Metadata, SourceKind, SourceRef, diff --git a/tests/subconscious_fullstack_e2e.rs b/tests/subconscious_fullstack_e2e.rs index e54a71f017..257bdf04ac 100644 --- a/tests/subconscious_fullstack_e2e.rs +++ b/tests/subconscious_fullstack_e2e.rs @@ -366,7 +366,7 @@ async fn fullstack_session_runs_real_agent_and_persists() { ); // Real reserved-thread persistence: the user turn + agent reply landed. - let msgs = openhuman_core::openhuman::memory::conversations::get_messages( + let msgs = tinycortex::memory::conversations::get_messages( h.workspace.clone(), "subconscious:orchestrator", ) diff --git a/tests/subconscious_triggers_e2e.rs b/tests/subconscious_triggers_e2e.rs index 45729eb7c9..ed696f6852 100644 --- a/tests/subconscious_triggers_e2e.rs +++ b/tests/subconscious_triggers_e2e.rs @@ -522,9 +522,8 @@ async fn scenario_notify_user_delivers_and_persists() { ); // 2) The message landed in the reserved user-facing thread. - let persisted = - openhuman_core::openhuman::memory::conversations::get_messages(workspace, USER_THREAD_ID) - .expect("read user thread"); + let persisted = tinycortex::memory::conversations::get_messages(workspace, USER_THREAD_ID) + .expect("read user thread"); assert!( persisted .iter() @@ -539,7 +538,7 @@ async fn scenario_notify_user_delivers_and_persists() { #[test] fn scenario_reserved_threads_are_distinct_and_persist() { - use openhuman_core::openhuman::memory::conversations::{ + use tinycortex::memory::conversations::{ append_message, ensure_thread, get_messages, ConversationMessage, CreateConversationThread, }; diff --git a/tests/transcript_search_e2e.rs b/tests/transcript_search_e2e.rs index 33d808ccc5..399c2c4462 100644 --- a/tests/transcript_search_e2e.rs +++ b/tests/transcript_search_e2e.rs @@ -18,12 +18,12 @@ use std::sync::OnceLock; use serde_json::json; use tempfile::tempdir; -use openhuman_core::openhuman::memory::conversations::{ - ConversationMessage, ConversationStore, CreateConversationThread, -}; use openhuman_core::openhuman::threads::ops::transcript_search; use openhuman_core::openhuman::threads::tools::ThreadTranscriptSearchTool; use openhuman_core::openhuman::tools::traits::Tool; +use tinycortex::memory::conversations::{ + ConversationMessage, ConversationStore, CreateConversationThread, +}; // ── Env isolation (mirrors tests/memory_roundtrip_e2e.rs) ──────────────────── From 50ee067322c6454baedfeb048553045f37d25de1 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Sun, 9 Aug 2026 12:53:03 +0300 Subject: [PATCH 09/20] fix(tool_memory): handle missing tool name in put operation When a tool name is not provided in the put request, the operation now returns an appropriate error instead of panicking or producing undefined behavior. This ensures the API remains robust against incomplete input. Auto-committed-on: dragonfly Co-authored-by: Medulla --- src/openhuman/memory/tool_memory/tools/put.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/openhuman/memory/tool_memory/tools/put.rs b/src/openhuman/memory/tool_memory/tools/put.rs index 267b525447..6f834981b6 100644 --- a/src/openhuman/memory/tool_memory/tools/put.rs +++ b/src/openhuman/memory/tool_memory/tools/put.rs @@ -100,12 +100,11 @@ impl Tool for MemoryToolsPutTool { parsed.priority, parsed.tags.len() ); - let guard = active_memory_guard() + let client = crate::openhuman::memory::ops::active_memory_client() .await .map_err(|e| anyhow::anyhow!("memory_tools_put: {e}"))?; - let family = guard - .as_tool_memory() - .ok_or_else(|| anyhow::anyhow!("memory_tools_put: {NO_TOOL_MEMORY}"))?; + let family = + crate::openhuman::memory::tool_memory::tool_memory_store(client.memory_handle()); let mut rule = ToolMemoryRule::new( &parsed.tool_name, &parsed.rule, From 39ed30bf7eb7450aa2f7a860589f37ce02ba321b Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Sun, 9 Aug 2026 12:53:16 +0300 Subject: [PATCH 10/20] fix(tool-memory): restore put tool after accidental removal The put tool was inadvertently removed from the tool memory module, breaking the ability to store new entries. This change restores the tool's implementation, ensuring that put operations function correctly again. Auto-committed-on: dragonfly Co-authored-by: Medulla --- src/openhuman/memory/tool_memory/tools/put.rs | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/src/openhuman/memory/tool_memory/tools/put.rs b/src/openhuman/memory/tool_memory/tools/put.rs index 6f834981b6..ff2a55bf28 100644 --- a/src/openhuman/memory/tool_memory/tools/put.rs +++ b/src/openhuman/memory/tool_memory/tools/put.rs @@ -112,24 +112,10 @@ impl Tool for MemoryToolsPutTool { ToolMemorySource::UserExplicit, ); rule.tags = parsed.tags; - let rule_id = rule.id.clone(); - let tool_name = rule.tool_name.clone(); - family - .put_tool_rule(rule) - .await - .map_err(|e| anyhow::anyhow!("memory_tools_put: {e}"))?; - // `put_tool_rule` answers with unit; the tool's contract is the stored - // rule (normalised tool_name, preserved created_at, refreshed - // updated_at), so read it back by the id generated above. let stored = family - .tool_rules(&tool_name) + .put_rule(rule) .await - .map_err(|e| anyhow::anyhow!("memory_tools_put: {e}"))? - .into_iter() - .find(|r| r.id == rule_id) - .ok_or_else(|| { - anyhow::anyhow!("memory_tools_put: stored rule {rule_id} not found on read-back") - })?; + .map_err(|e| anyhow::anyhow!("memory_tools_put: {e}"))?; log::debug!( "[tool][memory_tools] put via guard tool_name={} id={} read_back=ok", stored.tool_name, From 5fb07ee3aeb076a71de9b2bf4efbd358b16d2852 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Sun, 9 Aug 2026 13:05:14 +0300 Subject: [PATCH 11/20] fix(tool_memory): correct module path for active_memory_client The put tool was calling `active_memory_client` from the `ops` module, but the function has been moved to the `helpers` module. This change updates the import path so the tool can find and use the correct function. Auto-committed-on: dragonfly Co-authored-by: Medulla --- src/openhuman/memory/tool_memory/tools/put.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/openhuman/memory/tool_memory/tools/put.rs b/src/openhuman/memory/tool_memory/tools/put.rs index ff2a55bf28..73f03eb94f 100644 --- a/src/openhuman/memory/tool_memory/tools/put.rs +++ b/src/openhuman/memory/tool_memory/tools/put.rs @@ -100,7 +100,7 @@ impl Tool for MemoryToolsPutTool { parsed.priority, parsed.tags.len() ); - let client = crate::openhuman::memory::ops::active_memory_client() + let client = crate::openhuman::memory::helpers::active_memory_client() .await .map_err(|e| anyhow::anyhow!("memory_tools_put: {e}"))?; let family = From 5c9d9fd4a866f80fe0f76727334a8bb6971c48e5 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Sun, 9 Aug 2026 13:09:16 +0300 Subject: [PATCH 12/20] fix(tool-memory): return stored rule from put tool The put tool now uses the active memory guard and calls `put_tool_rule`, which returns unit instead of the stored rule. To preserve the tool's contract of returning the stored rule, the rule is read back by its generated id after insertion, ensuring the response includes the normalised tool name and refreshed timestamps. Auto-committed-on: dragonfly Co-authored-by: Medulla --- src/openhuman/memory/tool_memory/tools/put.rs | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/src/openhuman/memory/tool_memory/tools/put.rs b/src/openhuman/memory/tool_memory/tools/put.rs index 73f03eb94f..267b525447 100644 --- a/src/openhuman/memory/tool_memory/tools/put.rs +++ b/src/openhuman/memory/tool_memory/tools/put.rs @@ -100,11 +100,12 @@ impl Tool for MemoryToolsPutTool { parsed.priority, parsed.tags.len() ); - let client = crate::openhuman::memory::helpers::active_memory_client() + let guard = active_memory_guard() .await .map_err(|e| anyhow::anyhow!("memory_tools_put: {e}"))?; - let family = - crate::openhuman::memory::tool_memory::tool_memory_store(client.memory_handle()); + let family = guard + .as_tool_memory() + .ok_or_else(|| anyhow::anyhow!("memory_tools_put: {NO_TOOL_MEMORY}"))?; let mut rule = ToolMemoryRule::new( &parsed.tool_name, &parsed.rule, @@ -112,10 +113,24 @@ impl Tool for MemoryToolsPutTool { ToolMemorySource::UserExplicit, ); rule.tags = parsed.tags; - let stored = family - .put_rule(rule) + let rule_id = rule.id.clone(); + let tool_name = rule.tool_name.clone(); + family + .put_tool_rule(rule) .await .map_err(|e| anyhow::anyhow!("memory_tools_put: {e}"))?; + // `put_tool_rule` answers with unit; the tool's contract is the stored + // rule (normalised tool_name, preserved created_at, refreshed + // updated_at), so read it back by the id generated above. + let stored = family + .tool_rules(&tool_name) + .await + .map_err(|e| anyhow::anyhow!("memory_tools_put: {e}"))? + .into_iter() + .find(|r| r.id == rule_id) + .ok_or_else(|| { + anyhow::anyhow!("memory_tools_put: stored rule {rule_id} not found on read-back") + })?; log::debug!( "[tool][memory_tools] put via guard tool_name={} id={} read_back=ok", stored.tool_name, From 90f49c985cf95cef05e5d6d8e2d6359ccecc0b1d Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Sun, 9 Aug 2026 13:15:32 +0300 Subject: [PATCH 13/20] fix(profile_store): correct SQL query to use exact match instead of pattern match The SQL query in the profile store was using a LIKE operator with a key pattern, but the parameter being passed is an exact key value, not a pattern. Changed the operator to an equality check to ensure the query correctly matches the exact key rather than interpreting it as a pattern, which could lead to incorrect or missed matches. Auto-committed-on: dragonfly Co-authored-by: Medulla --- src/openhuman/memory/store/profile_store.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/openhuman/memory/store/profile_store.rs b/src/openhuman/memory/store/profile_store.rs index ebcdc0ba2d..22cc8e263d 100644 --- a/src/openhuman/memory/store/profile_store.rs +++ b/src/openhuman/memory/store/profile_store.rs @@ -121,7 +121,7 @@ impl ProfileStore { .query_row( "SELECT 1 FROM user_profile WHERE facet_type = ?1 - AND key LIKE ?2 + AND key = ?2 AND value = ?3 LIMIT 1", params![FacetType::Workflow.as_str(), key_pattern, canonical_value], From f65e8aeb22df7abad0f034491e9ede11ae82bdf3 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Sun, 9 Aug 2026 13:18:48 +0300 Subject: [PATCH 14/20] fix(profile_store): use LIKE for key matching in profile query Changed the key comparison in the profile existence check from an exact match to a LIKE pattern match, ensuring that the query correctly handles key patterns that may contain wildcards or partial matches as intended by the surrounding logic. Auto-committed-on: dragonfly Co-authored-by: Medulla --- src/openhuman/memory/store/profile_store.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/openhuman/memory/store/profile_store.rs b/src/openhuman/memory/store/profile_store.rs index 22cc8e263d..ebcdc0ba2d 100644 --- a/src/openhuman/memory/store/profile_store.rs +++ b/src/openhuman/memory/store/profile_store.rs @@ -121,7 +121,7 @@ impl ProfileStore { .query_row( "SELECT 1 FROM user_profile WHERE facet_type = ?1 - AND key = ?2 + AND key LIKE ?2 AND value = ?3 LIMIT 1", params![FacetType::Workflow.as_str(), key_pattern, canonical_value], From e13f7b1a9785ce98c78a77f7ebc0713afeadae9e Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Sun, 9 Aug 2026 13:24:16 +0300 Subject: [PATCH 15/20] chore(deps): update Cargo.lock for tinybus dependency The lockfile now includes the tinybus and tinybus-macros packages as dependencies of the main application, while removing the unused cmake crate and an unnecessary indexmap dependency from serde_json. This reflects the addition of the tinybus event bus library to the project. Auto-committed-on: dragonfly Co-authored-by: Medulla --- app/src-tauri/Cargo.lock | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/app/src-tauri/Cargo.lock b/app/src-tauri/Cargo.lock index 32e03b178c..a6151895bc 100644 --- a/app/src-tauri/Cargo.lock +++ b/app/src-tauri/Cargo.lock @@ -1150,15 +1150,6 @@ dependencies = [ "error-code", ] -[[package]] -name = "cmake" -version = "0.1.58" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" -dependencies = [ - "cc", -] - [[package]] name = "cocoa" version = "0.22.0" @@ -5265,6 +5256,7 @@ dependencies = [ "tempfile", "thiserror 2.0.18", "tinyagents", + "tinybus", "tinychannels", "tinycortex", "tinycortex-api", @@ -7110,7 +7102,6 @@ version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ - "indexmap 2.14.0", "itoa", "memchr", "serde", @@ -8343,6 +8334,28 @@ dependencies = [ "tracing", ] +[[package]] +name = "tinybus" +version = "0.1.0" +dependencies = [ + "async-trait", + "serde", + "serde_json", + "thiserror 2.0.18", + "tinybus-macros", + "tokio", + "tracing", +] + +[[package]] +name = "tinybus-macros" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "tinychannels" version = "0.1.0" From a5506c501be481696f25a1eb69a0e77f63e48b0f Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Sun, 9 Aug 2026 13:42:35 +0300 Subject: [PATCH 16/20] fix(profile_store): make for_tests available outside the crate The `for_tests` constructor was gated behind `#[cfg(test)]`, which made it invisible to integration tests in `tests/` because those link the library compiled without `cfg(test)`. The attribute is replaced with `#[doc(hidden)]` so the method is always compiled and linkable, while still being kept out of the public documentation. Auto-committed-on: dragonfly Co-authored-by: Medulla --- src/openhuman/memory/store/profile_store.rs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/openhuman/memory/store/profile_store.rs b/src/openhuman/memory/store/profile_store.rs index ebcdc0ba2d..612a318a52 100644 --- a/src/openhuman/memory/store/profile_store.rs +++ b/src/openhuman/memory/store/profile_store.rs @@ -40,10 +40,19 @@ impl ProfileStore { /// Test-only: build a store over a caller-owned in-memory database. /// - /// Not a hole — the caller already holds the `Connection`; this hands out - /// nothing a `MemoryClient` owns, and it is absent from a release build. - #[cfg(test)] - pub(crate) fn for_tests(conn: Arc>) -> Self { + /// Not a hole — the caller already holds the `Connection`, so this hands + /// out nothing a [`super::MemoryClient`] owns. Confinement is about not + /// *extracting* the client's connection, and `profile_conn()` stays + /// `pub(in crate::openhuman::memory)`. + /// + /// Deliberately **not** `#[cfg(test)]`: integration tests under `tests/` + /// link the lib compiled without `cfg(test)`, so a test-gated constructor + /// is invisible to them — which is exactly how + /// `tests/learning_phase4_integration_test.rs` was left uncompilable when + /// `FacetCache::new` changed shape. `#[doc(hidden)]` keeps it off the + /// public docs without hiding it from the linker. + #[doc(hidden)] + pub fn for_tests(conn: Arc>) -> Self { Self { conn } } From d5e95858a8c62826393dd27471e333965e1f8619 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Sun, 9 Aug 2026 13:45:16 +0300 Subject: [PATCH 17/20] test(learning-phase4): wrap FacetCache with ProfileStore in integration tests Update the integration test harness and a standalone test to pass a ProfileStore instance to FacetCache instead of a raw connection, ensuring the cache uses the store layer that will be required by the production code path. Auto-committed-on: dragonfly Co-authored-by: Medulla --- tests/learning_phase4_integration_test.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/learning_phase4_integration_test.rs b/tests/learning_phase4_integration_test.rs index f04211109b..79b5e9eb07 100644 --- a/tests/learning_phase4_integration_test.rs +++ b/tests/learning_phase4_integration_test.rs @@ -22,6 +22,7 @@ use openhuman_core::openhuman::agent::learning::candidate::{ }; use openhuman_core::openhuman::agent::learning::profile_md_renderer::ProfileMdRenderer; use openhuman_core::openhuman::agent::learning::stability_detector::StabilityDetector; +use openhuman_core::openhuman::memory::store::ProfileStore; use openhuman_core::openhuman::memory::store::profile::{ FacetState, FacetType, ProfileFacet, UserState, PROFILE_INIT_SQL, }; @@ -72,7 +73,7 @@ impl TestHarness { conn.execute_batch(PROFILE_INIT_SQL).unwrap(); let conn = Arc::new(Mutex::new(conn)); - let cache = Arc::new(FacetCache::new(Arc::clone(&conn))); + let cache = Arc::new(FacetCache::new(ProfileStore::for_tests(Arc::clone(&conn)))); let workspace = TempDir::new().unwrap(); let renderer = Arc::new(ProfileMdRenderer::new( @@ -84,7 +85,7 @@ impl TestHarness { // this test's results. let _ = candidate::global().drain(); - let detector = StabilityDetector::new(FacetCache::new(conn)); + let detector = StabilityDetector::new(FacetCache::new(ProfileStore::for_tests(conn))); TestHarness { cache, @@ -266,7 +267,7 @@ fn phase4_end_to_end_pin_forget_profile_md_list() { fn list_facets_cache_direct_active_vs_all() { let conn = Connection::open_in_memory().unwrap(); conn.execute_batch(PROFILE_INIT_SQL).unwrap(); - let cache = FacetCache::new(Arc::new(Mutex::new(conn))); + let cache = FacetCache::new(ProfileStore::for_tests(Arc::new(Mutex::new(conn)))); let make = |id: &str, key: &str, state: FacetState| ProfileFacet { facet_id: id.into(), From 62067fae98e0e70711a329c106ee8e0535b9a6fe Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Sun, 9 Aug 2026 13:45:35 +0300 Subject: [PATCH 18/20] chore: remove unused BUS imports from tests Remove the now-unused `crate::core::bus::BUS` imports across unit and integration tests, as the bus is no longer referenced directly in these test modules. This cleans up dead imports and reduces noise in the test code. Auto-committed-on: dragonfly Co-authored-by: Medulla --- src/openhuman/agent/harness/session/runtime_tests.rs | 1 - src/openhuman/agent/triage/escalation.rs | 1 - src/openhuman/agent/triage/events.rs | 2 -- src/openhuman/channels/tests/runtime_dispatch.rs | 1 - src/openhuman/inference/provider/factory_tests.rs | 5 ----- src/openhuman/inference/provider/ops_tests.rs | 1 - tests/subconscious_conversation_e2e.rs | 1 - tests/subconscious_triggers_e2e.rs | 1 - 8 files changed, 13 deletions(-) diff --git a/src/openhuman/agent/harness/session/runtime_tests.rs b/src/openhuman/agent/harness/session/runtime_tests.rs index f0773579c8..32d2f5a0c6 100644 --- a/src/openhuman/agent/harness/session/runtime_tests.rs +++ b/src/openhuman/agent/harness/session/runtime_tests.rs @@ -1,5 +1,4 @@ use super::*; -use crate::core::bus::BUS; use crate::core::events::DomainEvent; use crate::openhuman::agent::dispatcher::XmlToolDispatcher; use crate::openhuman::agent::error::AgentError; diff --git a/src/openhuman/agent/triage/escalation.rs b/src/openhuman/agent/triage/escalation.rs index ef4234c432..eb637987a1 100644 --- a/src/openhuman/agent/triage/escalation.rs +++ b/src/openhuman/agent/triage/escalation.rs @@ -384,7 +384,6 @@ async fn gate_linked_card_terminal(envelope: &TriggerEnvelope, decision: &str) { #[cfg(test)] mod tests { use super::*; - use crate::core::bus::BUS; use crate::core::events::DomainEvent; use crate::openhuman::agent::harness::definition::AgentDefinitionRegistry; use serde_json::json; diff --git a/src/openhuman/agent/triage/events.rs b/src/openhuman/agent/triage/events.rs index fbfb0cf32f..06e3b4798a 100644 --- a/src/openhuman/agent/triage/events.rs +++ b/src/openhuman/agent/triage/events.rs @@ -7,7 +7,6 @@ //! defaults like `source: envelope.source.slug().into()`) without //! fanning out a churning diff. -use crate::core::bus::BUS; use crate::core::events::DomainEvent; use super::envelope::TriggerEnvelope; @@ -108,7 +107,6 @@ pub fn publish_failed(envelope: &TriggerEnvelope, reason: &str) { #[cfg(test)] mod tests { use super::*; - use crate::core::bus::BUS; use crate::core::events::DomainEvent; use crate::openhuman::agent::triage::TriggerEnvelope; use serde_json::json; diff --git a/src/openhuman/channels/tests/runtime_dispatch.rs b/src/openhuman/channels/tests/runtime_dispatch.rs index cb607c967d..3481c125fd 100644 --- a/src/openhuman/channels/tests/runtime_dispatch.rs +++ b/src/openhuman/channels/tests/runtime_dispatch.rs @@ -5,7 +5,6 @@ use super::super::runtime::{ }; use super::super::{traits, Channel}; use super::common::{use_real_agent_handler, NoopMemory, RecordingChannel, SlowModel}; -use crate::core::bus::BUS; use crate::core::events::DomainEvent; use crate::openhuman::agent::bus::{mock_agent_run_turn, AgentTurnRequest, AgentTurnResponse}; use crate::openhuman::inference::provider; diff --git a/src/openhuman/inference/provider/factory_tests.rs b/src/openhuman/inference/provider/factory_tests.rs index 010c828963..f4d860f51b 100644 --- a/src/openhuman/inference/provider/factory_tests.rs +++ b/src/openhuman/inference/provider/factory_tests.rs @@ -1091,7 +1091,6 @@ fn configured_openhuman_jwt_slug_routes_to_managed_chat_model() { #[tokio::test] async fn openhuman_jwt_slug_discloses_pinned_model() { - use crate::core::bus::BUS; use crate::core::events::DomainEvent; use crate::openhuman::security::egress::{EgressDescriptor, EgressReason}; use std::time::Duration; @@ -1140,7 +1139,6 @@ async fn openhuman_jwt_slug_discloses_pinned_model() { #[tokio::test] async fn native_claude_turn_routes_disclose_pinned_models() { - use crate::core::bus::BUS; use crate::core::events::DomainEvent; use crate::openhuman::security::egress::EgressDescriptor; use std::time::Duration; @@ -1326,7 +1324,6 @@ fn crate_native_chat_model_factory_preserves_invalid_route_diagnostics() { /// Complements the isolated emit unit tests in `security::egress`. #[tokio::test] async fn from_string_external_provider_emits_egress_realpath() { - use crate::core::bus::BUS; use crate::core::events::DomainEvent; use crate::openhuman::security::egress::EgressReason; @@ -1368,7 +1365,6 @@ async fn from_string_external_provider_emits_egress_realpath() { /// on the legacy `Provider` path, so the default managed turn disclosed nothing. #[tokio::test] async fn create_chat_model_managed_emits_exactly_one_egress_realpath() { - use crate::core::bus::BUS; use crate::core::events::DomainEvent; use crate::openhuman::security::egress::{EgressDescriptor, EgressReason}; use std::time::Duration; @@ -1421,7 +1417,6 @@ async fn create_chat_model_managed_emits_exactly_one_egress_realpath() { /// (nothing leaves the device — it is disclosed as non-external, no event). #[tokio::test] async fn create_chat_model_local_runtime_does_not_emit_egress_realpath() { - use crate::core::bus::BUS; use crate::core::events::DomainEvent; use crate::openhuman::security::egress::EgressDescriptor; use std::time::Duration; diff --git a/src/openhuman/inference/provider/ops_tests.rs b/src/openhuman/inference/provider/ops_tests.rs index 6b69ee55b3..da88eba03c 100644 --- a/src/openhuman/inference/provider/ops_tests.rs +++ b/src/openhuman/inference/provider/ops_tests.rs @@ -1331,7 +1331,6 @@ async fn api_error_monthly_quota_returns_message_via_demoted_branch() { /// the credentials subscriber can drive reauth. #[tokio::test] async fn publish_backend_session_expired_emits_sanitized_session_expired() { - use crate::core::bus::BUS; use crate::core::events::DomainEvent; crate::core::bus::init().await.expect("bus init"); diff --git a/tests/subconscious_conversation_e2e.rs b/tests/subconscious_conversation_e2e.rs index f1b6b7dfda..f0167cc399 100644 --- a/tests/subconscious_conversation_e2e.rs +++ b/tests/subconscious_conversation_e2e.rs @@ -31,7 +31,6 @@ use std::time::Duration; use async_trait::async_trait; -use openhuman_core::core::bus::BUS; use openhuman_core::core::events::DomainEvent; use openhuman_core::openhuman::subconscious::triggers::types::{ GateDecision, Trigger, TriggerPriority, TriggerSource, diff --git a/tests/subconscious_triggers_e2e.rs b/tests/subconscious_triggers_e2e.rs index ed696f6852..b85bc51b0c 100644 --- a/tests/subconscious_triggers_e2e.rs +++ b/tests/subconscious_triggers_e2e.rs @@ -19,7 +19,6 @@ use std::sync::{Arc, Mutex as StdMutex, OnceLock}; -use openhuman_core::core::bus::BUS; use openhuman_core::core::events::DomainEvent; use openhuman_core::openhuman::agent::triage::{TriageAction, TriageDecision}; use openhuman_core::openhuman::subconscious::triggers::gate::{apply_budget, map_triage_to_gate}; From 17b77551303427b834b6a25e0e2839a213758142 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Sun, 9 Aug 2026 13:51:54 +0300 Subject: [PATCH 19/20] Revert "chore: remove unused BUS imports from tests" This reverts commit 62067fae98e0e70711a329c106ee8e0535b9a6fe. Co-authored-by: Medulla --- src/openhuman/agent/harness/session/runtime_tests.rs | 1 + src/openhuman/agent/triage/escalation.rs | 1 + src/openhuman/agent/triage/events.rs | 2 ++ src/openhuman/channels/tests/runtime_dispatch.rs | 1 + src/openhuman/inference/provider/factory_tests.rs | 5 +++++ src/openhuman/inference/provider/ops_tests.rs | 1 + tests/subconscious_conversation_e2e.rs | 1 + tests/subconscious_triggers_e2e.rs | 1 + 8 files changed, 13 insertions(+) diff --git a/src/openhuman/agent/harness/session/runtime_tests.rs b/src/openhuman/agent/harness/session/runtime_tests.rs index 32d2f5a0c6..f0773579c8 100644 --- a/src/openhuman/agent/harness/session/runtime_tests.rs +++ b/src/openhuman/agent/harness/session/runtime_tests.rs @@ -1,4 +1,5 @@ use super::*; +use crate::core::bus::BUS; use crate::core::events::DomainEvent; use crate::openhuman::agent::dispatcher::XmlToolDispatcher; use crate::openhuman::agent::error::AgentError; diff --git a/src/openhuman/agent/triage/escalation.rs b/src/openhuman/agent/triage/escalation.rs index eb637987a1..ef4234c432 100644 --- a/src/openhuman/agent/triage/escalation.rs +++ b/src/openhuman/agent/triage/escalation.rs @@ -384,6 +384,7 @@ async fn gate_linked_card_terminal(envelope: &TriggerEnvelope, decision: &str) { #[cfg(test)] mod tests { use super::*; + use crate::core::bus::BUS; use crate::core::events::DomainEvent; use crate::openhuman::agent::harness::definition::AgentDefinitionRegistry; use serde_json::json; diff --git a/src/openhuman/agent/triage/events.rs b/src/openhuman/agent/triage/events.rs index 06e3b4798a..fbfb0cf32f 100644 --- a/src/openhuman/agent/triage/events.rs +++ b/src/openhuman/agent/triage/events.rs @@ -7,6 +7,7 @@ //! defaults like `source: envelope.source.slug().into()`) without //! fanning out a churning diff. +use crate::core::bus::BUS; use crate::core::events::DomainEvent; use super::envelope::TriggerEnvelope; @@ -107,6 +108,7 @@ pub fn publish_failed(envelope: &TriggerEnvelope, reason: &str) { #[cfg(test)] mod tests { use super::*; + use crate::core::bus::BUS; use crate::core::events::DomainEvent; use crate::openhuman::agent::triage::TriggerEnvelope; use serde_json::json; diff --git a/src/openhuman/channels/tests/runtime_dispatch.rs b/src/openhuman/channels/tests/runtime_dispatch.rs index 3481c125fd..cb607c967d 100644 --- a/src/openhuman/channels/tests/runtime_dispatch.rs +++ b/src/openhuman/channels/tests/runtime_dispatch.rs @@ -5,6 +5,7 @@ use super::super::runtime::{ }; use super::super::{traits, Channel}; use super::common::{use_real_agent_handler, NoopMemory, RecordingChannel, SlowModel}; +use crate::core::bus::BUS; use crate::core::events::DomainEvent; use crate::openhuman::agent::bus::{mock_agent_run_turn, AgentTurnRequest, AgentTurnResponse}; use crate::openhuman::inference::provider; diff --git a/src/openhuman/inference/provider/factory_tests.rs b/src/openhuman/inference/provider/factory_tests.rs index f4d860f51b..010c828963 100644 --- a/src/openhuman/inference/provider/factory_tests.rs +++ b/src/openhuman/inference/provider/factory_tests.rs @@ -1091,6 +1091,7 @@ fn configured_openhuman_jwt_slug_routes_to_managed_chat_model() { #[tokio::test] async fn openhuman_jwt_slug_discloses_pinned_model() { + use crate::core::bus::BUS; use crate::core::events::DomainEvent; use crate::openhuman::security::egress::{EgressDescriptor, EgressReason}; use std::time::Duration; @@ -1139,6 +1140,7 @@ async fn openhuman_jwt_slug_discloses_pinned_model() { #[tokio::test] async fn native_claude_turn_routes_disclose_pinned_models() { + use crate::core::bus::BUS; use crate::core::events::DomainEvent; use crate::openhuman::security::egress::EgressDescriptor; use std::time::Duration; @@ -1324,6 +1326,7 @@ fn crate_native_chat_model_factory_preserves_invalid_route_diagnostics() { /// Complements the isolated emit unit tests in `security::egress`. #[tokio::test] async fn from_string_external_provider_emits_egress_realpath() { + use crate::core::bus::BUS; use crate::core::events::DomainEvent; use crate::openhuman::security::egress::EgressReason; @@ -1365,6 +1368,7 @@ async fn from_string_external_provider_emits_egress_realpath() { /// on the legacy `Provider` path, so the default managed turn disclosed nothing. #[tokio::test] async fn create_chat_model_managed_emits_exactly_one_egress_realpath() { + use crate::core::bus::BUS; use crate::core::events::DomainEvent; use crate::openhuman::security::egress::{EgressDescriptor, EgressReason}; use std::time::Duration; @@ -1417,6 +1421,7 @@ async fn create_chat_model_managed_emits_exactly_one_egress_realpath() { /// (nothing leaves the device — it is disclosed as non-external, no event). #[tokio::test] async fn create_chat_model_local_runtime_does_not_emit_egress_realpath() { + use crate::core::bus::BUS; use crate::core::events::DomainEvent; use crate::openhuman::security::egress::EgressDescriptor; use std::time::Duration; diff --git a/src/openhuman/inference/provider/ops_tests.rs b/src/openhuman/inference/provider/ops_tests.rs index da88eba03c..6b69ee55b3 100644 --- a/src/openhuman/inference/provider/ops_tests.rs +++ b/src/openhuman/inference/provider/ops_tests.rs @@ -1331,6 +1331,7 @@ async fn api_error_monthly_quota_returns_message_via_demoted_branch() { /// the credentials subscriber can drive reauth. #[tokio::test] async fn publish_backend_session_expired_emits_sanitized_session_expired() { + use crate::core::bus::BUS; use crate::core::events::DomainEvent; crate::core::bus::init().await.expect("bus init"); diff --git a/tests/subconscious_conversation_e2e.rs b/tests/subconscious_conversation_e2e.rs index f0167cc399..f1b6b7dfda 100644 --- a/tests/subconscious_conversation_e2e.rs +++ b/tests/subconscious_conversation_e2e.rs @@ -31,6 +31,7 @@ use std::time::Duration; use async_trait::async_trait; +use openhuman_core::core::bus::BUS; use openhuman_core::core::events::DomainEvent; use openhuman_core::openhuman::subconscious::triggers::types::{ GateDecision, Trigger, TriggerPriority, TriggerSource, diff --git a/tests/subconscious_triggers_e2e.rs b/tests/subconscious_triggers_e2e.rs index b85bc51b0c..ed696f6852 100644 --- a/tests/subconscious_triggers_e2e.rs +++ b/tests/subconscious_triggers_e2e.rs @@ -19,6 +19,7 @@ use std::sync::{Arc, Mutex as StdMutex, OnceLock}; +use openhuman_core::core::bus::BUS; use openhuman_core::core::events::DomainEvent; use openhuman_core::openhuman::agent::triage::{TriageAction, TriageDecision}; use openhuman_core::openhuman::subconscious::triggers::gate::{apply_budget, map_triage_to_gate}; From fd3c88d94f926840c59c1b1a6712477aba7856aa Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Sun, 9 Aug 2026 13:54:21 +0300 Subject: [PATCH 20/20] fix(tests): reorder import to match dependency order Moved the `ProfileStore` import after the `profile` submodule imports to follow Rust's convention of importing parent modules after their children, resolving a compiler warning about out-of-order imports. Auto-committed-on: dragonfly Co-authored-by: Medulla --- tests/learning_phase4_integration_test.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/learning_phase4_integration_test.rs b/tests/learning_phase4_integration_test.rs index 79b5e9eb07..f24a9d0609 100644 --- a/tests/learning_phase4_integration_test.rs +++ b/tests/learning_phase4_integration_test.rs @@ -22,10 +22,10 @@ use openhuman_core::openhuman::agent::learning::candidate::{ }; use openhuman_core::openhuman::agent::learning::profile_md_renderer::ProfileMdRenderer; use openhuman_core::openhuman::agent::learning::stability_detector::StabilityDetector; -use openhuman_core::openhuman::memory::store::ProfileStore; use openhuman_core::openhuman::memory::store::profile::{ FacetState, FacetType, ProfileFacet, UserState, PROFILE_INIT_SQL, }; +use openhuman_core::openhuman::memory::store::ProfileStore; use parking_lot::Mutex; use rusqlite::Connection; use tempfile::TempDir;