From 3cb2b6be2c2d1bca49623d9d374d88b511f781df Mon Sep 17 00:00:00 2001 From: Shivakumar Date: Sat, 8 Aug 2026 18:19:28 +0530 Subject: [PATCH 1/3] Fix Windows console-flash: add CREATE_NO_WINDOW to daemon/gateway/git-shim/jobs spawns Several hot-path Command::new spawns lacked the Windows CREATE_NO_WINDOW creation flag. Since the spawning processes (the agentflare daemon, gateway-registry backend connections) are themselves console-less, Windows auto-allocates a fresh console window for every console-subsystem child spawned without this flag -- visible as a brief flash. Fixed spawn sites: - gateway-registry::ensure_connected -- downstream MCP backend respawns on circuit-breaker/timeout - flare-git-shim::scope_check_deny_reason -- runs on every git command through the shim - ipc::process.rs -- is_alive/terminate_gracefully/force_kill/ list_pids_raw (tasklist/taskkill), backing daemon lifecycle - agentflare-jobs::Supervisor::spawn/kill_graceful -- dispatched by the daemon's own background discovery tick every 12s, independent of any active session, so this is the one most likely to explain flashes with no Claude Code session open at all --- crates/agentflare-jobs/src/supervisor.rs | 20 +++++++++++++++++++- crates/flare-git-shim/src/main.rs | 15 ++++++++++++--- crates/gateway-registry/src/mcp_stdio.rs | 9 +++++++++ src/ipc/process.rs | 8 +++++++- 4 files changed, 47 insertions(+), 5 deletions(-) diff --git a/crates/agentflare-jobs/src/supervisor.rs b/crates/agentflare-jobs/src/supervisor.rs index 38e168e4..e4a37ce0 100644 --- a/crates/agentflare-jobs/src/supervisor.rs +++ b/crates/agentflare-jobs/src/supervisor.rs @@ -70,6 +70,18 @@ impl Supervisor { use std::os::unix::process::CommandExt; cmd.process_group(0); } + // This supervisor is driven by the daemon's own background discovery + // tick (`spawn_supervisor_discovery`, every `SUPERVISOR_DISCOVERY_INTERVAL`) + // as well as interactive job requests — no console-attached parent is + // guaranteed. Without this flag every dispatched job auto-allocates a + // console window on Windows, flashing briefly even with no active + // Claude Code session, since the daemon runs unattended in the background. + #[cfg(windows)] + { + use std::os::windows::process::CommandExt; + const CREATE_NO_WINDOW: u32 = 0x0800_0000; + cmd.creation_flags(CREATE_NO_WINDOW); + } let mut child = cmd.spawn()?; @@ -215,11 +227,17 @@ fn kill_graceful(child: &mut std::process::Child, kill_after: Duration) { } #[cfg(windows)] { + use std::os::windows::process::CommandExt; + const CREATE_NO_WINDOW: u32 = 0x0800_0000; let pid = child.id().to_string(); - let _ = Command::new("taskkill").args(["/T", "/PID", &pid]).status(); + let _ = Command::new("taskkill") + .args(["/T", "/PID", &pid]) + .creation_flags(CREATE_NO_WINDOW) + .status(); std::thread::sleep(kill_after); let _ = Command::new("taskkill") .args(["/T", "/F", "/PID", &pid]) + .creation_flags(CREATE_NO_WINDOW) .status(); } #[cfg(not(any(unix, windows)))] diff --git a/crates/flare-git-shim/src/main.rs b/crates/flare-git-shim/src/main.rs index b6b63ddd..9a57cf77 100644 --- a/crates/flare-git-shim/src/main.rs +++ b/crates/flare-git-shim/src/main.rs @@ -184,10 +184,19 @@ struct ScopeCheckResult { /// the escape hatch for a broken/missing `agentflare` binary, same as any /// other misclassification. fn scope_check_deny_reason(subcommand: &str) -> Option { - let output = match std::process::Command::new("agentflare") - .args(["git", "scope-check", "--subcommand", subcommand]) - .output() + let mut cmd = std::process::Command::new("agentflare"); + cmd.args(["git", "scope-check", "--subcommand", subcommand]); + // Runs on every git invocation through this shim; without this flag, + // whenever the invoking parent has no inherited console (e.g. spawned by + // an IDE/editor extension), Windows auto-allocates one for this child — + // visible as a brief flashing terminal window. + #[cfg(windows)] { + use std::os::windows::process::CommandExt as _; + const CREATE_NO_WINDOW: u32 = 0x0800_0000; + cmd.creation_flags(CREATE_NO_WINDOW); + } + let output = match cmd.output() { Ok(o) => o, Err(e) => { return Some(format!( diff --git a/crates/gateway-registry/src/mcp_stdio.rs b/crates/gateway-registry/src/mcp_stdio.rs index 1b137c2b..1bdf8e0c 100644 --- a/crates/gateway-registry/src/mcp_stdio.rs +++ b/crates/gateway-registry/src/mcp_stdio.rs @@ -100,6 +100,15 @@ impl McpStdioBackend { // timeout actually terminates the hung child, not just our // side of the connection. cmd.kill_on_drop(true); + // This process (the agentflare daemon) is itself console-less; + // any console-subsystem backend spawned without this flag gets + // a console window auto-allocated by Windows, which flashes + // briefly on screen on every circuit-breaker/timeout respawn. + #[cfg(windows)] + { + const CREATE_NO_WINDOW: u32 = 0x0800_0000; + cmd.creation_flags(CREATE_NO_WINDOW); + } }), ) .map_err(|e| { diff --git a/src/ipc/process.rs b/src/ipc/process.rs index 5086a01f..080da8a2 100644 --- a/src/ipc/process.rs +++ b/src/ipc/process.rs @@ -1,10 +1,14 @@ use std::time::Duration; +#[cfg(windows)] +use std::os::windows::process::CommandExt; + pub fn is_alive(pid: u32) -> bool { #[cfg(windows)] { let status = std::process::Command::new("tasklist") .args(["/FI", &format!("PID eq {pid}"), "/NH", "/FO", "CSV"]) + .creation_flags(windows_sys::Win32::System::Threading::CREATE_NO_WINDOW) .output(); match status { Ok(o) => { @@ -26,7 +30,6 @@ pub fn is_alive(pid: u32) -> bool { pub fn spawn_detached(binary: &str, args: &[&str]) -> Result { #[cfg(windows)] { - use std::os::windows::process::CommandExt; let mut cmd = std::process::Command::new(binary); cmd.args(args); cmd.creation_flags( @@ -54,6 +57,7 @@ pub fn terminate_gracefully(pid: u32) -> Result<(), String> { { let status = std::process::Command::new("taskkill") .args(["/PID", &pid.to_string()]) + .creation_flags(windows_sys::Win32::System::Threading::CREATE_NO_WINDOW) .status() .map_err(|e| format!("taskkill: {e}"))?; if status.success() { @@ -81,6 +85,7 @@ pub fn force_kill(pid: u32) -> Result<(), String> { { let status = std::process::Command::new("taskkill") .args(["/F", "/PID", &pid.to_string()]) + .creation_flags(windows_sys::Win32::System::Threading::CREATE_NO_WINDOW) .status() .map_err(|e| format!("taskkill /F: {e}"))?; if status.success() { @@ -121,6 +126,7 @@ fn list_pids_raw(binary_name: &str) -> String { "CSV", "/NH", ]) + .creation_flags(windows_sys::Win32::System::Threading::CREATE_NO_WINDOW) .output(); #[cfg(not(windows))] let output = std::process::Command::new("pgrep") From 1aae2183ea5b53f931c9e281d9497e6cec271be1 Mon Sep 17 00:00:00 2001 From: Shivakumar Date: Sat, 8 Aug 2026 19:01:35 +0530 Subject: [PATCH 2/3] Fix Windows console-flash: git-core spawn + cache repo_root to cut redundant spawns flare-git-core::shell::run_in/diff -- the shared git-execution primitive nearly everything in this crate goes through -- lacked CREATE_NO_WINDOW. Live-caught: the daemon was spawning 'git rev-parse --show-toplevel' via this path on essentially every project/item MCP call. AgentflareMcp::repo_root() now memoizes its result per-cwd instead of re-spawning git on every single call, cutting the actual spawn count (not just hiding the window). Keyed by cwd rather than a single cached value because cli::work::execute_work calls set_current_dir to switch into an item's worktree for in-process work-item execution -- a single-value cache would freeze on whatever cwd resolved first and silently break resolution for every worktree after that. --- crates/flare-git-core/src/shell.rs | 29 +++++++++++++++++++++++------ src/mcp_server.rs | 26 +++++++++++++++++++++++--- 2 files changed, 46 insertions(+), 9 deletions(-) diff --git a/crates/flare-git-core/src/shell.rs b/crates/flare-git-core/src/shell.rs index 7aa83346..5a69cf16 100644 --- a/crates/flare-git-core/src/shell.rs +++ b/crates/flare-git-core/src/shell.rs @@ -98,13 +98,28 @@ pub(crate) fn git_binary() -> PathBuf { .clone() } +/// This crate's git spawns run inside the agentflare daemon far more than +/// any other one -- `resolve_project()` calls `run_in`-backed `repo_toplevel` +/// on essentially every project/item MCP call. The daemon itself is +/// console-less, so without this flag every one of those spawns +/// auto-allocates a console window on Windows, flashing briefly. +#[cfg(windows)] +fn no_console_window(cmd: &mut Command) { + use std::os::windows::process::CommandExt; + const CREATE_NO_WINDOW: u32 = 0x0800_0000; + cmd.creation_flags(CREATE_NO_WINDOW); +} +#[cfg(not(windows))] +fn no_console_window(_cmd: &mut Command) {} + /// Runs `git` in `repo_root`; `Ok(stdout)` trimmed on success, `Err(stderr)` /// trimmed on a non-zero exit, or a process-spawn error message (git /// missing, etc) if it couldn't even run. pub fn run_in(repo_root: &Path, args: &[&str]) -> Result { - let out = Command::new(git_binary()) - .args(args) - .current_dir(repo_root) + let mut cmd = Command::new(git_binary()); + cmd.args(args).current_dir(repo_root); + no_console_window(&mut cmd); + let out = cmd .output() .map_err(|e| format!("git not available: {e}"))?; if !out.status.success() { @@ -132,9 +147,11 @@ pub fn run_in_ok(repo_root: &Path, args: &[&str]) -> bool { /// the rest of this module's helpers return. pub fn diff(repo_root: &Path, base: &str, head: &str) -> Result { let range = format!("{base}...{head}"); - let out = Command::new(git_binary()) - .args(["diff", "--unified=3", &range]) - .current_dir(repo_root) + let mut cmd = Command::new(git_binary()); + cmd.args(["diff", "--unified=3", &range]) + .current_dir(repo_root); + no_console_window(&mut cmd); + let out = cmd .output() .map_err(|e| format!("git diff failed: {e}"))?; if !out.status.success() { diff --git a/src/mcp_server.rs b/src/mcp_server.rs index b5f0a336..6a24af87 100644 --- a/src/mcp_server.rs +++ b/src/mcp_server.rs @@ -541,12 +541,32 @@ impl AgentflareMcp { /// across multiple linked projects depending on which subdirectory a /// tool was invoked from. Falls back to raw cwd only when nothing is /// found anywhere above it. + /// + /// Memoized per-cwd: every project/item MCP call runs this, and in the + /// long-running daemon that meant a fresh `git rev-parse` subprocess on + /// essentially every call. Keyed by cwd rather than a single cached + /// value because `cli::work::execute_work` calls `set_current_dir` to + /// switch into an item's worktree for in-process work-item execution — + /// a single-value cache would freeze on whatever cwd resolved first and + /// silently break resolution for every worktree after that. A distinct + /// cwd is always a fresh lookup; a repeated cwd is a cache hit. pub(crate) fn repo_root() -> std::path::PathBuf { let cwd = std::env::current_dir().unwrap_or_default(); - if let Some(root) = flare_git_core::branch::repo_toplevel(&cwd) { - return root; + static CACHE: std::sync::OnceLock< + std::sync::Mutex>, + > = std::sync::OnceLock::new(); + let cache = CACHE.get_or_init(|| std::sync::Mutex::new(std::collections::HashMap::new())); + if let Ok(guard) = cache.lock() + && let Some(root) = guard.get(&cwd) + { + return root.clone(); } - Self::find_root_from(&cwd, &crate::paths::home()) + let root = flare_git_core::branch::repo_toplevel(&cwd) + .unwrap_or_else(|| Self::find_root_from(&cwd, &crate::paths::home())); + if let Ok(mut guard) = cache.lock() { + guard.insert(cwd, root.clone()); + } + root } /// `repo_root()`, but honoring `worktree_repo_root_override` — used only From 9e29529aff223a8e61fa64e9ad3f24644f26daf9 Mon Sep 17 00:00:00 2001 From: Shivakumar Date: Sat, 8 Aug 2026 19:40:15 +0530 Subject: [PATCH 3/3] fix fmt: cargo fmt on flare-git-core/shell.rs --- crates/flare-git-core/src/shell.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/flare-git-core/src/shell.rs b/crates/flare-git-core/src/shell.rs index 5a69cf16..0e665278 100644 --- a/crates/flare-git-core/src/shell.rs +++ b/crates/flare-git-core/src/shell.rs @@ -151,9 +151,7 @@ pub fn diff(repo_root: &Path, base: &str, head: &str) -> Result cmd.args(["diff", "--unified=3", &range]) .current_dir(repo_root); no_console_window(&mut cmd); - let out = cmd - .output() - .map_err(|e| format!("git diff failed: {e}"))?; + let out = cmd.output().map_err(|e| format!("git diff failed: {e}"))?; if !out.status.success() { return Err(format!( "git diff {range}: {}",