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:
@@ -50,11 +50,13 @@ export async function executeSyncContinuation(
|
||||
let resumeAgent: string | undefined
|
||||
let resumeModel: { providerID: string; modelID: string } | undefined
|
||||
let resumeVariant: string | undefined
|
||||
let anchorMessageCount: number | undefined
|
||||
|
||||
try {
|
||||
try {
|
||||
const messagesResp = await client.session.messages({ path: { id: args.session_id! } })
|
||||
const messages = (messagesResp.data ?? []) as SessionMessage[]
|
||||
anchorMessageCount = messages.length
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
const info = messages[i].info
|
||||
if (info?.agent || info?.model || (info?.modelID && info?.providerID)) {
|
||||
@@ -91,39 +93,34 @@ export async function executeSyncContinuation(
|
||||
parts: [{ type: "text", text: args.prompt }],
|
||||
},
|
||||
})
|
||||
} catch (promptError) {
|
||||
if (toastManager) {
|
||||
toastManager.removeTask(taskId)
|
||||
}
|
||||
const errorMessage = promptError instanceof Error ? promptError.message : String(promptError)
|
||||
return `Failed to send continuation prompt: ${errorMessage}\n\nSession ID: ${args.session_id}`
|
||||
}
|
||||
} catch (promptError) {
|
||||
if (toastManager) {
|
||||
toastManager.removeTask(taskId)
|
||||
}
|
||||
const errorMessage = promptError instanceof Error ? promptError.message : String(promptError)
|
||||
return `Failed to send continuation prompt: ${errorMessage}\n\nSession ID: ${args.session_id}`
|
||||
}
|
||||
|
||||
const pollError = await pollSyncSession(ctx, client, {
|
||||
sessionID: args.session_id!,
|
||||
agentToUse: resumeAgent ?? "continue",
|
||||
toastManager,
|
||||
taskId,
|
||||
})
|
||||
if (pollError) {
|
||||
return pollError
|
||||
}
|
||||
try {
|
||||
const pollError = await pollSyncSession(ctx, client, {
|
||||
sessionID: args.session_id!,
|
||||
agentToUse: resumeAgent ?? "continue",
|
||||
toastManager,
|
||||
taskId,
|
||||
anchorMessageCount,
|
||||
})
|
||||
if (pollError) {
|
||||
return pollError
|
||||
}
|
||||
|
||||
const result = await fetchSyncResult(client, args.session_id!)
|
||||
if (!result.ok) {
|
||||
if (toastManager) {
|
||||
toastManager.removeTask(taskId)
|
||||
}
|
||||
return result.error
|
||||
}
|
||||
const result = await fetchSyncResult(client, args.session_id!, anchorMessageCount)
|
||||
if (!result.ok) {
|
||||
return result.error
|
||||
}
|
||||
|
||||
if (toastManager) {
|
||||
toastManager.removeTask(taskId)
|
||||
}
|
||||
const duration = formatDuration(startTime)
|
||||
|
||||
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>
|
||||
session_id: ${args.session_id}
|
||||
</task_metadata>`
|
||||
} finally {
|
||||
if (toastManager) {
|
||||
toastManager.removeTask(taskId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,8 @@ import type { SessionMessage } from "./executor-types"
|
||||
|
||||
export async function fetchSyncResult(
|
||||
client: OpencodeClient,
|
||||
sessionID: string
|
||||
sessionID: string,
|
||||
anchorMessageCount?: number
|
||||
): Promise<{ ok: true; textContent: string } | { ok: false; error: string }> {
|
||||
const messagesResult = await client.session.messages({
|
||||
path: { id: sessionID },
|
||||
@@ -15,11 +16,27 @@ export async function fetchSyncResult(
|
||||
|
||||
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")
|
||||
.sort((a, b) => (b.info?.time?.created ?? 0) - (a.info?.time?.created ?? 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) {
|
||||
return { ok: false, error: `No assistant response found.\n\nSession ID: ${sessionID}` }
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ export async function pollSyncSession(
|
||||
agentToUse: string
|
||||
toastManager: { removeTask: (id: string) => void } | null | undefined
|
||||
taskId: string | undefined
|
||||
anchorMessageCount?: number
|
||||
}
|
||||
): Promise<string | null> {
|
||||
const syncTiming = getTimingConfig()
|
||||
@@ -48,7 +49,13 @@ export async function pollSyncSession(
|
||||
await new Promise(resolve => setTimeout(resolve, syncTiming.POLL_INTERVAL_MS))
|
||||
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 sessionStatus = allStatuses[input.sessionID]
|
||||
|
||||
@@ -65,8 +72,19 @@ export async function pollSyncSession(
|
||||
continue
|
||||
}
|
||||
|
||||
const messagesResult = await client.session.messages({ path: { id: input.sessionID } })
|
||||
const msgs = ((messagesResult as { data?: unknown }).data ?? messagesResult) as SessionMessage[]
|
||||
let messagesResult: { data?: unknown } | 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)) {
|
||||
log("[task] Poll complete - terminal finish detected", { sessionID: input.sessionID, pollCount })
|
||||
|
||||
Reference in New Issue
Block a user