mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
feat(chat): enhance background status handling and UI updates for terminal states
This commit is contained in:
parent
cb975a2e7b
commit
c6b7681622
3 changed files with 159 additions and 40 deletions
|
|
@ -25,6 +25,22 @@ import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from ".
|
|||
|
||||
export type OpenAiNativeModel = ReturnType<OpenAiNativeHandler["getModel"]>
|
||||
|
||||
// GPT-5 specific types
|
||||
|
||||
// Constants for model identification
|
||||
const GPT5_MODEL_PREFIX = "gpt-5"
|
||||
|
||||
// Marker for terminal background-mode failures so we don't attempt resume/poll fallbacks
|
||||
function createTerminalBackgroundError(message: string): Error {
|
||||
const err = new Error(message)
|
||||
;(err as any).isTerminalBackgroundError = true
|
||||
err.name = "TerminalBackgroundError"
|
||||
return err
|
||||
}
|
||||
function isTerminalBackgroundError(err: any): boolean {
|
||||
return !!(err && (err as any).isTerminalBackgroundError)
|
||||
}
|
||||
|
||||
export class OpenAiNativeHandler extends BaseProvider implements SingleCompletionHandler {
|
||||
protected options: ApiHandlerOptions
|
||||
private client: OpenAI
|
||||
|
|
@ -372,6 +388,10 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
|
|||
}
|
||||
}
|
||||
} catch (iterErr) {
|
||||
// If terminal failure, propagate and do not attempt resume/poll
|
||||
if (isTerminalBackgroundError(iterErr)) {
|
||||
throw iterErr
|
||||
}
|
||||
// Stream dropped mid-flight; attempt resume for background requests
|
||||
if (canAttemptResume()) {
|
||||
for await (const chunk of this.attemptResumeOrPoll(
|
||||
|
|
@ -386,11 +406,18 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
|
|||
throw iterErr
|
||||
}
|
||||
} catch (sdkErr: any) {
|
||||
// Propagate terminal background failures without fallback
|
||||
if (isTerminalBackgroundError(sdkErr)) {
|
||||
throw sdkErr
|
||||
}
|
||||
// For errors, fallback to manual SSE via fetch
|
||||
try {
|
||||
yield* this.makeResponsesApiRequest(requestBody, model, metadata, systemPrompt, messages)
|
||||
} catch (fallbackErr) {
|
||||
// If SSE fallback fails mid-stream and we can resume, try that
|
||||
if (isTerminalBackgroundError(fallbackErr)) {
|
||||
throw fallbackErr
|
||||
}
|
||||
if (canAttemptResume()) {
|
||||
for await (const chunk of this.attemptResumeOrPoll(
|
||||
this.lastResponseId!,
|
||||
|
|
@ -937,9 +964,20 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
|
|||
else if (parsed.type === "response.error" || parsed.type === "error") {
|
||||
// Error event from the API
|
||||
if (parsed.error || parsed.message) {
|
||||
throw new Error(
|
||||
`Responses API error: ${parsed.error?.message || parsed.message || "Unknown error"}`,
|
||||
)
|
||||
const errMsg = `Responses API error: ${parsed.error?.message || parsed.message || "Unknown error"}`
|
||||
// For background mode, treat as terminal to avoid futile resume attempts
|
||||
if (this.currentRequestIsBackground) {
|
||||
// Surface a failed status for UI lifecycle before terminating
|
||||
yield {
|
||||
type: "status",
|
||||
mode: "background",
|
||||
status: "failed",
|
||||
...(parsed.response?.id ? { responseId: parsed.response.id } : {}),
|
||||
}
|
||||
throw createTerminalBackgroundError(errMsg)
|
||||
}
|
||||
// Non-background: propagate as a standard error
|
||||
throw new Error(errMsg)
|
||||
}
|
||||
}
|
||||
// Handle incomplete event
|
||||
|
|
@ -975,7 +1013,7 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
|
|||
}
|
||||
// Response failed
|
||||
if (parsed.error || parsed.message) {
|
||||
throw new Error(
|
||||
throw createTerminalBackgroundError(
|
||||
`Response failed: ${parsed.error?.message || parsed.message || "Unknown failure"}`,
|
||||
)
|
||||
}
|
||||
|
|
@ -1110,6 +1148,10 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
|
|||
// This can happen in certain edge cases and shouldn't break the flow
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
// Preserve terminal background errors so callers can avoid resume attempts
|
||||
if ((error as any).isTerminalBackgroundError) {
|
||||
throw error
|
||||
}
|
||||
throw new Error(`Error processing response stream: ${error.message}`)
|
||||
}
|
||||
throw new Error("Unexpected error processing response stream")
|
||||
|
|
@ -1147,25 +1189,25 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
|
|||
},
|
||||
})
|
||||
|
||||
if (!res.ok || !res.body) {
|
||||
if (!res.ok) {
|
||||
throw new Error(`Resume request failed (${res.status})`)
|
||||
}
|
||||
if (!res.body) {
|
||||
throw new Error("Resume request failed (no body)")
|
||||
}
|
||||
|
||||
this.resumeCutoffSequence = lastSeq
|
||||
|
||||
let emittedInProgress = false
|
||||
// Handshake accepted: immediately switch UI from reconnecting -> in_progress
|
||||
yield {
|
||||
type: "status",
|
||||
mode: "background",
|
||||
status: "in_progress",
|
||||
responseId,
|
||||
}
|
||||
|
||||
try {
|
||||
for await (const chunk of this.handleStreamResponse(res.body, model)) {
|
||||
// After the handshake and first accepted chunk, emit in_progress once
|
||||
if (!emittedInProgress) {
|
||||
emittedInProgress = true
|
||||
yield {
|
||||
type: "status",
|
||||
mode: "background",
|
||||
status: "in_progress",
|
||||
responseId,
|
||||
}
|
||||
}
|
||||
// Avoid double-emitting in_progress if the inner handler surfaces it
|
||||
if (chunk.type === "status" && (chunk as any).status === "in_progress") {
|
||||
continue
|
||||
|
|
@ -1180,9 +1222,13 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
|
|||
this.resumeCutoffSequence = undefined
|
||||
throw e
|
||||
}
|
||||
} catch {
|
||||
// Wait with backoff before next attempt
|
||||
} catch (err) {
|
||||
// If terminal error, don't keep retrying resume; fall back to polling immediately
|
||||
const delay = resumeBaseDelayMs * Math.pow(2, attempt)
|
||||
if (isTerminalBackgroundError(err)) {
|
||||
break
|
||||
}
|
||||
// Otherwise retry with backoff
|
||||
if (delay > 0) {
|
||||
await new Promise((r) => setTimeout(r, delay))
|
||||
}
|
||||
|
|
@ -1296,10 +1342,21 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
|
|||
}
|
||||
|
||||
if (status === "failed" || status === "canceled") {
|
||||
throw new Error(`Response ${status}: ${respId || responseId}`)
|
||||
const detail: string | undefined = resp?.error?.message ?? raw?.error?.message
|
||||
const msg = detail ? `Response ${status}: ${detail}` : `Response ${status}: ${respId || responseId}`
|
||||
throw createTerminalBackgroundError(msg)
|
||||
}
|
||||
} catch {
|
||||
// ignore transient poll errors
|
||||
} catch (err) {
|
||||
// If we've already emitted a terminal status, propagate to consumer to stop polling.
|
||||
if (lastEmittedStatus === "failed" || lastEmittedStatus === "canceled") {
|
||||
throw err
|
||||
}
|
||||
// Otherwise ignore transient poll errors
|
||||
}
|
||||
|
||||
// Stop polling immediately on terminal background statuses
|
||||
if (lastEmittedStatus === "failed" || lastEmittedStatus === "canceled") {
|
||||
throw new Error(`Background polling terminated with status=${lastEmittedStatus} for ${responseId}`)
|
||||
}
|
||||
|
||||
await new Promise((r) => setTimeout(r, pollIntervalMs))
|
||||
|
|
@ -1350,6 +1407,11 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
|
|||
if (mappedStatus === "completed" || mappedStatus === "failed" || mappedStatus === "canceled") {
|
||||
this.currentRequestIsBackground = undefined
|
||||
}
|
||||
// Throw terminal error to integrate with standard failure path (surfaced in UI)
|
||||
if (mappedStatus === "failed" || mappedStatus === "canceled") {
|
||||
const msg = (event as any)?.error?.message || (event as any)?.message || `Response ${mappedStatus}`
|
||||
throw createTerminalBackgroundError(msg)
|
||||
}
|
||||
// Do not return; allow further handling (e.g., usage on done/completed)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2437,6 +2437,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
|
|||
if (!chunk) {
|
||||
// Sometimes chunk is undefined, no idea that can cause
|
||||
// it, but this workaround seems to fix it.
|
||||
item = await iterator.next()
|
||||
continue
|
||||
}
|
||||
|
||||
|
|
@ -2625,7 +2626,14 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
|
|||
if (chunk.responseId) {
|
||||
;(apiReqMsg as any).metadata.responseId = chunk.responseId
|
||||
}
|
||||
// Temporary debug to confirm UI metadata updates
|
||||
console.log(
|
||||
`[BackgroundMode] status update -> ${chunk.status} (resp=${chunk.responseId ?? "n/a"})`,
|
||||
)
|
||||
await this.updateClineMessage(apiReqMsg)
|
||||
// Force state refresh to ensure UI recomputes derived labels/memos
|
||||
const provider = this.providerRef.deref()
|
||||
await provider?.postStateToWebview()
|
||||
}
|
||||
} catch {}
|
||||
break
|
||||
|
|
@ -2704,6 +2712,10 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
|
|||
"\n\n[Response interrupted by a tool use result. Only one tool may be used at a time and should be placed at the end of the message.]"
|
||||
break
|
||||
}
|
||||
// Prefetch the next item after processing the current chunk.
|
||||
// This ensures terminal status chunks (e.g., failed/canceled/completed)
|
||||
// are not skipped when the provider throws on the following next().
|
||||
item = await iterator.next()
|
||||
}
|
||||
|
||||
// Finalize any remaining streaming tool calls that weren't explicitly ended
|
||||
|
|
@ -3186,8 +3198,31 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
|
|||
continue
|
||||
} else {
|
||||
// If there's no assistant_responses, that means we got no text
|
||||
// or tool_use content blocks from API which we should assume is
|
||||
// an error.
|
||||
// or tool_use content blocks from API which we should assume is an error.
|
||||
// Prefer any streaming failure details captured on the last api_req_started message.
|
||||
let errorText =
|
||||
"Unexpected API Response: The language model did not provide any assistant messages. This may indicate an issue with the API or the model's output."
|
||||
try {
|
||||
const lastApiReqStartedIdx = findLastIndex(
|
||||
this.clineMessages,
|
||||
(m) => m.type === "say" && m.say === "api_req_started",
|
||||
)
|
||||
if (lastApiReqStartedIdx !== -1) {
|
||||
const info = JSON.parse(
|
||||
this.clineMessages[lastApiReqStartedIdx].text || "{}",
|
||||
) as ClineApiReqInfo
|
||||
if (
|
||||
typeof info?.streamingFailedMessage === "string" &&
|
||||
info.streamingFailedMessage.trim().length > 0
|
||||
) {
|
||||
errorText = info.streamingFailedMessage
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// ignore parse issues and keep default message
|
||||
}
|
||||
|
||||
await this.say("error", errorText)
|
||||
|
||||
// IMPORTANT: For native tool protocol, we already added the user message to
|
||||
// apiConversationHistory at line 1876. Since the assistant failed to respond,
|
||||
|
|
|
|||
|
|
@ -505,27 +505,49 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
return false
|
||||
}
|
||||
|
||||
const isLastMessagePartial = modifiedMessages.at(-1)?.partial === true
|
||||
// Find the last api_req_started to inspect background status + payload
|
||||
const lastApiReqStarted = findLast(
|
||||
modifiedMessages,
|
||||
(message: ClineMessage) => message.say === "api_req_started",
|
||||
)
|
||||
|
||||
// Extract background terminal state and cancel reason/cost if present
|
||||
let bgDone = false
|
||||
let cancelReason: string | null | undefined = undefined
|
||||
let cost: any = undefined
|
||||
|
||||
if (lastApiReqStarted && lastApiReqStarted.say === "api_req_started") {
|
||||
const meta: any = (lastApiReqStarted as any).metadata
|
||||
const bgStatus = meta?.background === true ? meta?.backgroundStatus : undefined
|
||||
bgDone = bgStatus === "completed" || bgStatus === "failed" || bgStatus === "canceled"
|
||||
|
||||
try {
|
||||
if (lastApiReqStarted.text !== null && lastApiReqStarted.text !== undefined) {
|
||||
const info = JSON.parse(lastApiReqStarted.text)
|
||||
cost = info?.cost
|
||||
cancelReason = info?.cancelReason
|
||||
}
|
||||
} catch {
|
||||
// ignore malformed json
|
||||
}
|
||||
}
|
||||
|
||||
// If background reached a terminal state or the provider recorded a cancel reason,
|
||||
// treat UI as not streaming regardless of partial flags or missing cost.
|
||||
if (bgDone || cancelReason != null) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Partial assistant content means streaming unless overridden by the terminal checks above.
|
||||
const isLastMessagePartial = modifiedMessages.at(-1)?.partial === true
|
||||
if (isLastMessagePartial) {
|
||||
return true
|
||||
} else {
|
||||
const lastApiReqStarted = findLast(
|
||||
modifiedMessages,
|
||||
(message: ClineMessage) => message.say === "api_req_started",
|
||||
)
|
||||
}
|
||||
|
||||
if (
|
||||
lastApiReqStarted &&
|
||||
lastApiReqStarted.text !== null &&
|
||||
lastApiReqStarted.text !== undefined &&
|
||||
lastApiReqStarted.say === "api_req_started"
|
||||
) {
|
||||
const cost = JSON.parse(lastApiReqStarted.text).cost
|
||||
|
||||
if (cost === undefined) {
|
||||
return true // API request has not finished yet.
|
||||
}
|
||||
// Otherwise, if the API request hasn't finished (no cost yet), consider it streaming.
|
||||
if (lastApiReqStarted && lastApiReqStarted.say === "api_req_started") {
|
||||
if (cost === undefined) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue