fix: reduce idle agent checker CPU

refs #300
This commit is contained in:
Ogulcan Celik
2026-05-26 14:44:22 +03:00
parent 62b9795771
commit 9eec3029e1
5 changed files with 450 additions and 38 deletions
+8
View File
@@ -997,6 +997,14 @@ pub fn foreground_job(child_pid: u32) -> Option<crate::platform::ForegroundJob>
crate::platform::foreground_job(child_pid)
}
/// Get the foreground process group leader as a one-process job.
/// This is cheaper than collecting every process in the foreground job.
pub fn foreground_group_leader_job(
process_group_id: u32,
) -> Option<crate::platform::ForegroundJob> {
crate::platform::foreground_group_leader_job(process_group_id)
}
/// Get the foreground process group for a pane shell PID.
/// This is cheaper than collecting every process in the foreground job.
pub fn foreground_process_group_id(child_pid: u32) -> Option<u32> {
+398 -38
View File
@@ -102,6 +102,12 @@ async fn publish_state_changed_event(
}
const AGENT_MISS_CONFIRMATION_ATTEMPTS: u8 = 6;
const PROCESS_RECHECK_IDENTIFIED: std::time::Duration = std::time::Duration::from_secs(5);
const PROCESS_RECHECK_UNIDENTIFIED: std::time::Duration = std::time::Duration::from_secs(30);
const PROCESS_ACQUISITION_WINDOW: std::time::Duration = std::time::Duration::from_secs(8);
const PROCESS_ACQUISITION_FAST_WINDOW: std::time::Duration = std::time::Duration::from_millis(1500);
const PROCESS_ACQUISITION_FAST_RECHECK: std::time::Duration = std::time::Duration::from_millis(500);
const PROCESS_ACQUISITION_SLOW_RECHECK: std::time::Duration = std::time::Duration::from_secs(2);
#[derive(Debug, Clone, Copy)]
struct AgentDetectionPresence {
@@ -145,6 +151,99 @@ fn foreground_shell_agent_action(
}
}
#[derive(Debug, Clone, Copy)]
struct ProcessProbeInput {
current_agent: Option<Agent>,
suppressed_agent: Option<Agent>,
foreground_pgid: Option<u32>,
last_foreground_pgid: Option<u32>,
has_process_probe: bool,
acquisition_age: Option<std::time::Duration>,
pending_foreground_shell_clear: bool,
pending_restore_probe: bool,
elapsed_since_process_check: std::time::Duration,
}
fn foreground_group_changed(
foreground_pgid: Option<u32>,
last_foreground_pgid: Option<u32>,
) -> bool {
foreground_pgid != last_foreground_pgid
&& (foreground_pgid.is_some() || last_foreground_pgid.is_some())
}
fn should_probe_foreground_job(input: ProcessProbeInput) -> bool {
if input.pending_foreground_shell_clear || input.pending_restore_probe {
return true;
}
let foreground_group_changed =
foreground_group_changed(input.foreground_pgid, input.last_foreground_pgid);
if input.suppressed_agent.is_some() {
return !input.has_process_probe || foreground_group_changed;
}
if let Some(acquisition_age) = input.acquisition_age {
let acquisition_interval = if acquisition_age <= PROCESS_ACQUISITION_FAST_WINDOW {
PROCESS_ACQUISITION_FAST_RECHECK
} else {
PROCESS_ACQUISITION_SLOW_RECHECK
};
if acquisition_age <= PROCESS_ACQUISITION_WINDOW
&& input.elapsed_since_process_check >= acquisition_interval
{
return true;
}
}
if input.current_agent.is_none() {
return !input.has_process_probe
|| foreground_group_changed
|| input.elapsed_since_process_check >= PROCESS_RECHECK_UNIDENTIFIED;
}
foreground_group_changed || input.elapsed_since_process_check >= PROCESS_RECHECK_IDENTIFIED
}
#[derive(Debug, Clone)]
struct ProcessProbeResult {
process_group_id: Option<u32>,
foreground_is_pane_shell: bool,
agent: Option<Agent>,
process_name: Option<String>,
}
fn probe_foreground_process(pid: u32, foreground_pgid: Option<u32>) -> ProcessProbeResult {
if let Some(job) = foreground_pgid.and_then(crate::detect::foreground_group_leader_job) {
if let Some((agent, process_name)) = crate::detect::identify_agent_in_job(&job) {
return ProcessProbeResult {
process_group_id: Some(job.process_group_id),
foreground_is_pane_shell: job.processes.iter().any(|p| p.pid == pid),
agent: Some(agent),
process_name: Some(process_name),
};
}
}
if let Some(job) = crate::detect::foreground_job(pid) {
let identified = crate::detect::identify_agent_in_job(&job);
return ProcessProbeResult {
process_group_id: Some(job.process_group_id),
foreground_is_pane_shell: job.processes.iter().any(|p| p.pid == pid),
agent: identified.as_ref().map(|(agent, _)| *agent),
process_name: identified.map(|(_, process_name)| process_name),
};
}
ProcessProbeResult {
process_group_id: foreground_pgid,
foreground_is_pane_shell: false,
agent: None,
process_name: None,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct DetectionPublishState {
state: AgentState,
@@ -189,6 +288,11 @@ fn spawn_basic_detection_task(
let mut last_visible_blocker = false;
let mut last_visible_idle = false;
let mut last_visible_working = false;
let mut last_process_check = std::time::Instant::now();
let mut last_foreground_pgid = None;
let mut has_process_probe = false;
let mut acquisition_started_at = None;
let mut release_was_active = false;
loop {
tokio::select! {
@@ -199,18 +303,67 @@ fn spawn_basic_detection_task(
last_visible_blocker = false;
last_visible_idle = false;
last_visible_working = false;
last_process_check = std::time::Instant::now();
last_foreground_pgid = None;
has_process_probe = false;
acquisition_started_at = None;
release_was_active = false;
}
}
let now = std::time::Instant::now();
let suppressed_agent = active_pending_release(&pending_release_for_task, now);
if suppressed_agent.is_none() && release_was_active {
has_process_probe = false;
acquisition_started_at = None;
}
release_was_active = suppressed_agent.is_some();
let pid = child_pid.load(Ordering::Acquire);
let mut agent_changed = false;
let mut agent = agent_presence.current_agent();
let content = terminal.detection_text();
if pid > 0 {
let new_agent = crate::detect::foreground_job(pid).and_then(|job| {
crate::detect::identify_agent_in_job(&job).map(|(agent, _)| agent)
let foreground_pgid = (pid > 0)
.then(|| crate::detect::foreground_process_group_id(pid))
.flatten();
let process_group_changed =
foreground_group_changed(foreground_pgid, last_foreground_pgid);
let should_check_process = pid > 0
&& should_probe_foreground_job(ProcessProbeInput {
current_agent: agent_presence.current_agent(),
suppressed_agent,
foreground_pgid,
last_foreground_pgid,
has_process_probe,
acquisition_age: acquisition_started_at
.map(|started| now.duration_since(started)),
pending_foreground_shell_clear: false,
pending_restore_probe: false,
elapsed_since_process_check: now.duration_since(last_process_check),
});
if should_check_process {
last_process_check = now;
let had_process_probe = has_process_probe;
has_process_probe = true;
let probe = probe_foreground_process(pid, foreground_pgid);
let mut new_agent = probe.agent;
if let Some(suppressed_agent) = suppressed_agent {
if new_agent == Some(suppressed_agent) {
new_agent = None;
} else if let Ok(mut pending_release) = pending_release_for_task.lock() {
*pending_release = None;
}
}
if new_agent.is_none() {
last_foreground_pgid = probe.process_group_id.or(foreground_pgid);
if had_process_probe && process_group_changed {
acquisition_started_at = Some(now);
}
} else {
last_foreground_pgid = probe.process_group_id.or(foreground_pgid);
acquisition_started_at = None;
}
let previous_agent = agent_presence.current_agent();
if agent_presence.observe_process_probe(new_agent) {
agent = agent_presence.current_agent();
@@ -218,7 +371,6 @@ fn spawn_basic_detection_task(
}
}
let content = terminal.detection_text();
let detection = crate::detect::detect_agent(agent, &content);
let new_state = detection.state;
let visible_blocker = detection.visible_blocker && new_state == AgentState::Blocked;
@@ -258,8 +410,6 @@ fn spawn_basic_detection_task(
)
.await;
}
let _ = active_pending_release(&pending_release_for_task, now);
}
});
@@ -1293,7 +1443,6 @@ impl PaneRuntime {
const TICK_UNIDENTIFIED: Duration = Duration::from_millis(500);
const TICK_IDENTIFIED: Duration = Duration::from_millis(300);
const TICK_PENDING_RELEASE: Duration = Duration::from_millis(50);
const PROCESS_RECHECK: Duration = Duration::from_secs(5);
let child_pid = child_pid.clone();
let terminal = terminal.clone();
@@ -1315,8 +1464,11 @@ impl PaneRuntime {
};
let mut last_process_check = Instant::now();
let mut last_foreground_pgid = None;
let mut has_process_probe = false;
let mut acquisition_started_at = None;
let mut pending_foreground_shell_clear = false;
let mut foreground_shell_exit_reported = false;
let mut release_was_active = false;
let mut pending_restore_probe = initial_state.detected_agent.is_some();
let mut last_claude_working_at = None;
let mut last_visible_blocker = false;
@@ -1342,8 +1494,11 @@ impl PaneRuntime {
agent_presence = AgentDetectionPresence::from_agent(None);
state = AgentState::Unknown;
last_foreground_pgid = None;
has_process_probe = false;
acquisition_started_at = None;
pending_foreground_shell_clear = false;
foreground_shell_exit_reported = false;
release_was_active = false;
pending_restore_probe = false;
last_claude_working_at = None;
last_visible_blocker = false;
@@ -1354,44 +1509,44 @@ impl PaneRuntime {
let now = Instant::now();
let suppressed_agent = active_pending_release(&pending_release_for_task, now);
if suppressed_agent.is_none() && release_was_active {
has_process_probe = false;
acquisition_started_at = None;
}
release_was_active = suppressed_agent.is_some();
let pid = child_pid.load(Ordering::Acquire);
let foreground_pgid = (pid > 0 && agent_presence.current_agent().is_some())
let content = terminal.detection_text();
let foreground_pgid = (pid > 0)
.then(|| detect::foreground_process_group_id(pid))
.flatten();
let foreground_group_changed = foreground_pgid.is_some()
&& last_foreground_pgid.is_some()
&& foreground_pgid != last_foreground_pgid;
let should_check_process = suppressed_agent.is_some()
|| agent_presence.current_agent().is_none()
|| foreground_group_changed
|| pending_foreground_shell_clear
|| pending_restore_probe
|| now.duration_since(last_process_check) >= PROCESS_RECHECK;
let process_group_changed =
foreground_group_changed(foreground_pgid, last_foreground_pgid);
let should_check_process = pid > 0
&& should_probe_foreground_job(ProcessProbeInput {
current_agent: agent_presence.current_agent(),
suppressed_agent,
foreground_pgid,
last_foreground_pgid,
has_process_probe,
acquisition_age: acquisition_started_at
.map(|started| now.duration_since(started)),
pending_foreground_shell_clear,
pending_restore_probe,
elapsed_since_process_check: now.duration_since(last_process_check),
});
let mut agent_changed = false;
let mut agent = agent_presence.current_agent();
if should_check_process {
last_process_check = now;
let had_process_probe = has_process_probe;
has_process_probe = true;
if pid > 0 {
let mut process_name = None;
let mut process_group_id = None;
let mut foreground_is_pane_shell = false;
let mut new_agent = None;
if let Some(job) = detect::foreground_job(pid) {
process_group_id = Some(job.process_group_id);
last_foreground_pgid = Some(job.process_group_id);
foreground_is_pane_shell =
job.processes.iter().any(|p| p.pid == pid);
let identified = detect::identify_agent_in_job(&job);
process_name = identified
.as_ref()
.map(|(_, process_name)| process_name.clone());
new_agent = identified.as_ref().map(|(agent, _)| *agent);
} else if foreground_pgid.is_some() {
process_group_id = foreground_pgid;
last_foreground_pgid = foreground_pgid;
}
let probe = probe_foreground_process(pid, foreground_pgid);
let process_name = probe.process_name;
let process_group_id = probe.process_group_id;
let foreground_is_pane_shell = probe.foreground_is_pane_shell;
let mut new_agent = probe.agent;
if let Some(suppressed_agent) = suppressed_agent {
if new_agent == Some(suppressed_agent) {
@@ -1427,10 +1582,16 @@ impl PaneRuntime {
};
if new_agent.is_some() {
last_foreground_pgid = process_group_id;
acquisition_started_at = None;
pending_restore_probe = false;
} else if agent_presence.current_agent().is_none() {
last_foreground_pgid = None;
last_foreground_pgid = process_group_id.or(foreground_pgid);
if had_process_probe && process_group_changed {
acquisition_started_at = Some(now);
}
pending_restore_probe = false;
} else {
last_foreground_pgid = process_group_id.or(foreground_pgid);
}
if changed {
agent = agent_presence.current_agent();
@@ -1466,7 +1627,6 @@ impl PaneRuntime {
}
}
let content = terminal.detection_text();
let process_exited = pending_foreground_shell_clear
&& agent.is_some()
&& !foreground_shell_exit_reported;
@@ -2327,6 +2487,206 @@ mod tests {
));
}
fn process_probe_input() -> ProcessProbeInput {
ProcessProbeInput {
current_agent: None,
suppressed_agent: None,
foreground_pgid: Some(42),
last_foreground_pgid: Some(42),
has_process_probe: true,
acquisition_age: None,
pending_foreground_shell_clear: false,
pending_restore_probe: false,
elapsed_since_process_check: std::time::Duration::from_secs(1),
}
}
#[test]
fn unchanged_unidentified_foreground_group_skips_full_process_probe() {
assert!(!should_probe_foreground_job(process_probe_input()));
}
#[test]
fn unidentified_foreground_group_change_runs_full_process_probe() {
assert!(should_probe_foreground_job(ProcessProbeInput {
foreground_pgid: Some(43),
..process_probe_input()
}));
}
#[test]
fn unidentified_pane_gets_initial_and_safety_process_probes() {
assert!(should_probe_foreground_job(ProcessProbeInput {
has_process_probe: false,
..process_probe_input()
}));
assert!(should_probe_foreground_job(ProcessProbeInput {
elapsed_since_process_check: PROCESS_RECHECK_UNIDENTIFIED,
..process_probe_input()
}));
}
#[test]
fn unidentified_pane_without_foreground_group_uses_safety_process_probe() {
assert!(!should_probe_foreground_job(ProcessProbeInput {
foreground_pgid: None,
last_foreground_pgid: None,
..process_probe_input()
}));
assert!(should_probe_foreground_job(ProcessProbeInput {
foreground_pgid: None,
last_foreground_pgid: None,
elapsed_since_process_check: PROCESS_RECHECK_UNIDENTIFIED,
..process_probe_input()
}));
}
#[test]
fn unidentified_pane_probes_when_foreground_group_disappears() {
assert!(should_probe_foreground_job(ProcessProbeInput {
foreground_pgid: None,
last_foreground_pgid: Some(42),
..process_probe_input()
}));
}
#[test]
fn pending_shell_clear_and_restore_force_process_probes() {
assert!(should_probe_foreground_job(ProcessProbeInput {
current_agent: Some(Agent::Codex),
pending_foreground_shell_clear: true,
..process_probe_input()
}));
assert!(should_probe_foreground_job(ProcessProbeInput {
current_agent: Some(Agent::Codex),
pending_restore_probe: true,
..process_probe_input()
}));
}
#[test]
fn pending_release_forces_initial_process_probe() {
assert!(should_probe_foreground_job(ProcessProbeInput {
current_agent: Some(Agent::Codex),
suppressed_agent: Some(Agent::Codex),
has_process_probe: false,
..process_probe_input()
}));
}
#[test]
fn pending_release_forces_process_probe_after_runtime_identity_clears() {
assert!(should_probe_foreground_job(ProcessProbeInput {
current_agent: None,
suppressed_agent: Some(Agent::Codex),
has_process_probe: false,
..process_probe_input()
}));
}
#[test]
fn pending_release_skips_repeated_probe_when_foreground_group_is_stable() {
assert!(!should_probe_foreground_job(ProcessProbeInput {
current_agent: None,
suppressed_agent: Some(Agent::Codex),
..process_probe_input()
}));
}
#[test]
fn pending_release_probes_when_foreground_group_changes() {
assert!(should_probe_foreground_job(ProcessProbeInput {
current_agent: None,
suppressed_agent: Some(Agent::Codex),
foreground_pgid: Some(43),
..process_probe_input()
}));
}
#[test]
fn acquisition_window_catches_delayed_same_group_wrapper_startup() {
assert!(!should_probe_foreground_job(ProcessProbeInput {
current_agent: None,
acquisition_age: Some(std::time::Duration::from_millis(1250)),
elapsed_since_process_check: PROCESS_ACQUISITION_FAST_RECHECK
- std::time::Duration::from_millis(1),
..process_probe_input()
}));
assert!(should_probe_foreground_job(ProcessProbeInput {
current_agent: None,
acquisition_age: Some(std::time::Duration::from_millis(1250)),
elapsed_since_process_check: PROCESS_ACQUISITION_FAST_RECHECK,
..process_probe_input()
}));
assert!(should_probe_foreground_job(ProcessProbeInput {
current_agent: None,
acquisition_age: Some(std::time::Duration::from_secs(5)),
elapsed_since_process_check: PROCESS_ACQUISITION_SLOW_RECHECK,
..process_probe_input()
}));
assert!(!should_probe_foreground_job(ProcessProbeInput {
current_agent: None,
acquisition_age: Some(PROCESS_ACQUISITION_WINDOW + std::time::Duration::from_millis(1),),
elapsed_since_process_check: PROCESS_ACQUISITION_SLOW_RECHECK,
..process_probe_input()
}));
}
#[test]
fn release_expiry_can_force_reacquire_probe_by_resetting_probe_state() {
assert!(should_probe_foreground_job(ProcessProbeInput {
current_agent: None,
has_process_probe: false,
..process_probe_input()
}));
}
#[test]
fn identified_agent_uses_shorter_safety_process_probe() {
assert!(!should_probe_foreground_job(ProcessProbeInput {
current_agent: Some(Agent::Codex),
elapsed_since_process_check: PROCESS_RECHECK_IDENTIFIED
- std::time::Duration::from_millis(1),
..process_probe_input()
}));
assert!(should_probe_foreground_job(ProcessProbeInput {
current_agent: Some(Agent::Codex),
elapsed_since_process_check: PROCESS_RECHECK_IDENTIFIED,
..process_probe_input()
}));
}
#[test]
fn identified_agent_probes_when_foreground_group_disappears() {
assert!(should_probe_foreground_job(ProcessProbeInput {
current_agent: Some(Agent::Codex),
foreground_pgid: None,
last_foreground_pgid: Some(42),
elapsed_since_process_check: PROCESS_RECHECK_IDENTIFIED
- std::time::Duration::from_millis(1),
..process_probe_input()
}));
}
#[test]
fn stable_missing_foreground_group_uses_safety_process_probe() {
assert!(!should_probe_foreground_job(ProcessProbeInput {
current_agent: Some(Agent::Codex),
foreground_pgid: None,
last_foreground_pgid: None,
elapsed_since_process_check: PROCESS_RECHECK_IDENTIFIED
- std::time::Duration::from_millis(1),
..process_probe_input()
}));
assert!(should_probe_foreground_job(ProcessProbeInput {
current_agent: Some(Agent::Codex),
foreground_pgid: None,
last_foreground_pgid: None,
elapsed_since_process_check: PROCESS_RECHECK_IDENTIFIED,
..process_probe_input()
}));
}
#[test]
fn transient_process_miss_keeps_current_agent_detected() {
let mut presence = AgentDetectionPresence::from_agent(Some(Agent::Pi));
+5
View File
@@ -7,6 +7,11 @@ pub fn foreground_job(_child_pid: u32) -> Option<ForegroundJob> {
None
}
/// Unsupported platform stub.
pub fn foreground_group_leader_job(_process_group_id: u32) -> Option<ForegroundJob> {
None
}
/// Unsupported platform stub.
pub fn foreground_process_group_id(_child_pid: u32) -> Option<u32> {
None
+19
View File
@@ -54,6 +54,25 @@ pub fn foreground_job(child_pid: u32) -> Option<ForegroundJob> {
})
}
pub fn foreground_group_leader_job(process_group_id: u32) -> Option<ForegroundJob> {
let (pgrp, name) = process_pgrp_and_comm(process_group_id)?;
if pgrp as u32 != process_group_id {
return None;
}
let argv = process_argv(process_group_id);
Some(ForegroundJob {
process_group_id,
processes: vec![ForegroundProcess {
pid: process_group_id,
name,
argv0: None,
cmdline: argv.as_ref().map(|parts| parts.join(" ")),
argv,
}],
})
}
pub fn foreground_process_group_id(child_pid: u32) -> Option<u32> {
// /proc/<pid>/stat format: "pid (comm) state ppid pgrp session tty_nr tpgid ..."
// The (comm) field can contain spaces and parens, so we find the last ')' first.
+20
View File
@@ -52,6 +52,26 @@ pub fn foreground_job(child_pid: u32) -> Option<ForegroundJob> {
})
}
pub fn foreground_group_leader_job(process_group_id: u32) -> Option<ForegroundJob> {
let info = process_bsdinfo(process_group_id)?;
if info.pbi_pgid != process_group_id {
return None;
}
let name = comm_from_bsdinfo(&info)?;
let argv = process_argv(process_group_id);
Some(ForegroundJob {
process_group_id,
processes: vec![ForegroundProcess {
pid: process_group_id,
name,
argv0: process_argv0_name(process_group_id),
cmdline: argv.as_ref().map(|parts| parts.join(" ")),
argv,
}],
})
}
fn process_group_pids(process_group_id: u32) -> Vec<u32> {
let mut capacity = 16usize;