Skip to content

Commit 15ec2be

Browse files
committed
fix: prevent command_output ask from blocking in cloud/headless environments
Classifies command_output as NonBlockingAsk to prevent task timeouts when auto-approve is enabled in cloud/headless environments. After v3.30.0 changed the default terminal to inline (execa), command output streams immediately, causing onLine callbacks to fire early and create command_output asks. In cloud/headless with no user to respond, these asks would block indefinitely, causing task timeouts. Changes: - Add NonBlockingAsk classification for asks that should not block execution - Classify command_output as non-blocking - Return immediately for non-blocking asks without waiting for user response - Prevent multiple concurrent command_output asks with hasAskedForCommandOutput flag - Skip task status mutation for non-blocking asks Fixes the issue where cloud tasks would timeout waiting for approval even when auto-approve for command execution was enabled.
1 parent 62636ad commit 15ec2be

File tree

3 files changed

+35
-9
lines changed

3 files changed

+35
-9
lines changed

packages/types/src/message.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,21 @@ export type ClineAsk = z.infer<typeof clineAskSchema>
4646

4747
// Needs classification:
4848
// - `followup`
49-
// - `command_output
49+
50+
/**
51+
* NonBlockingAsk
52+
*
53+
* Asks that should not block task execution. These are informational or optional
54+
* asks where the task can proceed even without an immediate user response.
55+
*/
56+
57+
export const nonBlockingAsks = ["command_output"] as const satisfies readonly ClineAsk[]
58+
59+
export type NonBlockingAsk = (typeof nonBlockingAsks)[number]
60+
61+
export function isNonBlockingAsk(ask: ClineAsk): ask is NonBlockingAsk {
62+
return (nonBlockingAsks as readonly ClineAsk[]).includes(ask)
63+
}
5064

5165
/**
5266
* IdleAsk

src/core/task/Task.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import {
3434
isIdleAsk,
3535
isInteractiveAsk,
3636
isResumableAsk,
37+
isNonBlockingAsk,
3738
QueuedMessage,
3839
DEFAULT_CHECKPOINT_TIMEOUT_SECONDS,
3940
MAX_CHECKPOINT_TIMEOUT_SECONDS,
@@ -821,13 +822,12 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
821822
// block (via the `pWaitFor`).
822823
const isBlocking = !(this.askResponse !== undefined || this.lastMessageTs !== askTs)
823824
const isMessageQueued = !this.messageQueueService.isEmpty()
824-
const isStatusMutable = !partial && isBlocking && !isMessageQueued
825+
// Non-blocking asks should not mutate task status since they don't actually block execution
826+
const isStatusMutable = !partial && isBlocking && !isMessageQueued && !isNonBlockingAsk(type)
825827
let statusMutationTimeouts: NodeJS.Timeout[] = []
826828
const statusMutationTimeout = 5_000
827829

828830
if (isStatusMutable) {
829-
console.log(`Task#ask will block -> type: ${type}`)
830-
831831
if (isInteractiveAsk(type)) {
832832
statusMutationTimeouts.push(
833833
setTimeout(() => {
@@ -879,14 +879,21 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
879879
// the message if there's text/images.
880880
this.handleWebviewAskResponse("yesButtonClicked", message.text, message.images)
881881
} else {
882-
// For other ask types (like followup), fulfill the ask
882+
// For other ask types (like followup or command_output), fulfill the ask
883883
// directly.
884884
this.setMessageResponse(message.text, message.images)
885885
}
886886
}
887887
}
888888

889-
// Wait for askResponse to be set.
889+
// Non-blocking asks return immediately without waiting
890+
// The ask message is created in the UI, but the task doesn't wait for a response
891+
// This prevents blocking in cloud/headless environments
892+
if (isNonBlockingAsk(type)) {
893+
return { response: "yesButtonClicked" as ClineAskResponse, text: undefined, images: undefined }
894+
}
895+
896+
// Wait for askResponse to be set
890897
await pWaitFor(() => this.askResponse !== undefined || this.lastMessageTs !== askTs, { interval: 100 })
891898

892899
if (this.lastMessageTs !== askTs) {

src/core/tools/executeCommandTool.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,7 @@ export async function executeCommand(
179179
let result: string = ""
180180
let exitDetails: ExitCodeDetails | undefined
181181
let shellIntegrationError: string | undefined
182+
let hasAskedForCommandOutput = false
182183

183184
const terminalProvider = terminalShellIntegrationDisabled ? "execa" : "vscode"
184185
const provider = await task.providerRef.deref()
@@ -195,10 +196,13 @@ export async function executeCommand(
195196
const status: CommandExecutionStatus = { executionId, status: "output", output: compressedOutput }
196197
provider?.postMessageToWebview({ type: "commandExecutionStatus", text: JSON.stringify(status) })
197198

198-
if (runInBackground) {
199+
if (runInBackground || hasAskedForCommandOutput) {
199200
return
200201
}
201202

203+
// Mark that we've asked to prevent multiple concurrent asks
204+
hasAskedForCommandOutput = true
205+
202206
try {
203207
const { response, text, images } = await task.ask("command_output", "")
204208
runInBackground = true
@@ -207,7 +211,9 @@ export async function executeCommand(
207211
message = { text, images }
208212
process.continue()
209213
}
210-
} catch (_error) {}
214+
} catch (_error) {
215+
// Silently handle ask errors (e.g., "Current ask promise was ignored")
216+
}
211217
},
212218
onCompleted: (output: string | undefined) => {
213219
result = Terminal.compressTerminalOutput(
@@ -220,7 +226,6 @@ export async function executeCommand(
220226
completed = true
221227
},
222228
onShellExecutionStarted: (pid: number | undefined) => {
223-
console.log(`[executeCommand] onShellExecutionStarted: ${pid}`)
224229
const status: CommandExecutionStatus = { executionId, status: "started", pid, command }
225230
provider?.postMessageToWebview({ type: "commandExecutionStatus", text: JSON.stringify(status) })
226231
},

0 commit comments

Comments
 (0)