fix: bootstrap the async entry point on a 16 MiB thread to survive Windows

Windows links the process main thread with a ~1 MiB stack (unix defaults
to 8 MiB). Coyote's root async future is a compiler-generated state
machine whose debug-build poll frames blow through 1 MiB the moment
block_on polls it, so every invocation of the binary on Windows died at
startup with STATUS_STACK_OVERFLOW (0xC00000FD) before doing any work.

Replace #[tokio::main] with an explicit bootstrap: main() spawns a
dedicated thread (named "main", 16 MiB stack) that builds the same
multi-thread enable_all runtime and block_on's the async body, then joins
and propagates the Result unchanged; panics are re-raised on the real
main thread via resume_unwind without re-invoking the panic hook, so
panic output and exit codes stay identical. Runtime worker threads get
the same 16 MiB headroom since spawned tasks embed the same class of
giant state machine.

The bootstrap is unconditional rather than cfg(windows)-gated: the unix
arrangement is behaviorally identical (same runtime flavor and driver
set — tokio's ctrl_c signal driver is thread-agnostic), and one shared
code path removes the entire small-main-stack class everywhere instead
of special-casing one OS. Verified on Linux with the full test suite
(inspection_flags 12/12) and the hooks usage probe end-to-end, including
the terminal ctrl-c -> agent.interrupted scenario.
This commit is contained in:
Dark-Alex-17 committed 2026-09-16 23:38:28 -06:00
1 parent bf10aeb74d
commit 83978617bc
1 file changed
+40 -2
+40 -2
View File
@@ -58,8 +58,46 @@ use std::path::PathBuf;
use std::sync::atomic::Ordering;
use std::{env, fs, process, sync::Arc};
#[tokio::main]
async fn main() -> Result<()> {
/// Stack headroom for the thread that polls the root async future, and for
/// the runtime's worker threads.
///
/// Windows links the process main thread with a ~1 MiB stack (unix defaults
/// to 8 MiB), and the root future is a compiler-generated state machine
/// whose debug-build poll frames blow through 1 MiB the moment it is
/// polled — every binary invocation dies with STATUS_STACK_OVERFLOW
/// (0xC00000FD) before doing any work. 16 MiB gives comfortable headroom
/// over the deepest observed frames on every platform.
const MAIN_STACK_SIZE: usize = 16 * 1024 * 1024;
/// Bootstraps the async entry point on a dedicated thread with an explicit
/// large stack instead of `#[tokio::main]`, which would poll the root
/// future on the (small, non-configurable) OS main thread. Everything else
/// matches the macro expansion: same multi-thread runtime flavor, same
/// `enable_all()` driver set (so `tokio::signal::ctrl_c()` keeps working),
/// same `block_on` of the async body, and the `Result` propagates
/// unchanged. Worker threads get the same headroom because spawned tasks
/// (graph nodes, subagents) embed the same class of giant state machine.
/// The thread is named "main" and panics are re-raised on the real main
/// thread without re-invoking the panic hook, keeping panic output and the
/// exit code identical to the direct arrangement.
fn main() -> Result<()> {
std::thread::Builder::new()
.name("main".into())
.stack_size(MAIN_STACK_SIZE)
.spawn(|| {
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.thread_stack_size(MAIN_STACK_SIZE)
.build()
.expect("failed to build the tokio runtime")
.block_on(async_main())
})
.expect("failed to spawn the main bootstrap thread")
.join()
.unwrap_or_else(|panic| std::panic::resume_unwind(panic))
}
async fn async_main() -> Result<()> {
load_env_file()?;
CompleteEnv::with_factory(Cli::command).complete();
let cli = Cli::parse();