Spaces:
Paused
Paused
| import type { OpenClawConfig } from "../../config/config.js"; | |
| import type { FinalizedMsgContext, MsgContext } from "../templating.js"; | |
| import { resolveSessionAgentId } from "../../agents/agent-scope.js"; | |
| import { abortEmbeddedPiRun } from "../../agents/pi-embedded.js"; | |
| import { listSubagentRunsForRequester } from "../../agents/subagent-registry.js"; | |
| import { | |
| resolveInternalSessionKey, | |
| resolveMainSessionAlias, | |
| } from "../../agents/tools/sessions-helpers.js"; | |
| import { | |
| loadSessionStore, | |
| resolveStorePath, | |
| type SessionEntry, | |
| updateSessionStore, | |
| } from "../../config/sessions.js"; | |
| import { logVerbose } from "../../globals.js"; | |
| import { parseAgentSessionKey } from "../../routing/session-key.js"; | |
| import { resolveCommandAuthorization } from "../command-auth.js"; | |
| import { normalizeCommandBody } from "../commands-registry.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_MEMORY = new Map<string, boolean>(); | |
| export function isAbortTrigger(text?: string): boolean { | |
| if (!text) { | |
| return false; | |
| } | |
| const normalized = text.trim().toLowerCase(); | |
| return ABORT_TRIGGERS.has(normalized); | |
| } | |
| export function getAbortMemory(key: string): boolean | undefined { | |
| return ABORT_MEMORY.get(key); | |
| } | |
| export function setAbortMemory(key: string, value: boolean): void { | |
| ABORT_MEMORY.set(key, value); | |
| } | |
| export function formatAbortReplyText(stoppedSubagents?: number): string { | |
| if (typeof stoppedSubagents !== "number" || stoppedSubagents <= 0) { | |
| return "⚙️ Agent was aborted."; | |
| } | |
| const label = stoppedSubagents === 1 ? "sub-agent" : "sub-agents"; | |
| return `⚙️ Agent was aborted. Stopped ${stoppedSubagents} ${label}.`; | |
| } | |
| function resolveSessionEntryForKey( | |
| store: Record<string, SessionEntry> | undefined, | |
| sessionKey: string | undefined, | |
| ) { | |
| if (!store || !sessionKey) { | |
| return {}; | |
| } | |
| const direct = store[sessionKey]; | |
| if (direct) { | |
| return { entry: direct, key: sessionKey }; | |
| } | |
| return {}; | |
| } | |
| function resolveAbortTargetKey(ctx: MsgContext): string | undefined { | |
| const target = ctx.CommandTargetSessionKey?.trim(); | |
| if (target) { | |
| return target; | |
| } | |
| const sessionKey = ctx.SessionKey?.trim(); | |
| return sessionKey || undefined; | |
| } | |
| function normalizeRequesterSessionKey( | |
| cfg: OpenClawConfig, | |
| key: string | undefined, | |
| ): string | undefined { | |
| const cleaned = key?.trim(); | |
| if (!cleaned) { | |
| return undefined; | |
| } | |
| const { mainKey, alias } = resolveMainSessionAlias(cfg); | |
| return resolveInternalSessionKey({ key: cleaned, alias, mainKey }); | |
| } | |
| export function stopSubagentsForRequester(params: { | |
| cfg: OpenClawConfig; | |
| requesterSessionKey?: string; | |
| }): { stopped: number } { | |
| const requesterKey = normalizeRequesterSessionKey(params.cfg, params.requesterSessionKey); | |
| if (!requesterKey) { | |
| return { stopped: 0 }; | |
| } | |
| const runs = listSubagentRunsForRequester(requesterKey); | |
| if (runs.length === 0) { | |
| return { stopped: 0 }; | |
| } | |
| const storeCache = new Map<string, Record<string, SessionEntry>>(); | |
| const seenChildKeys = new Set<string>(); | |
| let stopped = 0; | |
| for (const run of runs) { | |
| if (run.endedAt) { | |
| continue; | |
| } | |
| const childKey = run.childSessionKey?.trim(); | |
| if (!childKey || seenChildKeys.has(childKey)) { | |
| continue; | |
| } | |
| seenChildKeys.add(childKey); | |
| const cleared = clearSessionQueues([childKey]); | |
| const parsed = parseAgentSessionKey(childKey); | |
| const storePath = resolveStorePath(params.cfg.session?.store, { agentId: parsed?.agentId }); | |
| let store = storeCache.get(storePath); | |
| if (!store) { | |
| store = loadSessionStore(storePath); | |
| storeCache.set(storePath, store); | |
| } | |
| const entry = store[childKey]; | |
| const sessionId = entry?.sessionId; | |
| const aborted = sessionId ? abortEmbeddedPiRun(sessionId) : false; | |
| if (aborted || cleared.followupCleared > 0 || cleared.laneCleared > 0) { | |
| stopped += 1; | |
| } | |
| } | |
| if (stopped > 0) { | |
| logVerbose(`abort: stopped ${stopped} subagent run(s) for ${requesterKey}`); | |
| } | |
| return { stopped }; | |
| } | |
| export async function tryFastAbortFromMessage(params: { | |
| ctx: FinalizedMsgContext; | |
| cfg: OpenClawConfig; | |
| }): Promise<{ handled: boolean; aborted: boolean; stoppedSubagents?: number }> { | |
| const { ctx, cfg } = params; | |
| const targetKey = resolveAbortTargetKey(ctx); | |
| const agentId = resolveSessionAgentId({ | |
| sessionKey: targetKey ?? ctx.SessionKey ?? "", | |
| config: cfg, | |
| }); | |
| // Use RawBody/CommandBody for abort detection (clean message without structural context). | |
| const raw = stripStructuralPrefixes(ctx.CommandBody ?? ctx.RawBody ?? ctx.Body ?? ""); | |
| const isGroup = ctx.ChatType?.trim().toLowerCase() === "group"; | |
| const stripped = isGroup ? stripMentions(raw, ctx, cfg, agentId) : raw; | |
| const normalized = normalizeCommandBody(stripped); | |
| const abortRequested = normalized === "/stop" || isAbortTrigger(stripped); | |
| if (!abortRequested) { | |
| return { handled: false, aborted: false }; | |
| } | |
| const commandAuthorized = ctx.CommandAuthorized; | |
| const auth = resolveCommandAuthorization({ | |
| ctx, | |
| cfg, | |
| commandAuthorized, | |
| }); | |
| if (!auth.isAuthorizedSender) { | |
| return { handled: false, aborted: false }; | |
| } | |
| const abortKey = targetKey ?? auth.from ?? auth.to; | |
| const requesterSessionKey = targetKey ?? ctx.SessionKey ?? abortKey; | |
| if (targetKey) { | |
| const storePath = resolveStorePath(cfg.session?.store, { agentId }); | |
| const store = loadSessionStore(storePath); | |
| const { entry, key } = resolveSessionEntryForKey(store, targetKey); | |
| const sessionId = entry?.sessionId; | |
| const aborted = sessionId ? abortEmbeddedPiRun(sessionId) : false; | |
| const cleared = clearSessionQueues([key ?? targetKey, sessionId]); | |
| if (cleared.followupCleared > 0 || cleared.laneCleared > 0) { | |
| logVerbose( | |
| `abort: cleared followups=${cleared.followupCleared} lane=${cleared.laneCleared} keys=${cleared.keys.join(",")}`, | |
| ); | |
| } | |
| if (entry && key) { | |
| entry.abortedLastRun = true; | |
| entry.updatedAt = Date.now(); | |
| store[key] = entry; | |
| await updateSessionStore(storePath, (nextStore) => { | |
| const nextEntry = nextStore[key] ?? entry; | |
| if (!nextEntry) { | |
| return; | |
| } | |
| nextEntry.abortedLastRun = true; | |
| nextEntry.updatedAt = Date.now(); | |
| nextStore[key] = nextEntry; | |
| }); | |
| } else if (abortKey) { | |
| setAbortMemory(abortKey, true); | |
| } | |
| const { stopped } = stopSubagentsForRequester({ cfg, requesterSessionKey }); | |
| return { handled: true, aborted, stoppedSubagents: stopped }; | |
| } | |
| if (abortKey) { | |
| setAbortMemory(abortKey, true); | |
| } | |
| const { stopped } = stopSubagentsForRequester({ cfg, requesterSessionKey }); | |
| return { handled: true, aborted: false, stoppedSubagents: stopped }; | |
| } | |