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:
parent
02d9359435
commit
5da6962e4a
192 changed files with 10337 additions and 3421 deletions
|
|
@ -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")];
|
||||
|
|
|
|||
Loading…
Reference in a new issue