From ab593cb3e138701aa288191b15edc4beb10e943c Mon Sep 17 00:00:00 2001 From: laofoot Date: Wed, 17 Jun 2026 14:47:21 +0800 Subject: [PATCH] fix(sessions): detach child sessions before delete to avoid FK rollback deleteSessionRows deleted messages + the session row but never handled rows referencing it via the self-referential FK parent_session_id -> sessions.id (set by the agent backend for subagent runs / branches). better-sqlite3 enables PRAGMA foreign_keys=ON by default, so deleting a parent that still has a child threw 'FOREIGN KEY constraint failed'. deleteSession/deleteSessions run inside one transaction, so the violation rolled back the entire transaction -> 0 rows deleted, with no surfaced error (the renderer only console.errors it). A select-all delete almost always contains a parent->child pair, so the whole batch deleted nothing. Detach children (UPDATE parent_session_id = NULL) before deleting, guarded by a column-existence check for older schemas. Children are preserved (not cascade-deleted) so a session the user didn't select is never removed. Closes #276 --- src/main/sessions.ts | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/main/sessions.ts b/src/main/sessions.ts index d75994395..cf69cfad5 100644 --- a/src/main/sessions.ts +++ b/src/main/sessions.ts @@ -724,9 +724,39 @@ function normalizeSessionIds(sessionIds: string[]): string[] { return normalized; } +// sessions has a self-referential FK parent_session_id -> sessions.id (set by +// the agent for subagent runs / branches). Cached after first lookup. +let sessionsHasParentColumn: boolean | null = null; +function hasParentSessionColumn(db: Database.Database): boolean { + if (sessionsHasParentColumn !== null) return sessionsHasParentColumn; + try { + sessionsHasParentColumn = + db + .prepare( + "SELECT 1 FROM pragma_table_info('sessions') WHERE name = 'parent_session_id'", + ) + .get() != null; + } catch { + sessionsHasParentColumn = false; + } + return sessionsHasParentColumn; +} + function deleteSessionRows(db: Database.Database, sessionId: string): number { deletePromptImageAttachmentsForSession(db, sessionId); deleteSessionContinuationForSession(db, sessionId); + // Unlink any child sessions first. better-sqlite3 enables + // PRAGMA foreign_keys=ON by default, so deleting a parent while a child + // still references it via parent_session_id throws "FOREIGN KEY constraint + // failed", which rolls back the whole delete transaction -> 0 rows deleted + // with no surfaced error (and a select-all batch deletes nothing, since one + // violation aborts the entire transaction). Detach children rather than + // cascade-delete so a session the user didn't select is never removed. + if (hasParentSessionColumn(db)) { + db.prepare( + "UPDATE sessions SET parent_session_id = NULL WHERE parent_session_id = ?", + ).run(sessionId); + } db.prepare("DELETE FROM messages WHERE session_id = ?").run(sessionId); const result = db.prepare("DELETE FROM sessions WHERE id = ?").run(sessionId); return result.changes;