From bd553962665bdb8b109d76e8dc57fe37b3caeb3a Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Thu, 21 May 2026 12:54:56 +0900 Subject: [PATCH] fix(look-at): avoid empty stable idle completion --- src/tools/look-at/look-at-session-runner.ts | 3 +++ src/tools/look-at/session-poller.test.ts | 12 ++++++++++++ src/tools/look-at/session-poller.ts | 6 +++++- 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/tools/look-at/look-at-session-runner.ts b/src/tools/look-at/look-at-session-runner.ts index 746dc9682..db8850156 100644 --- a/src/tools/look-at/look-at-session-runner.ts +++ b/src/tools/look-at/look-at-session-runner.ts @@ -61,6 +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 try { await promptSyncWithModelSuggestionRetry(ctx.client, { path: { id: sessionID }, @@ -83,6 +84,7 @@ Original error: ${createResult.error}` queueBehavior: "defer", }) } catch (promptError) { + promptFailed = true log("[look_at] Prompt error (ignored, will still fetch messages):", promptError) } @@ -91,6 +93,7 @@ Original error: ${createResult.error}` if (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 9f6d093c9..69b4c8b9e 100644 --- a/src/tools/look-at/session-poller.test.ts +++ b/src/tools/look-at/session-poller.test.ts @@ -78,6 +78,18 @@ describe("waitForLookAtSessionResult", () => { expect(result.outcome.text).toBe("done") }) + test("#given session is absent and has no assistant output #when stable idle is allowed #then keeps polling", async () => { + const client = createMockClient([{ data: {} }], []) + + await expect( + waitForLookAtSessionResult(unsafeTestValue(client), "ses_test", { + pollIntervalMs: 10, + timeoutMs: 50, + allowStableIdleWithoutActivity: true, + }), + ).rejects.toThrow("timed out") + }) + 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 152022c57..e17e9cd2e 100644 --- a/src/tools/look-at/session-poller.ts +++ b/src/tools/look-at/session-poller.ts @@ -9,6 +9,7 @@ export interface PollOptions { timeoutMs?: number abortSignal?: AbortSignal allowStableIdleWithoutActivity?: boolean + allowEmptyStableIdleWithoutActivity?: boolean } const DEFAULT_POLL_INTERVAL_MS = 1000 @@ -136,7 +137,10 @@ export async function waitForLookAtSessionResult( const canConcludeIdle = sawNonIdleStatus || !status.supported || - Boolean(options?.allowStableIdleWithoutActivity) + ( + Boolean(options?.allowStableIdleWithoutActivity) + && (outcome.hasAssistant || Boolean(options?.allowEmptyStableIdleWithoutActivity)) + ) if (canConcludeIdle && stableIdlePolls >= IDLE_STABILITY_POLLS_REQUIRED) { return { messages, outcome, statusType }