fix(sync-continuation): improve error handling and toast cleanup
- Add proper error handling in executeSyncContinuation with try-catch blocks - Ensure toast cleanup happens in all error paths via finally block - Add anchorMessageCount tracking for accurate result fetching after continuation - Improve fetchSyncResult to filter messages after anchor point - Add silent failure detection when no new response is generated
This commit is contained in:
parent
45dfc4ec66
commit
231e790a0c
@ -50,11 +50,13 @@ export async function executeSyncContinuation(
|
|||||||
let resumeAgent: string | undefined
|
let resumeAgent: string | undefined
|
||||||
let resumeModel: { providerID: string; modelID: string } | undefined
|
let resumeModel: { providerID: string; modelID: string } | undefined
|
||||||
let resumeVariant: string | undefined
|
let resumeVariant: string | undefined
|
||||||
|
let anchorMessageCount: number | undefined
|
||||||
|
|
||||||
try {
|
try {
|
||||||
try {
|
try {
|
||||||
const messagesResp = await client.session.messages({ path: { id: args.session_id! } })
|
const messagesResp = await client.session.messages({ path: { id: args.session_id! } })
|
||||||
const messages = (messagesResp.data ?? []) as SessionMessage[]
|
const messages = (messagesResp.data ?? []) as SessionMessage[]
|
||||||
|
anchorMessageCount = messages.length
|
||||||
for (let i = messages.length - 1; i >= 0; i--) {
|
for (let i = messages.length - 1; i >= 0; i--) {
|
||||||
const info = messages[i].info
|
const info = messages[i].info
|
||||||
if (info?.agent || info?.model || (info?.modelID && info?.providerID)) {
|
if (info?.agent || info?.model || (info?.modelID && info?.providerID)) {
|
||||||
@ -91,39 +93,34 @@ export async function executeSyncContinuation(
|
|||||||
parts: [{ type: "text", text: args.prompt }],
|
parts: [{ type: "text", text: args.prompt }],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
} catch (promptError) {
|
} catch (promptError) {
|
||||||
if (toastManager) {
|
if (toastManager) {
|
||||||
toastManager.removeTask(taskId)
|
toastManager.removeTask(taskId)
|
||||||
}
|
}
|
||||||
const errorMessage = promptError instanceof Error ? promptError.message : String(promptError)
|
const errorMessage = promptError instanceof Error ? promptError.message : String(promptError)
|
||||||
return `Failed to send continuation prompt: ${errorMessage}\n\nSession ID: ${args.session_id}`
|
return `Failed to send continuation prompt: ${errorMessage}\n\nSession ID: ${args.session_id}`
|
||||||
}
|
}
|
||||||
|
|
||||||
const pollError = await pollSyncSession(ctx, client, {
|
try {
|
||||||
sessionID: args.session_id!,
|
const pollError = await pollSyncSession(ctx, client, {
|
||||||
agentToUse: resumeAgent ?? "continue",
|
sessionID: args.session_id!,
|
||||||
toastManager,
|
agentToUse: resumeAgent ?? "continue",
|
||||||
taskId,
|
toastManager,
|
||||||
})
|
taskId,
|
||||||
if (pollError) {
|
anchorMessageCount,
|
||||||
return pollError
|
})
|
||||||
}
|
if (pollError) {
|
||||||
|
return pollError
|
||||||
|
}
|
||||||
|
|
||||||
const result = await fetchSyncResult(client, args.session_id!)
|
const result = await fetchSyncResult(client, args.session_id!, anchorMessageCount)
|
||||||
if (!result.ok) {
|
if (!result.ok) {
|
||||||
if (toastManager) {
|
return result.error
|
||||||
toastManager.removeTask(taskId)
|
}
|
||||||
}
|
|
||||||
return result.error
|
|
||||||
}
|
|
||||||
|
|
||||||
if (toastManager) {
|
const duration = formatDuration(startTime)
|
||||||
toastManager.removeTask(taskId)
|
|
||||||
}
|
|
||||||
|
|
||||||
const duration = formatDuration(startTime)
|
return `Task continued and completed in ${duration}.
|
||||||
|
|
||||||
return `Task continued and completed in ${duration}.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -132,4 +129,9 @@ ${result.textContent || "(No text output)"}
|
|||||||
<task_metadata>
|
<task_metadata>
|
||||||
session_id: ${args.session_id}
|
session_id: ${args.session_id}
|
||||||
</task_metadata>`
|
</task_metadata>`
|
||||||
|
} finally {
|
||||||
|
if (toastManager) {
|
||||||
|
toastManager.removeTask(taskId)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,8 @@ import type { SessionMessage } from "./executor-types"
|
|||||||
|
|
||||||
export async function fetchSyncResult(
|
export async function fetchSyncResult(
|
||||||
client: OpencodeClient,
|
client: OpencodeClient,
|
||||||
sessionID: string
|
sessionID: string,
|
||||||
|
anchorMessageCount?: number
|
||||||
): Promise<{ ok: true; textContent: string } | { ok: false; error: string }> {
|
): Promise<{ ok: true; textContent: string } | { ok: false; error: string }> {
|
||||||
const messagesResult = await client.session.messages({
|
const messagesResult = await client.session.messages({
|
||||||
path: { id: sessionID },
|
path: { id: sessionID },
|
||||||
@ -15,11 +16,27 @@ export async function fetchSyncResult(
|
|||||||
|
|
||||||
const messages = ((messagesResult as { data?: unknown }).data ?? messagesResult) as SessionMessage[]
|
const messages = ((messagesResult as { data?: unknown }).data ?? messagesResult) as SessionMessage[]
|
||||||
|
|
||||||
const assistantMessages = messages
|
const messagesAfterAnchor = anchorMessageCount !== undefined ? messages.slice(anchorMessageCount) : messages
|
||||||
|
|
||||||
|
if (anchorMessageCount !== undefined && messagesAfterAnchor.length === 0) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: `Session completed but no new response was generated. The model may have failed silently.\n\nSession ID: ${sessionID}`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const assistantMessages = messagesAfterAnchor
|
||||||
.filter((m) => m.info?.role === "assistant")
|
.filter((m) => m.info?.role === "assistant")
|
||||||
.sort((a, b) => (b.info?.time?.created ?? 0) - (a.info?.time?.created ?? 0))
|
.sort((a, b) => (b.info?.time?.created ?? 0) - (a.info?.time?.created ?? 0))
|
||||||
const lastMessage = assistantMessages[0]
|
const lastMessage = assistantMessages[0]
|
||||||
|
|
||||||
|
if (anchorMessageCount !== undefined && !lastMessage) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: `Session completed but no new response was generated. The model may have failed silently.\n\nSession ID: ${sessionID}`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!lastMessage) {
|
if (!lastMessage) {
|
||||||
return { ok: false, error: `No assistant response found.\n\nSession ID: ${sessionID}` }
|
return { ok: false, error: `No assistant response found.\n\nSession ID: ${sessionID}` }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,6 +30,7 @@ export async function pollSyncSession(
|
|||||||
agentToUse: string
|
agentToUse: string
|
||||||
toastManager: { removeTask: (id: string) => void } | null | undefined
|
toastManager: { removeTask: (id: string) => void } | null | undefined
|
||||||
taskId: string | undefined
|
taskId: string | undefined
|
||||||
|
anchorMessageCount?: number
|
||||||
}
|
}
|
||||||
): Promise<string | null> {
|
): Promise<string | null> {
|
||||||
const syncTiming = getTimingConfig()
|
const syncTiming = getTimingConfig()
|
||||||
@ -48,7 +49,13 @@ export async function pollSyncSession(
|
|||||||
await new Promise(resolve => setTimeout(resolve, syncTiming.POLL_INTERVAL_MS))
|
await new Promise(resolve => setTimeout(resolve, syncTiming.POLL_INTERVAL_MS))
|
||||||
pollCount++
|
pollCount++
|
||||||
|
|
||||||
const statusResult = await client.session.status()
|
let statusResult: { data?: Record<string, { type: string }> }
|
||||||
|
try {
|
||||||
|
statusResult = await client.session.status()
|
||||||
|
} catch (error) {
|
||||||
|
log("[task] Poll status fetch failed, retrying", { sessionID: input.sessionID, error: String(error) })
|
||||||
|
continue
|
||||||
|
}
|
||||||
const allStatuses = (statusResult.data ?? {}) as Record<string, { type: string }>
|
const allStatuses = (statusResult.data ?? {}) as Record<string, { type: string }>
|
||||||
const sessionStatus = allStatuses[input.sessionID]
|
const sessionStatus = allStatuses[input.sessionID]
|
||||||
|
|
||||||
@ -65,8 +72,19 @@ export async function pollSyncSession(
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
const messagesResult = await client.session.messages({ path: { id: input.sessionID } })
|
let messagesResult: { data?: unknown } | SessionMessage[]
|
||||||
const msgs = ((messagesResult as { data?: unknown }).data ?? messagesResult) as SessionMessage[]
|
try {
|
||||||
|
messagesResult = await client.session.messages({ path: { id: input.sessionID } })
|
||||||
|
} catch (error) {
|
||||||
|
log("[task] Poll messages fetch failed, retrying", { sessionID: input.sessionID, error: String(error) })
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const rawData = (messagesResult as { data?: unknown })?.data ?? messagesResult
|
||||||
|
const msgs = Array.isArray(rawData) ? (rawData as SessionMessage[]) : []
|
||||||
|
|
||||||
|
if (input.anchorMessageCount !== undefined && msgs.length <= input.anchorMessageCount) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
if (isSessionComplete(msgs)) {
|
if (isSessionComplete(msgs)) {
|
||||||
log("[task] Poll complete - terminal finish detected", { sessionID: input.sessionID, pollCount })
|
log("[task] Poll complete - terminal finish detected", { sessionID: input.sessionID, pollCount })
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user