Compare commits

...
Author SHA1 Message Date
Arafatkatze 1f1c018d02 feat: add timeout for presentAssistantMessage during streaming
Introduce a configurable timeout for `presentAssistantMessage` calls
during stream consumption to prevent the stream from being blocked
indefinitely if message presentation stalls.

- Add `CLINE_PRESENT_ASSISTANT_MESSAGE_TIMEOUT_MS` env var support
  (defaults to 1000ms) with safe integer parsing
- Use `Promise.race` to continue stream consumption if presentation
  exceeds the timeout threshold
- Log a warning when timeout occurs and debug log when the deferred
  promise eventually settles, including elapsed time
2026-02-20 16:39:48 -08:00
+36 -2
View File
@@ -2618,6 +2618,14 @@ export class Task {
this.taskState.isStreaming = true
let didReceiveUsageChunk = false
let didFinalizeReasoningForUi = false
const presentAssistantMessageTimeoutMs = (() => {
const raw = process.env.CLINE_PRESENT_ASSISTANT_MESSAGE_TIMEOUT_MS
if (raw === undefined) {
return 1000
}
const parsed = Number.parseInt(raw, 10)
return Number.isFinite(parsed) && parsed > 0 ? parsed : 1000
})()
const finalizePendingReasoningMessage = async (thinking: string): Promise<boolean> => {
const pendingReasoningIndex = findLastIndex(
@@ -2731,9 +2739,35 @@ export class Task {
// Present content once per chunk. Calling this from multiple case branches can
// race partial updates and duplicate text rows in the chat.
await this.presentAssistantMessage().catch((error) =>
Logger.debug("[Task] Failed to present message: " + error),
const presentStartedAt = Date.now()
const presentPromise = this.presentAssistantMessage().catch((error) => {
Logger.debug(`[Task] Failed to present message: ${error}`)
})
const pendingBlocks = this.taskState.assistantMessageContent.slice(
this.taskState.currentStreamingContentIndex,
)
const hasPendingToolUseBlock = pendingBlocks.some((block) => block.type === "tool_use")
const shouldTimeboxPresent =
presentAssistantMessageTimeoutMs > 0 && (!hasPendingToolUseBlock || this.isParallelToolCallingEnabled())
if (shouldTimeboxPresent) {
const presentResult = await Promise.race([
presentPromise.then(() => "done" as const),
setTimeoutPromise(presentAssistantMessageTimeoutMs).then(() => "timeout" as const),
])
if (presentResult === "timeout") {
Logger.warn(
`[Task ${this.taskId}] presentAssistantMessage timed out after ${presentAssistantMessageTimeoutMs}ms; continuing stream consumption`,
)
void presentPromise.then(() => {
const elapsedMs = Date.now() - presentStartedAt
Logger.debug(
`[Task ${this.taskId}] presentAssistantMessage settled after timeout elapsedMs=${elapsedMs}`,
)
})
}
} else {
await presentPromise
}
if (this.taskState.abort) {
this.api.abort?.()