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:
1 file changed
+40
-2
+40
-2
@@ -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();
|
||||
|
||||
Reference in new issue
Block a user