Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion .github/workflows/build-desktop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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`)
Expand Down Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
21 changes: 17 additions & 4 deletions crates/openhuman-core/src/config/schema/load/dirs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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) =
Expand Down
1 change: 1 addition & 0 deletions crates/openhuman-core/src/config/schema/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
4 changes: 3 additions & 1 deletion crates/openhuman-core/src/config/schema/types/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
109 changes: 109 additions & 0 deletions crates/openhuman-core/src/config/schema/types/config_clone.rs
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -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"));
}
9 changes: 9 additions & 0 deletions crates/openhuman-core/src/core/runtime/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<R>(
f: impl FnOnce(&crate::config::Config) -> R,
) -> Option<R> {
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.
Expand Down
21 changes: 21 additions & 0 deletions crates/openhuman-core/src/core/runtime/context_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,
temperature: Option<f64>,
thread_id: Option<String>,
cwd: Option<String>,
route: Option<crate::config::schema::EphemeralRoute>,
) -> futures::future::BoxFuture<'a, Result<Outcome<String>, 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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -536,8 +536,8 @@ pub fn session_token_from_profile(profile: Option<&AuthProfile>) -> Option<Strin
/// when there is one; a host that discovers its config from disk has no API
/// key by construction (it is only ever installed by a library runtime).
pub fn ambient_config_has_api_key() -> 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)]
Expand Down
31 changes: 30 additions & 1 deletion crates/openhuman-core/src/web_chat/ops/start_chat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,
temperature: Option<f64>,
locale: Option<String>,
queue_mode: Option<String>,
metadata: ChatRequestMetadata,
) -> futures::future::BoxFuture<'a, Result<String, StartChatError>> {
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,
Expand Down
Loading