From 3a2f358d2731cec021907b8624b5de15bd98295e Mon Sep 17 00:00:00 2001 From: James Feng <47167674+GhostDragon124@users.noreply.github.com> Date: Mon, 8 Jun 2026 20:07:53 +0800 Subject: [PATCH] fix(acp): restore IteratorResult return type to nextSdkMessageOrAbort MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The simplified SDKMessage | undefined return type collapsed two distinct states: generator truly done vs generator yielding undefined. This broke forwardSessionUpdates which needs to distinguish the two — when the generator yields null/undefined it should continue (calling next() again), not break out of the loop. Restored the original IteratorResult return type so done and yielded-null are distinct again. --- src/services/acp/__tests__/bridge.test.ts | 12 ++++---- src/services/acp/bridge.ts | 36 ++++++++++------------- 2 files changed, 22 insertions(+), 26 deletions(-) diff --git a/src/services/acp/__tests__/bridge.test.ts b/src/services/acp/__tests__/bridge.test.ts index 5333bd532..b5a9cece2 100644 --- a/src/services/acp/__tests__/bridge.test.ts +++ b/src/services/acp/__tests__/bridge.test.ts @@ -698,7 +698,7 @@ describe('toDisplayPath', () => { // ── forwardSessionUpdates ───────────────────────────────────────── describe('nextSdkMessageOrAbort', () => { - test('returns undefined when aborted while waiting for next message', async () => { + test('returns done:true when aborted while waiting for next message', async () => { const ac = new AbortController() const pending = nextSdkMessageOrAbort(makeWaitingStream(), ac.signal) ac.abort() @@ -708,19 +708,19 @@ describe('nextSdkMessageOrAbort', () => { new Promise<'timeout'>(resolve => setTimeout(resolve, 100, 'timeout')), ]) - expect(result).toBeUndefined() + expect(result).toEqual({ done: true, value: undefined }) }) - test('returns undefined when stream is done', async () => { + test('returns done:true when stream is done', async () => { const result = await nextSdkMessageOrAbort( makeStream([]), new AbortController().signal, ) - expect(result).toBeUndefined() + expect(result).toEqual({ done: true, value: undefined }) }) - test('returns a valid SDKMessage', async () => { + test('returns a valid SDKMessage via IteratorResult', async () => { const msg = { type: 'assistant', message: { @@ -734,7 +734,7 @@ describe('nextSdkMessageOrAbort', () => { new AbortController().signal, ) - expect(result).toBe(msg) + expect(result).toEqual({ done: false, value: msg }) }) }) diff --git a/src/services/acp/bridge.ts b/src/services/acp/bridge.ts index 4e8f06172..2c51c0f51 100644 --- a/src/services/acp/bridge.ts +++ b/src/services/acp/bridge.ts @@ -719,28 +719,22 @@ export function toolUpdateFromEditToolResponse(toolResponse: unknown): { export function nextSdkMessageOrAbort( sdkMessages: AsyncGenerator, abortSignal: AbortSignal, -): Promise { +): Promise> { if (abortSignal.aborted) { - return Promise.resolve(undefined) + return Promise.resolve({ done: true, value: undefined }) } let abortHandler: (() => void) | undefined - const abortPromise = new Promise(resolve => { - abortHandler = () => resolve(undefined) - abortSignal.addEventListener('abort', abortHandler, { once: true }) + const abortPromise = new Promise>( + resolve => { + abortHandler = () => resolve({ done: true, value: undefined }) + abortSignal.addEventListener('abort', abortHandler, { once: true }) + }, + ) + return Promise.race([sdkMessages.next(), abortPromise]).finally(() => { + if (abortHandler) { + abortSignal.removeEventListener('abort', abortHandler) + } }) - const nextPromise = sdkMessages.next() - return Promise.race([nextPromise, abortPromise]) - .then(result => { - if (result === undefined) { - void nextPromise.catch(() => {}) - return undefined - } - if (result.done === true) return undefined - return result.value - }) - .finally(() => { - if (abortHandler) abortSignal.removeEventListener('abort', abortHandler) - }) } // ── Main forwarding function ────────────────────────────────────── @@ -779,8 +773,10 @@ export async function forwardSessionUpdates( // Race the next message against the abort signal so we unblock // immediately when cancelled, even if the generator is waiting for // a slow API response. - const rawMsg = await nextSdkMessageOrAbort(sdkMessages, abortSignal) - if (rawMsg === undefined || abortSignal.aborted) break + const nextResult = await nextSdkMessageOrAbort(sdkMessages, abortSignal) + if (nextResult.done || abortSignal.aborted) break + const rawMsg = nextResult.value + if (rawMsg == null) continue const msg = rawMsg as BridgeSDKMessage switch (msg.type) {