From 231e790a0c3dcba2a0e1be223ab8d30effc09096 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Tue, 10 Feb 2026 16:17:24 +0900 Subject: [PATCH] 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 --- src/tools/delegate-task/sync-continuation.ts | 60 ++++++++++--------- .../delegate-task/sync-result-fetcher.ts | 21 ++++++- .../delegate-task/sync-session-poller.ts | 24 +++++++- 3 files changed, 71 insertions(+), 34 deletions(-) diff --git a/src/tools/delegate-task/sync-continuation.ts b/src/tools/delegate-task/sync-continuation.ts index 7c3eadd8..61fa5898 100644 --- a/src/tools/delegate-task/sync-continuation.ts +++ b/src/tools/delegate-task/sync-continuation.ts @@ -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)"} session_id: ${args.session_id} ` + } finally { + if (toastManager) { + toastManager.removeTask(taskId) + } + } } diff --git a/src/tools/delegate-task/sync-result-fetcher.ts b/src/tools/delegate-task/sync-result-fetcher.ts index 977f93b7..64d1a278 100644 --- a/src/tools/delegate-task/sync-result-fetcher.ts +++ b/src/tools/delegate-task/sync-result-fetcher.ts @@ -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}` } } diff --git a/src/tools/delegate-task/sync-session-poller.ts b/src/tools/delegate-task/sync-session-poller.ts index 00f6fd96..458267e4 100644 --- a/src/tools/delegate-task/sync-session-poller.ts +++ b/src/tools/delegate-task/sync-session-poller.ts @@ -30,6 +30,7 @@ export async function pollSyncSession( agentToUse: string toastManager: { removeTask: (id: string) => void } | null | undefined taskId: string | undefined + anchorMessageCount?: number } ): Promise { 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 } + 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 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 })