Synced from monorepo

Synced from monorepo

Changes:
- Temporarily disable session share link creation in the TUI
- Do not approve plan on empty Enter from the revise prompt
- Expose chat product Skills via ACP available_commands_update
- Return immediately from a blocking wait on an already-completed ACP task
- Split headless pager module for clearer structure
- Stop git worktree prune from removing user registrations on resume
- Use compaction sampler tokenizer for item token counts
- Opt-in extra root CAs via GROK_EXTRA_CA_BUNDLE
- Cancel all session subagents when the user stops
- Let the session persistence actor exit when its session ends
- Make fullscreen terminal resize much cheaper on long sessions
- Report honestly from kill_task when an ACP task does not exist
- Hide /usage for external-auth deployments
- Forward the history-load trailer’s computer_reason to the client
- Remove ineffective no-op tool reminder
- Declare slash-command screen-mode support in one place
- Keep settings enum picker on the committed value until Enter
- Reap a PTY’s full process tree
- Stream tool calls from headless mode over ACP
- Bridge gateway task lifecycle to ACP for chat session background tasks
- Don’t warn about truncated history on a suppressed replay
- Fit full-replace summarizer input and recover on context-length errors
- Stop dropping agents over an unrecognized frontmatter color
- Add /undo as a slash alias for /rewind
- Harden sleep/wake token-refresh paths against forced re-login
- Add session/list ACP method
- Give each sampling backend its own conversion module
- Treat an unenrolled child process as a lint error
- Suppress the cancelled marker on send-now wake turns
- Stop tearing down Roslyn on every edit, and read C# diagnostics

Source-Revision: 2a28b4a86cfc4a4c133c35b7fc2a6a9964387c39
This commit is contained in:
grokkybara[bot] 2026-07-30 19:07:40 +00:00
commit dd04f397b1
367 changed files with 29489 additions and 10051 deletions

View file

@ -9,6 +9,14 @@
#![cfg(unix)]
#[cfg(feature = "dhat-heap")]
#[global_allocator]
static DHAT_ALLOC: dhat::Alloc = dhat::Alloc;
/// Warmup so the window measures steady state, not first-session cost.
#[cfg(feature = "dhat-heap")]
const HEAP_WARMUP_CYCLES: u64 = 2;
use std::sync::Arc;
use std::time::Duration;
@ -71,6 +79,24 @@ async fn rpc(client: &mut LeaderClient, payload: String, id: u64, what: &str) ->
}
}
/// Per-registry entry counts. `x.ai/debug/agent` answers under the extension
/// envelope's own `result`, nested inside the JSON-RPC `result`.
async fn registry_counts(client: &mut LeaderClient, id: u64) -> serde_json::Value {
let resp = rpc(
client,
format!(r#"{{"jsonrpc":"2.0","id":{id},"method":"_x.ai/debug/agent","params":{{}}}}"#),
id,
"x.ai/debug/agent",
)
.await;
let counts = resp["result"]["result"]["registries"].clone();
assert!(
counts.is_object(),
"x.ai/debug/agent returned no registries: {resp}"
);
counts
}
#[tokio::test(flavor = "current_thread")]
#[ignore = "leader soak; run with --ignored (LEADER_SOAK_SECS bounds the duration)"]
async fn leader_soak_churning_clients_no_leaks_no_zombies() {
@ -172,11 +198,14 @@ async fn leader_soak_churning_clients_no_leaks_no_zombies() {
eprintln!(
"[soak] budgets: {soak_secs}s, rss {max_growth_mb} MB, threads {max_thread_growth}"
);
#[cfg(feature = "dhat-heap")]
let mut heap_window: Option<(dhat::Profiler, dhat::HeapStats, u64)> = None;
let rss_before = ResourceSnapshot::capture();
let soak_deadline = tokio::time::Instant::now() + Duration::from_secs(soak_secs);
let workdir_str = workdir.path().to_string_lossy().to_string();
let mut cycles: u64 = 0;
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.
@ -252,6 +281,34 @@ async fn leader_soak_churning_clients_no_leaks_no_zombies() {
tokio::time::sleep(Duration::from_millis(50)).await;
}
// An entry that never drains names itself here, one cycle
// after it leaks, while memory is still within its budget.
let counts = registry_counts(&mut bootstrap, 1000 + cycles).await;
assert_eq!(
counts["sessions"], 0,
"cycle {cycles}: sessions outlived their close: {counts}"
);
match baseline.as_ref() {
None => baseline = Some(counts),
Some(first) => assert_eq!(
&counts, first,
"cycle {cycles}: registry counts left their baseline"
),
}
#[cfg(feature = "dhat-heap")]
if cycles == HEAP_WARMUP_CYCLES {
let profiler = dhat::Profiler::builder()
// The 10-frame default never reaches our own code.
.trim_backtraces(Some(48))
.file_name(
std::env::var("LEADER_SOAK_DHAT_OUT")
.unwrap_or_else(|_| "dhat-leader-soak.json".to_string()),
)
.build();
heap_window = Some((profiler, dhat::HeapStats::get(), cycles));
}
// Linear in cycles is a leak; flattening is the allocator.
if let Some(rss) = ResourceSnapshot::capture().rss {
eprintln!(
@ -259,28 +316,29 @@ async fn leader_soak_churning_clients_no_leaks_no_zombies() {
rss as f64 / (1024.0 * 1024.0)
);
}
if cycles == 1 {
let snap = rpc(
&mut bootstrap,
r#"{"jsonrpc":"2.0","id":901,"method":"_x.ai/debug/agent","params":{}}"#
.to_string(),
901,
"x.ai/debug/agent",
)
.await;
eprintln!("[soak] registries after cycle 1: {}", snap["result"]["registries"]);
}
}
let snap = rpc(
&mut bootstrap,
r#"{"jsonrpc":"2.0","id":902,"method":"_x.ai/debug/agent","params":{}}"#
.to_string(),
902,
"x.ai/debug/agent",
)
.await;
eprintln!("[soak] registries at end: {}", snap["result"]["registries"]);
// Retained heap is a leak; retained pages alone are the allocator.
#[cfg(feature = "dhat-heap")]
if let Some((profiler, before, start_cycle)) = heap_window.take() {
let after = dhat::HeapStats::get();
drop(profiler);
let measured = cycles.saturating_sub(start_cycle).max(1);
let net_bytes = after.curr_bytes as i64 - before.curr_bytes as i64;
let net_blocks = after.curr_blocks as i64 - before.curr_blocks as i64;
let per_cycle = net_bytes / measured as i64;
eprintln!(
"[soak] heap over {measured} cycles: {net_bytes} bytes, {net_blocks} blocks \
({:.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;
assert!(
per_cycle <= max_per_cycle,
"leader retained {per_cycle} heap bytes per cycle (bound {max_per_cycle})"
);
}
eprintln!("[soak] {cycles} cycles, {turns} turns in {soak_secs}s budget");
assert!(cycles > 0, "soak budget too small to complete one cycle");