From 50676416edc73a525ab4b331a9c93219f385f5d3 Mon Sep 17 00:00:00 2001 From: Alexis Dumas Date: Mon, 24 Nov 2025 11:46:02 -0500 Subject: [PATCH] blank session cleanup improvements - make the blank session cleanup system optionally fetch full message histories for each session to better judge if it's blank - make a command that does the deep clean, keep the clean that happens on new session creation shallow --- .dir-locals.el | 10 +++++ packages/ui/src/lib/hooks/use-commands.ts | 14 +++++++ packages/ui/src/stores/session-api.ts | 21 ---------- packages/ui/src/stores/session-state.ts | 48 +++++++++++++++++++++-- 4 files changed, 68 insertions(+), 25 deletions(-) create mode 100644 .dir-locals.el diff --git a/.dir-locals.el b/.dir-locals.el new file mode 100644 index 00000000..2f38598b --- /dev/null +++ b/.dir-locals.el @@ -0,0 +1,10 @@ +((typescript-ts-mode + . ((eglot-workspace-configuration + . (:typescript.format (:indentSize 2 + :tabSize 2 + :convertTabsToSpaces t + :semicolons "remove") + :javascript.format (:indentSize 2 + :tabSize 2 + :convertTabsToSpaces t + :semicolons "remove")))))) diff --git a/packages/ui/src/lib/hooks/use-commands.ts b/packages/ui/src/lib/hooks/use-commands.ts index 0bc52575..7fa8bb67 100644 --- a/packages/ui/src/lib/hooks/use-commands.ts +++ b/packages/ui/src/lib/hooks/use-commands.ts @@ -16,6 +16,7 @@ import { showAlertDialog } from "../../stores/alerts" import type { Instance } from "../../types/instance" import type { MessageRecord } from "../../stores/message-v2/types" import { messageStoreBus } from "../../stores/message-v2/bus" +import { cleanupBlankSessions } from "../../stores/session-state" export interface UseCommandsOptions { preferences: Accessor @@ -142,6 +143,19 @@ export function useCommands(options: UseCommandsOptions) { }, }) + commandRegistry.register({ + id: "cleanup-blank-sessions", + label: "Cleanup Blank Sessions", + description: "Remove empty sessions from the current instance", + category: "Session", + keywords: ["cleanup", "blank", "empty", "sessions", "remove", "delete"], + action: async () => { + const instance = activeInstance() + if (!instance) return + await cleanupBlankSessions(instance.id, undefined, true) + }, + }) + commandRegistry.register({ id: "switch-to-info", label: "Instance Info", diff --git a/packages/ui/src/stores/session-api.ts b/packages/ui/src/stores/session-api.ts index 556d625c..dcc19b62 100644 --- a/packages/ui/src/stores/session-api.ts +++ b/packages/ui/src/stores/session-api.ts @@ -407,27 +407,6 @@ async function deleteSession(instanceId: string, sessionId: string): Promise { - const instanceSessions = sessions().get(instanceId) - if (!instanceSessions) return - - const deletionPromises: Promise[] = [] - - for (const [sessionId, session] of instanceSessions) { - if (sessionId === excludeSessionId) continue - if (!isBlankSession(session, instanceId)) continue - - deletionPromises.push(deleteSession(instanceId, sessionId).catch((error) => { - console.error(`Failed to delete blank session ${sessionId}:`, error) - })) - } - - if (deletionPromises.length > 0) { - console.log(`Cleaning up ${deletionPromises.length} blank sessions`) - await Promise.all(deletionPromises) - } -} - async function fetchAgents(instanceId: string): Promise { const instance = instances().get(instanceId) if (!instance || !instance.client) { diff --git a/packages/ui/src/stores/session-state.ts b/packages/ui/src/stores/session-state.ts index 3fbb9d28..adf225c4 100644 --- a/packages/ui/src/stores/session-state.ts +++ b/packages/ui/src/stores/session-state.ts @@ -1,6 +1,8 @@ import { createSignal } from "solid-js" import type { Session, Agent, Provider } from "../types/session" +import { loadMessages, deleteSession } from "./session-api" +import { showToastNotification } from "../lib/notifications" export interface SessionInfo { cost: number @@ -221,7 +223,7 @@ function getSessionInfo(instanceId: string, sessionId: string): SessionInfo | un return sessionInfoByInstance().get(instanceId)?.get(sessionId) } -function isBlankSession(session: Session, instanceId: string): boolean { +async function isBlankSession(session: Session, instanceId: string, fetchIfNeeded = false): Promise { if (session.parentId === null) { // Parent session is only blank if actually blank AND has no children @@ -234,7 +236,10 @@ function isBlankSession(session: Session, instanceId: string): boolean { // Subagent const loadedSet = messagesLoaded().get(instanceId) || new Set() - if (!loadedSet.has(session.id)) return false + if (!loadedSet.has(session.id)) { + if (!fetchIfNeeded) return false + await loadMessages(instanceId, session.id) + } if (session.messages.length === 0) return true @@ -250,10 +255,14 @@ function isBlankSession(session: Session, instanceId: string): boolean { // Subagent is blank if last message was NOT a tool call return !lastMessageWasToolCall - } else if (session.revert?.messageID) { + } else if (!session.title?.includes("subagent") && session.parentId !== null) { // Fork + const loadedSet = messagesLoaded().get(instanceId) || new Set() - if (!loadedSet.has(session.id)) return false + if (!loadedSet.has(session.id)) { + if (!fetchIfNeeded) return false + await loadMessages(instanceId, session.id) + } if (session.messages.length === 0) return true @@ -264,6 +273,36 @@ function isBlankSession(session: Session, instanceId: string): boolean { return false // default to not saying it's blank, just to be safe } +async function cleanupBlankSessions(instanceId: string, excludeSessionId?: string, fetchIfNeeded = false): Promise { + const instanceSessions = sessions().get(instanceId) + if (!instanceSessions) return + + const cleanupPromises = Array.from(instanceSessions) + .filter(([sessionId]) => sessionId !== excludeSessionId) + .map(async ([sessionId, session]) => { + const isBlank = await isBlankSession(session, instanceId, fetchIfNeeded) + if (!isBlank) return false + + await deleteSession(instanceId, sessionId).catch((error: Error) => { + console.error(`Failed to delete blank session ${sessionId}:`, error) + }) + return true + }) + + if (cleanupPromises.length > 0) { + console.log(`Cleaning up ${cleanupPromises.length} blank sessions`) + const deletionResults = await Promise.all(cleanupPromises) + const deletedCount = deletionResults.filter(Boolean).length + + if (deletedCount > 0) { + showToastNotification({ + message: `Cleaned up ${deletedCount} blank session${deletedCount === 1 ? '' : 's'}`, + variant: "info" + }) + } + } +} + export { sessions, setSessions, @@ -303,4 +342,5 @@ export { isSessionMessagesLoading, getSessionInfo, isBlankSession, + cleanupBlankSessions, }