fix: prevent command_output ask from blocking in cloud/headless environments (#9152)

This commit is contained in:
Daniel 2025-11-10 11:47:26 -05:00 committed by GitHub
parent b09d0a7640
commit 4cd5c9022e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 35 additions and 9 deletions

View file

@ -46,7 +46,21 @@ export type ClineAsk = z.infer<typeof clineAskSchema>
// Needs classification:
// - `followup`
// - `command_output
/**
* NonBlockingAsk
*
* Asks that should not block task execution. These are informational or optional
* asks where the task can proceed even without an immediate user response.
*/
export const nonBlockingAsks = ["command_output"] as const satisfies readonly ClineAsk[]
export type NonBlockingAsk = (typeof nonBlockingAsks)[number]
export function isNonBlockingAsk(ask: ClineAsk): ask is NonBlockingAsk {
return (nonBlockingAsks as readonly ClineAsk[]).includes(ask)
}
/**
* IdleAsk

View file

@ -34,6 +34,7 @@ import {
isIdleAsk,
isInteractiveAsk,
isResumableAsk,
isNonBlockingAsk,
QueuedMessage,
DEFAULT_CHECKPOINT_TIMEOUT_SECONDS,
MAX_CHECKPOINT_TIMEOUT_SECONDS,
@ -821,13 +822,12 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
// block (via the `pWaitFor`).
const isBlocking = !(this.askResponse !== undefined || this.lastMessageTs !== askTs)
const isMessageQueued = !this.messageQueueService.isEmpty()
const isStatusMutable = !partial && isBlocking && !isMessageQueued
// Non-blocking asks should not mutate task status since they don't actually block execution
const isStatusMutable = !partial && isBlocking && !isMessageQueued && !isNonBlockingAsk(type)
let statusMutationTimeouts: NodeJS.Timeout[] = []
const statusMutationTimeout = 5_000
if (isStatusMutable) {
console.log(`Task#ask will block -> type: ${type}`)
if (isInteractiveAsk(type)) {
statusMutationTimeouts.push(
setTimeout(() => {
@ -879,14 +879,21 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
// the message if there's text/images.
this.handleWebviewAskResponse("yesButtonClicked", message.text, message.images)
} else {
// For other ask types (like followup), fulfill the ask
// For other ask types (like followup or command_output), fulfill the ask
// directly.
this.setMessageResponse(message.text, message.images)
}
}
}
// Wait for askResponse to be set.
// Non-blocking asks return immediately without waiting
// The ask message is created in the UI, but the task doesn't wait for a response
// This prevents blocking in cloud/headless environments
if (isNonBlockingAsk(type)) {
return { response: "yesButtonClicked" as ClineAskResponse, text: undefined, images: undefined }
}
// Wait for askResponse to be set
await pWaitFor(() => this.askResponse !== undefined || this.lastMessageTs !== askTs, { interval: 100 })
if (this.lastMessageTs !== askTs) {

View file

@ -179,6 +179,7 @@ export async function executeCommand(
let result: string = ""
let exitDetails: ExitCodeDetails | undefined
let shellIntegrationError: string | undefined
let hasAskedForCommandOutput = false
const terminalProvider = terminalShellIntegrationDisabled ? "execa" : "vscode"
const provider = await task.providerRef.deref()
@ -195,10 +196,13 @@ export async function executeCommand(
const status: CommandExecutionStatus = { executionId, status: "output", output: compressedOutput }
provider?.postMessageToWebview({ type: "commandExecutionStatus", text: JSON.stringify(status) })
if (runInBackground) {
if (runInBackground || hasAskedForCommandOutput) {
return
}
// Mark that we've asked to prevent multiple concurrent asks
hasAskedForCommandOutput = true
try {
const { response, text, images } = await task.ask("command_output", "")
runInBackground = true
@ -207,7 +211,9 @@ export async function executeCommand(
message = { text, images }
process.continue()
}
} catch (_error) {}
} catch (_error) {
// Silently handle ask errors (e.g., "Current ask promise was ignored")
}
},
onCompleted: (output: string | undefined) => {
result = Terminal.compressTerminalOutput(
@ -220,7 +226,6 @@ export async function executeCommand(
completed = true
},
onShellExecutionStarted: (pid: number | undefined) => {
console.log(`[executeCommand] onShellExecutionStarted: ${pid}`)
const status: CommandExecutionStatus = { executionId, status: "started", pid, command }
provider?.postMessageToWebview({ type: "commandExecutionStatus", text: JSON.stringify(status) })
},