feat(cleanup): default idle-shutdown + keep — stop leaving scratch tabs/groups behind
Release binaries / Build macOS ARM64 (push) Has been cancelled
Release binaries / Build macOS x64 (push) Has been cancelled
Release binaries / Build Linux ARM64 (push) Has been cancelled
Release binaries / Build Linux musl ARM64 (push) Has been cancelled
Release binaries / Build Linux musl x64 (push) Has been cancelled
Release binaries / Build Linux x64 (push) Has been cancelled
Release binaries / Build Windows x64 (push) Has been cancelled
Release binaries / Attach binaries to GitHub Release (push) Has been cancelled
Release binaries / Build macOS ARM64 (push) Has been cancelled
Release binaries / Build macOS x64 (push) Has been cancelled
Release binaries / Build Linux ARM64 (push) Has been cancelled
Release binaries / Build Linux musl ARM64 (push) Has been cancelled
Release binaries / Build Linux musl x64 (push) Has been cancelled
Release binaries / Build Linux x64 (push) Has been cancelled
Release binaries / Build Windows x64 (push) Has been cancelled
Release binaries / Attach binaries to GitHub Release (push) Has been cancelled
Agents finish a task and just stop (never calling `close`), so daemons used to run forever, leaving their per-session scratch tabs + tab group in the user's Chrome. Two cases now handled: - Default idle timeout (10 min; AGENT_BROWSER_IDLE_TIMEOUT_MS overrides, 0 disables). On idle the daemon close()s the tabs IT created → the empty tab group is auto-removed by Chrome. Timer resets on every command, so active sessions are untouched. Adopted user tabs are never owned, so never closed. - `keep` — leave the ACTIVE tab for the user: unown it (exempt from close/idle) + ask the extension to ungroup it (ABExt.ungroupTab → 0.4.12) so it becomes a normal tab. Scratch gets cleaned, deliverable tabs stay. Also fix two clippy violations in the concurrently-landed #47 viewport code (manual char comparison + iter().any→contains) that were failing main's CI. ext 0.4.12: handle ABExt.ungroupTab (chrome.tabs.ungroup). 870 tests pass.
This commit is contained in:
@@ -1317,6 +1317,7 @@ pub async fn execute_command(cmd: &Value, state: &mut DaemonState) -> Value {
|
||||
"evaluate" => handle_evaluate(cmd, state).await,
|
||||
"site" => handle_site(cmd, state).await,
|
||||
"close" => handle_close(state).await,
|
||||
"keep" => handle_keep(state).await,
|
||||
"stealth_status" => handle_stealth_status(state).await,
|
||||
"snapshot" => handle_snapshot(cmd, state).await,
|
||||
"screenshot" => handle_screenshot(cmd, state).await,
|
||||
@@ -2855,6 +2856,34 @@ async fn handle_stealth_status(state: &DaemonState) -> Result<Value, String> {
|
||||
}))
|
||||
}
|
||||
|
||||
/// `keep` — leave the ACTIVE tab for the user: stop owning it (so the daemon's
|
||||
/// `close()`/idle-shutdown won't close it) and best-effort remove it from this
|
||||
/// session's tab group so it looks like a normal user tab. The "leave for the
|
||||
/// user" half of the auto-close-on-idle cleanup: scratch tabs get closed, tabs
|
||||
/// the agent explicitly `keep`s stay. (Adopted user tabs are never owned, so
|
||||
/// they're already safe.)
|
||||
async fn handle_keep(state: &mut DaemonState) -> Result<Value, String> {
|
||||
let mgr = state.browser.as_mut().ok_or("Browser not launched")?;
|
||||
let target_id = mgr.active_target_id()?.to_string();
|
||||
let session_id = mgr.active_session_id()?.to_string();
|
||||
let was_owned = mgr.unown_target(&target_id);
|
||||
// Best-effort: ask the extension to ungroup the tab (relay only; no-ops on a
|
||||
// launched browser or an older extension that doesn't know ABExt.ungroupTab).
|
||||
let _ = mgr
|
||||
.client
|
||||
.send_command_typed::<_, Value>(
|
||||
"ABExt.ungroupTab",
|
||||
&json!({ "sessionId": session_id, "targetId": target_id }),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
Ok(json!({
|
||||
"kept": target_id,
|
||||
"wasOwned": was_owned,
|
||||
"note": "tab left for the user — exempt from auto-close, removed from the session tab group",
|
||||
}))
|
||||
}
|
||||
|
||||
async fn handle_close(state: &mut DaemonState) -> Result<Value, String> {
|
||||
if let Some(ref mgr) = state.browser {
|
||||
if let Some(ref session_name) = state.session_name {
|
||||
|
||||
@@ -1546,6 +1546,13 @@ impl BrowserManager {
|
||||
.ok_or_else(|| "No active page".to_string())
|
||||
}
|
||||
|
||||
/// Stop owning a tab — drop it from `created_targets` so it survives `close()`
|
||||
/// and idle-shutdown (the agent is leaving it for the user). Returns true if it
|
||||
/// was owned. Used by `keep`.
|
||||
pub fn unown_target(&mut self, target_id: &str) -> bool {
|
||||
self.created_targets.remove(target_id)
|
||||
}
|
||||
|
||||
/// Returns true if this manager was connected via CDP (as opposed to local launch).
|
||||
pub fn is_cdp_connection(&self) -> bool {
|
||||
self.browser_process.is_none()
|
||||
|
||||
@@ -130,12 +130,21 @@ pub async fn run_daemon(session: &str) {
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-shutdown the daemon after this many ms of inactivity (no commands received).
|
||||
// Disabled when unset or 0.
|
||||
let idle_timeout_ms = env::var("AGENT_BROWSER_IDLE_TIMEOUT_MS")
|
||||
.ok()
|
||||
.and_then(|s| s.parse::<u64>().ok())
|
||||
.filter(|&ms| ms > 0);
|
||||
// Auto-shutdown the daemon after this many ms of inactivity (no commands
|
||||
// received). On shutdown the daemon closes the tabs IT created (its per-session
|
||||
// tab group), so an agent that finishes a task and just stops — without ever
|
||||
// calling `close` — no longer leaves a pile of scratch tabs and a lingering
|
||||
// tab group in the user's Chrome. The timer resets on every command, so active
|
||||
// sessions are never interrupted; only genuinely-idle ones clean up.
|
||||
//
|
||||
// Defaults to 10 minutes. Set AGENT_BROWSER_IDLE_TIMEOUT_MS to override, or 0
|
||||
// to disable (keep the daemon alive forever — the old behaviour). Adopted
|
||||
// tabs (the user's own, via `adopt`) are never closed: only `created_targets`.
|
||||
const DEFAULT_IDLE_TIMEOUT_MS: u64 = 600_000;
|
||||
let idle_timeout_ms = match env::var("AGENT_BROWSER_IDLE_TIMEOUT_MS") {
|
||||
Ok(s) => s.trim().parse::<u64>().ok().filter(|&ms| ms > 0),
|
||||
Err(_) => Some(DEFAULT_IDLE_TIMEOUT_MS),
|
||||
};
|
||||
|
||||
let result = run_socket_server(
|
||||
&socket_path,
|
||||
|
||||
Reference in New Issue
Block a user