fix: avoid WhatsApp silent turns with final-only delivery (#24962) (thanks @SidQin-cyber)
This commit is contained in:
@@ -11,6 +11,7 @@ Docs: https://docs.openclaw.ai
|
||||
### Fixes
|
||||
|
||||
- WhatsApp/Logging: redact outbound recipient identifiers in WhatsApp outbound + heartbeat logs and remove message/poll preview text from those log lines. (#24980) Thanks @coygeek.
|
||||
- WhatsApp/Auto-reply: send only final payloads to WhatsApp, suppress tool/block payload leakage (reasoning/thinking), and force block streaming off for WhatsApp dispatch so final-only delivery cannot cause silent turns. (#24962) Thanks @SidQin-cyber.
|
||||
- Telegram/Media SSRF: keep RFC2544 benchmark range (`198.18.0.0/15`) blocked by default, add an explicit SSRF-policy opt-in for Telegram media downloads, and keep other channels/URL fetch paths blocked. (#24982) Thanks @stakeswky.
|
||||
- Security/Shell env fallback: remove trusted-prefix shell-path fallback and only trust login shells explicitly registered in `/etc/shells`, defaulting to `/bin/sh` when `SHELL` is not registered. This ships in the next npm release. Thanks @tdjackey for reporting.
|
||||
- Security/Exec approvals: bind `host=node` approvals to explicit `nodeId`, reject cross-node replay of approved `system.run` requests, and include the target node in approval prompts. This ships in the next npm release. Thanks @tdjackey for reporting.
|
||||
|
||||
@@ -9,6 +9,9 @@ let capturedDispatchParams: unknown;
|
||||
let sessionDir: string | undefined;
|
||||
let sessionStorePath: string;
|
||||
let backgroundTasks: Set<Promise<unknown>>;
|
||||
const { deliverWebReplyMock } = vi.hoisted(() => ({
|
||||
deliverWebReplyMock: vi.fn(async () => {}),
|
||||
}));
|
||||
|
||||
const defaultReplyLogger = {
|
||||
info: () => {},
|
||||
@@ -24,6 +27,7 @@ function makeProcessMessageArgs(params: {
|
||||
cfg?: unknown;
|
||||
groupHistories?: Map<string, Array<{ sender: string; body: string }>>;
|
||||
groupHistory?: Array<{ sender: string; body: string }>;
|
||||
rememberSentText?: (text: string | undefined, opts: unknown) => void;
|
||||
}) {
|
||||
return {
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
@@ -47,7 +51,8 @@ function makeProcessMessageArgs(params: {
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
replyLogger: defaultReplyLogger as any,
|
||||
backgroundTasks,
|
||||
rememberSentText: (_text: string | undefined, _opts: unknown) => {},
|
||||
rememberSentText:
|
||||
params.rememberSentText ?? ((_text: string | undefined, _opts: unknown) => {}),
|
||||
echoHas: () => false,
|
||||
echoForget: () => {},
|
||||
buildCombinedEchoKey: () => "echo",
|
||||
@@ -75,6 +80,10 @@ vi.mock("./last-route.js", () => ({
|
||||
updateLastRouteInBackground: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../deliver-reply.js", () => ({
|
||||
deliverWebReply: deliverWebReplyMock,
|
||||
}));
|
||||
|
||||
import { processMessage } from "./process-message.js";
|
||||
|
||||
describe("web processMessage inbound contract", () => {
|
||||
@@ -82,6 +91,7 @@ describe("web processMessage inbound contract", () => {
|
||||
capturedCtx = undefined;
|
||||
capturedDispatchParams = undefined;
|
||||
backgroundTasks = new Set();
|
||||
deliverWebReplyMock.mockClear();
|
||||
sessionDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-process-message-"));
|
||||
sessionStorePath = path.join(sessionDir, "sessions.json");
|
||||
});
|
||||
@@ -229,4 +239,67 @@ describe("web processMessage inbound contract", () => {
|
||||
|
||||
expect(groupHistories.get("whatsapp:default:group:123@g.us") ?? []).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("suppresses non-final WhatsApp payload delivery", async () => {
|
||||
const rememberSentText = vi.fn();
|
||||
await processMessage(
|
||||
makeProcessMessageArgs({
|
||||
routeSessionKey: "agent:main:whatsapp:direct:+1555",
|
||||
groupHistoryKey: "+1555",
|
||||
rememberSentText,
|
||||
cfg: {
|
||||
channels: { whatsapp: { blockStreaming: true } },
|
||||
messages: {},
|
||||
session: { store: sessionStorePath },
|
||||
} as unknown as ReturnType<typeof import("../../../config/config.js").loadConfig>,
|
||||
msg: {
|
||||
id: "msg1",
|
||||
from: "+1555",
|
||||
to: "+2000",
|
||||
chatType: "direct",
|
||||
body: "hi",
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
const deliver = (capturedDispatchParams as any)?.dispatcherOptions?.deliver as
|
||||
| ((payload: { text?: string }, info: { kind: "tool" | "block" | "final" }) => Promise<void>)
|
||||
| undefined;
|
||||
expect(deliver).toBeTypeOf("function");
|
||||
|
||||
await deliver?.({ text: "tool payload" }, { kind: "tool" });
|
||||
await deliver?.({ text: "block payload" }, { kind: "block" });
|
||||
expect(deliverWebReplyMock).not.toHaveBeenCalled();
|
||||
expect(rememberSentText).not.toHaveBeenCalled();
|
||||
|
||||
await deliver?.({ text: "final payload" }, { kind: "final" });
|
||||
expect(deliverWebReplyMock).toHaveBeenCalledTimes(1);
|
||||
expect(rememberSentText).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("forces disableBlockStreaming for WhatsApp dispatch", async () => {
|
||||
await processMessage(
|
||||
makeProcessMessageArgs({
|
||||
routeSessionKey: "agent:main:whatsapp:direct:+1555",
|
||||
groupHistoryKey: "+1555",
|
||||
cfg: {
|
||||
channels: { whatsapp: { blockStreaming: true } },
|
||||
messages: {},
|
||||
session: { store: sessionStorePath },
|
||||
} as unknown as ReturnType<typeof import("../../../config/config.js").loadConfig>,
|
||||
msg: {
|
||||
id: "msg1",
|
||||
from: "+1555",
|
||||
to: "+2000",
|
||||
chatType: "direct",
|
||||
body: "hi",
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
const replyOptions = (capturedDispatchParams as any)?.replyOptions;
|
||||
expect(replyOptions?.disableBlockStreaming).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -416,10 +416,9 @@ export async function processMessage(params: {
|
||||
onReplyStart: params.msg.sendComposing,
|
||||
},
|
||||
replyOptions: {
|
||||
disableBlockStreaming:
|
||||
typeof params.cfg.channels?.whatsapp?.blockStreaming === "boolean"
|
||||
? !params.cfg.channels.whatsapp.blockStreaming
|
||||
: undefined,
|
||||
// WhatsApp delivery intentionally suppresses non-final payloads.
|
||||
// Keep block streaming disabled so final replies are still produced.
|
||||
disableBlockStreaming: true,
|
||||
onModelSelected,
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user