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: 修复长任务和子任务造成的数据库异常膨胀;升级时自动整理旧记录并回收空间,保留聊天、执行详情和结果,并支持退出后继续处理。
103 lines
3.6 KiB
TypeScript
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()
|
|
}
|
|
}
|