|
| 1 | +// Delete operation - permanently remove a thread and clean up references |
| 2 | +// Returns result object for MCP compatibility |
| 3 | + |
| 4 | +import { loadMetaIndex, loadThread, deleteThreadFile, updateThread } from '../storage.js'; |
| 5 | +import { validateTag } from '../utils.js'; |
| 6 | +import { getSemanticIndex } from '../../embeddings/index.js'; |
| 7 | +import type { OperationResult } from '../types.js'; |
| 8 | + |
| 9 | +export interface DeleteInput { |
| 10 | + threadId: string; |
| 11 | +} |
| 12 | + |
| 13 | +export interface DeleteResult { |
| 14 | + threadId: string; |
| 15 | + snippetsRemoved: number; |
| 16 | + filesUnlinked: number; |
| 17 | + semanticEntriesRemoved: number; |
| 18 | + relatedThreadsUpdated: string[]; |
| 19 | +} |
| 20 | + |
| 21 | +export async function deleteThread(input: DeleteInput): Promise<OperationResult<DeleteResult>> { |
| 22 | + try { |
| 23 | + const validatedId = validateTag(input.threadId); |
| 24 | + |
| 25 | + // Check thread exists via meta index (fast) |
| 26 | + const meta = loadMetaIndex(); |
| 27 | + if (!meta.threads[validatedId]) { |
| 28 | + return { |
| 29 | + success: false, |
| 30 | + message: `Thread '${validatedId}' not found.`, |
| 31 | + error: 'THREAD_NOT_FOUND', |
| 32 | + }; |
| 33 | + } |
| 34 | + |
| 35 | + // Load full thread data so we can report what's being removed |
| 36 | + // and clean up bidirectional related-thread references |
| 37 | + const thread = loadThread(validatedId); |
| 38 | + const snippetsRemoved = thread?.snippets?.length ?? 0; |
| 39 | + const filesUnlinked = thread?.linked_files?.length ?? 0; |
| 40 | + const relatedThreads = thread?.related ?? []; |
| 41 | + |
| 42 | + // Remove this thread from every related thread's `related` array. |
| 43 | + // Without this, deleting A leaves stale references in B, C, etc. |
| 44 | + const relatedThreadsUpdated: string[] = []; |
| 45 | + for (const relatedId of relatedThreads) { |
| 46 | + try { |
| 47 | + updateThread(relatedId, (relatedThread) => { |
| 48 | + const related = relatedThread.related || []; |
| 49 | + relatedThread.related = related.filter((r) => r !== validatedId); |
| 50 | + relatedThread.date_modified = new Date().toISOString(); |
| 51 | + return relatedThread; |
| 52 | + }); |
| 53 | + relatedThreadsUpdated.push(relatedId); |
| 54 | + } catch { |
| 55 | + // Related thread may already be gone — not fatal |
| 56 | + } |
| 57 | + } |
| 58 | + |
| 59 | + // Delete the thread file and remove from meta index |
| 60 | + deleteThreadFile(validatedId); |
| 61 | + |
| 62 | + // Clean up semantic index embeddings (non-fatal if index doesn't exist) |
| 63 | + let semanticEntriesRemoved = 0; |
| 64 | + try { |
| 65 | + const semanticIndex = await getSemanticIndex(); |
| 66 | + semanticEntriesRemoved = await semanticIndex.deleteThread(validatedId); |
| 67 | + } catch { |
| 68 | + // Semantic index may not exist or may not be initialized |
| 69 | + } |
| 70 | + |
| 71 | + return { |
| 72 | + success: true, |
| 73 | + message: `Thread '${validatedId}' deleted.`, |
| 74 | + data: { |
| 75 | + threadId: validatedId, |
| 76 | + snippetsRemoved, |
| 77 | + filesUnlinked, |
| 78 | + semanticEntriesRemoved, |
| 79 | + relatedThreadsUpdated, |
| 80 | + }, |
| 81 | + }; |
| 82 | + } catch (error) { |
| 83 | + return { |
| 84 | + success: false, |
| 85 | + message: error instanceof Error ? error.message : String(error), |
| 86 | + error: 'DELETE_ERROR', |
| 87 | + }; |
| 88 | + } |
| 89 | +} |
0 commit comments