diff --git a/CHANGELOG.md b/CHANGELOG.md index b0dcf2b6d5..b2f4be2309 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ Docs: https://docs.openclaw.ai ### Fixes - iOS/Screen: move `WKWebView` lifecycle ownership into `ScreenWebView` coordinator and explicit attach/detach flow to reduce gesture/lifecycle crash risk (`__NSArrayM insertObject:atIndex:` paths) during screen tab updates. (#20366) Thanks @ngutman. +- Gateway/TUI: honor `agents.defaults.blockStreamingDefault` for `chat.send` by removing the hardcoded block-streaming disable override, so replies can use configured block-mode delivery. (#19693) Thanks @neipor. - Protocol/Apple: regenerate Swift gateway models for `push.test` so `pnpm protocol:check` stays green on main. Thanks @mbelinky. - Canvas/A2UI: improve bundled-asset resolution and empty-state handling so UI fallbacks render reliably. (#20312) Thanks @mbelinky. - UI/Sessions: accept the canonical main session-key alias in Chat UI flows so main-session routing stays consistent. (#20311) Thanks @mbelinky. diff --git a/src/gateway/server-methods/chat.ts b/src/gateway/server-methods/chat.ts index 6c0b90e943..29d099d93f 100644 --- a/src/gateway/server-methods/chat.ts +++ b/src/gateway/server-methods/chat.ts @@ -898,7 +898,6 @@ export const chatHandlers: GatewayRequestHandlers = { runId: clientRunId, abortSignal: abortController.signal, images: parsedImages.length > 0 ? parsedImages : undefined, - disableBlockStreaming: true, onAgentRunStart: (runId) => { agentRunStarted = true; const connId = typeof client?.connId === "string" ? client.connId : undefined; diff --git a/src/gateway/server.chat.gateway-server-chat-b.e2e.test.ts b/src/gateway/server.chat.gateway-server-chat-b.e2e.test.ts index de1d16acdf..2255443a07 100644 --- a/src/gateway/server.chat.gateway-server-chat-b.e2e.test.ts +++ b/src/gateway/server.chat.gateway-server-chat-b.e2e.test.ts @@ -2,6 +2,7 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { describe, expect, test, vi } from "vitest"; +import type { GetReplyOptions } from "../auto-reply/types.js"; import { __setMaxChatHistoryMessagesBytesForTest } from "./server-constants.js"; import { connectOk, @@ -136,6 +137,42 @@ describe("gateway server chat", () => { }); }); + test("chat.send does not force-disable block streaming", async () => { + await withGatewayChatHarness(async ({ ws, createSessionDir }) => { + const spy = getReplyFromConfig; + await connectOk(ws); + + await createSessionDir(); + await writeMainSessionStore(); + testState.agentConfig = { blockStreamingDefault: "on" }; + try { + spy.mockReset(); + let capturedOpts: GetReplyOptions | undefined; + spy.mockImplementationOnce(async (_ctx: unknown, opts?: GetReplyOptions) => { + capturedOpts = opts; + }); + + const sendRes = await rpcReq(ws, "chat.send", { + sessionKey: "main", + message: "hello", + idempotencyKey: "idem-block-streaming", + }); + expect(sendRes.ok).toBe(true); + + await vi.waitFor( + () => { + expect(spy.mock.calls.length).toBeGreaterThan(0); + }, + { timeout: 2_000, interval: 10 }, + ); + + expect(capturedOpts?.disableBlockStreaming).toBeUndefined(); + } finally { + testState.agentConfig = undefined; + } + }); + }); + test("chat.history hard-caps single oversized nested payloads", async () => { await withGatewayChatHarness(async ({ ws, createSessionDir }) => { const historyMaxBytes = 64 * 1024; @@ -251,7 +288,7 @@ describe("gateway server chat", () => { test("smoke: supports abort and idempotent completion", async () => { await withGatewayChatHarness(async ({ ws, createSessionDir }) => { - const spy = vi.mocked(getReplyFromConfig) as unknown as ReturnType; + const spy = getReplyFromConfig; let aborted = false; await connectOk(ws); diff --git a/src/gateway/test-helpers.mocks.ts b/src/gateway/test-helpers.mocks.ts index 5ebdc7859e..19c6d2e91a 100644 --- a/src/gateway/test-helpers.mocks.ts +++ b/src/gateway/test-helpers.mocks.ts @@ -4,7 +4,10 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { Mock, vi } from "vitest"; +import type { MsgContext } from "../auto-reply/templating.js"; +import type { GetReplyOptions, ReplyPayload } from "../auto-reply/types.js"; import type { ChannelPlugin, ChannelOutboundAdapter } from "../channels/plugins/types.js"; +import type { OpenClawConfig } from "../config/config.js"; import { applyPluginAutoEnable } from "../config/plugin-auto-enable.js"; import type { AgentBinding } from "../config/types.agents.js"; import type { HooksConfig } from "../config/types.hooks.js"; @@ -19,6 +22,12 @@ type StubChannelOptions = { summary?: Record; }; +type GetReplyFromConfigFn = ( + ctx: MsgContext, + opts?: GetReplyOptions, + configOverride?: OpenClawConfig, +) => Promise; + const createStubOutboundAdapter = (channelId: ChannelPlugin["id"]): ChannelOutboundAdapter => ({ deliveryMode: "direct", sendText: async () => ({ @@ -169,7 +178,7 @@ const hoisted = vi.hoisted(() => ({ waitResults: new Map(), }, testTailscaleWhois: { value: null as TailscaleWhoisIdentity | null }, - getReplyFromConfig: vi.fn().mockResolvedValue(undefined), + getReplyFromConfig: vi.fn().mockResolvedValue(undefined), sendWhatsAppMock: vi.fn().mockResolvedValue({ messageId: "msg-1", toJid: "jid-1" }), })); @@ -202,7 +211,7 @@ export const testTailscaleWhois = hoisted.testTailscaleWhois; export const piSdkMock = hoisted.piSdkMock; export const cronIsolatedRun = hoisted.cronIsolatedRun; export const agentCommand: Mock<() => void> = hoisted.agentCommand; -export const getReplyFromConfig: Mock<() => void> = hoisted.getReplyFromConfig; +export const getReplyFromConfig: Mock = hoisted.getReplyFromConfig; export const testState = { agentConfig: undefined as Record | undefined,