11import { db } from '@sim/db'
2- import { copilotMessages , workspaceFiles } from '@sim/db/schema'
2+ import { copilotChats , copilotMessages , workspaceFiles } from '@sim/db/schema'
33import { createLogger } from '@sim/logger'
44import { and , inArray , isNull } from 'drizzle-orm'
55import { chunkArray } from '@/lib/cleanup/batch-delete'
@@ -26,6 +26,7 @@ type ChatScopedContext = (typeof CHAT_SCOPED_CONTEXTS)[number]
2626interface FileRef {
2727 key : string
2828 context : ChatScopedContext
29+ chatId : string
2930}
3031
3132/**
@@ -42,7 +43,11 @@ export async function collectChatFiles(chatIds: string[]): Promise<FileRef[]> {
4243 for ( const chunk of chunkArray ( chatIds , CHAT_FILE_COLLECT_CHUNK_SIZE ) ) {
4344 const [ linkedFiles , messageRows ] = await Promise . all ( [
4445 db
45- . select ( { key : workspaceFiles . key , context : workspaceFiles . context } )
46+ . select ( {
47+ key : workspaceFiles . key ,
48+ context : workspaceFiles . context ,
49+ chatId : workspaceFiles . chatId ,
50+ } )
4651 . from ( workspaceFiles )
4752 . where (
4853 and (
@@ -54,15 +59,15 @@ export async function collectChatFiles(chatIds: string[]): Promise<FileRef[]> {
5459 // Scan every message row for the chat (no deleted_at filter): this is a
5560 // deletion path collecting blob keys, so attachments on any row count.
5661 db
57- . select ( { content : copilotMessages . content } )
62+ . select ( { content : copilotMessages . content , chatId : copilotMessages . chatId } )
5863 . from ( copilotMessages )
5964 . where ( inArray ( copilotMessages . chatId , chunk ) ) ,
6065 ] )
6166
6267 for ( const f of linkedFiles ) {
63- if ( ! seen . has ( f . key ) ) {
68+ if ( f . chatId && ! seen . has ( f . key ) ) {
6469 seen . add ( f . key )
65- files . push ( { key : f . key , context : f . context as ChatScopedContext } )
70+ files . push ( { key : f . key , context : f . context as ChatScopedContext , chatId : f . chatId } )
6671 }
6772 }
6873
@@ -80,7 +85,7 @@ export async function collectChatFiles(chatIds: string[]): Promise<FileRef[]> {
8085 const key = ( attachment as Record < string , unknown > ) . key as string
8186 if ( ! seen . has ( key ) ) {
8287 seen . add ( key )
83- files . push ( { key, context : 'copilot' } )
88+ files . push ( { key, context : 'copilot' , chatId : row . chatId } )
8489 }
8590 }
8691 }
@@ -195,17 +200,36 @@ export async function prepareChatCleanup(
195200
196201 return {
197202 execute : async ( ) => {
203+ // A chat can be restored (or its delete can fail) between selection and
204+ // the caller's row delete. Purge backend data and files only for chats
205+ // whose rows are actually gone, so a surviving row never loses its data.
206+ const survivors = new Set < string > ( )
207+ for ( const chunk of chunkArray ( chatIds , CHAT_FILE_COLLECT_CHUNK_SIZE ) ) {
208+ const rows = await db
209+ . select ( { id : copilotChats . id } )
210+ . from ( copilotChats )
211+ . where ( inArray ( copilotChats . id , chunk ) )
212+ for ( const row of rows ) survivors . add ( row . id )
213+ }
214+ if ( survivors . size > 0 ) {
215+ logger . info (
216+ `[${ label } ] Skipping external cleanup for ${ survivors . size } chats whose rows still exist`
217+ )
218+ }
219+ const confirmedChatIds = chatIds . filter ( ( id ) => ! survivors . has ( id ) )
220+ const confirmedFiles = files . filter ( ( file ) => ! survivors . has ( file . chatId ) )
221+
198222 // Call copilot backend
199- if ( chatIds . length > 0 ) {
200- const copilotResult = await cleanupCopilotBackend ( chatIds , label )
223+ if ( confirmedChatIds . length > 0 ) {
224+ const copilotResult = await cleanupCopilotBackend ( confirmedChatIds , label )
201225 logger . info (
202226 `[${ label } ] Copilot backend: ${ copilotResult . deleted } deleted, ${ copilotResult . failed } failed`
203227 )
204228 }
205229
206230 // Delete storage files with correct context per file
207- if ( files . length > 0 ) {
208- const fileStats = await deleteStorageFiles ( files , label )
231+ if ( confirmedFiles . length > 0 ) {
232+ const fileStats = await deleteStorageFiles ( confirmedFiles , label )
209233 logger . info (
210234 `[${ label } ] Storage cleanup: ${ fileStats . filesDeleted } deleted, ${ fileStats . filesFailed } failed`
211235 )
0 commit comments