@setoelkahfi / sigit / commits / 501af7e

Delay model loading in ACP mode to keep stdout clean

Add redirect_stdout_to_stderr for ACP mode so stdout contains only ACP JSON messages. Model loading is now delayed until after initialize/auth, preventing model libraries from emitting diagnostics to stdout.

paydii committed Apr 29, 2026 at 23:18 UTC 501af7e62f1e491baa59b346a764af9367f74a3d
1 file changed +40 -66
src/main.rs
+40 -66
index 5b234a7..cf38781 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1676,6 +1676,23 @@ fn init_logging(is_tty: bool) { .try_init(); } +#[cfg(unix)] +fn redirect_stdout_to_stderr() -> anyhow::Result<()> { + let stderr_dup = unsafe { libc::dup(libc::STDERR_FILENO) }; + anyhow::ensure!( + stderr_dup >= 0, + "dup(stderr) failed: {}", + std::io::Error::last_os_error() + ); + + unsafe { + libc::dup2(stderr_dup, libc::STDOUT_FILENO); + libc::close(stderr_dup); + } + + Ok(()) +} + // ── Interactive TUI mode ────────────────────────────────────────────────────── /// boot the TUI and load the model on a background thread. @@ -1767,47 +1784,26 @@ async fn run_interactive(tty: std::fs::File, mut cleanup_tty: std::fs::File) -> async fn run_acp_server() -> anyhow::Result<()> { log::info!("ACP mode — starting agent server"); - let engine = Arc::new(ChatEngine::new()); - let startup_selection = setup::startup_model_selection(); let config = startup_selection .as_ref() .and_then(|selection| { - models::build_model_picker_items() - .into_iter() - .find(|item| { - selection - .selected_model - .as_ref() - .map(|selected| { - item.config.model_id == selected.model_id - && item - .config - .files - .iter() - .any(|file| file == &selected.gguf_file) - }) - .unwrap_or(false) - }) - .map(|item| item.config) + selection.selected_model.as_ref().and_then(|selected| { + models::build_model_picker_items() + .into_iter() + .find(|item| { + item.config.model_id == selected.model_id + && item + .config + .files + .iter() + .any(|file| file == &selected.gguf_file) + }) + .map(|item| item.config) + }) }) .unwrap_or_else(GgufModelConfig::qwen25_3b); - let acp_tool_calling = models::build_model_picker_items() - .iter() - .find(|item| item.config.model_id == config.model_id) - .map(|item| (item.tool_calling, item.max_tokens)) - .unwrap_or((true, 4096)); - - let max_tokens = acp_tool_calling.1; - let tool_calling = acp_tool_calling.0; - - let sampling = SamplingConfig { - max_tokens: Some(max_tokens), - ..SamplingConfig::default() - }; - - // do we need to download, or is it cached? let needs_download = models::build_model_picker_items() .iter() .find(|item| item.config.model_id == config.model_id) @@ -1815,7 +1811,7 @@ async fn run_acp_server() -> anyhow::Result<()> { .unwrap_or(true); log::info!( - "ACP startup model: {} ({})", + "ACP startup model selected: {} ({})", config.display_name, if needs_download { "needs download" @@ -1824,44 +1820,19 @@ async fn run_acp_server() -> anyhow::Result<()> { } ); - let startup_config = config.clone(); + let engine = Arc::new(ChatEngine::new()); - // loader thread flips model_ready when done; agent polls it - let model_ready = Arc::new(AtomicBool::new(false)); + // Delay model loading until after initialize/auth so ACP stdout stays clean + // even if model libraries emit startup diagnostics. + let model_ready = Arc::new(AtomicBool::new(true)); let model_load_error: Arc<std::sync::Mutex<Option<String>>> = Arc::new(std::sync::Mutex::new(None)); - // real thread + own runtime: block_in_place panics inside spawn_local - let loader_engine = Arc::clone(&engine); - let loader_config = config.clone(); - let loader_ready = Arc::clone(&model_ready); - let loader_error = Arc::clone(&model_load_error); - let acp_system_prompt = system_prompt_for_model(tool_calling).to_string(); - - std::thread::spawn(move || { - let rt = tokio::runtime::Runtime::new().expect("failed to create loader runtime"); - match rt.block_on(loader_engine.load_gguf_model( - loader_config, - Some(acp_system_prompt), - Some(sampling), - )) { - Ok(_) => { - log::info!("startup model loaded and ready"); - loader_ready.store(true, Ordering::Release); - } - Err(error) => { - log::error!("startup model load failed: {error}"); - *loader_error.lock().unwrap() = Some(error.to_string()); - loader_ready.store(true, Ordering::Release); - } - } - }); - let (notification_tx, mut notification_rx) = mpsc::channel::<SessionNotification>(256); let agent = SiGitAgent::new( engine, notification_tx, - startup_config, + config, model_ready, model_load_error, needs_download, @@ -1923,7 +1894,10 @@ async fn main() -> anyhow::Result<()> { anyhow::bail!("interactive mode requires Unix (macOS / Linux)"); } } else { - // ACP mode: no redirect needed, logs go to stderr. + // ACP mode: stdout must contain only ACP JSON messages. + #[cfg(unix)] + redirect_stdout_to_stderr()?; + init_logging(false); setup::setup_shared_model_cache(); log::info!("siGit v{} starting (ACP mode)", env!("CARGO_PKG_VERSION"));