Synced from monorepo

Synced from monorepo

Changes:
- Workspace server: surface preview-proxy metrics through the hub metric pump
- Shell: reclaim a session’s retained state in one entry
- Shell: reclaim a session’s resident state in one entry
- Pager: withhold key event types from Alacritty builds that double keys
- Tools: cancel a session’s subagents when it closes
- Pager: keep the whole plan in scrollback and separate reasoning from output in minimal mode
- Pager: probe terminal version over DA2 and include it with feedback
- SuperGrok Plus: identity, CLI, and analytics tier surfaces
- Shell: inherit the session process scope into subagents
- Pager: build @-file-search matcher lazily on first use
- Tools: fix description and output contradictions in tool definitions
- Workspace: degrade @-file-search instead of aborting on thread exhaustion
- Tools: reap a session’s LSP servers when it closes
- Tools: fix contradictions and defects in tool descriptions, schemas, and harness pools
- MCP: reap stdio MCP children on session close
- Shell: reuse spawn-time skill discovery for session telemetry
- Tools: stop leaking shell-wrapper positional params into sourced scripts (fixes activate_conda under persistent/static shell)
- Shell: self-heal corrupt session-search SQLite cache
- Workspace: cap workspace-server tokio workers on many-core hosts
- Shell: reap a session’s child processes when it closes
- Crash handler: capture SIGABRT so panic-aborts leave crash reports
- CLI chat proxy: team-scoped Grok Code managed-config admin routes
- MCP: add CLI enable/disable for MCP servers
- Shell: cap tokio worker threads for startup thread demand
- Workspace: harden git_commit and add git_sync_base operation
- Circuit breaker: add feature-gated gRPC retry policy

Source-Revision: 2a818575225183d8ca915f5632a09b8067b5156a
This commit is contained in:
grokkybara[bot] 2026-07-28 22:50:19 +00:00
commit 5da6962e4a
192 changed files with 10337 additions and 3421 deletions

View file

