fix: stop subagent history from exhausting disk space
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build darwin arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux x64 Agent (push) Canceled after 0s
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build darwin arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux x64 Agent (push) Canceled after 0s
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
Repeated full subagent progress snapshots could grow the desktop database to many gigabytes during ordinary long-running tasks. Local and managed SSH writes now retain lossless progress changes while preserving conversation snapshots, event identities, remote replay deduplication and transaction rollback. Upgrade existing history in bounded worker-thread batches, show progress and retry controls, and reclaim freed disk space after conversion. Schema 34 cannot be opened by older clients; a pre-upgrade backup is required for client rollback. Document reusable database migration guidance and the verified large-database, portable, local Runtime and Linux Host evidence. Prepare Desktop 0.13.1 with the approved bilingual notes and aligned feature lists. Agent remains 0.11.24. Final candidate validation and production builds will run on main CI, as requested, without additional local release testing. Release note: 修复长任务和子任务造成的数据库异常膨胀;升级时自动整理旧记录并回收空间,保留聊天、执行详情和结果,并支持退出后继续处理。
This commit is contained in:
@@ -181,6 +181,10 @@ const terminalSnapshot: TerminalSnapshot = {
|
||||
};
|
||||
|
||||
const api: DesktopApi = {
|
||||
storageUpgrade: {
|
||||
getProgress: vi.fn(),
|
||||
act: vi.fn(async () => undefined)
|
||||
},
|
||||
app: {
|
||||
getInfo: vi.fn(async () => ({
|
||||
name: "GoodBuddy",
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
.storage-upgrade {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
background: var(--surface-raised);
|
||||
}
|
||||
|
||||
.storage-upgrade > .page-shell {
|
||||
flex: 1;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.storage-upgrade progress {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: var(--space-2);
|
||||
accent-color: var(--accent-solid);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { afterEach, expect, it, vi } from 'vitest'
|
||||
import { StorageUpgrade } from './StorageUpgrade'
|
||||
import type { DesktopApi } from '../../shared/contracts'
|
||||
|
||||
afterEach(() => { cleanup(); vi.unstubAllGlobals() })
|
||||
|
||||
it('shows real progress and lets the user quit without deleting records', async () => {
|
||||
const act = vi.fn(async () => undefined)
|
||||
vi.stubGlobal('goodbuddy', { storageUpgrade: {
|
||||
getProgress: vi.fn(async () => ({
|
||||
stage: 'converting', processed: 32, total: 100, bytesBefore: 90_000_000_000
|
||||
})),
|
||||
act
|
||||
} } as unknown as DesktopApi)
|
||||
render(<StorageUpgrade />)
|
||||
await waitFor(() => expect(screen.getByRole('progressbar')).toHaveAttribute('value', '32'))
|
||||
expect(screen.getByText(/保留聊天、执行详情和结果/)).toBeInTheDocument()
|
||||
fireEvent.click(screen.getByRole('button', { name: '退出,稍后继续' }))
|
||||
await waitFor(() => expect(act).toHaveBeenCalledWith('quit'))
|
||||
})
|
||||
|
||||
it('keeps a failed upgrade actionable through retry', async () => {
|
||||
const act = vi.fn(async () => undefined)
|
||||
vi.stubGlobal('goodbuddy', { storageUpgrade: {
|
||||
getProgress: vi.fn(async () => ({
|
||||
stage: 'failed', processed: 32, total: 100, bytesBefore: 90_000_000_000,
|
||||
error: '磁盘空间不足'
|
||||
})),
|
||||
act
|
||||
} } as unknown as DesktopApi)
|
||||
render(<StorageUpgrade />)
|
||||
expect(await screen.findByRole('alert')).toHaveTextContent('磁盘空间不足')
|
||||
fireEvent.click(screen.getByRole('button', { name: '重试' }))
|
||||
await waitFor(() => expect(act).toHaveBeenCalledWith('retry'))
|
||||
})
|
||||
@@ -0,0 +1,94 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import type { AssistantStorageProgress } from '../../shared/assistant-storage-contracts'
|
||||
import { PageHeader, PageShell } from './WorkspacePrimitives'
|
||||
import { useUiLocale } from './i18n/UiLocaleProvider'
|
||||
import './StorageUpgrade.css'
|
||||
|
||||
export function StorageUpgrade(): React.JSX.Element {
|
||||
const { resolvedLocale } = useUiLocale()
|
||||
const en = resolvedLocale === 'en-US'
|
||||
const [progress, setProgress] = useState<AssistantStorageProgress>()
|
||||
const [rate, setRate] = useState<number>()
|
||||
const [actionError, setActionError] = useState('')
|
||||
useEffect(() => {
|
||||
let active = true
|
||||
let timer: ReturnType<typeof setTimeout>
|
||||
let sample: { count: number; time: number } | undefined
|
||||
const poll = async (): Promise<void> => {
|
||||
try {
|
||||
const next = await window.goodbuddy.storageUpgrade.getProgress()
|
||||
if (active) {
|
||||
setProgress(next)
|
||||
if (next.stage === 'converting') {
|
||||
const now = performance.now()
|
||||
if (sample && now - sample.time >= 2_000) {
|
||||
setRate(Math.max(0, (next.processed - sample.count) * 1_000 / (now - sample.time)))
|
||||
sample = { count: next.processed, time: now }
|
||||
} else if (!sample) {
|
||||
sample = { count: next.processed, time: now }
|
||||
}
|
||||
} else {
|
||||
sample = undefined
|
||||
setRate(undefined)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// The normal application replaces this page when startup continues.
|
||||
}
|
||||
if (active) timer = setTimeout(() => void poll(), 500)
|
||||
}
|
||||
void poll()
|
||||
return () => { active = false; clearTimeout(timer) }
|
||||
}, [])
|
||||
const title = en ? 'Optimizing saved execution history' : '正在优化历史执行记录'
|
||||
const stage = progress?.stage
|
||||
const status = stage === 'failed'
|
||||
? progress?.error
|
||||
: stage === 'compacting'
|
||||
? en ? 'Reclaiming disk space…' : '正在回收磁盘空间…'
|
||||
: stage === 'complete'
|
||||
? en ? 'Optimization complete. Starting GoodBuddy…' : '优化完成,正在启动 GoodBuddy…'
|
||||
: progress?.stage === 'converting'
|
||||
? `${en ? 'Processed' : '已处理'} ${progress.processed.toLocaleString()} / ${progress.total.toLocaleString()}`
|
||||
: en ? 'Checking saved execution history…' : '正在检查历史执行记录…'
|
||||
const act = async (action: 'retry' | 'quit'): Promise<void> => {
|
||||
try {
|
||||
setActionError('')
|
||||
await window.goodbuddy.storageUpgrade.act(action)
|
||||
} catch {
|
||||
setActionError(en ? 'The action failed. Please try again.' : '操作未完成,请重试。')
|
||||
}
|
||||
}
|
||||
return (
|
||||
<main className="storage-upgrade" aria-labelledby="storage-upgrade-title">
|
||||
<PageShell variant="standard">
|
||||
<PageHeader
|
||||
headingId="storage-upgrade-title"
|
||||
title={title}
|
||||
description={en
|
||||
? 'This update removes repeated copies of subagent progress. Your chats, execution details and results are preserved. Time depends on the history size and disk speed.'
|
||||
: '本次更新清理子任务进度的重复副本,保留聊天、执行详情和结果。处理时间取决于历史数据量和磁盘速度。'}
|
||||
/>
|
||||
<p role={stage === 'failed' ? 'alert' : 'status'}>{status}</p>
|
||||
{progress?.stage === 'converting' && progress.total > 0 && (
|
||||
<progress aria-label={title} max={progress.total} value={progress.processed} />
|
||||
)}
|
||||
{stage === 'converting' && rate !== undefined && (
|
||||
<p>{en ? 'Processing speed' : '处理速度'}:{Math.round(rate).toLocaleString()} {en ? 'events/s' : '条/秒'}</p>
|
||||
)}
|
||||
<p>{en
|
||||
? 'You can quit and retry on the next launch. Completed batches are preserved.'
|
||||
: '可以退出并在下次启动时继续处理,已完成的批次会保留。'}</p>
|
||||
{actionError && <p role="alert">{actionError}</p>}
|
||||
{stage === 'failed' && (
|
||||
<button className="primary-button" onClick={() => void act('retry')}>
|
||||
{en ? 'Retry' : '重试'}
|
||||
</button>
|
||||
)}
|
||||
<button className="secondary-button" onClick={() => void act('quit')}>
|
||||
{en ? 'Quit and continue later' : '退出,稍后继续'}
|
||||
</button>
|
||||
</PageShell>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import '@fontsource-variable/noto-sans-sc/wght.css'
|
||||
import 'katex/dist/katex.min.css'
|
||||
import './markdown-math.css'
|
||||
import App from './App'
|
||||
import { StorageUpgrade } from './StorageUpgrade'
|
||||
import { installBundledUiFonts } from './fonts'
|
||||
import { changeUiLocale } from './i18n'
|
||||
import {
|
||||
@@ -53,7 +54,9 @@ createRoot(root).render(
|
||||
<UiLocaleProvider
|
||||
initialPreference={initialUiLocalePreference}
|
||||
>
|
||||
<App />
|
||||
{new URLSearchParams(window.location.search).get('storageUpgrade') === '1'
|
||||
? <StorageUpgrade />
|
||||
: <App />}
|
||||
</UiLocaleProvider>
|
||||
</StrictMode>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user