From 224935c11c92b4ffb1f9c88b1dbfdff6cb6e5775 Mon Sep 17 00:00:00 2001 From: daniel-lxs Date: Thu, 27 Nov 2025 12:02:29 -0500 Subject: [PATCH] fix: race condition in new_task tool for native protocol The pendingNewTaskToolCallId was being set AFTER startSubtask() returned. However, startSubtask() contains a 500ms delay during which the subtask could complete. If the subtask completed during this window, completeSubtask() would be called before pendingNewTaskToolCallId was set, causing it to fall through to the XML protocol path and add a text message instead of a proper tool_result block, breaking the API conversation structure. This fix moves the pendingNewTaskToolCallId assignment to happen BEFORE calling startSubtask(), ensuring the ID is set before the subtask starts. If the subtask creation fails, the pending ID is cleared. --- src/core/tools/NewTaskTool.ts | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/core/tools/NewTaskTool.ts b/src/core/tools/NewTaskTool.ts index 827a63c95c3..f406774eae4 100644 --- a/src/core/tools/NewTaskTool.ts +++ b/src/core/tools/NewTaskTool.ts @@ -126,22 +126,31 @@ export class NewTaskTool extends BaseTool<"new_task"> { // Preserve the current mode so we can resume with it later. task.pausedModeSlug = (await provider.getState()).mode ?? defaultModeSlug + // For native protocol, set the pending tool call ID BEFORE starting the subtask. + // This prevents a race condition where the subtask completes (during the delay + // in startSubtask) before we set the ID, which would cause completeSubtask to + // not push the tool_result, breaking the API conversation structure. + if (toolProtocol === "native" && toolCallId) { + task.pendingNewTaskToolCallId = toolCallId + } + const newTask = await task.startSubtask(unescapedMessage, todoItems, mode) if (!newTask) { + // Clear the pending ID since the subtask wasn't created + if (toolProtocol === "native" && toolCallId) { + task.pendingNewTaskToolCallId = undefined + } pushToolResult(t("tools:newTask.errors.policy_restriction")) return } - // For native protocol, defer the tool_result until the subtask completes. + // For native protocol with toolCallId, don't push tool_result here. // The actual result (including what the subtask accomplished) will be pushed // by completeSubtask. This gives the parent task useful information about // what the subtask actually did. - if (toolProtocol === "native" && toolCallId) { - task.pendingNewTaskToolCallId = toolCallId - // Don't push tool_result here - it will come from completeSubtask with the actual result. - // The task loop will stay alive because isPaused is true (see Task.ts stack push condition). - } else { + // The task loop will stay alive because isPaused is true (see Task.ts stack push condition). + if (toolProtocol !== "native" || !toolCallId) { // For XML protocol, push the result immediately (existing behavior) pushToolResult( `Successfully created new task in ${targetMode.name} mode with message: ${unescapedMessage} and ${todoItems.length} todo items`,