From 9e476d950c99c275ab3a32565a8b44c56596da7a Mon Sep 17 00:00:00 2001 From: Roo Code Date: Fri, 16 Jan 2026 15:11:13 +0000 Subject: [PATCH] fix: prevent task unsubscription on Roomote Stop to allow Continue/Message - Add _shouldUnsubscribeOnDispose flag to capture intent synchronously - Update abortTask() to set flag before any async operations - Update dispose() to check flag before unsubscribing from task room - Fixes race condition where abandoned flag was set after abortTask() yielded - Ensures Continue and Send Message work after stopping via Roomote Control This prevents the extension from leaving the Socket.IO task room when a task is stopped via Roomote Control, allowing subsequent Continue or Message commands to be received properly. --- src/core/task/Task.ts | 46 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 40 insertions(+), 6 deletions(-) diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 3acb6c2491..eae3051fb9 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -307,6 +307,20 @@ export class Task extends EventEmitter implements TaskLike { didFinishAbortingStream = false abandoned = false abortReason?: ClineApiReqCancelReason + + /** + * Controls whether the task should unsubscribe from its Socket.IO room when disposed. + * This flag is captured synchronously at the start of abortTask() to prevent race conditions + * where external code might modify state between abortTask() initiation and dispose() execution. + * + * - undefined: Default behavior - unsubscribe (backward compatibility) + * - true: Unsubscribe when disposed (user switched tasks) + * - false: Keep subscription when disposed (Roomote Stop, user wants to continue later) + * + * @see {@link abortTask} - Sets this flag based on isAbandoned parameter + * @see {@link dispose} - Uses this flag to decide whether to unsubscribe + */ + private _shouldUnsubscribeOnDispose?: boolean isInitialized = false isPaused: boolean = false @@ -2195,6 +2209,11 @@ export class Task extends EventEmitter implements TaskLike { public async abortTask(isAbandoned = false) { // Aborting task + // CRITICAL: Capture intent BEFORE any async operations + // This prevents race conditions where external code sets 'abandoned' + // between now and dispose() + this._shouldUnsubscribeOnDispose = isAbandoned + // Will stop any autonomously running promises. if (isAbandoned) { this.abandoned = true @@ -2269,13 +2288,28 @@ export class Task extends EventEmitter implements TaskLike { } if (this.enableBridge) { - BridgeOrchestrator.getInstance() - ?.unsubscribeFromTask(this.taskId) - .catch((error) => - console.error( - `[Task#dispose] BridgeOrchestrator#unsubscribeFromTask() failed: ${error instanceof Error ? error.message : String(error)}`, - ), + // Only unsubscribe if: + // 1. abortTask(true) was called (user switched tasks), OR + // 2. dispose() was called directly (backward compatibility) + // Keep subscription for abortTask(false) (Roomote Stop, user wants to continue later) + const shouldUnsubscribe = this._shouldUnsubscribeOnDispose ?? true + + if (shouldUnsubscribe) { + console.log(`[Task#dispose] Unsubscribing from task room ${this.taskId}`) + BridgeOrchestrator.getInstance() + ?.unsubscribeFromTask(this.taskId) + .catch((error) => + console.error( + `[Task#dispose] BridgeOrchestrator#unsubscribeFromTask() failed: ` + + `${error instanceof Error ? error.message : String(error)}`, + ), + ) + } else { + console.log( + `[Task#dispose] Keeping task room subscription for ${this.taskId} ` + + `(awaiting remote Continue/Message)`, ) + } } // Release any terminals associated with this task.