From 6be51ebaae84b3290ae4224b9b8773c4026250a2 Mon Sep 17 00:00:00 2001 From: Andrew Beal Date: Tue, 14 Oct 2025 23:45:10 +0100 Subject: [PATCH] fix: prevent race conditions in chat lifecycle and conversation management - Add abort flag to prevent semaphore double-release during stop - Stop active chats before deleting conversations to avoid state corruption - Treat empty search terms as explicit request for all files --- .../Functions/SearchVaultFiles.ts | 2 ++ Components/TopBar.svelte | 3 +++ Modals/ConversationHistoryModal.ts | 4 ++++ Services/AIFunctionService.ts | 4 ++-- Services/ChatService.ts | 13 ++++++++++++- 5 files changed, 23 insertions(+), 3 deletions(-) diff --git a/AIClasses/FunctionDefinitions/Functions/SearchVaultFiles.ts b/AIClasses/FunctionDefinitions/Functions/SearchVaultFiles.ts index 6517c5b..8c1b780 100644 --- a/AIClasses/FunctionDefinitions/Functions/SearchVaultFiles.ts +++ b/AIClasses/FunctionDefinitions/Functions/SearchVaultFiles.ts @@ -9,6 +9,8 @@ export const SearchVaultFiles: IAIFunctionDefinition = { **IMPORTANT: When a search returns 0 results, a complete list of all vault files will be automatically returned.** This allows you to verify the search scope and attempt alternative search strategies. + Use an empty search term to retrieve a complete list of all vault files explicitly. + Use this function when you need to: - Find files based on what's written INSIDE them - Search for specific concepts, keywords, or text within notes diff --git a/Components/TopBar.svelte b/Components/TopBar.svelte index 038e431..01a7ee8 100644 --- a/Components/TopBar.svelte +++ b/Components/TopBar.svelte @@ -7,12 +7,14 @@ import { conversationStore } from '../Stores/conversationStore'; import type { ConversationHistoryModal } from 'Modals/ConversationHistoryModal'; import { openPluginSettings } from 'Helpers/Helpers'; + import type { ChatService } from 'Services/ChatService'; export let leaf: WorkspaceLeaf; export let onNewConversation: (() => void) | undefined = undefined; const plugin = Resolve(Services.AIAgentPlugin); const conversationService = Resolve(Services.ConversationFileSystemService); + const chatService: ChatService = Resolve(Services.ChatService); function startNewConversation() { conversationService.resetCurrentConversation(); @@ -21,6 +23,7 @@ } async function deleteCurrentConversation() { + chatService.stop(); await conversationService.deleteCurrentConversation(); conversationStore.reset(); onNewConversation?.(); diff --git a/Modals/ConversationHistoryModal.ts b/Modals/ConversationHistoryModal.ts index 61b2bb2..942666e 100644 --- a/Modals/ConversationHistoryModal.ts +++ b/Modals/ConversationHistoryModal.ts @@ -9,6 +9,7 @@ import type { FileSystemService } from 'Services/FileSystemService'; import { dateToString } from 'Helpers/Helpers'; import { conversationStore } from 'Stores/conversationStore'; import { Selector } from 'Enums/Selector'; +import type { ChatService } from 'Services/ChatService'; interface ListItem { id: string; @@ -22,6 +23,7 @@ export class ConversationHistoryModal extends Modal { private readonly conversationFileSystemService: ConversationFileSystemService = Resolve(Services.ConversationFileSystemService); private readonly fileSystemService: FileSystemService = Resolve(Services.FileSystemService); + private readonly chatService: ChatService = Resolve(Services.ChatService); private component: Record | null = null; private items: ListItem[]; @@ -77,6 +79,8 @@ export class ConversationHistoryModal extends Modal { } async handleDelete(itemIds: string[]) { + this.chatService.stop(); + const itemsToDelete = this.items.filter(item => itemIds.includes(item.id)); let shouldResetChat = false; diff --git a/Services/AIFunctionService.ts b/Services/AIFunctionService.ts index 64bb6d0..034f5a6 100644 --- a/Services/AIFunctionService.ts +++ b/Services/AIFunctionService.ts @@ -37,8 +37,8 @@ export class AIFunctionService { } private async searchVaultFiles(searchTerm: string): Promise { - const matches: SearchMatch[] = await this.fileSystemService.searchVaultFiles(searchTerm); - + const matches: SearchMatch[] = searchTerm.trim() === "" ? [] : await this.fileSystemService.searchVaultFiles(searchTerm); + if (matches.length === 0) { const files: TFile[] = await this.fileSystemService.listFilesInDirectory("/"); return files.map((file) => ({ diff --git a/Services/ChatService.ts b/Services/ChatService.ts index 4db7079..1cde7bc 100644 --- a/Services/ChatService.ts +++ b/Services/ChatService.ts @@ -19,8 +19,12 @@ export class ChatService { private ai: IAIClass | undefined; private conversationService: ConversationFileSystemService; private aiFunctionService: AIFunctionService; + private abortController: AbortController | null = null; + private isAborting: boolean = false; + private semaphore: Semaphore; + private semaphoreHeld: boolean = false; constructor() { this.conversationService = Resolve(Services.ConversationFileSystemService); @@ -37,6 +41,8 @@ export class ChatService { return conversation; } + this.semaphoreHeld = true; + try { if (userRequest.trim() === "") { return conversation; @@ -74,12 +80,17 @@ export class ChatService { } finally { callbacks.onThoughtUpdate(null); this.abortController = null; - this.semaphore.release(); + this.isAborting = false; + if (this.semaphoreHeld) { + this.semaphoreHeld = false; + this.semaphore.release(); + } callbacks.onComplete(); } } stop(): void { + this.isAborting = true; if (this.abortController) { this.abortController.abort(); this.abortController = null;