// @vitest-environment node import { randomUUID } from 'node:crypto' import { mkdtemp, rm, stat } from 'node:fs/promises' import { tmpdir } from 'node:os' 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' import type { SubagentEvent } from '../../shared/contracts' import { AssistantDatabase, ASSISTANT_DATABASE_SCHEMA_VERSION } from './assistant-database' import { getPendingAssistantStorageUpgrade, hasPendingAssistantStorageUpgrade, upgradeAssistantStorage } from './assistant-storage-upgrade' import { compactSubagentPayload, restoreSubagentPayload, SubagentProgressStorage } from './subagent-progress-storage' const directories: string[] = [] const databases: AssistantDatabase[] = [] const historicalModeColumns = ` ALTER TABLE projects ADD COLUMN default_work_mode TEXT NOT NULL DEFAULT 'ask'; ALTER TABLE conversations ADD COLUMN work_mode TEXT NOT NULL DEFAULT 'ask'; ALTER TABLE tasks ADD COLUMN work_mode TEXT NOT NULL DEFAULT 'execute'; ` afterEach(async () => { vi.restoreAllMocks() for (const database of databases.splice(0)) database.close() await Promise.all(directories.splice(0).map( (path) => rm(path, { recursive: true, force: true }) )) }) async function fixture(): Promise<{ path: string; database: AssistantDatabase; taskId: string }> { const directory = await mkdtemp(join(tmpdir(), 'goodbuddy-storage-upgrade-')) directories.push(directory) const path = join(directory, 'assistant.sqlite') const database = new AssistantDatabase(path) databases.push(database) database.initialize(directory) const taskId = randomUUID() database.createTask({ id: taskId, title: 'Storage regression', instructions: 'Synthetic fixture', }) return { path, database, taskId } } function subagent(requestId: string): SubagentEvent { return { requestId, type: 'subagent', childTaskId: randomUUID(), expertId: randomUUID(), expertName: 'general', routingMode: 'native', runtimeCallId: 'test-call', state: 'running', progress: [] } } describe('subagent progress storage', () => { it.each(['ask', 'execute', 'plan', undefined])('removes schema 59 mode metadata (%s) without changing content or event identity', async (workMode) => { const { path, database, taskId } = await fixture() const schedule = database.createSchedule({ title: 'ask / execute', prompt: '/ask keep this text', recurrence: 'daily', nextRunAt: '2030-01-01T00:00:00Z' }) const header = { id: schedule.conversationId, title: 'ask / execute', updatedAt: 1000 } const event = { ...subagent(taskId), state: 'completed' as const, output: 'workMode: ask', progress: [{ id: randomUUID(), type: 'text' as const, content: '/execute' }] } const { requestId: _requestId, type: _type, ...activity } = event void _requestId void _type const message = { id: randomUUID(), role: 'assistant' as const, state: 'complete' as const, content: '/ask preserve user content', createdAt: 1000, subagents: [activity] } database.saveLocalConversations([{ header, messages: [message] }]) database.close() const sql = new DatabaseSync(path) const nested = { workMode: 'user-value', toolApproval: 'user-policy', text: 'ask execute plan' } const context = { workMode, knowledgeLibraryIds: [], storyGraphEnabled: true, custom: nested } const template = { workMode, title: schedule.title, prompt: schedule.prompt, custom: nested } const queuePayloads = [ { workMode, prompt: '/execute keep', custom: nested }, { input: { workMode, prompt: '/ask keep', custom: nested }, serializedContexts: [nested] } ] let before: ReturnType['all']> try { sql.exec(`${historicalModeColumns} PRAGMA user_version = 59`) sql.prepare('UPDATE projects SET default_work_mode = ?').run(workMode ?? 'ask') sql.prepare('UPDATE conversations SET work_mode = ?, context_state_json = ? WHERE id = ?') .run(workMode ?? 'ask', JSON.stringify(context), header.id) sql.prepare('UPDATE tasks SET work_mode = ?').run(workMode ?? 'ask') sql.prepare('UPDATE schedules SET task_template_json = ? WHERE id = ?').run(JSON.stringify(template), schedule.id) const metadata = JSON.parse(sql.prepare('SELECT metadata_json FROM messages WHERE id = ?').get(message.id)!.metadata_json as string) metadata.subagents[0].workMode = workMode metadata.custom = nested sql.prepare('UPDATE messages SET metadata_json = ? WHERE id = ?').run(JSON.stringify(metadata), message.id) queuePayloads.forEach((payload, index) => sql.prepare(`INSERT INTO conversation_queue_items (id, conversation_id, source, label, payload_json, status, created_at) VALUES (?, ?, 'user', ?, ?, 'pending', ?)`) .run(randomUUID(), header.id, `queued ${index}`, JSON.stringify(payload), `2026-01-0${index + 1}`)) sql.prepare(`INSERT INTO task_events (task_id, kind, payload_json, created_at, remote_binding_id, remote_operation_id, remote_semantic_sequence, remote_event_index) VALUES (?, 'subagent', ?, '2026-01-01', 'binding', 'operation', '7', 0)`) .run(taskId, JSON.stringify({ ...event, workMode, custom: nested })) before = sql.prepare('SELECT * FROM task_events ORDER BY id').all() } finally { sql.close() } expect(getPendingAssistantStorageUpgrade(path)).toEqual({ migrateNotes: false, reclaimSpace: false }) upgradeAssistantStorage(path, () => undefined) const check = new DatabaseSync(path) try { for (const [table, column] of [['projects', 'default_work_mode'], ['conversations', 'work_mode'], ['tasks', 'work_mode']]) { expect(check.prepare(`PRAGMA table_info(${table})`).all().map(row => row.name)).not.toContain(column) } expect(check.prepare('PRAGMA user_version').get()!.user_version).toBe(ASSISTANT_DATABASE_SCHEMA_VERSION) const readJson = (table: string, column: string, id: string) => JSON.parse(check.prepare(`SELECT ${column} AS value FROM ${table} WHERE id = ?`).get(id)!.value as string) expect(readJson('conversations', 'context_state_json', header.id)) .toEqual({ knowledgeLibraryIds: [], storyGraphEnabled: true, custom: nested }) expect(readJson('schedules', 'task_template_json', schedule.id)) .toEqual({ title: schedule.title, prompt: schedule.prompt, custom: nested }) expect(readJson('messages', 'metadata_json', message.id)).toMatchObject({ subagents: [activity], custom: nested }) expect(readJson('messages', 'metadata_json', message.id).subagents[0]).not.toHaveProperty('workMode') const queues = check.prepare('SELECT payload_json FROM conversation_queue_items ORDER BY created_at').all() expect(queues.map(row => JSON.parse(row.payload_json as string))).toEqual([ { prompt: '/execute keep', custom: nested }, { input: { prompt: '/ask keep', custom: nested }, serializedContexts: [nested] } ]) expect(check.prepare('SELECT * FROM task_events ORDER BY id').all()).toEqual(before.map(row => { const { workMode: obsolete, ...payload } = JSON.parse(row.payload_json as string) void obsolete return { ...row, payload_json: JSON.stringify(payload) } })) expect(check.prepare('PRAGMA integrity_check').get()!.integrity_check).toBe('ok') expect(check.prepare('PRAGMA foreign_key_check').all()).toEqual([]) } finally { check.close() } database.initialize(dirname(path)) expect(database.getConversation(header.id).messages[0]).toMatchObject(message) expect(database.listSchedules().find(candidate => candidate.id === schedule.id)).toMatchObject({ nextRunAt: schedule.nextRunAt }) expect(database.appendRemoteTaskEventOnce({ taskId, bindingId: 'binding', operationId: 'operation', semanticSequence: '7', eventIndex: 0, kind: 'subagent', payload: { ...event, workMode, custom: nested } })).toBe(false) database.close() expect(hasPendingAssistantStorageUpgrade(path)).toBe(false) }) it('resumes schema 60 batches after cancellation and rolls back malformed JSON without losing its source', async () => { const { path, database, taskId } = await fixture() database.close() const sql = new DatabaseSync(path) try { sql.exec(`${historicalModeColumns} PRAGMA user_version = 59`) const insert = sql.prepare("INSERT INTO task_events (task_id, kind, payload_json, created_at) VALUES (?, 'status', ?, '2026-01-01')") for (let index = 0; index < 70; index++) insert.run(taskId, JSON.stringify({ workMode: 'plan', index })) const damagedId = Number(insert.run(taskId, '{broken').lastInsertRowid) let cancelled = false expect(() => upgradeAssistantStorage(path, progress => { if (progress.processed >= 32) cancelled = true }, () => cancelled)).toThrow('cancelled') expect(sql.prepare('PRAGMA user_version').get()!.user_version).toBe(59) expect(sql.prepare("SELECT COUNT(*) AS count FROM task_events WHERE instr(payload_json, 'workMode') > 0").get()!.count).toBeLessThan(70) expect(() => upgradeAssistantStorage(path, () => undefined)).toThrow(/Invalid execution metadata: task_events/) expect(sql.prepare('SELECT payload_json FROM task_events WHERE id = ?').get(damagedId)!.payload_json).toBe('{broken') // The uncommitted final batch remains intact, while earlier batches survive. expect(sql.prepare("SELECT COUNT(*) AS count FROM task_events WHERE instr(payload_json, 'workMode') > 0").get()!.count).toBeGreaterThan(0) expect(sql.prepare('PRAGMA user_version').get()!.user_version).toBe(59) sql.prepare('UPDATE task_events SET payload_json = ? WHERE id = ?').run('{"workMode":"ask"}', damagedId) upgradeAssistantStorage(path, () => undefined) expect(sql.prepare('SELECT payload_json FROM task_events WHERE id = ?').get(damagedId)!.payload_json).toBe('{}') expect(sql.prepare('PRAGMA user_version').get()!.user_version).toBe(ASSISTANT_DATABASE_SCHEMA_VERSION) expect(sql.prepare('PRAGMA integrity_check').get()!.integrity_check).toBe('ok') } finally { sql.close() } }) it.each([45, 46, 47])('upgrades schema %i without reconverting history or vacuuming ordinary free pages', async (sourceVersion) => { const { path, database, taskId } = await fixture() const event = subagent(randomUUID()) event.progress = [{ id: randomUUID(), type: 'text', content: 'Keep current-format progress' }] database.appendTaskEvent(taskId, 'subagent', event) const note = database.getMagicNote(database.createMagicNote({ title: 'Already migrated', content: { version: 1, ops: [{ insert: 'Keep note\n' }] } }).id) const header = { id: randomUUID(), title: 'Keep chat', updatedAt: 1000 } const message = { id: randomUUID(), role: 'assistant' as const, state: 'complete' as const, content: 'Normal chat update'.repeat(100_000), createdAt: 1000 } database.saveLocalConversations([{ header, messages: [message] }]) database.saveLocalConversations([{ header, messages: [{ ...message, content: 'Final response' }] }]) const expected = database.getConversation(header.id) database.close() const sql = new DatabaseSync(path) try { if (sourceVersion < 47) sql.exec(`DROP VIEW supervision_review_current; DROP TABLE supervision_review_navigation; DROP TABLE supervision_review_batches; DROP TABLE supervision_review_sources; DROP TABLE supervision_review_runs;`) if (sourceVersion === 45) sql.exec(` DROP TRIGGER messages_review_insert; DROP TRIGGER messages_review_update; DROP TRIGGER messages_review_delete; DROP TRIGGER tasks_review_delete; DROP TABLE review_checkpoints; ALTER TABLE messages DROP COLUMN review_revision;`) sql.exec(`ALTER TABLE magic_note_entries DROP COLUMN source_json; ${historicalModeColumns} PRAGMA user_version = ${sourceVersion}; PRAGMA wal_checkpoint(TRUNCATE)`) expect(sql.prepare('PRAGMA freelist_count').get()!.freelist_count).toBeGreaterThan(0) const events = sql.prepare('SELECT * FROM task_events WHERE task_id = ? ORDER BY id').all(taskId) expect(events.length).toBeGreaterThan(0) expect(hasPendingAssistantStorageUpgrade(path)).toBe(true) const confirmedUpgrade = getPendingAssistantStorageUpgrade(path)! expect(confirmedUpgrade).toEqual({ migrateNotes: false, reclaimSpace: false }) const exec = vi.spyOn(DatabaseSync.prototype, 'exec') const progress = vi.fn() upgradeAssistantStorage(path, progress) expect(exec.mock.calls.some(([statement]) => /VACUUM/i.test(statement))).toBe(false) expect(progress.mock.calls.map(([value]) => value.stage)).not.toContain('compacting') // Schema 60 checks bounded metadata batches but does not compact history. expect(progress.mock.calls.map(([value]) => value.stage)).toContain('converting') expect(sql.prepare('PRAGMA user_version').get()!.user_version).toBe(ASSISTANT_DATABASE_SCHEMA_VERSION) expect(sql.prepare('SELECT * FROM task_events WHERE task_id = ? ORDER BY id').all(taskId)).toEqual(events) // A retry in the same startup must not gain reclamation just because free pages remain. upgradeAssistantStorage(path, progress, () => false, { confirmedUpgrade }) expect(exec.mock.calls.some(([statement]) => /VACUUM/i.test(statement))).toBe(false) database.initialize(dirname(path)) expect(database.getConversation(header.id)).toEqual(expected) expect(database.getMagicNote(note.id)).toEqual(note) database.close() progress.mockClear() upgradeAssistantStorage(path, progress) expect(progress).not.toHaveBeenCalled() expect(hasPendingAssistantStorageUpgrade(path)).toBe(false) expect(sql.prepare('PRAGMA integrity_check').get()).toEqual({ integrity_check: 'ok' }) } finally { sql.close() } }) it('does not migrate or vacuum ordinary chat free pages on repeated startup', async () => { const { path, database } = await fixture() const header = { id: randomUUID(), title: 'Chat restart', 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 expected = database.getConversation(header.id) database.close() const sql = new DatabaseSync(path) try { const free = sql.prepare('PRAGMA freelist_count').get()!.freelist_count expect(free).toBeGreaterThan(0) expect(sql.prepare('PRAGMA user_version').get()!.user_version).toBe(ASSISTANT_DATABASE_SCHEMA_VERSION) expect(hasPendingAssistantStorageUpgrade(path)).toBe(false) const exec = vi.spyOn(DatabaseSync.prototype, 'exec') for (let restart = 0; restart < 2; restart++) { const progress = vi.fn() upgradeAssistantStorage(path, progress) expect(progress).not.toHaveBeenCalled() expect(exec.mock.calls.some(([statement]) => /VACUUM/i.test(statement))).toBe(false) const reopened = new AssistantDatabase(path) try { reopened.initialize(tmpdir()) expect(reopened.getConversation(header.id)).toEqual(expected) } finally { reopened.close() } } expect(sql.prepare('PRAGMA freelist_count').get()!.freelist_count).toBe(free) expect(sql.prepare('PRAGMA integrity_check').get()).toEqual({ integrity_check: 'ok' }) } 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.getMagicNote(database.createMagicNote({ title: 'Legacy note', content: { version: 1, ops: [{ insert: 'note body\n' }] } }).id) 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(`ALTER TABLE supervision_results DROP COLUMN graph_snapshot_json; DROP TABLE activity_history_records; ALTER TABLE activity_history RENAME COLUMN record_order_json TO records_json; DROP TRIGGER messages_review_insert; DROP TRIGGER messages_review_update; DROP TRIGGER messages_review_delete; DROP TRIGGER tasks_review_delete; DROP VIEW IF EXISTS supervision_review_current; DROP TABLE IF EXISTS supervision_review_navigation; DROP TABLE IF EXISTS supervision_review_batches; DROP TABLE IF EXISTS supervision_review_sources; DROP TABLE IF EXISTS supervision_review_runs; DROP TABLE review_checkpoints; ALTER TABLE messages DROP COLUMN review_revision; ALTER TABLE magic_note_entries DROP COLUMN source_json; ${historicalModeColumns} 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() const toolId = randomUUID() const text: ConversationMessageBlock = { id: textId, type: 'text', content: 'seed' } const tool: ConversationMessageBlock = { id: toolId, type: 'tool', tool: { name: 'read', state: 'completed', summary: 'Read', output: 'seed' } } const encoder = new Map() const decoder = new Map() for (const progress of [ [], [text], [text, tool], [{ ...text, content: 'seed tail' }, tool], [{ ...text, content: 'corrected' }, tool], [tool, text], [], [text] ]) { const full = { ...event, progress } const compact = compactSubagentPayload(full, encoder) restoreSubagentPayload(full, encoder) expect(restoreSubagentPayload(compact, decoder)).toEqual(full) } }) it('stores small changes without repeating large tool outputs, including across reopen', async () => { const fixtureData = await fixture() let database = fixtureData.database const event = subagent(fixtureData.taskId) const tool: ConversationMessageBlock = { id: randomUUID(), type: 'tool', tool: { name: 'read', summary: 'Read', state: 'completed', input: undefined, output: 'x'.repeat(200_000), error: undefined } } const textId = randomUUID() try { for (let index = 0; index < 200; index++) { event.progress = [tool, { id: textId, type: 'text', content: `seed${'中'.repeat(index)}` }] database.appendTaskEvent(fixtureData.taskId, 'subagent', event) if (index === 99) { database.close() database = new AssistantDatabase(fixtureData.path) database.initialize(tmpdir()) } } database.close() const inspection = new DatabaseSync(fixtureData.path) try { const rows = inspection.prepare( `SELECT payload_json FROM task_events WHERE kind = 'subagent' ORDER BY id` ).all() as Array<{ payload_json: string }> expect(rows).toHaveLength(200) expect(rows.reduce((sum, row) => sum + row.payload_json.length, 0)).toBeLessThan(400_000) const state = new Map() let restored: unknown for (const row of rows) restored = restoreSubagentPayload(JSON.parse(row.payload_json), state) expect(restored).toStrictEqual(JSON.parse(JSON.stringify(event))) } finally { inspection.close() } } finally { database.close() } }) it('ignores omitted optional tool fields but preserves every actual field change', () => { const event = subagent(randomUUID()) const block: ConversationMessageBlock = { id: randomUUID(), type: 'tool', tool: { callId: 'read-1', name: 'read', summary: 'Read', state: 'completed', input: undefined, output: 'unchanged', error: undefined } } const state = new Map([ [event.childTaskId, JSON.parse(JSON.stringify([block]))] ]) expect(compactSubagentPayload({ ...event, progress: [block] }, state)) .toMatchObject({ progressUpdates: [] }) for (const change of [ { callId: 'read-2' }, { name: 'write' }, { summary: 'Updated' }, { state: 'failed' as const }, { input: '' }, { output: 'changed' }, { error: 'failed' }, { output: undefined } ]) { const updated = { ...block, tool: { ...block.tool, ...change } } const compact = compactSubagentPayload({ ...event, progress: [updated] }, state) expect(compact).toMatchObject({ progressUpdates: [{ type: 'upsert', block: updated }] }) } }) it('releases terminal task caches without clearing peers, and rebuilds after rollback', () => { const database = new DatabaseSync(':memory:') database.exec(`CREATE TABLE task_events( id INTEGER PRIMARY KEY, task_id TEXT, kind TEXT, payload_json TEXT )`) const storage = new SubagentProgressStorage(database) const caches = (storage as unknown as { tasks: Map }).tasks const tool: ConversationMessageBlock = { id: randomUUID(), type: 'tool', tool: { name: 'read', state: 'completed', summary: 'Read', output: 'x'.repeat(200_000), error: undefined } } let id = 0 const append = (taskId: string, kind: string, event: unknown) => { const payload = storage.serialize(taskId, kind, event) database.prepare('INSERT INTO task_events VALUES (?, ?, ?, ?)') .run(++id, taskId, kind, payload) storage.inserted(taskId, kind, id, payload) return JSON.parse(payload) as { progressUpdates: unknown[] } } try { const peer = subagent(randomUUID()) append(peer.requestId, 'subagent', { ...peer, progress: [tool] }) for (const [kind, payload] of [ ['done', {}], ['error', {}], ...['completed', 'failed', 'cancelled', 'interrupted'] .map((status) => ['status', { status }]) ] as Array<[string, unknown]>) { const event = { ...subagent(randomUUID()), progress: [tool] } append(event.requestId, 'subagent', event) expect(caches.size).toBe(2) database.exec('BEGIN') append(event.requestId, kind, payload) expect(caches.has(event.requestId)).toBe(false) expect(caches.has(peer.requestId)).toBe(true) database.exec('ROLLBACK') expect(append(event.requestId, 'subagent', event).progressUpdates).toEqual([]) append(event.requestId, kind, payload) expect(caches.size).toBe(1) } for (let index = 0; index < 40; index++) { const event = subagent(randomUUID()) append(event.requestId, 'subagent', { ...event, progress: [tool] }) expect(caches.size).toBeLessThanOrEqual(16) } } finally { database.close() } }) it('keeps remote replay deduplication and conflicting payload checks after compaction', async () => { const { database, taskId, path } = await fixture() const event = subagent(taskId) const textId = randomUUID() const tool: ConversationMessageBlock = { id: randomUUID(), type: 'tool', tool: { name: 'read', summary: 'Read', state: 'completed', input: undefined, output: 'x'.repeat(20_000), error: undefined } } const provenance = { taskId, bindingId: 'binding', operationId: 'operation', eventIndex: 0, kind: 'subagent' } const first = { ...event, progress: [tool, { id: textId, type: 'text' as const, content: 'seed' }] } const second = { ...event, progress: [tool, { id: textId, type: 'text' as const, content: 'seed tail' }] } try { expect(database.appendRemoteTaskEventOnce({ ...provenance, semanticSequence: '1', payload: first })).toBe(true) expect(database.appendRemoteTaskEventOnce({ ...provenance, semanticSequence: '2', payload: second })).toBe(true) expect(database.appendRemoteTaskEventOnce({ ...provenance, semanticSequence: '1', payload: first })).toBe(false) expect(() => database.appendRemoteTaskEventOnce({ ...provenance, semanticSequence: '1', payload: second })).toThrow('conflicts') database.appendTaskEvent(taskId, 'done', {}) const caches = (database as unknown as { subagentProgress: { tasks: Map } }).subagentProgress.tasks expect(caches.size).toBe(0) expect(database.appendRemoteTaskEventOnce({ ...provenance, semanticSequence: '2', payload: second })).toBe(false) expect(caches.size).toBe(0) } finally { database.close() } const reopened = new AssistantDatabase(path) reopened.initialize(tmpdir()) try { expect(reopened.appendRemoteTaskEventOnce({ ...provenance, semanticSequence: '2', payload: second })).toBe(false) expect(reopened.getHighestCommittedRemoteTaskEventSequence('binding', 'operation')).toBe('2') } finally { reopened.close() } }) it('rebuilds progress after a remote batch rolls back', async () => { const { database, taskId } = await fixture() const event = subagent(taskId) const textId = randomUUID() const base = { taskId, bindingId: 'binding', operationId: 'operation', eventIndex: 0, kind: 'subagent' } const first = { ...event, progress: [{ id: textId, type: 'text' as const, content: 'seed' }] } try { database.appendRemoteTaskEventOnce({ ...base, semanticSequence: '2', payload: first }) expect(() => database.appendRemoteTaskEventsBatch([ { ...base, semanticSequence: '1', payload: { ...first, progress: [] } }, { ...base, semanticSequence: '2', payload: { ...first, output: 'conflict' } } ])).toThrow('conflicts') expect(database.appendRemoteTaskEventOnce({ ...base, semanticSequence: '3', payload: first })).toBe(true) expect(database.appendRemoteTaskEventOnce({ ...base, semanticSequence: '3', payload: first })).toBe(false) } finally { database.close() } }) it('keeps full remote conversation progress atomic with compact events', async () => { const { database, path } = await fixture() const hostId = randomUUID() const project = database.createSshProject({ project: { name: 'Remote regression', description: '', rootPath: '/tmp/goodbuddy-storage-test', runtimeSelection: { provider: 'opencode' } }, executionSpace: { kind: 'ssh', hostId, remoteRootPath: '/tmp/goodbuddy-storage-test' }, assertCurrent: () => undefined }) const conversationId = randomUUID() const userMessageId = randomUUID() const assistantMessageId = randomUUID() database.saveLocalConversations([{ header: { id: conversationId, projectId: project.id, title: 'Atomic regression', updatedAt: Date.now() }, messages: [] }]) const remoteTaskId = randomUUID() database.createTask({ id: remoteTaskId, projectId: project.id, conversationId, title: 'Atomic remote', instructions: 'Synthetic', remoteRecovery: { recoverable: true, currentUserMessageId: userMessageId, currentAssistantMessageId: assistantMessageId } }) const event = subagent(remoteTaskId) const textId = randomUUID() const first = { ...event, progress: [{ id: textId, type: 'text' as const, content: 'seed' }] } const second = { ...event, state: 'completed' as const, output: 'Complete', progress: [ { id: textId, type: 'text' as const, content: 'seed tail' } ] } const owner = { taskId: remoteTaskId, conversationId, assistantMessageId, bindingId: 'atomic-binding', operationId: 'atomic-operation', eventIndex: 0 } const inspection = new DatabaseSync(path) try { database.appendRemoteConversationTaskEventOnce({ ...owner, semanticSequence: '1', event: first }) inspection.exec(`CREATE TRIGGER reject_message BEFORE UPDATE ON messages BEGIN SELECT RAISE(ABORT, 'fixture message write failed'); END;`) expect(() => database.appendRemoteConversationTaskEventOnce({ ...owner, semanticSequence: '2', event: second })).toThrow('fixture message write failed') expect(database.getHighestCommittedRemoteTaskEventSequence('atomic-binding', 'atomic-operation')).toBe('1') inspection.exec('DROP TRIGGER reject_message') expect(database.appendRemoteConversationTaskEventOnce({ ...owner, semanticSequence: '2', event: second })).toBe(true) expect(database.appendRemoteConversationTaskEventOnce({ ...owner, semanticSequence: '2', event: second })).toBe(false) const message = database.getConversation(conversationId).messages.find((row) => row.id === assistantMessageId) expect(message?.subagents?.[0]).toMatchObject({ state: 'completed', progress: second.progress, output: 'Complete' }) expect(inspection.prepare( `SELECT COUNT(*) AS count FROM task_events WHERE task_id = ? AND kind = 'subagent'` ).get(remoteTaskId)).toEqual({ count: 2 }) } finally { inspection.close(); database.close() } }) it.each([33, 34])('converts schema %i rows, resumes and shrinks without losing provenance', async (sourceVersion) => { const { database, taskId, path } = await fixture() database.close() const event = subagent(taskId) const tool: ConversationMessageBlock = { id: randomUUID(), type: 'tool', tool: { name: 'read', summary: 'Read', state: 'completed', output: 'x'.repeat(300_000) } } const textId = randomUUID() const legacy = new DatabaseSync(path) const expected: SubagentEvent[] = [] try { legacy.exec(`ALTER TABLE conversations DROP COLUMN pinned; ALTER TABLE supervision_results DROP COLUMN graph_snapshot_json; DROP TABLE activity_history_records; ALTER TABLE activity_history RENAME COLUMN record_order_json TO records_json; DROP TRIGGER messages_review_insert; DROP TRIGGER messages_review_update; DROP TRIGGER messages_review_delete; DROP TRIGGER tasks_review_delete; DROP VIEW IF EXISTS supervision_review_current; DROP TABLE IF EXISTS supervision_review_navigation; DROP TABLE IF EXISTS supervision_review_batches; DROP TABLE IF EXISTS supervision_review_sources; DROP TABLE IF EXISTS supervision_review_runs; DROP TABLE review_checkpoints; ALTER TABLE messages DROP COLUMN review_revision; ALTER TABLE magic_note_entries DROP COLUMN source_json; ${historicalModeColumns} PRAGMA user_version = ${sourceVersion}; BEGIN`) const insert = legacy.prepare( `INSERT INTO task_events( task_id, kind, payload_json, created_at, remote_binding_id, remote_operation_id, remote_semantic_sequence, remote_event_index ) VALUES (?, 'subagent', ?, ?, 'binding', 'operation', ?, 0)` ) for (let index = 0; index < 150; index++) { const full = { ...event, progress: [tool, { id: textId, type: 'text' as const, content: `seed${'中'.repeat(index)}` }] } expected.push(full) const { progress, ...metadata } = full const stored = sourceVersion === 34 ? { ...metadata, progressUpdates: progress.map((block) => ({ type: 'upsert', block })) } : full insert.run(taskId, JSON.stringify(stored), '2026-09-13T00:00:00Z', String(index + 1)) } legacy.exec(`COMMIT; CREATE TRIGGER reject_unchanged_payload BEFORE UPDATE OF payload_json ON task_events WHEN OLD.payload_json = NEW.payload_json BEGIN SELECT RAISE(ABORT, 'unchanged payload rewritten'); END;`) } finally { legacy.close() } const before = (await stat(path)).size expect(before).toBeGreaterThan(40_000_000) let cancelled = false expect(() => upgradeAssistantStorage(path, (progress) => { if (progress.processed >= 32) cancelled = true }, () => cancelled)).toThrow('cancelled') upgradeAssistantStorage(path, () => undefined) const after = (await stat(path)).size expect(after).toBeLessThan(before / 20) const inspected = new DatabaseSync(path) try { expect(inspected.prepare('PRAGMA integrity_check').get()).toEqual({ integrity_check: 'ok' }) const rows = inspected.prepare( `SELECT id, task_id, payload_json, created_at, remote_binding_id, remote_operation_id, remote_semantic_sequence, remote_event_index FROM task_events WHERE kind = 'subagent' ORDER BY id` ).all() as Array<{ id: number; task_id: string; payload_json: string; created_at: string remote_binding_id: string; remote_operation_id: string remote_semantic_sequence: string; remote_event_index: number }> expect(rows).toHaveLength(expected.length) const blocks = new Map() rows.forEach((row, index) => { expect(restoreSubagentPayload(JSON.parse(row.payload_json), blocks)).toEqual(expected[index]) expect(row.created_at).toBe('2026-09-13T00:00:00Z') expect(row.task_id).toBe(taskId) expect(row.remote_binding_id).toBe('binding') expect(row.remote_operation_id).toBe('operation') expect(row.remote_semantic_sequence).toBe(String(index + 1)) expect(row.remote_event_index).toBe(0) if (index > 0) expect(row.id).toBe(rows[index - 1]!.id + 1) }) } finally { inspected.close() } const reopened = new AssistantDatabase(path) reopened.initialize(tmpdir()) reopened.close() const messages: unknown[] = [] upgradeAssistantStorage(path, (value) => messages.push(value)) expect(messages).toEqual([]) const latest = new DatabaseSync(path) try { expect(latest.prepare('PRAGMA user_version').get()).toEqual({ user_version: ASSISTANT_DATABASE_SCHEMA_VERSION }) } finally { latest.close() } }, 30_000) it('recompacts mixed full and delta events without changing corrections, resets or child state', () => { const first = subagent(randomUUID()) const second = { ...subagent(first.requestId), state: 'completed' as const, output: 'Done' } const textId = randomUUID() const tool: ConversationMessageBlock = { id: randomUUID(), type: 'tool', tool: { name: 'read', state: 'completed', summary: 'Read', output: 'payload' } } const text: ConversationMessageBlock = { id: textId, type: 'text', content: 'seed' } const stored = [ { ...first, progress: [tool, text] }, { ...second, progressUpdates: [{ type: 'upsert', block: text }] }, { ...first, progress: undefined, progressUpdates: [ { type: 'upsert', block: tool }, { type: 'append', id: textId, blockType: 'text', delta: ' tail' } ] }, { ...first, progress: undefined, progressUpdates: [ { type: 'upsert', block: { ...text, content: 'corrected' } } ] }, { ...first, progress: undefined, progressUpdates: [ { type: 'reset', blocks: [text, tool] } ] }, { ...second, progressUpdates: [] }, { ...first, progress: [] }, { ...first, progress: [text] } ] const oldState = new Map() const newState = new Map() for (const input of stored) { const event = JSON.parse(JSON.stringify(input)) const compact = compactSubagentPayload(event, oldState) const expected = restoreSubagentPayload(event, oldState) expect(restoreSubagentPayload(compact, newState)).toStrictEqual(expected) } }) })