Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
3fc8434
feat(about_app): add app info operations and schemas
senamakel Oct 15, 2026
87d10b3
fix(cron): deliver scheduled run output to the configured channel
senamakel Oct 15, 2026
e57c8bb
feat(core): gate component dispatch behind a components check
senamakel Oct 15, 2026
07db5e7
feat(inference): add tokenjuice schema definitions
senamakel Oct 15, 2026
72e2009
refactor(sandbox): thread dispatch context into sandbox policy resolu…
senamakel Oct 15, 2026
6c717c9
refactor(sandbox): thread dispatch context through sandbox policy res…
senamakel Oct 15, 2026
ab2bbf0
feat(memory): ingest session transcripts into memory
senamakel Oct 15, 2026
0c069af
chore(relay): remove unused relay ops module
senamakel Oct 15, 2026
dd195c1
feat(profiles): add capability and provider settings modules
senamakel Oct 15, 2026
a352ecb
feat(profiles): add capability and tool profile definitions
senamakel Oct 15, 2026
ca30f6a
feat(agent): route host agent turns through session host
senamakel Oct 15, 2026
d3d21cb
feat(agent): add host agent support to session host builder
senamakel Oct 15, 2026
0018a26
feat(search): add proxy support for search requests
senamakel Oct 15, 2026
c040636
chore: files changed crates/openhuman-core/src/modules/search/proxy.r…
senamakel Oct 15, 2026
7ba5d86
feat(agent): add graph harness module
senamakel Oct 15, 2026
7b1ccc2
test(sandbox): add coverage for sandbox ops
senamakel Oct 15, 2026
92a0d34
test(browser): add coverage for computer tool actions
senamakel Oct 15, 2026
8545946
feat(agent): add post-commit hook for learning sink
senamakel Oct 15, 2026
67a01ed
test(agent): add hook tests for tool call lifecycle
senamakel Oct 15, 2026
8fcb69d
feat(tinyagents): register tools for tinyagents
senamakel Oct 15, 2026
9978c82
feat(tinyagents): register tools for tinyagents
senamakel Oct 15, 2026
949034a
fix(threads): purge thread data when a thread is deleted
senamakel Oct 15, 2026
e7934f4
chore(tests): add coverage for about app and thread title ops
senamakel Oct 15, 2026
d2abded
refactor(session_host): tidy imports and comment in session host factory
senamakel Oct 15, 2026
0ee584b
refactor(session_host): hoist ambient dispatch context into a binding
senamakel Oct 15, 2026
41074e4
chore(session_host): trim stale comment in agent factory
senamakel Oct 15, 2026
8d9ff1e
chore(ci): refresh saas ambient baseline
senamakel Oct 15, 2026
3a52fee
test(embed): add host profile tests
senamakel Oct 15, 2026
627f958
test(agent-harness): pass dispatch context to fire_hooks
senamakel Oct 15, 2026
e7182b2
refactor(config): thread dispatch context into search settings
senamakel Oct 15, 2026
20d492d
chore: drop unrelated rustfmt churn
senamakel Oct 15, 2026
93cf6ea
Merge upstream/main into fu-7801e-9
senamakel Oct 15, 2026
4eebdfe
chore(vendor): bump tinyconnectors submodule
senamakel Oct 15, 2026
ed8d776
chore(vendor): bump tinyconnectors submodule
senamakel Oct 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions crates/openhuman-core/src/agent/harness/graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,8 +141,8 @@ pub(crate) async fn run_channel_turn_via_graph(
"[channel:graph] routing channel turn through tinyagents harness"
);
let turn_origin = origin.clone();
let mut run_context =
crate::agent::tinyagents::host::OpenHumanRunContext::new().with_core(dispatch.into_core());
let mut run_context = crate::agent::tinyagents::host::OpenHumanRunContext::new()
.with_core(dispatch.core().cloned());
run_context.origin = origin;
run_context.approval = approval;
run_context.memory_identity = Some(memory_identity);
Expand Down Expand Up @@ -192,7 +192,7 @@ pub(crate) async fn run_channel_turn_via_graph(
defer_turn_completed_to_caller: false,
}
.run();
let outcome = match (turn_origin, crate::core::runtime::CoreContext::current()) {
let outcome = match (turn_origin, dispatch.into_core()) {
(Some(origin), Some(context)) => {
crate::core::runtime::CoreContext::scope_with_turn_origin(
context,
Expand Down
14 changes: 9 additions & 5 deletions crates/openhuman-core/src/agent/hooks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -309,23 +309,27 @@ pub fn embedder_tool_hooks_in(ctx: &crate::core::runtime::DispatchCtx) -> Vec<Ar
mod tests;

/// Fire all hooks in parallel, logging errors without blocking the caller.
pub fn fire_hooks(hooks: &[Arc<dyn PostTurnHook>], ctx: TurnContext) {
pub fn fire_hooks(
hooks: &[Arc<dyn PostTurnHook>],
dispatch: &crate::core::runtime::DispatchCtx,
ctx: TurnContext,
) {
log::debug!(
"[learning] dispatching {} post-turn hook(s) (tool_calls={}, response_chars={})",
hooks.len(),
ctx.tool_calls.len(),
ctx.assistant_response.chars().count()
);
// Capture the ambient CoreContext before detaching: a bare `tokio::spawn`
// Take the caller's CoreContext before detaching: a bare `tokio::spawn`
// does not inherit the `CURRENT_CONTEXT` task-local, so under a scoped
// multi-tenant dispatch a detached hook would fall back to the process
// default context — and anything context-derived inside the hook (for
// example a workspace-scoped store) would read and
// write another tenant's workspace. Re-entering the scope inside the task
// keeps the hook on the dispatch it belongs to; when there is no scoped
// context (the desktop's single-tenant path), `current()` already answers
// the process default and re-scoping it is a no-op.
let core_ctx = crate::core::runtime::context::CoreContext::current();
// context (the desktop's single-tenant path), the process default is
// already what the hook reads and re-scoping it is a no-op.
let core_ctx = dispatch.core().cloned();
for (idx, hook) in hooks.iter().enumerate() {
let hook = Arc::clone(hook);
let ctx = ctx.clone();
Expand Down
19 changes: 11 additions & 8 deletions crates/openhuman-core/src/agent/hooks_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,17 +111,17 @@ async fn fire_hooks_accepts_empty_hook_list() {
iteration_count: 1,
};
// Should not panic
fire_hooks(&[], ctx);
fire_hooks(&[], &crate::core::runtime::DispatchCtx::unbound(), ctx);
}

// ── Detached hooks keep the dispatch's CoreContext ────────────────────────────

/// A detached hook task must observe the same ambient [`CoreContext`] as the
/// dispatch that fired it. A bare `tokio::spawn` loses the `CURRENT_CONTEXT`
/// A detached hook task must observe the same [`CoreContext`] as the dispatch
/// that fired it, handed over explicitly. A bare `tokio::spawn` loses the `CURRENT_CONTEXT`
/// task-local — under a scoped multi-tenant dispatch that meant the hook fell
/// back to the process default context, and any context-derived work behind
/// it read and wrote another tenant's workspace. `fire_hooks` now re-enters
/// the captured scope inside the spawned task; this pins that.
/// the context it was given inside the spawned task; this pins that.
#[tokio::test]
async fn fired_hooks_observe_the_firing_dispatchs_core_context() {
use crate::core::runtime::context::CoreContext;
Expand Down Expand Up @@ -164,10 +164,13 @@ async fn fired_hooks_observe_the_firing_dispatchs_core_context() {
iteration_count: 0,
};

CoreContext::scope(std::sync::Arc::clone(&tenant_ctx), async {
fire_hooks(&[probe], turn);
})
.await;
// The explicit context is what the hook runs under; no task scope is
// entered around the call.
fire_hooks(
&[probe],
&crate::core::runtime::DispatchCtx::new(std::sync::Arc::clone(&tenant_ctx)),
turn,
);

let seen = tokio::time::timeout(std::time::Duration::from_secs(5), rx)
.await
Expand Down
7 changes: 6 additions & 1 deletion crates/openhuman-core/src/agent/host_agents.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
//! `TrustedAutomation { Cron }` and a workflow node under its workflow
//! origin. Resolving an agent does not change who is asking.

use crate::core::runtime::DispatchCtx;
use std::sync::{Arc, LazyLock, PoisonError, RwLock};

use crate::agent::harness::host_definition::AgentDefinition;
Expand Down Expand Up @@ -70,7 +71,11 @@ impl HostAgent {
"[host_agents] building session for host agent"
);
let mut session = CoreContext::sync_scope(Arc::clone(&self.context), || {
if crate::profiles::capabilities::ProfileCapabilities::current().is_some() {
if crate::profiles::capabilities::ProfileCapabilities::in_ctx(&DispatchCtx::new(
Arc::clone(&self.context),
))
.is_some()
{
return OpenHumanSessionHost::from_config_host_only(
config,
&self.definition,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use crate::agent::host_runtime;
use crate::agent::prompts::SystemPromptBuilder;
use crate::agent::session_host::types::OpenHumanSessionHost;
use crate::config::Config;
use crate::core::runtime::DispatchCtx;
use crate::security::SecurityPolicy;
use crate::tools;
use anyhow::Result;
Expand Down Expand Up @@ -78,9 +79,8 @@ impl OpenHumanSessionHost {
/// Tauri web channel (see `channels::provider::web::build_session_agent`).
pub fn from_config_for_agent(config: &Config, agent_id: &str) -> Result<Self> {
// A hosted profile never receives the ordinary config-derived belt.
// Its operator-selected factory carries registration and permission
// boundaries into the same session builder, under this profile scope.
if crate::profiles::capabilities::ProfileCapabilities::current().is_some() {
let ambient = DispatchCtx::ambient();
if crate::profiles::capabilities::ProfileCapabilities::in_ctx(&ambient).is_some() {
return Self::from_config_for_agent_session(config, agent_id, None);
}
let target_def = resolve_target_definition(config, agent_id)?;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

use crate::agent::OpenHumanSessionHost;
use crate::config::Config;
use crate::core::runtime::DispatchCtx;

impl OpenHumanSessionHost {
/// Build a config-routed agent with its caller's exact conversation key.
Expand All @@ -15,7 +16,9 @@ impl OpenHumanSessionHost {
agent_id: &str,
session_id: Option<&str>,
) -> anyhow::Result<Self> {
if let Some(capabilities) = crate::profiles::capabilities::ProfileCapabilities::current() {
if let Some(capabilities) =
crate::profiles::capabilities::ProfileCapabilities::in_ctx(&DispatchCtx::ambient())
{
let mut definition = super::factory::resolve_target_definition(config, agent_id)?
.ok_or_else(|| anyhow::anyhow!("profile agent definition is unavailable"))?;
capabilities.apply_system_prompt(&mut definition);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,10 @@ impl OpenHumanTurnPrelude {
if self.omit_memory_context {
identity.recall = false;
}
let profile_context = capture_profile_context(crate::profiles::host::host().as_deref());
let profile_context = capture_profile_context(
&self.dispatch_ctx(),
crate::profiles::host::host().as_deref(),
);
if matches!(profile_context, ProfileMemoryContext::Denied) {
return None;
}
Expand Down Expand Up @@ -237,8 +240,11 @@ impl OpenHumanTurnPrelude {

// Resolve the profile owner at preparation; a canonical commit may run after
// the immutable turn-origin scope has ended.
fn capture_profile_context(host: Option<&crate::profiles::ProfileHost>) -> ProfileMemoryContext {
let Some(context) = crate::core::runtime::CoreContext::scoped() else {
fn capture_profile_context(
ctx: &DispatchCtx,
host: Option<&crate::profiles::ProfileHost>,
) -> ProfileMemoryContext {
let Some(context) = ctx.core().cloned() else {
return if crate::core::runtime::is_saas() {
ProfileMemoryContext::Denied
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,8 @@ async fn deferred_memory_commit_survives_the_turn_scope_without_pinning_the_prof
let id = ProfileId::parse("deferred-memory").unwrap();
host.provision(&id).await.unwrap();
let profile = host.open(&id).await.unwrap();
let captured = CoreContext::scope_with_turn_origin(profile.context().clone(), None, async {
capture_profile_context(Some(&host))
})
.await;
let captured =
capture_profile_context(&DispatchCtx::new(profile.context().clone()), Some(&host));
// The entire temporary turn context is gone before this callback is polled.
let config = profile.config.clone();
let work = async {
Expand Down Expand Up @@ -161,8 +159,7 @@ async fn missing_profile_owner_refuses_deferred_memory_instead_of_running_unscop
)
.profile("missing-owner"),
);
let captured =
CoreContext::scope(missing, async { capture_profile_context(Some(&host)) }).await;
let captured = capture_profile_context(&DispatchCtx::new(missing), Some(&host));
deferred_memory_work(captured, async {
panic!("a failed profile owner capture must not run under the configured external engine")
})
Expand All @@ -183,14 +180,14 @@ async fn failed_profile_capture_is_denied_but_ordinary_desktop_work_still_runs()
)
.profile(id),
);
let captured = CoreContext::scope(profile, async { capture_profile_context(None) }).await;
let captured = capture_profile_context(&DispatchCtx::new(profile), None);
assert!(matches!(captured, ProfileMemoryContext::Denied));
deferred_memory_work(captured, async {
panic!("a profile without its host cannot resolve an external engine")
})
.await;
}
let captured = CoreContext::scope(operator, async { capture_profile_context(None) }).await;
let captured = capture_profile_context(&DispatchCtx::new(operator), None);
assert!(matches!(captured, ProfileMemoryContext::NonProfile));
let mut ran = false;
deferred_memory_work(captured, async {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,8 @@ pub(super) async fn finalize_committed_turn(
receipt: tinyagents_runtime::CommitReceipt<crate::agent::tinyagents::host::OpenHumanRunContext>,
) {
let commit_started = std::time::Instant::now();
// The turn's own context, taken before the receipt is dropped.
let dispatch = receipt.options.context.dispatch_ctx();
let output = receipt.outcome.output.clone().unwrap_or_default();
// Skips compaction checkpoints (user-role, not the user's words).
let input = tinyagents_harness::summarization::last_user_message(&receipt.outcome.history)
Expand Down Expand Up @@ -235,6 +237,7 @@ pub(super) async fn finalize_committed_turn(
}
crate::agent::hooks::fire_hooks(
post_turn_hooks,
&dispatch,
crate::agent::hooks::TurnContext {
user_message: input,
assistant_response: output,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,12 @@ impl LearningSink for OpenHumanLearningSink {
self.hooks.len()
);

hooks::fire_hooks(&self.hooks, Self::turn_context_from(summary));
// Transitional (#7801): the learning sink is built without a context.
hooks::fire_hooks(
&self.hooks,
&crate::core::runtime::DispatchCtx::ambient(),
Self::turn_context_from(summary),
);
Ok(())
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,10 @@ impl ToolExecScope for TurnOriginScope {
.and_then(|extension| extension.downcast_ref::<ToolExecutionContext>())
.and_then(|context| context.state::<HostOperationContext>())
.and_then(|context| context.origin.clone());
match (origin, CoreContext::current()) {
match (
origin,
super::host::run_context::dispatch_ctx_for_tool(context).into_core(),
) {
(Some(origin), Some(core_context)) => Box::pin(CoreContext::scope_with_turn_origin(
core_context,
Some(origin),
Expand Down
2 changes: 1 addition & 1 deletion crates/openhuman-core/src/channels/providers/relay/ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ pub(crate) async fn run_relay_turn(
let mut runtime_message = crate::channels::runtime::RuntimeChannelMessage::from(message);
runtime_message.prepared_inbound = Some(prepared);
let turn = process_channel_runtime_message(ctx, runtime_message);
if let Some(context) = crate::core::runtime::CoreContext::scoped() {
if let Some(context) = dispatch_ctx.core() {
crate::core::runtime::CoreContext::scope(
context.derive_channel_turn(Some(thread_id.clone()), None),
Box::pin(turn),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ use crate::channels::{ChannelSendExt, SendMessage};
use crate::config::Config;
use crate::core::bus::BUS;
use crate::core::events::DomainEvent;
use crate::core::runtime::DispatchCtx;
use tinychannels_bus as traits;

pub(crate) mod posture;
Expand Down Expand Up @@ -53,9 +54,13 @@ pub(crate) fn bound_agent_id<'a>(config: &'a Config, channel: &str) -> Option<&'

/// Resolve `channel`'s binding against the installed host agents.
pub(crate) fn route(ctx: &ChannelRuntimeContext, channel: &str) -> HostRoute {
if let Some(capabilities) = crate::profiles::capabilities::ProfileCapabilities::current() {
// Transitional (#7801): the channel runtime context carries no core yet.
let dispatch = DispatchCtx::ambient();
if let Some(capabilities) =
crate::profiles::capabilities::ProfileCapabilities::in_ctx(&dispatch)
{
if let (Some(context), Some(config), Some(definitions)) = (
crate::core::runtime::CoreContext::scoped(),
dispatch.core().cloned(),
ctx.config.as_ref(),
crate::agent::harness::host_definition::AgentDefinitionRegistry::current(),
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use crate::agent::host_runtime;
use crate::channels::context::ChannelRuntimeContext;
use crate::channels::system_prompt::{ChannelPromptInputs, ChannelSystemPrompt};
use crate::config::Config;
use crate::core::runtime::DispatchCtx;
use crate::inference::provider;
use crate::security::SecurityPolicy;
use crate::tools;
Expand Down Expand Up @@ -141,7 +142,8 @@ pub(crate) fn build_channel_turn_parts(
config.workspace_dir.clone(),
)?;
let workspace = config.workspace_dir.clone();
let capabilities = crate::profiles::capabilities::ProfileCapabilities::current();
let capabilities =
crate::profiles::capabilities::ProfileCapabilities::in_ctx(&DispatchCtx::ambient());
let tools_registry = Arc::new(if capabilities.is_some() {
// The profile host route builds the selected belt once with the exact
// durable thread. No second, unnamed factory invocation is needed here.
Expand Down
11 changes: 1 addition & 10 deletions crates/openhuman-core/src/config/ops/loader/load.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,7 @@ pub async fn load_config_with_timeout() -> Result<Config, String> {
// read the task's ambient context. SaaS reads only the task's own scope, as
// `Config::load_or_init` does, so a task that lost its user scope fails
// rather than getting the operator's.
let ambient = if crate::core::runtime::is_saas() {
crate::core::runtime::CoreContext::scoped()
} else {
crate::core::runtime::CoreContext::current()
};
let ctx = ambient.map_or_else(
crate::core::runtime::DispatchCtx::unbound,
crate::core::runtime::DispatchCtx::new,
);
load_config_in(&ctx).await
load_config_in(&crate::core::runtime::DispatchCtx::ambient()).await
}

/// [`load_config_with_timeout`] for the context the caller was handed.
Expand Down
5 changes: 3 additions & 2 deletions crates/openhuman-core/src/config/ops/search.rs
Original file line number Diff line number Diff line change
Expand Up @@ -289,13 +289,14 @@ pub fn apply_search_patch(config: &mut Config, update: SearchSettingsPatch) -> R

/// Validate, save, refresh the module, and return the new settings.
pub async fn apply_search_settings(
ctx: &DispatchCtx,
config: &mut Config,
update: SearchSettingsPatch,
) -> Result<Outcome<Value>, String> {
apply_search_patch(config, update)?;
config.save().await.map_err(|e| e.to_string())?;
#[cfg(feature = "modules")]
crate::modules::search::refresh_loaded(config).await?;
crate::modules::search::refresh_loaded(ctx, config).await?;
tracing::debug!(
enabled = crate::search::is_active(config),
providers = ?config.search.enabled_provider_names(),
Expand All @@ -315,7 +316,7 @@ pub async fn load_and_apply_search_settings(
update: SearchSettingsPatch,
) -> Result<Outcome<Value>, String> {
let mut config = ctx.load_config().await?;
apply_search_settings(&mut config, update).await
apply_search_settings(ctx, &mut config, update).await
}

/// The settings view: every listed provider's resolved state, the role
Expand Down
Loading
Loading