perf(disk): remove runtime caches left by earlier versions after startup

This commit is contained in:
mesalogo
2026-10-04 09:45:50 +08:00
parent 91edcf5e8b
commit ae4c6d2423
5 changed files with 151 additions and 1 deletions
+2 -1
View File
@@ -55,6 +55,7 @@ import { readBoundedResponseText } from './bounded-response'
import { scopedReadToolNames } from '../../shared/scoped-data-tools'
import { readBoundedFile } from '../workspace-file-access'
import { terminateProcessTreeAndWait } from './child-process-termination'
import { CONTINUE_HOST_LAYOUT_VERSION } from './continue-host-layout'
const supportedVersion = '1.5.47'
const supportedBundleHashes = new Set([
@@ -1052,7 +1053,7 @@ export class ContinueHostAdapter {
const digest = sourceHash.slice(0, 16)
const targetRoot = join(
this.options.cacheRoot,
`host-v7-${supportedVersion}-${digest}`
`host-v${CONTINUE_HOST_LAYOUT_VERSION}-${supportedVersion}-${digest}`
)
const targetDist = join(targetRoot, 'dist')
const targetBundle = join(targetDist, 'index.js')
+2
View File
@@ -0,0 +1,2 @@
/** Prepared Continue host directories are `host-v<layout>-…`; older layouts are never read again. */
export const CONTINUE_HOST_LAYOUT_VERSION = 7
+10
View File
@@ -65,6 +65,8 @@ import {
import { createTrayIcon } from './tray-icon'
import { resolveBundledRuntimePaths } from './agent/bundled-runtimes'
import type { ContinueHostLauncher } from './agent/continue-host-adapter'
import { CONTINUE_HOST_LAYOUT_VERSION } from './agent/continue-host-layout'
import { removeStaleRuntimeCaches } from './stale-runtime-cache-cleanup'
import { resolvePortableUserDataPath } from './portable-user-data'
import { BrowserService } from './browser/browser-service'
import { SubagentService } from './assistant/subagent-service'
@@ -1626,6 +1628,14 @@ if (hasSingleInstanceLock) {
startupMark('main:load-main-window')
removeDeviceSharingIpc = registerDeviceSharingIpc(mainWindow,
new DeviceSharingService(join(app.getPath('userData'), 'device-sharing-settings.json'), app.getVersion()))
// Disk only: old versions' runtime caches, removed well after the first frame.
setTimeout(() => {
void removeStaleRuntimeCaches({
userDataPath: app.getPath('userData'),
appVersion: app.getVersion(),
continueHostLayoutVersion: CONTINUE_HOST_LAYOUT_VERSION
})
}, 30_000).unref()
setImmediate(() => {
void repairStaleWindowsNotificationShortcuts({
platform: process.platform,
@@ -0,0 +1,61 @@
// @vitest-environment node
import { existsSync } from 'node:fs'
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import { removeStaleRuntimeCaches, staleRuntimeCacheDirectories } from './stale-runtime-cache-cleanup'
const directories: string[] = []
afterEach(async () => {
for (const directory of directories.splice(0)) await rm(directory, { recursive: true, force: true })
})
describe('stale runtime cache cleanup', () => {
it('selects only older app-version and Continue layout directories', () => {
expect(staleRuntimeCacheDirectories({
opencode: ['0.12.8', '0.15.8', '0.15.9', '0.16.0-beta.1', 'cache', 'custom', '1.0'],
continueHost: ['host-v1-1.5.47-a', 'host-v6-1.5.47-a', 'host-v7-1.5.47-a', 'host-v7-1.6.0-b',
'host-v8-1.5.47-a', 'isolated-global', 'isolated-global-123', 'host-v7-1.5.47-a.staging-x', 'sessions']
}, { appVersion: '0.15.9', continueHostLayoutVersion: 7 })).toEqual({
opencode: ['0.12.8', '0.15.8', '0.16.0-beta.1'],
continueHost: ['host-v1-1.5.47-a', 'host-v6-1.5.47-a']
})
})
it('removes stale directories and leaves everything else in the user data directory', async () => {
const root = await mkdtemp(join(tmpdir(), 'goodbuddy-cache-cleanup-'))
directories.push(root)
const make = async (path: string): Promise<void> => {
await mkdir(join(root, path), { recursive: true })
await writeFile(join(root, path, 'file.txt'), 'x')
}
await make('opencode-runtime/0.15.8/config')
await make('opencode-runtime/0.15.9/config')
await make('continue-host/host-v6-1.5.47-a/dist')
await make('continue-host/host-v7-1.5.47-a/dist')
await make('continue-host/isolated-global')
await make('continue-host/sessions')
await writeFile(join(root, 'continue-host', 'config.yaml'), 'x')
await make('models/embedding')
const { removed } = await removeStaleRuntimeCaches({
userDataPath: root, appVersion: '0.15.9', continueHostLayoutVersion: 7
})
expect(removed.map((path) => path.slice(root.length + 1).replaceAll('\\', '/')).sort())
.toEqual(['continue-host/host-v6-1.5.47-a', 'opencode-runtime/0.15.8'])
for (const kept of ['opencode-runtime/0.15.9/config/file.txt', 'continue-host/host-v7-1.5.47-a/dist/file.txt',
'continue-host/isolated-global/file.txt', 'continue-host/sessions/file.txt', 'continue-host/config.yaml',
'models/embedding/file.txt']) {
expect(existsSync(join(root, kept))).toBe(true)
}
})
it('does nothing when the cache roots do not exist', async () => {
const root = await mkdtemp(join(tmpdir(), 'goodbuddy-cache-cleanup-'))
directories.push(root)
await expect(removeStaleRuntimeCaches({ userDataPath: root, appVersion: '0.15.9', continueHostLayoutVersion: 7 }))
.resolves.toEqual({ removed: [] })
})
})
+76
View File
@@ -0,0 +1,76 @@
import { readdir, rm } from 'node:fs/promises'
import { join } from 'node:path'
/**
* Removes runtime caches left by earlier GoodBuddy versions (PERF, disk).
*
* - `opencode-runtime/<app version>/`: OpenCode's shared config and skills
* are prepared per app version and only the current version's directory is
* ever used, so every other version-named directory is stale (~50 MB each).
* - `continue-host/host-v<layout>-…/`: patched Continue host bundles; only
* the current layout is read. Older layouts are stale. Directories of the
* current layout (other Continue versions or digests) are left alone, as
* are run directories, logs, sessions and config files.
*
* Only names that match these exact patterns are touched, never anything
* else in the user data directory. Failures (a file in use, permissions) are
* ignored; the next start tries again.
*/
export type StaleRuntimeCacheCleanupOptions = {
userDataPath: string
appVersion: string
continueHostLayoutVersion: number
}
const appVersionPattern = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/u
const continueHostPattern = /^host-v(\d+)-/u
async function directoryNames(path: string): Promise<string[]> {
try {
return (await readdir(path, { withFileTypes: true }))
.filter((entry) => entry.isDirectory() && !entry.isSymbolicLink())
.map((entry) => entry.name)
} catch {
return []
}
}
export function staleRuntimeCacheDirectories(
names: { opencode: readonly string[]; continueHost: readonly string[] },
options: Pick<StaleRuntimeCacheCleanupOptions, 'appVersion' | 'continueHostLayoutVersion'>
): { opencode: string[]; continueHost: string[] } {
return {
opencode: names.opencode.filter((name) =>
appVersionPattern.test(name) && name !== options.appVersion),
continueHost: names.continueHost.filter((name) => {
const match = continueHostPattern.exec(name)
return match !== null && Number(match[1]) < options.continueHostLayoutVersion
})
}
}
export async function removeStaleRuntimeCaches(
options: StaleRuntimeCacheCleanupOptions
): Promise<{ removed: string[] }> {
const opencodeRoot = join(options.userDataPath, 'opencode-runtime')
const continueRoot = join(options.userDataPath, 'continue-host')
const stale = staleRuntimeCacheDirectories({
opencode: await directoryNames(opencodeRoot),
continueHost: await directoryNames(continueRoot)
}, options)
const targets = [
...stale.opencode.map((name) => join(opencodeRoot, name)),
...stale.continueHost.map((name) => join(continueRoot, name))
]
const removed: string[] = []
// One at a time: this runs in the background and should not compete for disk.
for (const target of targets) {
try {
await rm(target, { recursive: true, force: true, maxRetries: 2, retryDelay: 200 })
removed.push(target)
} catch {
// Left for the next start.
}
}
return { removed }
}