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
This commit is contained in:
Alexis Dumas
2025-11-24 11:46:02 -05:00
committed by Alexis Purslane
parent f633d75005
commit 50676416ed
4 changed files with 68 additions and 25 deletions

10
.dir-locals.el Normal file
View File

@@ -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"))))))

View File

@@ -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<Preferences>
@@ -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",

View File

@@ -407,27 +407,6 @@ async function deleteSession(instanceId: string, sessionId: string): Promise<voi
}
}
async function cleanupBlankSessions(instanceId: string, excludeSessionId?: string): Promise<void> {
const instanceSessions = sessions().get(instanceId)
if (!instanceSessions) return
const deletionPromises: Promise<void>[] = []
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<void> {
const instance = instances().get(instanceId)
if (!instance || !instance.client) {

View File

@@ -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<boolean> {
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<void> {
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,
}