Skip to content

Commit d2d311e

Browse files
authored
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.
1 parent 400d0cd commit d2d311e

File tree

1 file changed

+15
-6
lines changed

1 file changed

+15
-6
lines changed

src/core/tools/NewTaskTool.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -126,22 +126,31 @@ export class NewTaskTool extends BaseTool<"new_task"> {
126126
// Preserve the current mode so we can resume with it later.
127127
task.pausedModeSlug = (await provider.getState()).mode ?? defaultModeSlug
128128

129+
// For native protocol, set the pending tool call ID BEFORE starting the subtask.
130+
// This prevents a race condition where the subtask completes (during the delay
131+
// in startSubtask) before we set the ID, which would cause completeSubtask to
132+
// not push the tool_result, breaking the API conversation structure.
133+
if (toolProtocol === "native" && toolCallId) {
134+
task.pendingNewTaskToolCallId = toolCallId
135+
}
136+
129137
const newTask = await task.startSubtask(unescapedMessage, todoItems, mode)
130138

131139
if (!newTask) {
140+
// Clear the pending ID since the subtask wasn't created
141+
if (toolProtocol === "native" && toolCallId) {
142+
task.pendingNewTaskToolCallId = undefined
143+
}
132144
pushToolResult(t("tools:newTask.errors.policy_restriction"))
133145
return
134146
}
135147

136-
// For native protocol, defer the tool_result until the subtask completes.
148+
// For native protocol with toolCallId, don't push tool_result here.
137149
// The actual result (including what the subtask accomplished) will be pushed
138150
// by completeSubtask. This gives the parent task useful information about
139151
// what the subtask actually did.
140-
if (toolProtocol === "native" && toolCallId) {
141-
task.pendingNewTaskToolCallId = toolCallId
142-
// Don't push tool_result here - it will come from completeSubtask with the actual result.
143-
// The task loop will stay alive because isPaused is true (see Task.ts stack push condition).
144-
} else {
152+
// The task loop will stay alive because isPaused is true (see Task.ts stack push condition).
153+
if (toolProtocol !== "native" || !toolCallId) {
145154
// For XML protocol, push the result immediately (existing behavior)
146155
pushToolResult(
147156
`Successfully created new task in ${targetMode.name} mode with message: ${unescapedMessage} and ${todoItems.length} todo items`,

0 commit comments

Comments
 (0)