From b3a195d6625a29c6589e72f8d73ca32663041b59 Mon Sep 17 00:00:00 2001 From: ilseob lee Date: Thu, 21 May 2026 15:48:03 +0900 Subject: [PATCH] Avoid look_at status map wait hang --- src/tools/look-at/look-at-session-runner.ts | 11 ++++---- src/tools/look-at/session-poller.test.ts | 30 +++++++++++++++++++++ src/tools/look-at/session-poller.ts | 4 +-- 3 files changed, 37 insertions(+), 8 deletions(-) diff --git a/src/tools/look-at/look-at-session-runner.ts b/src/tools/look-at/look-at-session-runner.ts index db8850156..795ef8504 100644 --- a/src/tools/look-at/look-at-session-runner.ts +++ b/src/tools/look-at/look-at-session-runner.ts @@ -1,6 +1,6 @@ import type { PluginInput } from "@opencode-ai/plugin" import type { ToolContext } from "@opencode-ai/plugin/tool" -import { log, promptSyncWithModelSuggestionRetry } from "../../shared" +import { isAmbiguousPromptDispatchFailure, log, promptSyncWithModelSuggestionRetry } from "../../shared" import { extractLatestAssistantText } from "./assistant-message-extractor" import { MULTIMODAL_LOOKER_AGENT } from "./constants" import { READ_ENABLED, buildLookAtPrompt } from "./look-at-prompt" @@ -61,7 +61,7 @@ Original error: ${createResult.error}` log(`[look_at] Created session: ${sessionID}`) log(`[look_at] Sending prompt with ${isBase64Input ? "base64 image" : "file"} to session ${sessionID}`) - let promptFailed = false + let shouldWaitForStatus = true try { await promptSyncWithModelSuggestionRetry(ctx.client, { path: { id: sessionID }, @@ -84,16 +84,15 @@ Original error: ${createResult.error}` queueBehavior: "defer", }) } catch (promptError) { - promptFailed = true - log("[look_at] Prompt error (ignored, will still fetch messages):", promptError) + log("[look_at] Prompt dispatch failed; checking child session evidence:", promptError) + shouldWaitForStatus = isAmbiguousPromptDispatchFailure(promptError) } let observedMessages: unknown[] | undefined let observedText: string | undefined - if (typeof ctx.client.session.status === "function") { + if (shouldWaitForStatus && typeof ctx.client.session.status === "function") { const waitResult = await waitForLookAtSessionResult(ctx.client, sessionID, { allowStableIdleWithoutActivity: true, - allowEmptyStableIdleWithoutActivity: promptFailed, }) observedText = waitResult.outcome.text ?? undefined if (observedText) { diff --git a/src/tools/look-at/session-poller.test.ts b/src/tools/look-at/session-poller.test.ts index 69b4c8b9e..1996de762 100644 --- a/src/tools/look-at/session-poller.test.ts +++ b/src/tools/look-at/session-poller.test.ts @@ -90,6 +90,36 @@ describe("waitForLookAtSessionResult", () => { ).rejects.toThrow("timed out") }) + test("#given status omits session before it starts #when later idle has response #then waits instead of treating empty status as done", async () => { + const assistantMessages: RawMessage[] = [ + { info: { role: "user" }, parts: [{ type: "text", text: "analyze this" }] }, + { info: { role: "assistant" }, parts: [{ type: "text", text: "late result" }] }, + ] + let statusCalls = 0 + const client = { + session: { + status: mock(async () => { + statusCalls += 1 + if (statusCalls <= 3) return { data: {} } + if (statusCalls === 4) return { data: { ses_test: { type: "busy" } } } + return { data: { ses_test: { type: "idle" } } } + }), + messages: mock(async () => ({ + data: statusCalls >= 5 ? assistantMessages : [], + error: null, + })), + }, + } + + const result = await waitForLookAtSessionResult(unsafeTestValue(client), "ses_test", { + pollIntervalMs: 10, + timeoutMs: 5000, + }) + + expect(result.outcome.text).toBe("late result") + expect(statusCalls).toBe(5) + }) + test("#given session never becomes idle #when polling exceeds timeout #then rejects", async () => { const client = createMockClient( [{ data: { ses_test: { type: "busy" } } }], diff --git a/src/tools/look-at/session-poller.ts b/src/tools/look-at/session-poller.ts index e17e9cd2e..406d4f019 100644 --- a/src/tools/look-at/session-poller.ts +++ b/src/tools/look-at/session-poller.ts @@ -109,11 +109,11 @@ export async function waitForLookAtSessionResult( const { messages, error: messagesError } = await getSessionMessages(client, sessionID) const outcome = extractLatestAssistantOutcome(messages) - if (outcome.text && !isActive) { + if (outcome.text && (!isActive || supportedButNeverSeen)) { return { messages, outcome, statusType } } - if (outcome.errorName && !isActive) { + if (outcome.errorName && (!isActive || supportedButNeverSeen)) { return { messages, outcome, statusType } }