diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml index 19bbdadc9fd..14bffe55cca 100644 --- a/.github/workflows/build-desktop.yml +++ b/.github/workflows/build-desktop.yml @@ -218,7 +218,7 @@ jobs: libnss3 libnspr4 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 \ libxkbcommon0 libxcomposite1 libxdamage1 libxfixes3 libxrandr2 \ libgbm1 libpango-1.0-0 libcairo2 libatspi2.0-0 libxshmfence1 libu2f-udev \ - xvfb dbus dbus-x11 + xvfb dbus dbus-x11 mold command -v xdg-mime >/dev/null 2>&1 \ || { echo "::error::xdg-mime is still missing after installing xdg-utils"; exit 1; } # NOTE: The post-build dump lives further down (after `cargo tauri build`) @@ -431,6 +431,16 @@ jobs: TAURI_CONFIG_OVERRIDE: ${{ steps.config-overrides.outputs.json }} MATRIX_ARGS: ${{ matrix.settings.args }} PROFILE_FLAG: ${{ inputs.build_profile == 'debug' && '--debug' || '' }} + # Linux only: link with mold and fold byte-identical functions. The + # release profile's 16 codegen units and cross-crate instantiation + # leave many identical copies of the same function; MSVC already + # folds them (/OPT:ICF) and Apple's ld deduplicates by default, GNU ld + # cannot. `safe` only folds functions whose address is never taken, + # so C code comparing function pointers (sqlite) is unaffected. + # Measured on openhuman-core: 99.9 -> 97.6 MiB stripped. Per-target + # variables only reach the `--target` build, never build scripts. + CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS: -C link-arg=-fuse-ld=mold -C link-arg=-Wl,--icf=safe + CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_RUSTFLAGS: -C link-arg=-fuse-ld=mold -C link-arg=-Wl,--icf=safe run: | # Inline NODE_OPTIONS so it reaches the vite child spawned by # beforeBuildCommand. Step-level env was observed not to propagate diff --git a/AGENTS.md b/AGENTS.md index ec8f1a06a89..ec93fbfbdb8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -120,7 +120,8 @@ Keep matching profile settings synchronized between `Cargo.toml` and `crates/openhuman-app/Cargo.toml`: - Development dependencies use `debug = false`. -- Release builds use thin LTO, one codegen unit, symbol stripping, and +- Release builds use thin LTO, 16 codegen units (one unit pushed the desktop + release matrix from ~46 to ~84 min, 6941b18c85), symbol stripping, and `debug = "line-tables-only"`. ## Testing and CI diff --git a/crates/openhuman-core/src/config/schema/load/dirs.rs b/crates/openhuman-core/src/config/schema/load/dirs.rs index b0a8e57c334..ae783a38d26 100644 --- a/crates/openhuman-core/src/config/schema/load/dirs.rs +++ b/crates/openhuman-core/src/config/schema/load/dirs.rs @@ -322,7 +322,16 @@ pub(crate) async fn resolve_runtime_config_dirs( /// revision separately: a switch between those two reads pairs workspace A /// with B's revision, and a receiver comparing revisions then ranks the stale /// A above the B it should yield to. -pub async fn active_workspace_snapshot() -> Result<(PathBuf, u64)> { +/// Returned boxed and `#[inline(never)]` on purpose: other crates await this +/// (`openhuman-rpc`, `openhuman-embed`), and an `async fn` body is otherwise +/// re-instantiated inside every calling crate's state machine. Boxing here +/// keeps one copy, compiled in this crate. +#[inline(never)] +pub fn active_workspace_snapshot() -> futures::future::BoxFuture<'static, Result<(PathBuf, u64)>> { + Box::pin(active_workspace_snapshot_inner()) +} + +async fn active_workspace_snapshot_inner() -> Result<(PathBuf, u64)> { // An embedding host that supplied its own `Config` is authoritative, and // `config::ops::load_config_with_timeout` already short-circuits on it for // exactly this reason. Resolving from disk/env here instead would answer @@ -332,9 +341,13 @@ pub async fn active_workspace_snapshot() -> Result<(PathBuf, u64)> { // mismatch there is not a wrong banner, it is *no* banner, permanently, // with only a `debug!` line to say so. See AGENTS.md, "CoreBuilder::config // alone configures boot and nothing else". - if let Some(config) = crate::core::runtime::context::CoreContext::current_embedder_config() { - let revision = super::active_workspace::publish_active_workspace(&config.workspace_dir); - return Ok((config.workspace_dir, revision)); + if let Some(workspace_dir) = + crate::core::runtime::context::CoreContext::with_current_embedder_config(|config| { + config.workspace_dir.clone() + }) + { + let revision = super::active_workspace::publish_active_workspace(&workspace_dir); + return Ok((workspace_dir, revision)); } let (default_openhuman_dir, default_workspace_dir) = default_config_and_workspace_dirs()?; let (_, workspace_dir, source) = diff --git a/crates/openhuman-core/src/config/schema/types.rs b/crates/openhuman-core/src/config/schema/types.rs index 119292d68fc..0e7a0762a67 100644 --- a/crates/openhuman-core/src/config/schema/types.rs +++ b/crates/openhuman-core/src/config/schema/types.rs @@ -4,6 +4,7 @@ //! Load/save and env overrides extend `Config` in `load/`. mod config; +mod config_clone; mod defaults; mod model_ids; mod output_language; diff --git a/crates/openhuman-core/src/config/schema/types/config.rs b/crates/openhuman-core/src/config/schema/types/config.rs index 6b4ef6b4bc3..aa68e842f02 100644 --- a/crates/openhuman-core/src/config/schema/types/config.rs +++ b/crates/openhuman-core/src/config/schema/types/config.rs @@ -50,7 +50,9 @@ pub struct CustomEmbeddingsConfig { } /// Top-level configuration (config.toml root). -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +/// +/// `Clone` is implemented by hand in `config_clone.rs` so it is emitted once. +#[derive(Debug, Serialize, Deserialize, JsonSchema)] pub struct Config { #[serde(skip)] pub workspace_dir: PathBuf, diff --git a/crates/openhuman-core/src/config/schema/types/config_clone.rs b/crates/openhuman-core/src/config/schema/types/config_clone.rs new file mode 100644 index 00000000000..e54ecffbae1 --- /dev/null +++ b/crates/openhuman-core/src/config/schema/types/config_clone.rs @@ -0,0 +1,109 @@ +//! Hand-written `Clone` for [`Config`]. +//! +//! `#[derive(Clone)]` marks `clone` `#[inline]`, so every codegen unit and +//! every crate that clones a `Config` emits its own copy of this ~48 KiB +//! function: 17 copies in a release `openhuman-core`. `#[inline(never)]` +//! keeps exactly one. The struct literal lists every field, so adding a field +//! to `Config` without adding it here is a compile error, not a silent skip. + +use super::config::Config; + +impl Clone for Config { + #[inline(never)] + fn clone(&self) -> Self { + Self { + workspace_dir: self.workspace_dir.clone(), + action_dir: self.action_dir.clone(), + action_dir_override: self.action_dir_override.clone(), + files_dir_override: self.files_dir_override.clone(), + files_dir_history: self.files_dir_history.clone(), + config_path: self.config_path.clone(), + cli_inference_snapshot: self.cli_inference_snapshot.clone(), + recovered_from_corruption: self.recovered_from_corruption, + schema_version: self.schema_version, + api_url: self.api_url.clone(), + api_key: self.api_key.clone(), + inference_url: self.inference_url.clone(), + default_model: self.default_model.clone(), + default_temperature: self.default_temperature, + output_language: self.output_language.clone(), + temperature_unsupported_models: self.temperature_unsupported_models.clone(), + dashboard: self.dashboard.clone(), + observability: self.observability.clone(), + autonomy: self.autonomy.clone(), + desktop: self.desktop.clone(), + computer: self.computer.clone(), + hooks: self.hooks.clone(), + privacy: self.privacy, + sandbox: self.sandbox.clone(), + runtime: self.runtime.clone(), + shell: self.shell.clone(), + web_chat: self.web_chat.clone(), + reliability: self.reliability.clone(), + scheduler: self.scheduler.clone(), + scheduler_gate: self.scheduler_gate.clone(), + agent: self.agent.clone(), + orchestrator: self.orchestrator.clone(), + teams: self.teams.clone(), + context: self.context.clone(), + model_routes: self.model_routes.clone(), + embedding_routes: self.embedding_routes.clone(), + cron: self.cron.clone(), + task_sources: self.task_sources.clone(), + channels_config: self.channels_config.clone(), + memory: self.memory.clone(), + composio: self.composio.clone(), + secrets: self.secrets.clone(), + browser: self.browser.clone(), + http_request: self.http_request.clone(), + curl: self.curl.clone(), + gitbooks: self.gitbooks.clone(), + mcp_client: self.mcp_client.clone(), + modules: self.modules.clone(), + capability_providers: self.capability_providers.clone(), + multimodal: self.multimodal.clone(), + multimodal_files: self.multimodal_files.clone(), + seltz: self.seltz.clone(), + searxng: self.searxng.clone(), + web_search: self.web_search.clone(), + search: self.search.clone(), + proxy: self.proxy.clone(), + cost: self.cost.clone(), + legacy_memory_sources: self.legacy_memory_sources.clone(), + agent_registry: self.agent_registry.clone(), + agents: self.agents.clone(), + local_ai: self.local_ai.clone(), + claude_agent_sdk: self.claude_agent_sdk.clone(), + cloud_providers: self.cloud_providers.clone(), + primary_cloud: self.primary_cloud.clone(), + ephemeral_route: self.ephemeral_route.clone(), + chat_provider: self.chat_provider.clone(), + reasoning_provider: self.reasoning_provider.clone(), + agentic_provider: self.agentic_provider.clone(), + coding_provider: self.coding_provider.clone(), + vision_provider: self.vision_provider.clone(), + memory_provider: self.memory_provider.clone(), + embeddings_provider: self.embeddings_provider.clone(), + custom_embeddings: self.custom_embeddings.clone(), + node: self.node.clone(), + runtime_python: self.runtime_python.clone(), + runtime_pool: self.runtime_pool.clone(), + tokenjuice: self.tokenjuice.clone(), + hosting: self.hosting.clone(), + voice_server: self.voice_server.clone(), + voice_providers: self.voice_providers.clone(), + stt_provider: self.stt_provider.clone(), + tts_provider: self.tts_provider.clone(), + integrations: self.integrations.clone(), + update: self.update.clone(), + dictation: self.dictation.clone(), + onboarding_completed: self.onboarding_completed, + chat_onboarding_completed: self.chat_onboarding_completed, + model_registry: self.model_registry.clone(), + } + } +} + +#[cfg(test)] +#[path = "config_clone_tests.rs"] +mod tests; diff --git a/crates/openhuman-core/src/config/schema/types/config_clone_tests.rs b/crates/openhuman-core/src/config/schema/types/config_clone_tests.rs new file mode 100644 index 00000000000..bdffc514592 --- /dev/null +++ b/crates/openhuman-core/src/config/schema/types/config_clone_tests.rs @@ -0,0 +1,31 @@ +use super::*; +use std::path::PathBuf; + +#[test] +fn clone_copies_every_serialized_field() { + let mut config = Config::default(); + config.workspace_dir = PathBuf::from("/tmp/clone-ws"); + config.default_model = Some("clone-model".into()); + config.chat_onboarding_completed = true; + + let cloned = config.clone(); + + assert_eq!(cloned.workspace_dir, config.workspace_dir); + assert_eq!(cloned.default_model, config.default_model); + assert!(cloned.chat_onboarding_completed); + assert_eq!( + serde_json::to_value(&cloned).expect("cloned config serializes"), + serde_json::to_value(&config).expect("config serializes"), + ); +} + +#[test] +fn clone_is_independent_of_the_original() { + let config = Config { + default_model: Some("before".into()), + ..Config::default() + }; + let mut cloned = config.clone(); + cloned.default_model = Some("after".into()); + assert_eq!(config.default_model.as_deref(), Some("before")); +} diff --git a/crates/openhuman-core/src/core/runtime/context.rs b/crates/openhuman-core/src/core/runtime/context.rs index 3c5a3696e2e..74d1144516a 100644 --- a/crates/openhuman-core/src/core/runtime/context.rs +++ b/crates/openhuman-core/src/core/runtime/context.rs @@ -489,6 +489,15 @@ impl CoreContext { Self::current().and_then(|ctx| ctx.embedder_config.clone()) } + /// Read the embedder-supplied config for the current dispatch without + /// cloning it. Prefer this over [`Self::current_embedder_config`] when only + /// a field or a predicate is needed: a `Config` clone is a large copy. + pub fn with_current_embedder_config( + f: impl FnOnce(&crate::config::Config) -> R, + ) -> Option { + Self::current().and_then(|ctx| ctx.embedder_config.as_ref().map(f)) + } + /// Rebind the process default context to the current active user's /// workspace. Desktop login, logout, and pending-session revalidation can /// switch the active workspace after boot without rebuilding the core. diff --git a/crates/openhuman-core/src/core/runtime/context_tests.rs b/crates/openhuman-core/src/core/runtime/context_tests.rs index d52b245d07a..4f2ae9df327 100644 --- a/crates/openhuman-core/src/core/runtime/context_tests.rs +++ b/crates/openhuman-core/src/core/runtime/context_tests.rs @@ -86,6 +86,27 @@ async fn the_current_dispatch_sees_the_scoped_embedder_config() { assert_eq!(scoped.workspace_dir, PathBuf::from("/tmp/scoped-ws")); } +#[tokio::test] +async fn the_current_embedder_config_can_be_read_without_cloning_it() { + // `with_current_embedder_config` is the borrow-only twin of + // `current_embedder_config`: same scoped config, projected through a closure. + let mut config = crate::config::Config::default(); + config.workspace_dir = PathBuf::from("/tmp/borrowed-ws"); + + let read = CoreContext::scope(ctx_with_config(config), async { + CoreContext::with_current_embedder_config(|c| c.workspace_dir.clone()) + }) + .await; + assert_eq!(read, Some(PathBuf::from("/tmp/borrowed-ws"))); + + // Without an embedder config the closure never runs. + let none = CoreContext::scope(ctx("/tmp/no-embedder-ws"), async { + CoreContext::with_current_embedder_config(|_| panic!("no config to read")) + }) + .await; + assert_eq!(none, None::<()>); +} + // ---- derived per-agent contexts (the multi-agent library seam) ----------- // // `derive_with` is how one booted runtime hosts many independently configured diff --git a/crates/openhuman-core/src/inference/host_runtime/ops/agent_chat.rs b/crates/openhuman-core/src/inference/host_runtime/ops/agent_chat.rs index b4aa7160e3a..f819f959e27 100644 --- a/crates/openhuman-core/src/inference/host_runtime/ops/agent_chat.rs +++ b/crates/openhuman-core/src/inference/host_runtime/ops/agent_chat.rs @@ -219,8 +219,37 @@ fn build_turn_agent( /// thread. /// * The agent is built by `target`, so a library host can run one booted /// core with many independently defined agents. +/// +/// Returned boxed and `#[inline(never)]` on purpose: other crates await this +/// (`openhuman-rpc`, `openhuman-embed`), and an `async fn` body is otherwise +/// re-instantiated inside every calling crate's state machine. Boxing here +/// keeps one copy, compiled in this crate. +#[allow(clippy::too_many_arguments)] +#[inline(never)] +pub fn agent_chat_for<'a>( + config: &'a mut Config, + target: AgentChatTarget<'a>, + message: &'a str, + model_override: Option, + temperature: Option, + thread_id: Option, + cwd: Option, + route: Option, +) -> futures::future::BoxFuture<'a, Result, String>> { + Box::pin(agent_chat_for_inner( + config, + target, + message, + model_override, + temperature, + thread_id, + cwd, + route, + )) +} + #[allow(clippy::too_many_arguments)] -pub async fn agent_chat_for( +async fn agent_chat_for_inner( config: &mut Config, target: AgentChatTarget<'_>, message: &str, diff --git a/crates/openhuman-core/src/security/credentials/ops/gated_services.rs b/crates/openhuman-core/src/security/credentials/ops/gated_services.rs index 7411393ba7c..6ed6fa01820 100644 --- a/crates/openhuman-core/src/security/credentials/ops/gated_services.rs +++ b/crates/openhuman-core/src/security/credentials/ops/gated_services.rs @@ -12,7 +12,7 @@ use crate::config::Config; /// that config's path and never touch the operator's global /// `~/.openhuman/active_user.toml` / `users/` tree. pub(super) fn is_embedder_host() -> bool { - crate::core::runtime::context::CoreContext::current_embedder_config().is_some() + crate::core::runtime::context::CoreContext::with_current_embedder_config(|_| ()).is_some() } /// Populate the process cache away from the next chat turn after startup or a diff --git a/crates/openhuman-core/src/security/credentials/session_support.rs b/crates/openhuman-core/src/security/credentials/session_support.rs index 744495dd5f5..cc614409226 100644 --- a/crates/openhuman-core/src/security/credentials/session_support.rs +++ b/crates/openhuman-core/src/security/credentials/session_support.rs @@ -536,8 +536,8 @@ pub fn session_token_from_profile(profile: Option<&AuthProfile>) -> Option bool { - crate::core::runtime::CoreContext::current_embedder_config() - .is_some_and(|config| super::api_key::has_api_key(&config)) + crate::core::runtime::CoreContext::with_current_embedder_config(super::api_key::has_api_key) + .unwrap_or(false) } #[cfg(test)] diff --git a/crates/openhuman-core/src/web_chat/ops/start_chat.rs b/crates/openhuman-core/src/web_chat/ops/start_chat.rs index b082978eead..64b72552fd0 100644 --- a/crates/openhuman-core/src/web_chat/ops/start_chat.rs +++ b/crates/openhuman-core/src/web_chat/ops/start_chat.rs @@ -131,7 +131,36 @@ fn prompt_guard_user_message(action: PromptEnforcementAction) -> &'static str { } } -pub async fn start_chat( +/// Returned boxed and `#[inline(never)]` on purpose: other crates await this +/// (`openhuman-rpc`, `openhuman-embed`), and an `async fn` body is otherwise +/// re-instantiated inside every calling crate's state machine. Boxing here +/// keeps one copy, compiled in this crate. +#[allow(clippy::too_many_arguments)] +#[inline(never)] +pub fn start_chat<'a>( + client_id: &'a str, + thread_id: &'a str, + message: &'a str, + model_override: Option, + temperature: Option, + locale: Option, + queue_mode: Option, + metadata: ChatRequestMetadata, +) -> futures::future::BoxFuture<'a, Result> { + Box::pin(start_chat_inner( + client_id, + thread_id, + message, + model_override, + temperature, + locale, + queue_mode, + metadata, + )) +} + +#[allow(clippy::too_many_arguments)] +async fn start_chat_inner( client_id: &str, thread_id: &str, message: &str,