fix(channels): bypass debounce for bare abort triggers [AI-assisted] (#83348)

Summary:
- The PR changes shared, Feishu, Mattermost, Microsoft Teams, and WhatsApp inbound debounce predicates so bare abort text bypasses debounce, then adds focused tests and a changelog entry.
- Reproducibility: yes. source-level. Current main sends bare `stop`, `abort`, and `wait` through a `hasContro ... ()` debounce gate, while the existing abort-aware detector and trigger set already recognize those phrases.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(channels): bypass debounce for bare abort triggers [AI-assisted]
- PR branch already contained follow-up commit before automerge: fix(clawsweeper): address review for automerge-openclaw-openclaw-8334…

Validation:
- ClawSweeper review passed for head c96bf84270.
- Required merge gates passed before the squash merge.

Prepared head SHA: c96bf84270
Review: https://github.com/openclaw/openclaw/pull/83348#issuecomment-4473176095

Co-authored-by: IWhatsskill <284122573+IWhatsskill@users.noreply.github.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
This commit is contained in:
WhatsSkiLL
2026-05-22 06:09:33 +02:00
committed by GitHub
parent e399a92e6c
commit 7dc2e50ac3
12 changed files with 356 additions and 34 deletions

View File

@@ -262,7 +262,7 @@ export function createFeishuMessageReceiveHandler({
return false;
}
const text = resolveDebounceText(event);
return Boolean(text) && !core.channel.text.hasControlCommand(text, cfg);
return Boolean(text) && !core.channel.commands.isControlCommandMessage(text, cfg);
},
onFlush: async (entries) => {
const last = entries.at(-1);

View File

@@ -2,7 +2,7 @@ import {
createInboundDebouncer,
resolveInboundDebounceMs,
} from "openclaw/plugin-sdk/channel-inbound-debounce";
import { hasControlCommand } from "openclaw/plugin-sdk/command-detection";
import { hasControlCommand, isControlCommandMessage } from "openclaw/plugin-sdk/command-detection";
import { createNonExitingRuntimeEnv } from "openclaw/plugin-sdk/plugin-test-runtime";
import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { ClawdbotConfig, PluginRuntime } from "../runtime-api.js";
@@ -255,10 +255,14 @@ function mentionOpenIds(event: FeishuMessageEvent): string[] {
function createFeishuMonitorRuntime(params?: {
createInboundDebouncer?: PluginRuntime["channel"]["debounce"]["createInboundDebouncer"];
resolveInboundDebounceMs?: PluginRuntime["channel"]["debounce"]["resolveInboundDebounceMs"];
isControlCommandMessage?: PluginRuntime["channel"]["commands"]["isControlCommandMessage"];
hasControlCommand?: PluginRuntime["channel"]["text"]["hasControlCommand"];
}): PluginRuntime {
return {
channel: {
commands: {
isControlCommandMessage: params?.isControlCommandMessage ?? isControlCommandMessage,
},
debounce: {
createInboundDebouncer: params?.createInboundDebouncer ?? createInboundDebouncer,
resolveInboundDebounceMs: params?.resolveInboundDebounceMs ?? resolveInboundDebounceMs,
@@ -533,6 +537,32 @@ describe("Feishu inbound debounce regressions", () => {
vi.restoreAllMocks();
});
it("releases pending text before a bare abort trigger instead of debouncing it", async () => {
setDedupPassThroughMocks();
const onMessage = await setupDebounceMonitor();
await enqueueDebouncedMessage(onMessage, createTextEvent({ messageId: "om_1", text: "first" }));
expect(handleFeishuMessageMock).not.toHaveBeenCalled();
await enqueueDebouncedMessage(
onMessage,
createTextEvent({ messageId: "om_stop", text: "stop" }),
);
await Promise.resolve();
await Promise.resolve();
await vi.advanceTimersByTimeAsync(0);
expect(handleFeishuMessageMock).toHaveBeenCalledTimes(2);
const first = getFirstDispatchedEvent();
const secondCall = mockCallAt(handleFeishuMessageMock, 1, "Feishu stop dispatch")[0] as
| { event?: FeishuMessageEvent }
| undefined;
const second = secondCall?.event;
expect(JSON.parse(first.message.content)).toEqual({ text: "first" });
expect(second?.message.message_id).toBe("om_stop");
expect(JSON.parse(second?.message.content ?? "{}")).toEqual({ text: "stop" });
});
it("keeps bot mention when per-message mention keys collide across non-forward messages", async () => {
setDedupPassThroughMocks();
const onMessage = await setupDebounceMonitor();