fix(runtime): apply 16 MiB worker stack to desktop core + agent CLI runtimes (#3159) (#3175)

This commit is contained in:
YOMXXX
2026-06-01 20:14:27 -07:00
committed by GitHub
parent 12fd0f7c63
commit 33245e8e64
5 changed files with 41 additions and 10 deletions
+15 -9
View File
@@ -1821,21 +1821,27 @@ pub fn run() {
// burns through the same 2 MB. In `crahs.log` (2026-05-17, build
// 0.53.49) that tower plus the serde-monomorphised `Config` Visitor
// frames pushed past the guard page and aborted with
// `SIGBUS / KERN_PROTECTION_FAILURE`. The structural fix
// (`spawn_blocking` for the TOML parse + cache in
// `src/openhuman/config/{schema/load.rs, ops.rs}`) moves the
// largest contributor off the worker; bumping the worker stack
// itself gives the rest of the tower comfortable headroom so future
// additions don't immediately re-tip the same scale. 8 MiB matches
// the OS-default pthread main-thread stack on macOS, so we can
// assume "as much room as the main thread" everywhere.
// `SIGBUS / KERN_PROTECTION_FAILURE`.
//
// The structural fix (`spawn_blocking` for the TOML parse + cache in
// `src/openhuman/config/{schema/load.rs, ops.rs}`) moves the largest
// contributor off the worker. An initial 8 MiB bump shipped here was
// enough for that single tower, but sub-agent delegation (issue #3159
// / PR #3155) re-tipped the scale: the standalone `openhuman-core`
// CLI server still aborted with `Abort trap: 6 / fatal runtime error:
// stack overflow` once an orchestrator delegated. PR #3155 raised the
// standalone server to 16 MiB; the desktop Tauri host is the *same*
// tower running on a *different* runtime and needs the same headroom.
// Share the constant with the rest of `src/core/*` via
// [`openhuman_core::core::runtime::AGENT_WORKER_STACK_BYTES`] so all
// multi-thread runtimes that may host an agent turn stay in sync.
//
// Must happen before any `tauri::async_runtime::*` call, otherwise
// `set(...)` panics with "runtime already initialized".
{
let custom_runtime = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.thread_stack_size(8 * 1024 * 1024)
.thread_stack_size(openhuman_core::core::runtime::AGENT_WORKER_STACK_BYTES)
.build()
.expect("build custom tokio runtime for tauri async surface");
let handle = custom_runtime.handle().clone();
+2
View File
@@ -122,6 +122,7 @@ fn run_dump_all(args: &[String]) -> Result<()> {
let rt = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.thread_stack_size(crate::core::runtime::AGENT_WORKER_STACK_BYTES)
.build()?;
log::debug!("[agent-cli] run_dump_all: calling dump_all_agent_prompts");
let dumps = rt.block_on(async {
@@ -253,6 +254,7 @@ fn run_dump_prompt(args: &[String]) -> Result<()> {
let rt = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.thread_stack_size(crate::core::runtime::AGENT_WORKER_STACK_BYTES)
.build()?;
log::debug!("[agent-cli] run_dump_prompt: calling dump_agent_prompt");
let dumped = rt.block_on(async { dump_agent_prompt(options).await })?;
+7 -1
View File
@@ -288,7 +288,7 @@ fn run_server_command(args: &[String]) -> Result<()> {
// the JSON-RPC server down mid-request. Give workers a roomier stack.
let rt = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.thread_stack_size(16 * 1024 * 1024)
.thread_stack_size(crate::core::runtime::AGENT_WORKER_STACK_BYTES)
.build()?;
rt.block_on(async {
crate::core::jsonrpc::run_server(host.as_deref(), port, socketio_enabled).await
@@ -337,8 +337,11 @@ fn run_call_command(args: &[String]) -> Result<()> {
let method = method.ok_or_else(|| anyhow::anyhow!("--method is required"))?;
let params = parse_json_params(&params).map_err(anyhow::Error::msg)?;
// `call` invokes a JSON-RPC method that may run an orchestrator turn
// (e.g. `agent.chat`), so it needs the same roomy stack as the server.
let rt = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.thread_stack_size(crate::core::runtime::AGENT_WORKER_STACK_BYTES)
.build()?;
let value = rt
.block_on(async { invoke_method(default_state(), &method, params).await })
@@ -415,8 +418,11 @@ fn run_namespace_command(
let method = all::rpc_method_from_parts(namespace, function)
.ok_or_else(|| anyhow::anyhow!("unregistered controller '{namespace}.{function}'"))?;
// Same as the explicit `call` path above — any registered controller may
// ultimately drive an orchestrator turn.
let rt = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.thread_stack_size(crate::core::runtime::AGENT_WORKER_STACK_BYTES)
.build()?;
let value = rt
.block_on(async { invoke_method(default_state(), &method, Value::Object(params)).await })
+1
View File
@@ -20,6 +20,7 @@ pub mod logging;
pub mod memory_cli;
pub mod observability;
pub mod rpc_log;
pub mod runtime;
pub mod shutdown;
pub mod socketio;
pub mod types;
+16
View File
@@ -0,0 +1,16 @@
//! Shared tokio runtime tuning constants.
//!
//! A single agent turn is a very large async state machine (system prompt +
//! hundreds of tool specs + the nested provider/tool loop), and delegating
//! to a sub-agent runs another full turn one level down. Even with the inner
//! sub-agent future boxed, that nesting overflows tokio's default 2 MiB
//! worker-thread stack and aborts the whole process (SIGABRT:
//! "thread 'tokio-rt-worker' has overflowed its stack").
//!
//! PR #3155 set this on the standalone `openhuman-core run` JSON-RPC server.
//! Issue #3159 calls out that every other multi-thread runtime that can host
//! an agent turn (the desktop Tauri host's runtime, `agent_cli`, the rest of
//! `cli.rs`, …) shares the same exposure. Centralising the value keeps them
//! in sync; downstream call sites should set `.thread_stack_size(AGENT_WORKER_STACK_BYTES)`
//! on every multi-thread runtime that may host an agent turn.
pub const AGENT_WORKER_STACK_BYTES: usize = 16 * 1024 * 1024;