fix: race condition in new_task tool for native protocol (#9655)

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.
This commit is contained in:
Daniel 2025-11-27 12:30:55 -05:00 committed by GitHub
parent 400d0cd183
commit d2d311e501
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

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