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
@@ -1676,6 +1676,23 @@ fn init_logging(is_tty: bool) {
1676
.try_init();
1677
}
1678
1679
+#[cfg(unix)]
1680
+fn redirect_stdout_to_stderr() -> anyhow::Result<()> {
1681
+ let stderr_dup = unsafe { libc::dup(libc::STDERR_FILENO) };
1682
+ anyhow::ensure!(
1683
+ stderr_dup >= 0,
1684
+ "dup(stderr) failed: {}",
1685
+ std::io::Error::last_os_error()
1686
+ );
1687
+
1688
+ unsafe {
1689
+ libc::dup2(stderr_dup, libc::STDOUT_FILENO);
1690
+ libc::close(stderr_dup);
1691
+ }
1692
+
1693
+ Ok(())
1694
+}
1695
+
1696
// ── Interactive TUI mode ──────────────────────────────────────────────────────
1697
1698
/// 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) ->
1784
async fn run_acp_server() -> anyhow::Result<()> {
1785
log::info!("ACP mode — starting agent server");
1786
1770
- let engine = Arc::new(ChatEngine::new());
1771
-
1787
let startup_selection = setup::startup_model_selection();
1788
let config = startup_selection
1789
.as_ref()
1790
.and_then(|selection| {
1776
- models::build_model_picker_items()
1777
- .into_iter()
1778
- .find(|item| {
1779
- selection
1780
- .selected_model
1781
- .as_ref()
1782
- .map(|selected| {
1783
- item.config.model_id == selected.model_id
1784
- && item
1785
- .config
1786
- .files
1787
- .iter()
1788
- .any(|file| file == &selected.gguf_file)
1789
- })
1790
- .unwrap_or(false)
1791
- })
1792
- .map(|item| item.config)
1791
+ selection.selected_model.as_ref().and_then(|selected| {
1792
+ models::build_model_picker_items()
1793
+ .into_iter()
1794
+ .find(|item| {
1795
+ item.config.model_id == selected.model_id
1796
+ && item
1797
+ .config
1798
+ .files
1799
+ .iter()
1800
+ .any(|file| file == &selected.gguf_file)
1801
+ })
1802
+ .map(|item| item.config)
1803
+ })
1804
})
1805
.unwrap_or_else(GgufModelConfig::qwen25_3b);
1806
1796
- let acp_tool_calling = models::build_model_picker_items()
1797
- .iter()
1798
- .find(|item| item.config.model_id == config.model_id)
1799
- .map(|item| (item.tool_calling, item.max_tokens))
1800
- .unwrap_or((true, 4096));
1801
-
1802
- let max_tokens = acp_tool_calling.1;
1803
- let tool_calling = acp_tool_calling.0;
1804
-
1805
- let sampling = SamplingConfig {
1806
- max_tokens: Some(max_tokens),
1807
- ..SamplingConfig::default()
1808
- };
1809
-
1810
- // do we need to download, or is it cached?
1807
let needs_download = models::build_model_picker_items()
1808
.iter()
1809
.find(|item| item.config.model_id == config.model_id)
@@ -1815,7 +1811,7 @@ async fn run_acp_server() -> anyhow::Result<()> {
1811
.unwrap_or(true);
1812
1813
log::info!(
1818
- "ACP startup model: {} ({})",
1814
+ "ACP startup model selected: {} ({})",
1815
config.display_name,
1816
if needs_download {
1817
"needs download"
@@ -1824,44 +1820,19 @@ async fn run_acp_server() -> anyhow::Result<()> {
1820
}
1821
);
1822
1827
- let startup_config = config.clone();
1823
+ let engine = Arc::new(ChatEngine::new());
1824
1829
- // loader thread flips model_ready when done; agent polls it
1830
- let model_ready = Arc::new(AtomicBool::new(false));
1825
+ // Delay model loading until after initialize/auth so ACP stdout stays clean
1826
+ // even if model libraries emit startup diagnostics.
1827
+ let model_ready = Arc::new(AtomicBool::new(true));
1828
let model_load_error: Arc<std::sync::Mutex<Option<String>>> =
1829
Arc::new(std::sync::Mutex::new(None));
1830
1834
- // real thread + own runtime: block_in_place panics inside spawn_local
1835
- let loader_engine = Arc::clone(&engine);
1836
- let loader_config = config.clone();
1837
- let loader_ready = Arc::clone(&model_ready);
1838
- let loader_error = Arc::clone(&model_load_error);
1839
- let acp_system_prompt = system_prompt_for_model(tool_calling).to_string();
1840
-
1841
- std::thread::spawn(move || {
1842
- let rt = tokio::runtime::Runtime::new().expect("failed to create loader runtime");
1843
- match rt.block_on(loader_engine.load_gguf_model(
1844
- loader_config,
1845
- Some(acp_system_prompt),
1846
- Some(sampling),
1847
- )) {
1848
- Ok(_) => {
1849
- log::info!("startup model loaded and ready");
1850
- loader_ready.store(true, Ordering::Release);
1851
- }
1852
- Err(error) => {
1853
- log::error!("startup model load failed: {error}");
1854
- *loader_error.lock().unwrap() = Some(error.to_string());
1855
- loader_ready.store(true, Ordering::Release);
1856
- }
1857
- }
1858
- });
1859
-
1831
let (notification_tx, mut notification_rx) = mpsc::channel::<SessionNotification>(256);
1832
let agent = SiGitAgent::new(
1833
engine,
1834
notification_tx,
1864
- startup_config,
1835
+ config,
1836
model_ready,
1837
model_load_error,
1838
needs_download,
@@ -1923,7 +1894,10 @@ async fn main() -> anyhow::Result<()> {
1894
anyhow::bail!("interactive mode requires Unix (macOS / Linux)");
1895
}
1896
} else {
1926
- // ACP mode: no redirect needed, logs go to stderr.
1897
+ // ACP mode: stdout must contain only ACP JSON messages.
1898
+ #[cfg(unix)]
1899
+ redirect_stdout_to_stderr()?;
1900
+
1901
init_logging(false);
1902
setup::setup_shared_model_cache();
1903
log::info!("siGit v{} starting (ACP mode)", env!("CARGO_PKG_VERSION"));