Synced from monorepo

Synced from monorepo

Changes:
- Release a shell session's resources in one drop
- Make the tools blocking-wait cap client-configurable and self-describing
- Recognize API "exceeds budget" errors as context overflow
- Retry /btw on model overload
- Carry running background tasks and subagents across compaction
- Require round-trip time for SDK liveness checks
- Background-subagent completion reminders with a selectable delivery surface
- Make a PTY shell reap itself until it reaches the registry
- Recover the OS error code from a TLS-phase connection reset
- Consume the attached-client signal and report why idle is withheld
- Treat `.grok/sandbox.toml` edits as protected so auto mode prompts before writing
- Surface history/search in the Ctrl+. cheatsheet and keep it working in history view
- Delete sessions from the dashboard and welcome list
- Release a session's activity record when the session ends
- Stop charging auth-retry budget for fail-closed 401s; reset it across suspends
- Scope skills watches on project vendor roots
- Make [stop] cancel in-flight compaction
- Make the leader soak measure the leader, not its harness

Source-Revision: 8d69c91f02bcacf01e98d5aebbf2f92547c45738
This commit is contained in:
grokkybara[bot] 2026-07-31 18:08:03 +00:00
commit a422116582
165 changed files with 15161 additions and 1969 deletions

View file

@ -105,6 +105,8 @@ async fn leader_soak_churning_clients_no_leaks_no_zombies() {
let server = xai_grok_test_support::MockInferenceServer::start()
.await
.unwrap();
// Measure the leader, not the harness's copy of every conversation.
server.set_keep_requests(false);
let grok_home = TempDir::new().unwrap();
let workdir = TempDir::new().unwrap();
@ -207,8 +209,6 @@ async fn leader_soak_churning_clients_no_leaks_no_zombies() {
let mut turns: u64 = 0;
let mut baseline: Option<serde_json::Value> = None;
// Each cycle: 10 fresh clients, 2 sessions each, one scripted
// turn per session, then all disconnect.
while tokio::time::Instant::now() < soak_deadline {
cycles += 1;
let mut clients = Vec::new();
@ -282,7 +282,7 @@ async fn leader_soak_churning_clients_no_leaks_no_zombies() {
}
// An entry that never drains names itself here, one cycle
// after it leaks, while memory is still within its budget.
// after it leaks.
let counts = registry_counts(&mut bootstrap, 1000 + cycles).await;
assert_eq!(
counts["sessions"], 0,
@ -332,7 +332,7 @@ async fn leader_soak_churning_clients_no_leaks_no_zombies() {
({:.2} MB per cycle)",
net_bytes as f64 / measured as f64 / (1024.0 * 1024.0)
);
let max_per_cycle = env_u64("LEADER_SOAK_MAX_HEAP_BYTES_PER_CYCLE", 4 << 20) as i64;
let max_per_cycle = env_u64("LEADER_SOAK_MAX_HEAP_BYTES_PER_CYCLE", 1 << 20) as i64;
assert!(
per_cycle <= max_per_cycle,
"leader retained {per_cycle} heap bytes per cycle (bound {max_per_cycle})"

View file

@ -31,6 +31,7 @@ const RPC_TIMEOUT: Duration = Duration::from_secs(60);
#[serde(deny_unknown_fields)]
struct Counts {
sessions: usize,
loading_sessions: usize,
session_threads: usize,
resident_resources: usize,
retained_resources: usize,
@ -45,6 +46,7 @@ struct Counts {
subagent_active: usize,
subagent_completed: usize,
workspace_bindings: Option<usize>,
workspace_activity_sessions: Option<usize>,
}
struct AutoApproveClient;
#[async_trait::async_trait(?Send)]
@ -91,6 +93,19 @@ async fn read_counts(conn: &acp::ClientSideConnection) -> Counts {
serde_json::from_value(resp["result"]["registries"].clone())
.unwrap_or_else(|e| panic!("x.ai/debug/agent: bad registries payload: {e}\n{resp}"))
}
/// Counts read once the actor threads are reaped. Nothing signals a thread
/// exit, so this polls; both ends settle, so neither catches one mid-exit.
async fn settled_counts(conn: &acp::ClientSideConnection) -> Counts {
let mut counts = read_counts(conn).await;
for _ in 0..100 {
if counts.session_threads == 0 {
break;
}
tokio::time::sleep(Duration::from_millis(100)).await;
counts = read_counts(conn).await;
}
counts
}
async fn new_session(conn: &acp::ClientSideConnection, cwd: &std::path::Path) -> acp::SessionId {
tokio::time::timeout(
RPC_TIMEOUT,
@ -252,21 +267,29 @@ fn session_churn_returns_registry_snapshot_to_baseline() {
agent_rt.block_on(local.run_until(async move {
let client_conn = connect_and_auth().await;
churn_one(&client_conn, workdir.path(), 0).await;
let baseline = read_counts(&client_conn).await;
let baseline = settled_counts(&client_conn).await;
assert_eq!(
baseline.sessions, 0,
"warmup session must be fully removed before baseline"
);
assert_eq!(
(baseline.resident_resources, baseline.retained_resources),
(0, 0),
(
baseline.resident_resources,
baseline.retained_resources,
baseline.loading_sessions
),
(0, 0, 0),
"warmup must leave no per-session resource entries, including \
entries holding no resources"
);
assert_eq!(
baseline.workspace_bindings,
Some(0),
"warmup must have built the local workspace and released its binding"
(
baseline.workspace_bindings,
baseline.workspace_activity_sessions
),
(Some(0), Some(0)),
"warmup must have built the local workspace and released both its \
binding and its activity record"
);
assert_eq!(
(
@ -295,7 +318,7 @@ fn session_churn_returns_registry_snapshot_to_baseline() {
}))
.await;
futures::future::join_all(concurrent.iter().map(|sid| close_session(conn, sid))).await;
let after = read_counts(&client_conn).await;
let after = settled_counts(&client_conn).await;
assert_eq!(
after, baseline,
"session churn must return every registry count to baseline \

View file

@ -896,7 +896,7 @@ async fn test_chat_completions_401_unauthorized() {
let result = client.conversation_stream(request).await;
assert!(result.is_err());
if let Err(SamplingError::Auth(_)) = result {
if let Err(SamplingError::Auth { .. }) = result {
// Expected
} else {
panic!("Expected Auth error");
@ -939,7 +939,7 @@ async fn test_responses_api_401_unauthorized() {
let result = client.conversation_stream_responses(request).await;
assert!(result.is_err());
if let Err(SamplingError::Auth(_)) = result {
if let Err(SamplingError::Auth { .. }) = result {
// Expected
} else {
panic!("Expected Auth error");