Files
openclaw/src/gateway/chat-abort.ts
Vincent Koc 4b316c33db Auto-reply: normalize stop matching and add multilingual triggers (#25103)
* Auto-reply tests: cover multilingual abort triggers

* Auto-reply: normalize multilingual abort triggers

* Gateway: route chat stop matching through abort parser

* Gateway tests: cover chat stop parsing variants

* Auto-reply tests: cover Russian and German stop words

* Auto-reply: add Russian and German abort triggers

* Gateway tests: include Russian and German stop forms

* Telegram tests: route Russian and German stop forms to control lane

* Changelog: note multilingual abort stop coverage

* Changelog: add shared credit for abort shortcut update
2026-02-24 01:07:25 -05:00

126 lines
3.5 KiB
TypeScript

import { isAbortRequestText } from "../auto-reply/reply/abort.js";
export type ChatAbortControllerEntry = {
controller: AbortController;
sessionId: string;
sessionKey: string;
startedAtMs: number;
expiresAtMs: number;
};
export function isChatStopCommandText(text: string): boolean {
return isAbortRequestText(text);
}
export function resolveChatRunExpiresAtMs(params: {
now: number;
timeoutMs: number;
graceMs?: number;
minMs?: number;
maxMs?: number;
}): number {
const { now, timeoutMs, graceMs = 60_000, minMs = 2 * 60_000, maxMs = 24 * 60 * 60_000 } = params;
const boundedTimeoutMs = Math.max(0, timeoutMs);
const target = now + boundedTimeoutMs + graceMs;
const min = now + minMs;
const max = now + maxMs;
return Math.min(max, Math.max(min, target));
}
export type ChatAbortOps = {
chatAbortControllers: Map<string, ChatAbortControllerEntry>;
chatRunBuffers: Map<string, string>;
chatDeltaSentAt: Map<string, number>;
chatAbortedRuns: Map<string, number>;
removeChatRun: (
sessionId: string,
clientRunId: string,
sessionKey?: string,
) => { sessionKey: string; clientRunId: string } | undefined;
agentRunSeq: Map<string, number>;
broadcast: (event: string, payload: unknown, opts?: { dropIfSlow?: boolean }) => void;
nodeSendToSession: (sessionKey: string, event: string, payload: unknown) => void;
};
function broadcastChatAborted(
ops: ChatAbortOps,
params: {
runId: string;
sessionKey: string;
stopReason?: string;
partialText?: string;
},
) {
const { runId, sessionKey, stopReason, partialText } = params;
const payload = {
runId,
sessionKey,
seq: (ops.agentRunSeq.get(runId) ?? 0) + 1,
state: "aborted" as const,
stopReason,
message: partialText
? {
role: "assistant",
content: [{ type: "text", text: partialText }],
timestamp: Date.now(),
}
: undefined,
};
ops.broadcast("chat", payload);
ops.nodeSendToSession(sessionKey, "chat", payload);
}
export function abortChatRunById(
ops: ChatAbortOps,
params: {
runId: string;
sessionKey: string;
stopReason?: string;
},
): { aborted: boolean } {
const { runId, sessionKey, stopReason } = params;
const active = ops.chatAbortControllers.get(runId);
if (!active) {
return { aborted: false };
}
if (active.sessionKey !== sessionKey) {
return { aborted: false };
}
const bufferedText = ops.chatRunBuffers.get(runId);
const partialText = bufferedText && bufferedText.trim() ? bufferedText : undefined;
ops.chatAbortedRuns.set(runId, Date.now());
active.controller.abort();
ops.chatAbortControllers.delete(runId);
ops.chatRunBuffers.delete(runId);
ops.chatDeltaSentAt.delete(runId);
const removed = ops.removeChatRun(runId, runId, sessionKey);
broadcastChatAborted(ops, { runId, sessionKey, stopReason, partialText });
ops.agentRunSeq.delete(runId);
if (removed?.clientRunId) {
ops.agentRunSeq.delete(removed.clientRunId);
}
return { aborted: true };
}
export function abortChatRunsForSessionKey(
ops: ChatAbortOps,
params: {
sessionKey: string;
stopReason?: string;
},
): { aborted: boolean; runIds: string[] } {
const { sessionKey, stopReason } = params;
const runIds: string[] = [];
for (const [runId, active] of ops.chatAbortControllers) {
if (active.sessionKey !== sessionKey) {
continue;
}
const res = abortChatRunById(ops, { runId, sessionKey, stopReason });
if (res.aborted) {
runIds.push(runId);
}
}
return { aborted: runIds.length > 0, runIds };
}