diff --git a/apps/mobile/lib/locales/de.ts b/apps/mobile/lib/locales/de.ts index ac8dff8..aea56e1 100644 --- a/apps/mobile/lib/locales/de.ts +++ b/apps/mobile/lib/locales/de.ts @@ -261,6 +261,8 @@ export const DE_MESSAGES: Record = { Eyeshadow: "Lidschatten", Failed: "Fehlgeschlagen", "Failed to refresh": "Aktualisieren fehlgeschlagen", + Resume: "Fortsetzen", + Stopped: "Gestoppt", "Failed to remove message": "Nachricht konnte nicht entfernt werden", "Failed to send message": "Nachricht konnte nicht gesendet werden", "Failed to stop work": "Arbeit konnte nicht gestoppt werden", diff --git a/apps/mobile/lib/locales/ru.ts b/apps/mobile/lib/locales/ru.ts index 70f4e44..93b41f6 100644 --- a/apps/mobile/lib/locales/ru.ts +++ b/apps/mobile/lib/locales/ru.ts @@ -274,6 +274,8 @@ export const RU_MESSAGES: Record = { "Executor token": "Токен Executor", Failed: "Ошибка", "Failed to refresh": "Не удалось обновить", + Resume: "Продолжить", + Stopped: "Остановлено", "Failed to remove message": "Не удалось удалить сообщение", "Failed to send message": "Не удалось отправить сообщение", "Failed to stop work": "Не удалось остановить работу", diff --git a/apps/mobile/lib/locales/zh.ts b/apps/mobile/lib/locales/zh.ts index 96e6b95..01f96f7 100644 --- a/apps/mobile/lib/locales/zh.ts +++ b/apps/mobile/lib/locales/zh.ts @@ -259,6 +259,8 @@ export const ZH_MESSAGES: Record = { "Executor token": "Executor 令牌", Failed: "失败", "Failed to refresh": "刷新失败", + Resume: "继续", + Stopped: "已停止", "Failed to remove message": "删除消息失败", "Failed to send message": "发送消息失败", "Failed to stop work": "停止工作失败", diff --git a/apps/web/e2e/group-chats.spec.ts b/apps/web/e2e/group-chats.spec.ts index a3eceb7..30f23bf 100644 --- a/apps/web/e2e/group-chats.spec.ts +++ b/apps/web/e2e/group-chats.spec.ts @@ -63,6 +63,10 @@ test("create group from + and see two bots in one transcript", async ({ page }, const researcherId = await createBot(page, "Researcher"); const writerId = await createBot(page, "Research Writer"); + // Replies read themselves aloud by default; keep that off so only the Speak click below talks. + for (const botId of [researcherId, writerId]) { + await rpc(page, "bots/update", { botId, autoSpeak: false }); + } await openNewGroup(page); await page.locator("label:has-text('Name') input").fill("Draft team"); diff --git a/apps/web/e2e/voice.spec.ts b/apps/web/e2e/voice.spec.ts index b54be0f..0458bf6 100644 --- a/apps/web/e2e/voice.spec.ts +++ b/apps/web/e2e/voice.spec.ts @@ -7,17 +7,15 @@ async function turnOnSpeaking(page: Page) { await expect(voice).toBeVisible(); await expect(voice.getByRole("button", { name: "Other voice providers" })).toHaveCount(0); await expect(voice.getByLabel("API key", { exact: true })).toHaveCount(0); - // The app turns the local voice on by itself the first time it starts. + // The app turns the local voice on by itself the first time it starts, and the switch stays + // disabled while that runs; only click once it settled off. const speaking = voice.getByRole("switch", { name: "Speaking on or off" }); - // Clicking before the automatic start has settled would turn the voice off again. - const startedByItself = await expect(speaking) - .toHaveAttribute("aria-checked", "true", { timeout: 5_000 }) - .then( - () => true, - () => false, - ); - if (!startedByItself) await speaking.click(); - await expect(speaking).toHaveAttribute("aria-checked", "true"); + await expect(async () => { + if ((await speaking.getAttribute("aria-checked")) !== "true" && (await speaking.isEnabled())) { + await speaking.click(); + } + await expect(speaking).toHaveAttribute("aria-checked", "true", { timeout: 3_000 }); + }).toPass({ timeout: 30_000 }); await expect(voice.getByTestId("voice-carousel")).toBeVisible(); return voice; } diff --git a/apps/web/src/components/browser/InAppBrowser.tsx b/apps/web/src/components/browser/InAppBrowser.tsx index 07fcd86..4d744c9 100644 --- a/apps/web/src/components/browser/InAppBrowser.tsx +++ b/apps/web/src/components/browser/InAppBrowser.tsx @@ -18,6 +18,7 @@ export function InAppBrowser({ sandboxProvider, booting, onRefreshScreen, + onConnectScreen, onOpenComputer, hideCloseButton = false, }: { @@ -29,6 +30,7 @@ export function InAppBrowser({ sandboxProvider: string | undefined; booting: boolean; onRefreshScreen: () => Promise; + onConnectScreen: () => Promise; onOpenComputer?: () => void; hideCloseButton?: boolean; }) { @@ -92,6 +94,7 @@ export function InAppBrowser({ sandboxProvider={sandboxProvider} booting={booting} onRefreshScreen={onRefreshScreen} + onConnectScreen={onConnectScreen} onOpenComputer={onOpenComputer} /> ) : ( diff --git a/apps/web/src/components/browser/OoComputerBrowser.tsx b/apps/web/src/components/browser/OoComputerBrowser.tsx index 4b4155b..7b21409 100644 --- a/apps/web/src/components/browser/OoComputerBrowser.tsx +++ b/apps/web/src/components/browser/OoComputerBrowser.tsx @@ -35,6 +35,7 @@ export function OoComputerBrowser({ sandboxProvider, booting, onRefreshScreen, + onConnectScreen, onOpenComputer, }: { active: Bot; @@ -44,6 +45,8 @@ export function OoComputerBrowser({ sandboxProvider: string | undefined; booting: boolean; onRefreshScreen: () => Promise; + /** Fetches the screen of an already running computer, without touching the thread. */ + onConnectScreen: () => Promise; /** Opens the full computer window, where the user can take control or teach. */ onOpenComputer?: () => void; }) { @@ -88,12 +91,12 @@ export function OoComputerBrowser({ }; }, [active.id, computer?.state, refreshChrome]); - const refreshScreen = useRef(onRefreshScreen); - refreshScreen.current = onRefreshScreen; + const connectScreen = useRef(onConnectScreen); + connectScreen.current = onConnectScreen; // Showing the browser never starts a stopped computer; it only reconnects a running screen. useEffect(() => { if (computer?.state !== "running" || embeddedScreenUrl || computer?.kind === "desktop") return; - void refreshScreen.current().catch(() => undefined); + void connectScreen.current().catch(() => undefined); }, [active.id, computer?.kind, computer?.state, embeddedScreenUrl]); const runNav = useCallback( diff --git a/apps/web/src/locales/de/messages.po b/apps/web/src/locales/de/messages.po index e3adf50..2a93096 100644 --- a/apps/web/src/locales/de/messages.po +++ b/apps/web/src/locales/de/messages.po @@ -2354,6 +2354,7 @@ msgstr "Verbindung wird getrennt…" #: src/components/ComputerUpdateProgress.tsx #: src/components/teach/TeachComputerOverlay.tsx #: src/components/ThreadCard.tsx +#: src/pages/shell/composer.tsx #: src/pages/shell/message-cards.tsx msgid "Dismiss" msgstr "Ausblenden" @@ -4720,6 +4721,11 @@ msgstr "Stellt den zuletzt gespeicherten Arbeitsbereich wieder her. Nicht gespei msgid "Restoring your workspace" msgstr "Dein Arbeitsbereich wird wiederhergestellt" +#: src/pages/shell/composer.tsx +#: src/pages/shell/message-cards.tsx +msgid "Resume" +msgstr "Fortsetzen" + #: src/components/FolderPickerDialog.tsx msgid "Retry" msgstr "Wiederholen" @@ -5362,6 +5368,10 @@ msgstr "Anlernen beenden" msgid "Stop timer" msgstr "Timer stoppen" +#: src/pages/shell/composer.tsx +msgid "Stopped" +msgstr "Gestoppt" + #. js-lingui-explicit-id #: src/lib/browser-notifications.ts msgid "Stopped." diff --git a/apps/web/src/locales/en/messages.po b/apps/web/src/locales/en/messages.po index aacf8d4..6c28cad 100644 --- a/apps/web/src/locales/en/messages.po +++ b/apps/web/src/locales/en/messages.po @@ -2354,6 +2354,7 @@ msgstr "Disconnecting…" #: src/components/ComputerUpdateProgress.tsx #: src/components/teach/TeachComputerOverlay.tsx #: src/components/ThreadCard.tsx +#: src/pages/shell/composer.tsx #: src/pages/shell/message-cards.tsx msgid "Dismiss" msgstr "Dismiss" @@ -4720,6 +4721,11 @@ msgstr "Restore the last saved workspace. Unsaved work on the computer is lost." msgid "Restoring your workspace" msgstr "Restoring your workspace" +#: src/pages/shell/composer.tsx +#: src/pages/shell/message-cards.tsx +msgid "Resume" +msgstr "Resume" + #: src/components/FolderPickerDialog.tsx msgid "Retry" msgstr "Retry" @@ -5362,6 +5368,10 @@ msgstr "Stop teaching" msgid "Stop timer" msgstr "Stop timer" +#: src/pages/shell/composer.tsx +msgid "Stopped" +msgstr "Stopped" + #. js-lingui-explicit-id #: src/lib/browser-notifications.ts msgid "Stopped." diff --git a/apps/web/src/locales/es/messages.po b/apps/web/src/locales/es/messages.po index 9393905..502caa2 100644 --- a/apps/web/src/locales/es/messages.po +++ b/apps/web/src/locales/es/messages.po @@ -2354,6 +2354,7 @@ msgstr "Desconectando…" #: src/components/ComputerUpdateProgress.tsx #: src/components/teach/TeachComputerOverlay.tsx #: src/components/ThreadCard.tsx +#: src/pages/shell/composer.tsx #: src/pages/shell/message-cards.tsx msgid "Dismiss" msgstr "Descartar" @@ -4720,6 +4721,11 @@ msgstr "Restaura el último espacio de trabajo guardado. Se pierde el trabajo si msgid "Restoring your workspace" msgstr "" +#: src/pages/shell/composer.tsx +#: src/pages/shell/message-cards.tsx +msgid "Resume" +msgstr "" + #: src/components/FolderPickerDialog.tsx msgid "Retry" msgstr "" @@ -5362,6 +5368,10 @@ msgstr "Detener enseñanza" msgid "Stop timer" msgstr "" +#: src/pages/shell/composer.tsx +msgid "Stopped" +msgstr "" + #. js-lingui-explicit-id #: src/lib/browser-notifications.ts msgid "Stopped." diff --git a/apps/web/src/locales/hi/messages.po b/apps/web/src/locales/hi/messages.po index eaddbc2..effdf3c 100644 --- a/apps/web/src/locales/hi/messages.po +++ b/apps/web/src/locales/hi/messages.po @@ -2354,6 +2354,7 @@ msgstr "डिस्कनेक्ट हो रहा है…" #: src/components/ComputerUpdateProgress.tsx #: src/components/teach/TeachComputerOverlay.tsx #: src/components/ThreadCard.tsx +#: src/pages/shell/composer.tsx #: src/pages/shell/message-cards.tsx msgid "Dismiss" msgstr "" @@ -4720,6 +4721,11 @@ msgstr "अंतिम सहेजे गए वर्कस्पेस क msgid "Restoring your workspace" msgstr "" +#: src/pages/shell/composer.tsx +#: src/pages/shell/message-cards.tsx +msgid "Resume" +msgstr "" + #: src/components/FolderPickerDialog.tsx msgid "Retry" msgstr "" @@ -5362,6 +5368,10 @@ msgstr "सिखाना रोकें" msgid "Stop timer" msgstr "" +#: src/pages/shell/composer.tsx +msgid "Stopped" +msgstr "" + #. js-lingui-explicit-id #: src/lib/browser-notifications.ts msgid "Stopped." diff --git a/apps/web/src/locales/ko/messages.po b/apps/web/src/locales/ko/messages.po index 99be890..1b29905 100644 --- a/apps/web/src/locales/ko/messages.po +++ b/apps/web/src/locales/ko/messages.po @@ -2354,6 +2354,7 @@ msgstr "연결 해제 중…" #: src/components/ComputerUpdateProgress.tsx #: src/components/teach/TeachComputerOverlay.tsx #: src/components/ThreadCard.tsx +#: src/pages/shell/composer.tsx #: src/pages/shell/message-cards.tsx msgid "Dismiss" msgstr "닫기" @@ -4720,6 +4721,11 @@ msgstr "마지막으로 저장된 작업 공간을 복원합니다. 컴퓨터에 msgid "Restoring your workspace" msgstr "" +#: src/pages/shell/composer.tsx +#: src/pages/shell/message-cards.tsx +msgid "Resume" +msgstr "" + #: src/components/FolderPickerDialog.tsx msgid "Retry" msgstr "" @@ -5362,6 +5368,10 @@ msgstr "가르치기 종료" msgid "Stop timer" msgstr "" +#: src/pages/shell/composer.tsx +msgid "Stopped" +msgstr "" + #. js-lingui-explicit-id #: src/lib/browser-notifications.ts msgid "Stopped." diff --git a/apps/web/src/locales/pt-BR/messages.po b/apps/web/src/locales/pt-BR/messages.po index e8b0621..1227b2c 100644 --- a/apps/web/src/locales/pt-BR/messages.po +++ b/apps/web/src/locales/pt-BR/messages.po @@ -2354,6 +2354,7 @@ msgstr "Desconectando…" #: src/components/ComputerUpdateProgress.tsx #: src/components/teach/TeachComputerOverlay.tsx #: src/components/ThreadCard.tsx +#: src/pages/shell/composer.tsx #: src/pages/shell/message-cards.tsx msgid "Dismiss" msgstr "" @@ -4720,6 +4721,11 @@ msgstr "Restaure o último espaço de trabalho salvo. O trabalho não salvo no c msgid "Restoring your workspace" msgstr "" +#: src/pages/shell/composer.tsx +#: src/pages/shell/message-cards.tsx +msgid "Resume" +msgstr "" + #: src/components/FolderPickerDialog.tsx msgid "Retry" msgstr "" @@ -5362,6 +5368,10 @@ msgstr "Parar de ensinar" msgid "Stop timer" msgstr "" +#: src/pages/shell/composer.tsx +msgid "Stopped" +msgstr "" + #. js-lingui-explicit-id #: src/lib/browser-notifications.ts msgid "Stopped." diff --git a/apps/web/src/locales/ru/messages.po b/apps/web/src/locales/ru/messages.po index 9a356a8..ec38a25 100644 --- a/apps/web/src/locales/ru/messages.po +++ b/apps/web/src/locales/ru/messages.po @@ -2354,6 +2354,7 @@ msgstr "Отключение…" #: src/components/ComputerUpdateProgress.tsx #: src/components/teach/TeachComputerOverlay.tsx #: src/components/ThreadCard.tsx +#: src/pages/shell/composer.tsx #: src/pages/shell/message-cards.tsx msgid "Dismiss" msgstr "Закрыть" @@ -4720,6 +4721,11 @@ msgstr "Восстановите последнее сохраненное ра msgid "Restoring your workspace" msgstr "Восстановление вашего рабочего пространства" +#: src/pages/shell/composer.tsx +#: src/pages/shell/message-cards.tsx +msgid "Resume" +msgstr "" + #: src/components/FolderPickerDialog.tsx msgid "Retry" msgstr "" @@ -5362,6 +5368,10 @@ msgstr "Остановить обучение" msgid "Stop timer" msgstr "" +#: src/pages/shell/composer.tsx +msgid "Stopped" +msgstr "" + #. js-lingui-explicit-id #: src/lib/browser-notifications.ts msgid "Stopped." diff --git a/apps/web/src/locales/tr/messages.po b/apps/web/src/locales/tr/messages.po index cb18ba9..0f7a212 100644 --- a/apps/web/src/locales/tr/messages.po +++ b/apps/web/src/locales/tr/messages.po @@ -2354,6 +2354,7 @@ msgstr "Bağlantı kesiliyor…" #: src/components/ComputerUpdateProgress.tsx #: src/components/teach/TeachComputerOverlay.tsx #: src/components/ThreadCard.tsx +#: src/pages/shell/composer.tsx #: src/pages/shell/message-cards.tsx msgid "Dismiss" msgstr "" @@ -4720,6 +4721,11 @@ msgstr "" msgid "Restoring your workspace" msgstr "" +#: src/pages/shell/composer.tsx +#: src/pages/shell/message-cards.tsx +msgid "Resume" +msgstr "" + #: src/components/FolderPickerDialog.tsx msgid "Retry" msgstr "" @@ -5362,6 +5368,10 @@ msgstr "Öğretmeyi durdur" msgid "Stop timer" msgstr "" +#: src/pages/shell/composer.tsx +msgid "Stopped" +msgstr "" + #. js-lingui-explicit-id #: src/lib/browser-notifications.ts msgid "Stopped." diff --git a/apps/web/src/locales/zh-CN/messages.po b/apps/web/src/locales/zh-CN/messages.po index 2657a08..8294f7f 100644 --- a/apps/web/src/locales/zh-CN/messages.po +++ b/apps/web/src/locales/zh-CN/messages.po @@ -2354,6 +2354,7 @@ msgstr "正在断开…" #: src/components/ComputerUpdateProgress.tsx #: src/components/teach/TeachComputerOverlay.tsx #: src/components/ThreadCard.tsx +#: src/pages/shell/composer.tsx #: src/pages/shell/message-cards.tsx msgid "Dismiss" msgstr "" @@ -4720,6 +4721,11 @@ msgstr "还原上次保存的工作区。电脑上未保存的工作会丢失。 msgid "Restoring your workspace" msgstr "" +#: src/pages/shell/composer.tsx +#: src/pages/shell/message-cards.tsx +msgid "Resume" +msgstr "" + #: src/components/FolderPickerDialog.tsx msgid "Retry" msgstr "" @@ -5362,6 +5368,10 @@ msgstr "停止示范" msgid "Stop timer" msgstr "" +#: src/pages/shell/composer.tsx +msgid "Stopped" +msgstr "" + #. js-lingui-explicit-id #: src/lib/browser-notifications.ts msgid "Stopped." diff --git a/apps/web/src/pages/Shell.tsx b/apps/web/src/pages/Shell.tsx index db4cb16..44fcd23 100644 --- a/apps/web/src/pages/Shell.tsx +++ b/apps/web/src/pages/Shell.tsx @@ -4687,6 +4687,10 @@ export function ShellPage() { await refreshComputerScreen(target.id); await refreshThread(target.id).catch(() => undefined); }} + onConnectScreen={async () => { + const target = active ?? coordinatorBot; + if (target) await refreshComputerScreen(target.id); + }} onOpenComputer={() => void openComputer()} /> diff --git a/infra/sandboxes/supervisor/src/github-broker.test.ts b/infra/sandboxes/supervisor/src/github-broker.test.ts index 3d38be3..57e622b 100644 --- a/infra/sandboxes/supervisor/src/github-broker.test.ts +++ b/infra/sandboxes/supervisor/src/github-broker.test.ts @@ -166,7 +166,11 @@ describe("github broker remotes", () => { assertRemoteAllowed(linked, ["git", "fetch", "origin"], repo), ).resolves.toBeUndefined(); await expect( - assertRemoteAllowed(linked, ["git", "ls-remote", "https://github.com/cynar/umbra.git"], repo), + assertRemoteAllowed( + linked, + ["git", "ls-remote", "https://github.com/cynar/umbra.git"], + repo, + ), ).resolves.toBeUndefined(); } finally { rmSync(repo, { recursive: true, force: true }); diff --git a/packages/adapters/src/executor.ts b/packages/adapters/src/executor.ts index 9a92637..c9902a4 100644 --- a/packages/adapters/src/executor.ts +++ b/packages/adapters/src/executor.ts @@ -1233,7 +1233,9 @@ export function createRunExecutor(deps: ExecutorDeps) { hasModelOverride && bot.modelProvider ? findModelCredential(deps.prisma, run, bot.modelProvider, bot.modelId) : Promise.resolve(null), - bot.modelTier ? findSpaceModelTier(deps.prisma, run, bot.modelTier) : Promise.resolve(null), + bot.modelTier + ? findSpaceModelTier(deps.prisma, run, bot.modelTier) + : Promise.resolve(null), ]); const brokerWorkspacePath = projectWorkspacePath ?? @@ -5296,12 +5298,9 @@ export function createRunExecutor(deps: ExecutorDeps) { getLogger().error("history.compact enqueue failed", error); } } catch (error) { - if ( - approvalPausePending || - !leaseValid || - deps.shutdownSignal?.aborted || - runAbortController?.signal.aborted - ) { + // A runtime error also aborts runAbortController through withRuntimeCleanup, + // so only an abort from outside the run may skip recording the failure. + if (approvalPausePending || !leaseValid || deps.shutdownSignal?.aborted) { return; } if (run.trigger === "routine") { diff --git a/packages/adapters/src/pi-runtime-tool-dispatch.test.ts b/packages/adapters/src/pi-runtime-tool-dispatch.test.ts index 3ea19a5..644cbac 100644 --- a/packages/adapters/src/pi-runtime-tool-dispatch.test.ts +++ b/packages/adapters/src/pi-runtime-tool-dispatch.test.ts @@ -1167,30 +1167,31 @@ describe("Pi connector tool dispatch", () => { return { ok: true }; }); - await expect(async () => { - for await (const _event of runtime.run( - { - botId: "b", - threadId: "t", - runId: "fuse-resume", - prompt: "keep going", - instructions: "Use shell.", - history: [], - tools: [shellTool], - model: { provider: "test", id: "dispatch-test-model" }, - executeTool, - }, - { - operationId: "2d", - traceId: "2d", - spaceId: "w", - userId: "u", - signal: abort.signal, - }, - )) { - // Exhaust until abort fails the turn. - } - }).rejects.toThrow(); + // A caller abort ends the turn as stopped rather than as a provider error. + const stopped: unknown[] = []; + for await (const event of runtime.run( + { + botId: "b", + threadId: "t", + runId: "fuse-resume", + prompt: "keep going", + instructions: "Use shell.", + history: [], + tools: [shellTool], + model: { provider: "test", id: "dispatch-test-model" }, + executeTool, + }, + { + operationId: "2d", + traceId: "2d", + spaceId: "w", + userId: "u", + signal: abort.signal, + }, + )) { + stopped.push(event); + } + expect(stopped).toContainEqual({ type: "done", text: "stopped" }); executeTool.mockImplementation(async () => ({ ok: true })); const events: unknown[] = []; diff --git a/packages/adapters/src/pi-runtime.ts b/packages/adapters/src/pi-runtime.ts index f7834fe..be967d0 100644 --- a/packages/adapters/src/pi-runtime.ts +++ b/packages/adapters/src/pi-runtime.ts @@ -558,7 +558,8 @@ function ensureHostSteeringPoller(host: ToolHost): void { // Nested subagents run inside the parent's active `run_subagent` tool call, whose // root host poller already claims steering and cascades abort to nested tools. if (host.depth > 0 || !host.request.claimSteering || host.signal.aborted) return; - const poller = (host.steeringPoller ??= { inFlight: false }); + host.steeringPoller ??= { inFlight: false }; + const poller = host.steeringPoller; if (poller.timer) return; poller.timer = setInterval(async () => { if (host.signal.aborted || host.activeToolControllers.size === 0) { diff --git a/packages/testkit/src/computer-recording.test.ts b/packages/testkit/src/computer-recording.test.ts index 7ce6768..ad08c43 100644 --- a/packages/testkit/src/computer-recording.test.ts +++ b/packages/testkit/src/computer-recording.test.ts @@ -209,9 +209,8 @@ describe("sanitized real-model computer recordings", () => { fixture.computer, interruptedContext, ); - await expect( - executeContactsJourney(emulator.model, recorder, interruptedContext), - ).rejects.toThrow(/aborted/i); + // A caller abort ends the turn as stopped, not as a provider error. + await executeContactsJourney(emulator.model, recorder, interruptedContext); emulator.assertComplete(); expect(held).toBe(true); expect(controller.signal.aborted).toBe(true); diff --git a/packages/testkit/src/pi-offline.test.ts b/packages/testkit/src/pi-offline.test.ts index 1bf1f82..49d1aa1 100644 --- a/packages/testkit/src/pi-offline.test.ts +++ b/packages/testkit/src/pi-offline.test.ts @@ -236,17 +236,15 @@ describe("real Pi against an offline model HTTP endpoint", () => { new PiAgentRuntime().run(runRequest(server.model), { signal: controller.signal }), events, ); - // Attach the rejection handler before aborting to avoid an unhandled rejection. - const outcome = work.then( - () => undefined, - (error: unknown) => error, - ); await opened.promise; controller.abort(); - expect(await outcome).toBeInstanceOf(Error); + // A caller abort ends the turn as stopped, not as a provider error. + await work; await closed.promise; server.assertComplete(); - expect(events.some((event) => event.type === "done")).toBe(false); + expect(events.filter((event) => event.type === "done")).toEqual([ + { type: "done", text: "stopped" }, + ]); }); it("keeps two model connections and their responses isolated", async () => {