From bb278ffdff01ba0e3eeae24a6e4f7c98d66c0929 Mon Sep 17 00:00:00 2001 From: ciregenz Date: Tue, 11 Aug 2026 19:13:09 -0700 Subject: [PATCH] [eric] copy an agent and its inline browser comes with it (ENG-250); browsing caches are swept past a 1.5GB cap at boot so runaway disk use cannot get the app killed (ENG-247) --- electron/main.js | 38 ++++++++++++++++++- .../interaction/useDashboardClipboard.ts | 16 +++++++- 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/electron/main.js b/electron/main.js index 971c04e9..401f2a36 100644 --- a/electron/main.js +++ b/electron/main.js @@ -138,7 +138,7 @@ try { } } catch (_) {} const path = require('path'); -const { spawn, execFileSync } = require('child_process'); +const { spawn, execFileSync, spawnSync } = require('child_process'); const os = require('os'); const fs = require('fs'); const hiddenBrowser = require('./hiddenBrowser'); @@ -1923,6 +1923,40 @@ async function clearStaleFrontendCache() { } } +// Browsing caches grow without bound (measured on a real profile: Partitions 2.2GB, Code Cache +// 291MB), and a machine that blows through macOS's disk-write ceiling gets the app killed with no +// crash report (ENG-247). So bound them: past the cap, drop the REGENERABLE bytes at boot. Cookies, +// localStorage and IndexedDB are deliberately untouched, clearing those would sign the user out of +// every site their agents rely on; clearCache only drops fetched bytes the network can refetch. +const DISK_CACHE_CAP_MB = 1500; + +function dirSizeMb(dir) { + try { + const out = spawnSync('du', ['-sk', dir], { encoding: 'utf8', timeout: 20000 }); + const kb = parseInt(String((out && out.stdout) || '0').trim().split(/\s+/)[0], 10); + return Number.isFinite(kb) ? Math.round(kb / 1024) : 0; + } catch (_) { + return 0; + } +} + +async function sweepOversizedCaches() { + try { + const root = app.getPath('userData'); + const before = dirSizeMb(path.join(root, 'Partitions')) + dirSizeMb(path.join(root, 'Code Cache')); + if (before < DISK_CACHE_CAP_MB) { + console.log(`[cache] browsing caches ${before}MB, under the ${DISK_CACHE_CAP_MB}MB cap; nothing to sweep`); + return; + } + await session.fromPartition(BROWSER_PARTITION).clearCache(); + await session.defaultSession.clearCache(); + const after = dirSizeMb(path.join(root, 'Partitions')) + dirSizeMb(path.join(root, 'Code Cache')); + console.log(`[cache] swept oversized browsing caches: ${before}MB -> ${after}MB (logins untouched)`); + } catch (err) { + console.warn('[cache] sweepOversizedCaches failed:', err && err.message); + } +} + function setupAutoUpdater() { if (!autoUpdater) return; // Escape hatch for locally-built packaged smokes: an unpublished build otherwise downloads the @@ -2418,6 +2452,8 @@ app.whenReady().then(async () => { // Must run before createWindow loads the URL, or the renderer fetches the stale bundle first. await clearStaleFrontendCache(); createWindow(); + // After first paint, never before: the sweep shells out to du and must not delay the window. + setTimeout(() => { void sweepOversizedCaches(); }, 8000); if (!isDev) { setupAutoUpdater(); mainWindow.webContents.on('did-finish-load', () => { diff --git a/frontend/src/app/pages/Dashboard/hooks/interaction/useDashboardClipboard.ts b/frontend/src/app/pages/Dashboard/hooks/interaction/useDashboardClipboard.ts index 276d7307..5bf85602 100644 --- a/frontend/src/app/pages/Dashboard/hooks/interaction/useDashboardClipboard.ts +++ b/frontend/src/app/pages/Dashboard/hooks/interaction/useDashboardClipboard.ts @@ -66,6 +66,18 @@ export function useDashboardClipboard({ expanded: expandedSessionIds.includes(id), }); names.push(session.name || id); + // A chat's browser lives INSIDE it, so copying the agent has to carry it along; selecting a + // card the user sees as one thing must not paste half of it (ENG-250). Deduped below in case + // the browser was independently selected too. + for (const bc of Object.values(browserCards)) { + if (bc.docked_to !== id && bc.spawned_by !== id) continue; + const tab = bc.tabs.find((t) => t.id === bc.activeTabId); + copied.push({ + type: 'browser', id: bc.browser_id, name: tab?.title || 'Browser', + meta: { name: tab?.title || 'Browser', url: tab?.url || bc.url, tabs: bc.tabs, spawnedBy: id }, + x: bc.x, y: bc.y, width: bc.width, height: bc.height, + }); + } } else if (type === 'view') { const output = outputs[id]; const vc = viewCards[id]; @@ -90,7 +102,9 @@ export function useDashboardClipboard({ names.push(title); } } - setClipboardCards(copied); + // Selecting an agent AND its browser must not paste the browser twice. + const deduped = copied.filter((c, i) => copied.findIndex((o) => o.type === c.type && o.id === c.id) === i); + setClipboardCards(deduped); navigator.clipboard.writeText(names.join(', ')).catch(() => {}); // Copy IS attach: the selection lands in the composer as context chips, no select mode, no paste step. if (copied.length > 0) onCopiedToContext?.(copied);