fix: stop repeated history migration after restart
Desktop 0.13.8 still treated SQLite freelist pages left by normal chat and note writes as evidence of pending migration when a file-backed note existed. Migration detection now checks only the schema version and legacy note payloads, while the startup worker reuses its confirmed pending decision during retries so actual migrations can still finish compaction. Release note: 修复已有笔记用户正常聊天后重启仍重复显示历史迁移的问题;已完成迁移的数据继续按新格式保存,普通写入产生的空闲页不再触发迁移。
This commit is contained in:
@@ -7,10 +7,12 @@ const input = workerData as {
|
||||
}
|
||||
const cancellation = new Int32Array(input.cancellation)
|
||||
try {
|
||||
// Startup checks for legacy data before spawning this worker, including retries.
|
||||
upgradeAssistantStorage(
|
||||
input.databasePath,
|
||||
(progress) => parentPort?.postMessage({ progress }),
|
||||
() => Atomics.load(cancellation, 0) !== 0
|
||||
() => Atomics.load(cancellation, 0) !== 0,
|
||||
{ pendingUpgradeConfirmed: true }
|
||||
)
|
||||
parentPort?.postMessage({ done: true })
|
||||
} catch (error) {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import { mkdtemp, rm, stat } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { DatabaseSync } from 'node:sqlite'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { ConversationMessageBlock } from '../../shared/assistant-contracts'
|
||||
@@ -88,6 +88,58 @@ describe('subagent progress storage', () => {
|
||||
} finally { sql.close() }
|
||||
})
|
||||
|
||||
it('does not re-show migration after a note is already migrated and later chats free pages', async () => {
|
||||
const { path, database } = await fixture()
|
||||
const note = database.createMagicNote({ title: 'Legacy note', content: { version: 1, ops: [{ insert: 'note body\n' }] } })
|
||||
database.close()
|
||||
const legacy = new DatabaseSync(path)
|
||||
try {
|
||||
legacy.prepare('UPDATE magic_note_entries SET content_json = ? WHERE id = ?')
|
||||
.run(JSON.stringify(note.entries[0]!.content), note.entries[0]!.id)
|
||||
legacy.exec('PRAGMA user_version = 37')
|
||||
} finally { legacy.close() }
|
||||
expect(hasPendingAssistantStorageUpgrade(path)).toBe(true)
|
||||
upgradeAssistantStorage(path, () => undefined)
|
||||
expect(hasPendingAssistantStorageUpgrade(path)).toBe(false)
|
||||
database.initialize(dirname(path))
|
||||
expect(database.getMagicNote(note.id)).toEqual(note)
|
||||
const entry = database.createMagicNoteEntry({
|
||||
noteId: note.id, content: { version: 1, ops: [{ insert: 'New entry\n' }] }, plainText: ''
|
||||
}).entries[1]!
|
||||
database.updateMagicNoteEntry({
|
||||
entryId: entry.id, expectedRevision: entry.revision,
|
||||
content: { version: 1, ops: [{ insert: 'Edited entry\n' }] }, plainText: ''
|
||||
})
|
||||
const expectedNote = database.getMagicNote(note.id)
|
||||
const header = { id: randomUUID(), title: 'Chat after note', updatedAt: 1000 }
|
||||
const message = {
|
||||
id: randomUUID(), role: 'assistant' as const, state: 'complete' as const,
|
||||
content: 'Long draft'.repeat(100_000), createdAt: 1000
|
||||
}
|
||||
database.saveLocalConversations([{ header, messages: [message] }])
|
||||
database.saveLocalConversations([{ header, messages: [{ ...message, content: 'Final response' }] }])
|
||||
const expectedConversation = database.getConversation(header.id)
|
||||
database.close()
|
||||
const sql = new DatabaseSync(path)
|
||||
try {
|
||||
expect(sql.prepare('PRAGMA freelist_count').get()!.freelist_count).toBeGreaterThan(0)
|
||||
expect(sql.prepare("SELECT COUNT(*) AS count FROM magic_note_entries WHERE json_extract(content_json, '$.storage') = 'file'").get()!.count).toBe(2)
|
||||
const exec = vi.spyOn(DatabaseSync.prototype, 'exec')
|
||||
for (let restart = 0; restart < 3; restart++) {
|
||||
expect(hasPendingAssistantStorageUpgrade(path)).toBe(false)
|
||||
const progress = vi.fn()
|
||||
upgradeAssistantStorage(path, progress)
|
||||
expect(progress).not.toHaveBeenCalled()
|
||||
database.initialize(dirname(path))
|
||||
expect(database.getMagicNote(note.id)).toEqual(expectedNote)
|
||||
expect(database.getConversation(header.id)).toEqual(expectedConversation)
|
||||
database.close()
|
||||
}
|
||||
expect(exec.mock.calls.some(([statement]) => /VACUUM/i.test(statement))).toBe(false)
|
||||
expect(sql.prepare('PRAGMA integrity_check').get()).toEqual({ integrity_check: 'ok' })
|
||||
} finally { sql.close() }
|
||||
})
|
||||
|
||||
it('roundtrips every snapshot including corrections, reordered blocks and empty progress', () => {
|
||||
const event = subagent(randomUUID())
|
||||
const textId = randomUUID()
|
||||
|
||||
@@ -33,22 +33,13 @@ export function hasPendingAssistantStorageUpgrade(
|
||||
LIMIT 1`
|
||||
).get()
|
||||
if (!noteTable) return false
|
||||
const noteEntry = database.prepare(
|
||||
'SELECT 1 FROM magic_note_entries LIMIT 1'
|
||||
).get()
|
||||
if (!noteEntry) return false
|
||||
const hasLegacyPayload = Boolean(database.prepare(
|
||||
return Boolean(database.prepare(
|
||||
`SELECT 1
|
||||
FROM magic_note_entries
|
||||
WHERE json_extract(content_json, '$.storage') IS NULL
|
||||
OR json_extract(content_json, '$.storage') <> 'file'
|
||||
LIMIT 1`
|
||||
).get())
|
||||
if (hasLegacyPayload) return true
|
||||
const free = database.prepare('PRAGMA freelist_count').get() as {
|
||||
freelist_count: number
|
||||
}
|
||||
return free.freelist_count > 0
|
||||
} finally {
|
||||
database.close()
|
||||
}
|
||||
@@ -57,9 +48,10 @@ export function hasPendingAssistantStorageUpgrade(
|
||||
export function upgradeAssistantStorage(
|
||||
databasePath: string,
|
||||
onProgress: (progress: AssistantStorageProgress) => void,
|
||||
isCancelled: () => boolean = () => false
|
||||
isCancelled: () => boolean = () => false,
|
||||
options: { pendingUpgradeConfirmed?: boolean } = {}
|
||||
): void {
|
||||
if (!hasPendingAssistantStorageUpgrade(databasePath)) return
|
||||
if (!options.pendingUpgradeConfirmed && !hasPendingAssistantStorageUpgrade(databasePath)) return
|
||||
upgradeSubagentStorage(databasePath, onProgress, isCancelled)
|
||||
new AssistantDatabase(databasePath).upgradeMagicNoteStorage(onProgress, isCancelled)
|
||||
}
|
||||
|
||||
@@ -163,7 +163,9 @@ describe('Magic note SQLite and filesystem storage', () => {
|
||||
expect(sql.prepare("SELECT json_extract(content_json, '$.storage') AS storage FROM magic_note_entries").get()!.storage).toBe('file')
|
||||
expect(sql.prepare('PRAGMA freelist_count').get()!.freelist_count).toBeGreaterThan(0)
|
||||
const stages: string[] = []
|
||||
upgradeAssistantStorage(path, (progress) => stages.push(progress.stage))
|
||||
upgradeAssistantStorage(path, (progress) => stages.push(progress.stage), () => false, {
|
||||
pendingUpgradeConfirmed: true
|
||||
})
|
||||
expect(stages).toContain('compacting')
|
||||
expect(sql.prepare('PRAGMA freelist_count').get()!.freelist_count).toBe(0)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user