diff --git a/src/agents/sandbox/prune.ts b/src/agents/sandbox/prune.ts index de3616f7e49..c3b37534e36 100644 --- a/src/agents/sandbox/prune.ts +++ b/src/agents/sandbox/prune.ts @@ -8,69 +8,80 @@ import { readRegistry, removeBrowserRegistryEntry, removeRegistryEntry, + type SandboxBrowserRegistryEntry, + type SandboxRegistryEntry, } from "./registry.js"; let lastPruneAtMs = 0; -async function pruneSandboxContainers(cfg: SandboxConfig) { - const now = Date.now(); +type PruneableRegistryEntry = Pick< + SandboxRegistryEntry, + "containerName" | "createdAtMs" | "lastUsedAtMs" +>; + +function shouldPruneSandboxEntry(cfg: SandboxConfig, now: number, entry: PruneableRegistryEntry) { const idleHours = cfg.prune.idleHours; const maxAgeDays = cfg.prune.maxAgeDays; if (idleHours === 0 && maxAgeDays === 0) { + return false; + } + const idleMs = now - entry.lastUsedAtMs; + const ageMs = now - entry.createdAtMs; + return ( + (idleHours > 0 && idleMs > idleHours * 60 * 60 * 1000) || + (maxAgeDays > 0 && ageMs > maxAgeDays * 24 * 60 * 60 * 1000) + ); +} + +async function pruneSandboxRegistryEntries(params: { + cfg: SandboxConfig; + read: () => Promise<{ entries: TEntry[] }>; + remove: (containerName: string) => Promise; + onRemoved?: (entry: TEntry) => Promise; +}) { + const now = Date.now(); + if (params.cfg.prune.idleHours === 0 && params.cfg.prune.maxAgeDays === 0) { return; } - const registry = await readRegistry(); + const registry = await params.read(); for (const entry of registry.entries) { - const idleMs = now - entry.lastUsedAtMs; - const ageMs = now - entry.createdAtMs; - if ( - (idleHours > 0 && idleMs > idleHours * 60 * 60 * 1000) || - (maxAgeDays > 0 && ageMs > maxAgeDays * 24 * 60 * 60 * 1000) - ) { - try { - await execDocker(["rm", "-f", entry.containerName], { - allowFailure: true, - }); - } catch { - // ignore prune failures - } finally { - await removeRegistryEntry(entry.containerName); - } + if (!shouldPruneSandboxEntry(params.cfg, now, entry)) { + continue; + } + try { + await execDocker(["rm", "-f", entry.containerName], { + allowFailure: true, + }); + } catch { + // ignore prune failures + } finally { + await params.remove(entry.containerName); + await params.onRemoved?.(entry); } } } +async function pruneSandboxContainers(cfg: SandboxConfig) { + await pruneSandboxRegistryEntries({ + cfg, + read: readRegistry, + remove: removeRegistryEntry, + }); +} + async function pruneSandboxBrowsers(cfg: SandboxConfig) { - const now = Date.now(); - const idleHours = cfg.prune.idleHours; - const maxAgeDays = cfg.prune.maxAgeDays; - if (idleHours === 0 && maxAgeDays === 0) { - return; - } - const registry = await readBrowserRegistry(); - for (const entry of registry.entries) { - const idleMs = now - entry.lastUsedAtMs; - const ageMs = now - entry.createdAtMs; - if ( - (idleHours > 0 && idleMs > idleHours * 60 * 60 * 1000) || - (maxAgeDays > 0 && ageMs > maxAgeDays * 24 * 60 * 60 * 1000) - ) { - try { - await execDocker(["rm", "-f", entry.containerName], { - allowFailure: true, - }); - } catch { - // ignore prune failures - } finally { - await removeBrowserRegistryEntry(entry.containerName); - const bridge = BROWSER_BRIDGES.get(entry.sessionKey); - if (bridge?.containerName === entry.containerName) { - await stopBrowserBridgeServer(bridge.bridge.server).catch(() => undefined); - BROWSER_BRIDGES.delete(entry.sessionKey); - } + await pruneSandboxRegistryEntries({ + cfg, + read: readBrowserRegistry, + remove: removeBrowserRegistryEntry, + onRemoved: async (entry) => { + const bridge = BROWSER_BRIDGES.get(entry.sessionKey); + if (bridge?.containerName === entry.containerName) { + await stopBrowserBridgeServer(bridge.bridge.server).catch(() => undefined); + BROWSER_BRIDGES.delete(entry.sessionKey); } - } - } + }, + }); } export async function maybePruneSandboxes(cfg: SandboxConfig) {