File size: 3,502 Bytes
4440aec
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
// Recognizes stop requests before loading active-run cancellation machinery.
import {
  normalizeOptionalLowercaseString,
  normalizeOptionalString,
} from "@openclaw/normalization-core/string-coerce";
import { resolveSessionAgentId } from "../../agents/agent-scope.js";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import type { FinalizedRuntimeMsgContext } from "../templating.js";
import { isAbortRequestText } from "./abort-primitives.js";
import { stripMentions, stripStructuralPrefixes } from "./mentions.js";

export type FastAbortRequestParams = {
  ctx: FinalizedRuntimeMsgContext;
  cfg: OpenClawConfig;
  isCommandTargetCurrent?: () => boolean;
};

export type FastAbortResult = {
  handled: boolean;
  aborted: boolean;
  rejectionReason?: "finalizing";
  stoppedSubagents?: number;
  failedSubagents?: number;
};

export function formatAbortReplyText(
  stoppedSubagents?: number,
  rejectionReason?: "finalizing",
  failedSubagents?: number,
): string {
  const failureSuffix =
    typeof failedSubagents === "number" && failedSubagents > 0
      ? ` Cancellation was incomplete for ${failedSubagents} sub-agent${failedSubagents === 1 ? "" : "s"}. Retry /stop.`
      : "";
  if (rejectionReason === "finalizing") {
    const base = "Agent reply is already finalizing and can no longer be aborted.";
    if (typeof stoppedSubagents !== "number" || stoppedSubagents <= 0) {
      return `${base}${failureSuffix}`;
    }
    const label = stoppedSubagents === 1 ? "sub-agent" : "sub-agents";
    return `${base} Stopped ${stoppedSubagents} ${label}.${failureSuffix}`;
  }
  if (typeof stoppedSubagents !== "number" || stoppedSubagents <= 0) {
    return `⚙️ Agent was aborted.${failureSuffix}`;
  }
  const label = stoppedSubagents === 1 ? "sub-agent" : "sub-agents";
  return `⚙️ Agent was aborted. Stopped ${stoppedSubagents} ${label}.${failureSuffix}`;
}

/** Normalize ingress once; current authorization belongs to the loaded operation. */
function resolveFastAbortRequest(params: FastAbortRequestParams) {
  const { ctx, cfg } = params;
  const commandSessionKey =
    normalizeOptionalString(ctx.SessionKey) ?? normalizeOptionalString(ctx.ParentSessionKey);
  const targetKey = normalizeOptionalString(ctx.CommandTargetSessionKey) ?? commandSessionKey;
  const resolveTargetAgentId = () =>
    resolveSessionAgentId({
      sessionKey: targetKey ?? ctx.SessionKey ?? "",
      config: cfg,
      fallbackAgentId: ctx.AgentId,
    });
  const raw = stripStructuralPrefixes(ctx.commandText);
  const isGroup = normalizeOptionalLowercaseString(ctx.ChatType) === "group";
  const stripped = isGroup ? stripMentions(raw, ctx, cfg, resolveTargetAgentId()) : raw;
  const abortRequested = isAbortRequestText(stripped);
  if (!abortRequested) {
    return undefined;
  }

  return { commandSessionKey, targetKey, resolveTargetAgentId };
}

export type PreparedFastAbortRequest = NonNullable<ReturnType<typeof resolveFastAbortRequest>>;

export async function tryFastAbortFromMessage(
  params: FastAbortRequestParams,
): Promise<FastAbortResult> {
  const request = resolveFastAbortRequest(params);
  if (!request) {
    return { handled: false, aborted: false };
  }
  // Normalized ingress is not authority. The operation resolves authorization
  // and retains current-target validation after loading cancellation machinery.
  const { executeFastAbortRequest } = await import("./abort-operation.js");
  return executeFastAbortRequest(params, request);
}