fix(auto-reply): expand standalone stop phrases

This commit is contained in:
Peter Steinberger
2026-02-24 04:02:18 +00:00
parent 588a188d6f
commit aea28e26fb
6 changed files with 92 additions and 14 deletions

View File

@@ -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)

View File

@@ -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 whats 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.

View File

@@ -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

View File

@@ -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

View File

@@ -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 dont 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);
});

View File

@@ -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<string, boolean>();
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 {