Skip to content

Bug: Multiple execute_command tool calls in single request only ask permission for first command #998

Description

@easonLiangWorldedtech

Bug: Multiple execute_command tool calls in single request only ask permission for first command

Description

When the AI sends multiple execute_command tool calls in a single request (e.g., 3 commands), Zoo Code processes them via a recursive loop (presentAssistantMessage.ts line 962). After the user approves the first command, all subsequent commands execute automatically without showing individual permission prompts.

This means if AI sends:

[
    {"tool": "execute_command", "command": "echo \"cmd1\""},
    {"tool": "execute_command", "command": "echo \"cmd2\""},
    {"tool": "execute_command", "command": "echo \"cmd3\""}
]

User sees prompt for cmd1 only. After clicking approve, all 3 commands execute immediately without further prompts — even with auto-approve completely disabled.

Root Cause Analysis

Code Flow

In src/core/assistant-message/presentAssistantMessage.ts, tool calls from a single AI request are processed via a recursive sequential loop:

// Line 962 - Recursive loop processes each tool call sequentially
return presentAssistantMessage(cline)

Each execute_command tool call invokes askApproval("command", ...) which calls task.ask("tool", message) in Task.ts. The bug occurs because of two interacting mechanisms:

Mechanism 1 — Message Queue Auto-Approve (Line 1354-1368):

} else if (isMessageQueued && shouldDrainQueuedMessageForAsk) {
    const message = this.messageQueueService.dequeueMessage()
    if (message) {
        // For tool approvals, auto-dequeue and approve!
        if (type === "tool" || type === "command" || type === "use_mcp_server") {
            this.handleWebviewAskResponse("yesButtonClicked", message.text, message.images)
        }
    }
}

Mechanism 2 — Shared Timestamp Check (Line 1374):

await pWaitFor(() => {
    if (this.askResponse !== undefined || this.lastMessageTs !== askTs) {
        return true // Skip waiting, use existing response
    }
    ...
})

Bug Flow

  1. Command feat: support OAuth 2.1 for streamable-http MCP servers #1: askApproval("command", "echo \"cmd1\"") → displays prompt → user clicks approve → handleWebviewAskResponse("yesButtonClicked") sets this.askResponse = "yesButtonClicked"

  2. Loop continues to command Roo to zoo upgrade #2 (line 962):

    return presentAssistantMessage(cline) // Recursive sequential loop
  3. Command Roo to zoo upgrade #2: askApproval("command", "echo \"cmd2\"")task.ask("tool", message) → line 1256 resets this.askResponse = undefined → BUT:

    • Line 1354: If a queued message exists (from the rapid iteration), it's auto-dequeued and handled as "yesButtonClicked" — no prompt shown to user.
    • OR Line 1374: The lastMessageTs !== askTs check may pass if timestamps are too close together, causing the wait to be skipped entirely.
  4. Result: Command Roo to zoo upgrade #2 (and all subsequent commands) return "yesButtonClicked" without showing any prompt to user.

Key Suspects

  • messageQueueService auto-dequeue behavior (Task.ts line 1354-1368): Queued messages from rapid loop iteration are auto-approved for command tool calls
  • Loop iteration speed vs UI update delay: The recursive loop may iterate faster than the UI can display each prompt
  • askResponse global state: Shared across batch command tool calls, causing approval state to leak between iterations

Example

Input: AI sends 3x execute_command in one request

[
    {"tool": "execute_command", "command": "echo \"cmd1\""},
    {"tool": "execute_command", "command": "echo \"cmd2\""},
    {"tool": "execute_command", "command": "echo \"cmd3\""}
]

Current behavior:

  • User sees: Prompt for echo "cmd1" only
  • User clicks ✅ Approve
  • All 3 commands execute immediately without further prompts

Expected behavior:

  • Show prompt for each command individually
  • Wait for user approval before executing each command

Affected Files

  • src/core/task/Task.ts — Line 1354-1368: Message queue auto-dequeue approves tool calls silently; Line 1374: Shared timestamp check may skip waiting
  • src/core/assistant-message/presentAssistantMessage.ts — Line 962: Recursive loop processes batch tool calls sequentially without per-call UI delay
  • src/core/tools/ExecuteCommandTool.ts — Line 109: Each call uses askApproval("command", canonicalCommand) but may not display prompt due to queue/state issues

Suggested Fix Directions

Option A: Prevent message queue auto-dequeue for tool calls within the same batch. Only dequeue when the user explicitly queues a follow-up action, not during rapid sequential iteration.

Option B: Add a per-tool-use-id approval cache so that each individual command in a batch gets its own prompt and approval state, rather than relying on global askResponse.

Option C: Introduce a small delay or UI lock between tool calls in the same batch to ensure each prompt is displayed before the next iteration begins.

ref

Sometimes it runs the next command directly.

20260724-1713-25.7491772.mp4

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions