Files
goodbuddy/src/main/assistant/assistant-storage-upgrade.ts
T
mesalogo 49377206cc
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
fix: stop subagent history from exhausting disk space
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: 修复长任务和子任务造成的数据库异常膨胀;升级时自动整理旧记录并回收空间,保留聊天、执行详情和结果,并支持退出后继续处理。
2026-09-13 20:50:33 +08:00

103 lines
3.6 KiB
TypeScript

import { statSync } from 'node:fs'
import { DatabaseSync } from 'node:sqlite'
import type { ConversationMessageBlock } from '../../shared/assistant-contracts'
import type { AssistantStorageProgress } from '../../shared/assistant-storage-contracts'
import {
compactSubagentPayload,
restoreSubagentPayload
} from './subagent-progress-storage'
export function upgradeAssistantStorage(
databasePath: string,
onProgress: (progress: AssistantStorageProgress) => void,
isCancelled: () => boolean = () => false
): void {
const database = new DatabaseSync(databasePath, { timeout: 5_000 })
const checkCancelled = (): void => {
if (isCancelled()) throw new DOMException('Upgrade cancelled', 'AbortError')
}
const bytesBefore = statSync(databasePath).size
try {
const version = database.prepare('PRAGMA user_version').get() as {
user_version: number
}
if (version.user_version < 1 || version.user_version >= 34) return
checkCancelled()
const progress: AssistantStorageProgress = {
stage: 'scanning', processed: 0, total: 0, bytesBefore
}
onProgress({ ...progress })
database.exec(`
PRAGMA journal_mode = WAL;
PRAGMA busy_timeout = 5000;
PRAGMA secure_delete = OFF;
PRAGMA cache_size = -65536;
CREATE INDEX IF NOT EXISTS task_events_subagent_idx
ON task_events(task_id, id) WHERE kind = 'subagent';
`)
const tasks = database.prepare(
`SELECT task_id, COUNT(*) AS count FROM task_events
WHERE kind = 'subagent' GROUP BY task_id`
).all() as Array<{ task_id: string; count: number }>
progress.total = tasks.reduce((sum, task) => sum + task.count, 0)
progress.stage = 'converting'
const select = database.prepare(
`SELECT id, payload_json FROM task_events
WHERE task_id = ? AND kind = 'subagent' AND id > ?
ORDER BY id LIMIT 32`
)
const update = database.prepare(
'UPDATE task_events SET payload_json = ? WHERE id = ?'
)
for (const task of tasks) {
const blocks = new Map<string, ConversationMessageBlock[]>()
let afterId = 0
for (;;) {
checkCancelled()
const rows = select.all(task.task_id, afterId) as Array<{
id: number; payload_json: string
}>
if (!rows.length) break
database.exec('BEGIN IMMEDIATE')
try {
for (const row of rows) {
const payload: unknown = JSON.parse(row.payload_json)
const compact = compactSubagentPayload(payload, blocks)
// Replay mixed old/new rows on retry. Never delete events or provenance.
restoreSubagentPayload(payload, blocks)
if (compact !== payload) {
update.run(JSON.stringify(compact), row.id)
}
afterId = row.id
}
database.exec('COMMIT')
} catch (error) {
database.exec('ROLLBACK')
throw error
}
progress.processed += rows.length
onProgress({ ...progress })
}
database.exec('PRAGMA wal_checkpoint(TRUNCATE)')
}
checkCancelled()
const free = database.prepare('PRAGMA freelist_count').get() as {
freelist_count: number
}
if (free.freelist_count > 0) {
progress.stage = 'compacting'
onProgress({ ...progress })
// Rebuild the now-small live database, not a second copy of the bloated input.
database.exec('PRAGMA wal_checkpoint(TRUNCATE); VACUUM;')
database.exec('PRAGMA wal_checkpoint(TRUNCATE)')
}
checkCancelled()
onProgress({
...progress, stage: 'complete',
bytesAfter: statSync(databasePath).size
})
} finally {
database.close()
}
}