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
This commit is contained in:
Andrew Beal 2025-10-14 23:45:10 +01:00
parent 1a57bb4872
commit 6be51ebaae
5 changed files with 23 additions and 3 deletions

View file

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

View file

@ -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<AIAgentPlugin>(Services.AIAgentPlugin);
const conversationService = Resolve<ConversationFileSystemService>(Services.ConversationFileSystemService);
const chatService: ChatService = Resolve<ChatService>(Services.ChatService);
function startNewConversation() {
conversationService.resetCurrentConversation();
@ -21,6 +23,7 @@
}
async function deleteCurrentConversation() {
chatService.stop();
await conversationService.deleteCurrentConversation();
conversationStore.reset();
onNewConversation?.();

View file

@ -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<ConversationFileSystemService>(Services.ConversationFileSystemService);
private readonly fileSystemService: FileSystemService = Resolve<FileSystemService>(Services.FileSystemService);
private readonly chatService: ChatService = Resolve<ChatService>(Services.ChatService);
private component: Record<string, any> | 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;

View file

@ -37,8 +37,8 @@ export class AIFunctionService {
}
private async searchVaultFiles(searchTerm: string): Promise<object> {
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) => ({

View file

@ -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<ConversationFileSystemService>(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;