From aea28e26fb592cf56f03bf342f640d8a707d2410 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 24 Feb 2026 04:02:18 +0000 Subject: [PATCH] fix(auto-reply): expand standalone stop phrases --- CHANGELOG.md | 1 + docs/concepts/session.md | 2 +- docs/help/faq.md | 13 +++++++++ docs/web/control-ui.md | 2 +- src/auto-reply/reply/abort.test.ts | 45 ++++++++++++++++++++++++------ src/auto-reply/reply/abort.ts | 43 ++++++++++++++++++++++++++-- 6 files changed, 92 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 036017f16e7..6df352d6c8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ Docs: https://docs.openclaw.ai - Discord/Threading: recover missing thread parent IDs by refetching thread metadata before resolving parent channel context. (#24897) Thanks @z-x-yang. - Web UI/i18n: load and hydrate saved locale translations during startup so non-English sessions apply immediately without manual toggling. (#24795) Thanks @chilu18. - Plugins/Config schema: support legacy plugin schemas without `toJSONSchema()` by falling back to permissive object schema generation. (#24933) Thanks @pandego. +- 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. - Cron/Isolated sessions: use full prompt mode for isolated cron runs so skills/extensions are available during cron execution. (#24944) - Discord/Reasoning: suppress reasoning/thinking-only payload blocks from Discord delivery output. (#24969) - Sessions/Reasoning: persist `reasoningLevel: "off"` explicitly instead of deleting it so session overrides survive patch/update flows. (#24406, #24559) diff --git a/docs/concepts/session.md b/docs/concepts/session.md index 81550a032ed..6c9010d2c11 100644 --- a/docs/concepts/session.md +++ b/docs/concepts/session.md @@ -283,7 +283,7 @@ Runtime override (owner only): - `openclaw gateway call sessions.list --params '{}'` — fetch sessions from the running gateway (use `--url`/`--token` for remote gateway access). - Send `/status` as a standalone message in chat to see whether the agent is reachable, how much of the session context is used, current thinking/verbose toggles, and when your WhatsApp web creds were last refreshed (helps spot relink needs). - Send `/context list` or `/context detail` to see what’s in the system prompt and injected workspace files (and the biggest context contributors). -- Send `/stop` as a standalone message to abort the current run, clear queued followups for that session, and stop any sub-agent runs spawned from it (the reply includes the stopped count). +- Send `/stop` (or standalone abort phrases like `stop`, `stop action`, `stop run`, `stop openclaw`) to abort the current run, clear queued followups for that session, and stop any sub-agent runs spawned from it (the reply includes the stopped count). - Send `/compact` (optional instructions) as a standalone message to summarize older context and free up window space. See [/concepts/compaction](/concepts/compaction). - JSONL transcripts can be opened directly to review full turns. diff --git a/docs/help/faq.md b/docs/help/faq.md index d6a5f3f1205..4cf1c7447ed 100644 --- a/docs/help/faq.md +++ b/docs/help/faq.md @@ -2814,6 +2814,19 @@ Send any of these **as a standalone message** (no slash): ``` stop +stop action +stop current action +stop run +stop current run +stop agent +stop the agent +stop openclaw +openclaw stop +stop don't do anything +stop do not do anything +stop doing anything +please stop +stop please abort esc wait diff --git a/docs/web/control-ui.md b/docs/web/control-ui.md index b1ff11c3243..ad6d2393523 100644 --- a/docs/web/control-ui.md +++ b/docs/web/control-ui.md @@ -99,7 +99,7 @@ Cron jobs panel notes: - `chat.inject` appends an assistant note to the session transcript and broadcasts a `chat` event for UI-only updates (no agent run, no channel delivery). - Stop: - Click **Stop** (calls `chat.abort`) - - Type `/stop` (or `stop|esc|abort|wait|exit|interrupt`) to abort out-of-band + - Type `/stop` (or standalone abort phrases like `stop`, `stop action`, `stop run`, `stop openclaw`, `please stop`) to abort out-of-band - `chat.abort` supports `{ sessionKey }` (no `runId`) to abort all active runs for that session - Abort partial retention: - When a run is aborted, partial assistant text can still be shown in the UI diff --git a/src/auto-reply/reply/abort.test.ts b/src/auto-reply/reply/abort.test.ts index f5bca4b677a..b36855eb80c 100644 --- a/src/auto-reply/reply/abort.test.ts +++ b/src/auto-reply/reply/abort.test.ts @@ -122,25 +122,52 @@ describe("abort detection", () => { expect(result.triggerBodyNormalized).toBe("/stop"); }); - it("isAbortTrigger matches bare word triggers (without slash)", () => { - expect(isAbortTrigger("stop")).toBe(true); - expect(isAbortTrigger("esc")).toBe(true); - expect(isAbortTrigger("abort")).toBe(true); - expect(isAbortTrigger("wait")).toBe(true); - expect(isAbortTrigger("exit")).toBe(true); - expect(isAbortTrigger("interrupt")).toBe(true); + it("isAbortTrigger matches standalone abort trigger phrases", () => { + const positives = [ + "stop", + "esc", + "abort", + "wait", + "exit", + "interrupt", + "stop openclaw", + "openclaw stop", + "stop action", + "stop current action", + "stop run", + "stop current run", + "stop agent", + "stop the agent", + "stop don't do anything", + "stop dont do anything", + "stop do not do anything", + "stop doing anything", + "please stop", + "stop please", + "STOP OPENCLAW", + "stop openclaw!!!", + "stop don’t do anything", + ]; + for (const candidate of positives) { + expect(isAbortTrigger(candidate)).toBe(true); + } + expect(isAbortTrigger("hello")).toBe(false); - // /stop is NOT matched by isAbortTrigger - it's handled separately + expect(isAbortTrigger("do not do that")).toBe(false); + // /stop is NOT matched by isAbortTrigger - it's handled separately. expect(isAbortTrigger("/stop")).toBe(false); }); it("isAbortRequestText aligns abort command semantics", () => { expect(isAbortRequestText("/stop")).toBe(true); + expect(isAbortRequestText("/stop!!!")).toBe(true); expect(isAbortRequestText("stop")).toBe(true); + expect(isAbortRequestText("stop action")).toBe(true); + expect(isAbortRequestText("stop openclaw!!!")).toBe(true); expect(isAbortRequestText("/stop@openclaw_bot", { botUsername: "openclaw_bot" })).toBe(true); expect(isAbortRequestText("/status")).toBe(false); - expect(isAbortRequestText("stop please")).toBe(false); + expect(isAbortRequestText("do not do that")).toBe(false); expect(isAbortRequestText("/abort")).toBe(false); }); diff --git a/src/auto-reply/reply/abort.ts b/src/auto-reply/reply/abort.ts index 4cb89483077..38bf576a435 100644 --- a/src/auto-reply/reply/abort.ts +++ b/src/auto-reply/reply/abort.ts @@ -23,15 +23,47 @@ import type { FinalizedMsgContext, MsgContext } from "../templating.js"; import { stripMentions, stripStructuralPrefixes } from "./mentions.js"; import { clearSessionQueues } from "./queue.js"; -const ABORT_TRIGGERS = new Set(["stop", "esc", "abort", "wait", "exit", "interrupt"]); +const ABORT_TRIGGERS = new Set([ + "stop", + "esc", + "abort", + "wait", + "exit", + "interrupt", + "stop openclaw", + "openclaw stop", + "stop action", + "stop current action", + "stop run", + "stop current run", + "stop agent", + "stop the agent", + "stop don't do anything", + "stop dont do anything", + "stop do not do anything", + "stop doing anything", + "please stop", + "stop please", +]); const ABORT_MEMORY = new Map(); const ABORT_MEMORY_MAX = 2000; +const TRAILING_ABORT_PUNCTUATION_RE = /[.!?…,,。;;::'"’”)\]}]+$/u; + +function normalizeAbortTriggerText(text: string): string { + return text + .trim() + .toLowerCase() + .replace(/[’`]/g, "'") + .replace(/\s+/g, " ") + .replace(TRAILING_ABORT_PUNCTUATION_RE, "") + .trim(); +} export function isAbortTrigger(text?: string): boolean { if (!text) { return false; } - const normalized = text.trim().toLowerCase(); + const normalized = normalizeAbortTriggerText(text); return ABORT_TRIGGERS.has(normalized); } @@ -43,7 +75,12 @@ export function isAbortRequestText(text?: string, options?: CommandNormalizeOpti if (!normalized) { return false; } - return normalized.toLowerCase() === "/stop" || isAbortTrigger(normalized); + const normalizedLower = normalized.toLowerCase(); + return ( + normalizedLower === "/stop" || + normalizeAbortTriggerText(normalizedLower) === "/stop" || + isAbortTrigger(normalizedLower) + ); } export function getAbortMemory(key: string): boolean | undefined {