@ -37,7 +37,7 @@ use xai_grok_tools::types::{
tool::{ToolKind, ToolNamespace},
tool_metadata::ToolMetadata,
};
use xai_grok_tools::util::ProcessGroup;
use xai_grok_tools::util::{ProcessGroup, ProcessScope};
/// MCP tool name delimiter: server names are qualified as `"server__tool"`.
/// Canonical definition lives in `xai_grok_workspace_types`; re-exported here
@ -2017,16 +2017,19 @@ where
/// grandchildren (e.g. `npx` -> `node`) before reaping the leader.
pub struct SafeTokioChildProcess {
child: Option<tokio::process::Child>,
process_group: Option<ProcessGroup>,
/// Strong `Arc` owner; the scope holds only a `Weak`, dropped on reap.
process_group: Option<Arc<ProcessGroup>>,
transport: ResilientRwTransport<tokio::process::ChildStdout, tokio::process::ChildStdin>,
}
impl SafeTokioChildProcess {
/// `server_name` + `event_writer` are threaded into the transport so a
/// skipped (undecodable) stdout line emits an `McpTransportDecodeError`
/// event for that server.
/// event for that server. `scope`, when set, enrolls the child's group for
/// session-close reaping.
fn spawn(
mut cmd: Command,
scope: Option<&ProcessScope>,
server_name: String,
event_writer: xai_file_utils::events::EventWriter,
) -> std::io::Result<(Self, Option<ChildStderr>)> {
@ -2048,7 +2051,7 @@ impl SafeTokioChildProcess {
// Best-effort: a missing group just degrades to direct-child-only cleanup.
let process_group = match ProcessGroup::new() {
Ok(mut group) => match group.attach(&child) {
Ok(()) => Some(group),
Ok(()) => Some(Arc::new(group)),
Err(e) => {
tracing::warn!("Failed to attach MCP child to process group: {e}");
None
@ -2059,6 +2062,30 @@ impl SafeTokioChildProcess {
None
}
};
// Enrollment ties this child to the *spawning* session's lifetime.
// `SharedMcpPool` may hand the resulting client Arc to subagent
// sessions, but subagents inherit the root session's scope, so the
// root's kill_all cannot strand an in-tree subagent. Residual: any
// detached holder of the Arc loses the transport when the spawning
// session closes — session close is deliberately the reap boundary.
if let (Some(scope), Some(group)) = (scope, process_group.as_ref())
&& !scope.register(group)
{
// The scope latched closed (spawn raced session teardown), so
// `register` already killpg'd the child. Fail fast with a clear
// error instead of proceeding into a doomed rmcp handshake; the
// reap below mirrors `Drop`'s best-effort leader cleanup.
if let Ok(handle) = tokio::runtime::Handle::try_current() {
handle.spawn(async move {
let _ = child.kill().await;
});
} else if let Err(e) = child.start_kill() {
tracing::warn!("Error signaling MCP child killed by closed scope: {e}");
}
return Err(std::io::Error::other(
"session is closing (process scope already reclaimed); MCP server not started",
));
}
Ok((
Self {
@ -4043,14 +4070,45 @@ fn stdio_path_override(env: &[acp::EnvVariable]) -> Option<&str> {
.map(|e| e.value.as_str())
}
/// Borrowed cross-cutting spawn context whose `scope`, when set, enrolls the stdio child for session-close reaping.
pub struct McpSpawnCtx<'a> {
pub(crate) session_id: Option<&'a str>,
pub(crate) event_writer: &'a xai_file_utils::events::EventWriter,
pub(crate) mode: OauthInteractivity,
pub(crate) scope: Option<&'a ProcessScope>,
}
impl<'a> McpSpawnCtx<'a> {
pub fn for_session(
session_id: &'a str,
event_writer: &'a xai_file_utils::events::EventWriter,
mode: OauthInteractivity,
scope: Option<&'a ProcessScope>,
) -> Self {
Self {
session_id: Some(session_id),
event_writer,
mode,
scope,
}
}
pub fn session_less(event_writer: &'a xai_file_utils::events::EventWriter) -> Self {
Self {
session_id: None,
event_writer,
mode: OauthInteractivity::Interactive,
scope: None,
}
}
}
pub async fn start_mcp_server(
mcp_server: acp::McpServer,
session_id: Option<&str>,
overrides: Option<&McpClientTimeoutOverrides>,
meta_config: Option<&McpServerMetaConfig>,
byo_config: Option<&McpOAuthConfig>,
event_writer: &xai_file_utils::events::EventWriter,
mode: OauthInteractivity,
ctx: &McpSpawnCtx<'_>,
) -> Result<McpClient, McpError> {
let _per_server_timer = xai_grok_telemetry::instrumentation::timer("mcp_start_one_server");
match mcp_server {
@ -4086,24 +4144,27 @@ pub async fn start_mcp_server(
}
xai_grok_tools::util::detach_command(&mut cmd);
let (transport, stderr_handle) =
SafeTokioChildProcess::spawn(cmd, name.clone(), event_writer.clone()).map_err(
|e| {
tracing::error!("Failed to spawn MCP server '{}': {}", name, e);
xai_grok_telemetry::session_ctx::log_event(
xai_grok_telemetry::events::McpServerFailed {
server_name: name.clone(),
error_type: xai_grok_telemetry::events::McpErrorType::SpawnFailed,
duration_ms: spawn_start.elapsed().as_millis() as u64,
timeout_sec: startup_timeout,
},
);
McpError::SpawnFailed {
server: name.clone(),
source: e,
}
let (transport, stderr_handle) = SafeTokioChildProcess::spawn(
cmd,
ctx.scope,
name.clone(),
ctx.event_writer.clone(),
)
.map_err(|e| {
tracing::error!("Failed to spawn MCP server '{}': {}", name, e);
xai_grok_telemetry::session_ctx::log_event(
xai_grok_telemetry::events::McpServerFailed {
server_name: name.clone(),
error_type: xai_grok_telemetry::events::McpErrorType::SpawnFailed,
duration_ms: spawn_start.elapsed().as_millis() as u64,
timeout_sec: startup_timeout,
},
)?;
);
McpError::SpawnFailed {
server: name.clone(),
source: e,
}
})?;
tracing::debug!("MCP server '{}' spawned: PID={:?}", name, transport.id());
@ -4128,7 +4189,7 @@ pub async fn start_mcp_server(
tracing::info!(server = %name, %url, ?mc, "MCP http: meta config override");
}
let headers = expand_session_id_headers(headers, session_id);
let headers = expand_session_id_headers(headers, ctx.session_id);
let http_config = HttpConfig {
url: url.clone(),
headers,
@ -4150,7 +4211,7 @@ pub async fn start_mcp_server(
xai_grok_telemetry::instrumentation::timer("mcp_http_auth_discovery");
match tokio::time::timeout(
OAUTH_DISCOVERY_TIMEOUT,
discover_and_prepare_auth(&name, &url, mode),
discover_and_prepare_auth(&name, &url, ctx.mode),
)
.await
{
@ -4159,17 +4220,17 @@ pub async fn start_mcp_server(
tracing::warn!(
server = %name,
url = %url,
?mode,
mode = ?ctx.mode,
timeout_secs = OAUTH_DISCOVERY_TIMEOUT.as_secs(),
"OAuth discovery timed out"
);
event_writer.emit(
ctx.event_writer.emit(
xai_file_utils::events::Event::McpOAuthDiscoveryTimeout {
server_name: name.clone(),
url: url.clone(),
},
);
HttpOauthPrep::on_probe_failure(mode)
HttpOauthPrep::on_probe_failure(ctx.mode)
}
}
};
@ -4203,12 +4264,10 @@ pub async fn start_mcp_server(
pub async fn start_mcp_servers(
mcp_servers: Vec<acp::McpServer>,
session_id: Option<&str>,
overrides_map: &HashMap<String, McpClientTimeoutOverrides>,
meta_config_map: &McpMetaConfigMap,
oauth_config_map: &crate::oauth_config::McpOAuthConfigMap,
event_writer: &xai_file_utils::events::EventWriter,
mode: OauthInteractivity,
ctx: &McpSpawnCtx<'_>,
) -> Vec<Result<McpClient, McpError>> {
let _mcp_start_timer = xai_grok_telemetry::instrumentation::timer("mcp_start_servers");
@ -4226,7 +4285,7 @@ pub async fn start_mcp_servers(
let overrides = overrides_map.get(server_name);
let mc = meta_config_map.get(server_name);
let byo = oauth_config_map.get(server_name);
start_mcp_server(server, session_id, overrides, mc, byo, event_writer, mode)
start_mcp_server(server, overrides, mc, byo, ctx)
})
.buffer_unordered(8)
.collect::<Vec<_>>()
@ -4585,6 +4644,7 @@ mod tests {
xai_grok_tools::util::detach_command(&mut cmd);
let (transport, _stderr) = SafeTokioChildProcess::spawn(
cmd,
None,
"test".to_string(),
xai_file_utils::events::EventWriter::noop(),
)
@ -4616,6 +4676,55 @@ mod tests {
std::io::Error::last_os_error().raw_os_error() != Some(libc::ESRCH)
}
/// `scope.kill_all()` reaps an enrolled MCP child even when its owner never
/// runs Drop. Non-vacuous: dropping the `Some(&scope)` enrollment makes this
/// time out.
#[cfg(unix)]
#[tokio::test]
async fn scope_kill_all_reaps_enrolled_mcp_child_while_owner_wedged() {
use std::time::Duration;
let scope = ProcessScope::new();
let mut cmd = Command::new("sleep");
cmd.arg("600").kill_on_drop(true);
xai_grok_tools::util::detach_command(&mut cmd);
let (mut child_process, _stderr) = SafeTokioChildProcess::spawn(
cmd,
Some(&scope),
"wedge-test".to_string(),
xai_file_utils::events::EventWriter::noop(),
)
.expect("spawn enrolled MCP child");
assert_eq!(
scope.live_count(),
1,
"the enrolled MCP child group must be tracked by the scope"
);
// Wedge: owner never runs Drop, so kill_all is the only reclaim path.
scope.kill_all();
// Take only the handle, not the group, so kill-on-drop can't mask a
// missing enrollment.
let mut child = child_process.child.take().expect("child handle present");
// Null the strong Arc<ProcessGroup> before reaping the leader below:
// holding it across the reap would let `child_process`'s later Drop
// killpg a reusable pgid — the PID-reuse pattern the Weak ownership
// contract exists to prevent.
child_process.process_group = None;
let status = tokio::time::timeout(Duration::from_secs(5), child.wait())
.await
.expect("scope.kill_all must have SIGKILL'd the enrolled MCP child group")
.expect("wait on the reclaimed child succeeds");
use std::os::unix::process::ExitStatusExt;
assert_eq!(
status.signal(),
Some(libc::SIGKILL),
"the MCP child must have been SIGKILL'd by the scope, not have exited cleanly"
);
}
#[test]
fn test_mcp_state_new() {
let configs = vec![make_stdio_server("test", "/bin/test")];