diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c2f881c03c..4807eba7c7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,7 @@ Docs: https://docs.openclaw.ai - Subagents/Sessions: add `agents.defaults.subagents.runTimeoutSeconds` so `sessions_spawn` can inherit a configurable default timeout when the tool call omits `runTimeoutSeconds` (unset remains `0`, meaning no timeout). (#24594) Thanks @mitchmcalister. - Config/Kilo Gateway: Kilo provider flow now surfaces an updated list of models. (#24921) thanks @gumadeiras. -- Auto-reply/Abort shortcuts: expand standalone stop phrases (`stop openclaw`, `stop action`, `stop run`, `stop agent`, `please stop`, and related variants) and accept trailing punctuation (for example `STOP OPENCLAW!!!`) so emergency stop messages are caught more reliably. +- Auto-reply/Abort shortcuts: expand standalone stop phrases (`stop openclaw`, `stop action`, `stop run`, `stop agent`, `please stop`, and related variants), accept trailing punctuation (for example `STOP OPENCLAW!!!`), and add multilingual stop keywords (including ES/FR/ZH/HI/AR/JP/DE/PT/RU forms) so emergency stop messages are caught more reliably. Thanks @steipete and @vincentkoc. ### Fixes diff --git a/src/auto-reply/reply/abort.test.ts b/src/auto-reply/reply/abort.test.ts index b36855eb80c..b35937a6003 100644 --- a/src/auto-reply/reply/abort.test.ts +++ b/src/auto-reply/reply/abort.test.ts @@ -147,6 +147,25 @@ describe("abort detection", () => { "STOP OPENCLAW", "stop openclaw!!!", "stop don’t do anything", + "detente", + "detén", + "arrête", + "停止", + "やめて", + "止めて", + "रुको", + "توقف", + "стоп", + "остановись", + "останови", + "остановить", + "прекрати", + "halt", + "anhalten", + "aufhören", + "hoer auf", + "stopp", + "pare", ]; for (const candidate of positives) { expect(isAbortTrigger(candidate)).toBe(true); @@ -164,6 +183,12 @@ describe("abort detection", () => { expect(isAbortRequestText("stop")).toBe(true); expect(isAbortRequestText("stop action")).toBe(true); expect(isAbortRequestText("stop openclaw!!!")).toBe(true); + expect(isAbortRequestText("やめて")).toBe(true); + expect(isAbortRequestText("остановись")).toBe(true); + expect(isAbortRequestText("halt")).toBe(true); + expect(isAbortRequestText("stopp")).toBe(true); + expect(isAbortRequestText("pare")).toBe(true); + expect(isAbortRequestText(" توقف ")).toBe(true); expect(isAbortRequestText("/stop@openclaw_bot", { botUsername: "openclaw_bot" })).toBe(true); expect(isAbortRequestText("/status")).toBe(false); diff --git a/src/auto-reply/reply/abort.ts b/src/auto-reply/reply/abort.ts index 38bf576a435..1f3572464e8 100644 --- a/src/auto-reply/reply/abort.ts +++ b/src/auto-reply/reply/abort.ts @@ -30,6 +30,27 @@ const ABORT_TRIGGERS = new Set([ "wait", "exit", "interrupt", + "detente", + "deten", + "detén", + "arrete", + "arrête", + "停止", + "やめて", + "止めて", + "रुको", + "توقف", + "стоп", + "остановись", + "останови", + "остановить", + "прекрати", + "halt", + "anhalten", + "aufhören", + "hoer auf", + "stopp", + "pare", "stop openclaw", "openclaw stop", "stop action", diff --git a/src/gateway/chat-abort.test.ts b/src/gateway/chat-abort.test.ts index 9829f45c999..b008d7cc591 100644 --- a/src/gateway/chat-abort.test.ts +++ b/src/gateway/chat-abort.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it, vi } from "vitest"; import { abortChatRunById, + isChatStopCommandText, type ChatAbortOps, type ChatAbortControllerEntry, } from "./chat-abort.js"; @@ -42,6 +43,22 @@ function createOps(params: { }; } +describe("isChatStopCommandText", () => { + it("matches slash and standalone multilingual stop forms", () => { + expect(isChatStopCommandText(" /STOP!!! ")).toBe(true); + expect(isChatStopCommandText("stop please")).toBe(true); + expect(isChatStopCommandText("停止")).toBe(true); + expect(isChatStopCommandText("やめて")).toBe(true); + expect(isChatStopCommandText("توقف")).toBe(true); + expect(isChatStopCommandText("остановись")).toBe(true); + expect(isChatStopCommandText("halt")).toBe(true); + expect(isChatStopCommandText("stopp")).toBe(true); + expect(isChatStopCommandText("pare")).toBe(true); + expect(isChatStopCommandText("/status")).toBe(false); + expect(isChatStopCommandText("keep going")).toBe(false); + }); +}); + describe("abortChatRunById", () => { it("broadcasts aborted payload with partial message when buffered text exists", () => { const runId = "run-1"; diff --git a/src/gateway/chat-abort.ts b/src/gateway/chat-abort.ts index 0d544324133..0210f9223f7 100644 --- a/src/gateway/chat-abort.ts +++ b/src/gateway/chat-abort.ts @@ -1,4 +1,4 @@ -import { isAbortTrigger } from "../auto-reply/reply/abort.js"; +import { isAbortRequestText } from "../auto-reply/reply/abort.js"; export type ChatAbortControllerEntry = { controller: AbortController; @@ -9,11 +9,7 @@ export type ChatAbortControllerEntry = { }; export function isChatStopCommandText(text: string): boolean { - const trimmed = text.trim(); - if (!trimmed) { - return false; - } - return trimmed.toLowerCase() === "/stop" || isAbortTrigger(trimmed); + return isAbortRequestText(text); } export function resolveChatRunExpiresAtMs(params: { diff --git a/src/telegram/bot.create-telegram-bot.test.ts b/src/telegram/bot.create-telegram-bot.test.ts index f5c4735ea75..816cf224dd3 100644 --- a/src/telegram/bot.create-telegram-bot.test.ts +++ b/src/telegram/bot.create-telegram-bot.test.ts @@ -184,6 +184,16 @@ describe("createTelegramBot", () => { message: mockMessage({ chat: mockChat({ id: 123 }), text: "stop please" }), }), ).toBe("telegram:123:control"); + expect( + getTelegramSequentialKey({ + message: mockMessage({ chat: mockChat({ id: 123 }), text: "остановись" }), + }), + ).toBe("telegram:123:control"); + expect( + getTelegramSequentialKey({ + message: mockMessage({ chat: mockChat({ id: 123 }), text: "halt" }), + }), + ).toBe("telegram:123:control"); expect( getTelegramSequentialKey({ message: mockMessage({ chat: mockChat({ id: 123 }), text: "/abort" }),