Skip to content

Commit 5369ea5

Browse files
authored
perf(inbound): trim dispatch and command startup imports (openclaw#52374)
* perf(inbound): trim dispatch and command startup imports * fix(reply): restore command alias canonicalization * style(reply): format command context * fix(reply): restore runtime shim exports * test(reply): mock ACP route seam * fix(reply): repair dispatch type seams
1 parent 3025760 commit 5369ea5

12 files changed

+234
-131
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { formatAbortReplyText, tryFastAbortFromMessage } from "./abort.js";
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { describe, expect, it } from "vitest";
2+
import type { OpenClawConfig } from "../../config/config.js";
3+
import { buildCommandContext } from "./commands-context.js";
4+
import { buildTestCtx } from "./test-ctx.js";
5+
6+
describe("buildCommandContext", () => {
7+
it("canonicalizes registered aliases like /id to their primary command", () => {
8+
const ctx = buildTestCtx({
9+
Provider: "discord",
10+
Surface: "discord",
11+
From: "user",
12+
To: "bot",
13+
Body: "/id",
14+
RawBody: "/id",
15+
CommandBody: "/id",
16+
BodyForCommands: "/id",
17+
});
18+
19+
const result = buildCommandContext({
20+
ctx,
21+
cfg: {} as OpenClawConfig,
22+
isGroup: false,
23+
triggerBodyNormalized: "/id",
24+
commandAuthorized: true,
25+
});
26+
27+
expect(result.commandBodyNormalized).toBe("/whoami");
28+
});
29+
});

src/auto-reply/reply/commands-context.ts

Lines changed: 3 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,10 @@
11
import type { OpenClawConfig } from "../../config/config.js";
22
import { resolveCommandAuthorization } from "../command-auth.js";
3+
import { normalizeCommandBody } from "../commands-registry.js";
34
import type { MsgContext } from "../templating.js";
45
import type { CommandContext } from "./commands-types.js";
56
import { stripMentions } from "./mentions.js";
67

7-
function normalizeCommandBodyLite(raw: string, botUsername?: string): string {
8-
const trimmed = raw.trim();
9-
if (!trimmed.startsWith("/")) {
10-
return trimmed;
11-
}
12-
13-
const newline = trimmed.indexOf("\n");
14-
const singleLine = newline === -1 ? trimmed : trimmed.slice(0, newline).trim();
15-
const colonMatch = singleLine.match(/^\/([^\s:]+)\s*:(.*)$/);
16-
const normalized = colonMatch
17-
? (() => {
18-
const [, command, rest] = colonMatch;
19-
const normalizedRest = rest.trimStart();
20-
return normalizedRest ? `/${command} ${normalizedRest}` : `/${command}`;
21-
})()
22-
: singleLine;
23-
24-
const normalizedBotUsername = botUsername?.trim().toLowerCase();
25-
const mentionMatch = normalizedBotUsername
26-
? normalized.match(/^\/([^\s@]+)@([^\s]+)(.*)$/)
27-
: null;
28-
const mentionNormalized =
29-
mentionMatch && mentionMatch[2].toLowerCase() === normalizedBotUsername
30-
? `/${mentionMatch[1]}${mentionMatch[3] ?? ""}`
31-
: normalized;
32-
return mentionNormalized.replace(/^\/([^\s]+)(.*)$/, (_, command: string, rest: string) => {
33-
return `/${command.toLowerCase()}${rest ?? ""}`;
34-
});
35-
}
36-
378
export function buildCommandContext(params: {
389
ctx: MsgContext;
3910
cfg: OpenClawConfig;
@@ -53,9 +24,9 @@ export function buildCommandContext(params: {
5324
const channel = (ctx.Provider ?? surface).trim().toLowerCase();
5425
const abortKey = sessionKey ?? (auth.from || undefined) ?? (auth.to || undefined);
5526
const rawBodyNormalized = triggerBodyNormalized;
56-
const commandBodyNormalized = normalizeCommandBodyLite(
27+
const commandBodyNormalized = normalizeCommandBody(
5728
isGroup ? stripMentions(rawBodyNormalized, ctx, cfg, agentId) : rawBodyNormalized,
58-
ctx.BotUsername,
29+
{ botUsername: ctx.BotUsername },
5930
);
6031

6132
return {

src/auto-reply/reply/commands-core.ts

Lines changed: 16 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -6,44 +6,26 @@ import { getGlobalHookRunner } from "../../plugins/hook-runner-global.js";
66
import { isAcpSessionKey, resolveAgentIdFromSessionKey } from "../../routing/session-key.js";
77
import { resolveSendPolicy } from "../../sessions/send-policy.js";
88
import { shouldHandleTextCommands } from "../commands-registry.js";
9-
import { handleAcpCommand } from "./commands-acp.js";
109
import { resolveBoundAcpThreadSessionKey } from "./commands-acp/targets.js";
11-
import { handleAllowlistCommand } from "./commands-allowlist.js";
12-
import { handleApproveCommand } from "./commands-approve.js";
13-
import { handleBashCommand } from "./commands-bash.js";
14-
import { handleBtwCommand } from "./commands-btw.js";
15-
import { handleCompactCommand } from "./commands-compact.js";
16-
import { handleConfigCommand, handleDebugCommand } from "./commands-config.js";
17-
import {
18-
handleCommandsListCommand,
19-
handleContextCommand,
20-
handleExportSessionCommand,
21-
handleHelpCommand,
22-
handleStatusCommand,
23-
handleWhoamiCommand,
24-
} from "./commands-info.js";
25-
import { handleMcpCommand } from "./commands-mcp.js";
26-
import { handleModelsCommand } from "./commands-models.js";
27-
import { handlePluginCommand } from "./commands-plugin.js";
28-
import { handlePluginsCommand } from "./commands-plugins.js";
29-
import {
30-
handleAbortTrigger,
31-
handleActivationCommand,
32-
handleFastCommand,
33-
handleRestartCommand,
34-
handleSessionCommand,
35-
handleSendPolicyCommand,
36-
handleStopCommand,
37-
handleUsageCommand,
38-
} from "./commands-session.js";
39-
import { handleSubagentsCommand } from "./commands-subagents.js";
40-
import { handleTtsCommands } from "./commands-tts.js";
4110
import type {
4211
CommandHandler,
4312
CommandHandlerResult,
4413
HandleCommandsParams,
4514
} from "./commands-types.js";
46-
import { routeReply } from "./route-reply.js";
15+
16+
let routeReplyRuntimePromise: Promise<typeof import("./route-reply.runtime.js")> | null = null;
17+
let commandHandlersRuntimePromise: Promise<typeof import("./commands-handlers.runtime.js")> | null =
18+
null;
19+
20+
function loadRouteReplyRuntime() {
21+
routeReplyRuntimePromise ??= import("./route-reply.runtime.js");
22+
return routeReplyRuntimePromise;
23+
}
24+
25+
function loadCommandHandlersRuntime() {
26+
commandHandlersRuntimePromise ??= import("./commands-handlers.runtime.js");
27+
return commandHandlersRuntimePromise;
28+
}
4729

4830
let HANDLERS: CommandHandler[] | null = null;
4931

@@ -82,6 +64,7 @@ export async function emitResetCommandHooks(params: {
8264
const to = params.ctx.OriginatingTo || params.command.from || params.command.to;
8365

8466
if (channel && to) {
67+
const { routeReply } = await loadRouteReplyRuntime();
8568
const hookReply = { text: hookEvent.messages.join("\n\n") };
8669
await routeReply({
8770
payload: hookReply,
@@ -174,37 +157,7 @@ function resolveSessionEntryForHookSessionKey(
174157

175158
export async function handleCommands(params: HandleCommandsParams): Promise<CommandHandlerResult> {
176159
if (HANDLERS === null) {
177-
HANDLERS = [
178-
// Plugin commands are processed first, before built-in commands
179-
handlePluginCommand,
180-
handleBtwCommand,
181-
handleBashCommand,
182-
handleActivationCommand,
183-
handleSendPolicyCommand,
184-
handleFastCommand,
185-
handleUsageCommand,
186-
handleSessionCommand,
187-
handleRestartCommand,
188-
handleTtsCommands,
189-
handleHelpCommand,
190-
handleCommandsListCommand,
191-
handleStatusCommand,
192-
handleAllowlistCommand,
193-
handleApproveCommand,
194-
handleContextCommand,
195-
handleExportSessionCommand,
196-
handleWhoamiCommand,
197-
handleSubagentsCommand,
198-
handleAcpCommand,
199-
handleMcpCommand,
200-
handlePluginsCommand,
201-
handleConfigCommand,
202-
handleDebugCommand,
203-
handleModelsCommand,
204-
handleStopCommand,
205-
handleCompactCommand,
206-
handleAbortTrigger,
207-
];
160+
HANDLERS = (await loadCommandHandlersRuntime()).loadCommandHandlers();
208161
}
209162
const resetMatch = params.command.commandBodyNormalized.match(/^\/(new|reset)(?:\s|$)/);
210163
const resetRequested = Boolean(resetMatch);
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { handleAcpCommand } from "./commands-acp.js";
2+
import { handleAllowlistCommand } from "./commands-allowlist.js";
3+
import { handleApproveCommand } from "./commands-approve.js";
4+
import { handleBashCommand } from "./commands-bash.js";
5+
import { handleBtwCommand } from "./commands-btw.js";
6+
import { handleCompactCommand } from "./commands-compact.js";
7+
import { handleConfigCommand, handleDebugCommand } from "./commands-config.js";
8+
import {
9+
handleCommandsListCommand,
10+
handleContextCommand,
11+
handleExportSessionCommand,
12+
handleHelpCommand,
13+
handleStatusCommand,
14+
handleWhoamiCommand,
15+
} from "./commands-info.js";
16+
import { handleMcpCommand } from "./commands-mcp.js";
17+
import { handleModelsCommand } from "./commands-models.js";
18+
import { handlePluginCommand } from "./commands-plugin.js";
19+
import { handlePluginsCommand } from "./commands-plugins.js";
20+
import {
21+
handleAbortTrigger,
22+
handleActivationCommand,
23+
handleFastCommand,
24+
handleRestartCommand,
25+
handleSendPolicyCommand,
26+
handleSessionCommand,
27+
handleStopCommand,
28+
handleUsageCommand,
29+
} from "./commands-session.js";
30+
import { handleSubagentsCommand } from "./commands-subagents.js";
31+
import { handleTtsCommands } from "./commands-tts.js";
32+
import type { CommandHandler } from "./commands-types.js";
33+
34+
export function loadCommandHandlers(): CommandHandler[] {
35+
return [
36+
handlePluginCommand,
37+
handleBtwCommand,
38+
handleBashCommand,
39+
handleActivationCommand,
40+
handleSendPolicyCommand,
41+
handleFastCommand,
42+
handleUsageCommand,
43+
handleSessionCommand,
44+
handleRestartCommand,
45+
handleTtsCommands,
46+
handleHelpCommand,
47+
handleCommandsListCommand,
48+
handleStatusCommand,
49+
handleAllowlistCommand,
50+
handleApproveCommand,
51+
handleContextCommand,
52+
handleExportSessionCommand,
53+
handleWhoamiCommand,
54+
handleSubagentsCommand,
55+
handleAcpCommand,
56+
handleMcpCommand,
57+
handlePluginsCommand,
58+
handleConfigCommand,
59+
handleDebugCommand,
60+
handleModelsCommand,
61+
handleStopCommand,
62+
handleCompactCommand,
63+
handleAbortTrigger,
64+
];
65+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { shouldBypassAcpDispatchForCommand, tryDispatchAcpReply } from "./dispatch-acp.js";

src/auto-reply/reply/dispatch-from-config.test.ts

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,24 @@ const ttsMocks = vi.hoisted(() => {
104104
};
105105
});
106106

107+
vi.mock("./route-reply.runtime.js", () => ({
108+
isRoutableChannel: (channel: string | undefined) =>
109+
Boolean(
110+
channel &&
111+
[
112+
"telegram",
113+
"slack",
114+
"discord",
115+
"signal",
116+
"imessage",
117+
"whatsapp",
118+
"feishu",
119+
"mattermost",
120+
].includes(channel),
121+
),
122+
routeReply: mocks.routeReply,
123+
}));
124+
107125
vi.mock("./route-reply.js", () => ({
108126
isRoutableChannel: (channel: string | undefined) =>
109127
Boolean(
@@ -122,7 +140,7 @@ vi.mock("./route-reply.js", () => ({
122140
routeReply: mocks.routeReply,
123141
}));
124142

125-
vi.mock("./abort.js", () => ({
143+
vi.mock("./abort.runtime.js", () => ({
126144
tryFastAbortFromMessage: mocks.tryFastAbortFromMessage,
127145
formatAbortReplyText: (stoppedSubagents?: number) => {
128146
if (typeof stoppedSubagents !== "number" || stoppedSubagents <= 0) {
@@ -138,15 +156,21 @@ vi.mock("../../logging/diagnostic.js", () => ({
138156
logMessageProcessed: diagnosticMocks.logMessageProcessed,
139157
logSessionStateChange: diagnosticMocks.logSessionStateChange,
140158
}));
141-
vi.mock("../../config/sessions.js", async (importOriginal) => {
142-
const actual = await importOriginal<typeof import("../../config/sessions.js")>();
159+
vi.mock("../../config/sessions/store.js", async (importOriginal) => {
160+
const actual = await importOriginal<typeof import("../../config/sessions/store.js")>();
143161
return {
144162
...actual,
145163
loadSessionStore: sessionStoreMocks.loadSessionStore,
146-
resolveStorePath: sessionStoreMocks.resolveStorePath,
147164
resolveSessionStoreEntry: sessionStoreMocks.resolveSessionStoreEntry,
148165
};
149166
});
167+
vi.mock("../../config/sessions/paths.js", async (importOriginal) => {
168+
const actual = await importOriginal<typeof import("../../config/sessions/paths.js")>();
169+
return {
170+
...actual,
171+
resolveStorePath: sessionStoreMocks.resolveStorePath,
172+
};
173+
});
150174

151175
vi.mock("../../plugins/hook-runner-global.js", () => ({
152176
getGlobalHookRunner: () => hookMocks.runner,
@@ -192,6 +216,13 @@ vi.mock("../../tts/tts.js", () => ({
192216
normalizeTtsAutoMode: (value: unknown) => ttsMocks.normalizeTtsAutoMode(value),
193217
resolveTtsConfig: (cfg: OpenClawConfig) => ttsMocks.resolveTtsConfig(cfg),
194218
}));
219+
vi.mock("../../tts/tts.runtime.js", () => ({
220+
maybeApplyTtsToPayload: (params: unknown) => ttsMocks.maybeApplyTtsToPayload(params),
221+
}));
222+
vi.mock("../../tts/tts-config.js", () => ({
223+
normalizeTtsAutoMode: (value: unknown) => ttsMocks.normalizeTtsAutoMode(value),
224+
resolveConfiguredTtsMode: (cfg: OpenClawConfig) => ttsMocks.resolveTtsConfig(cfg).mode,
225+
}));
195226

196227
const noAbortResult = { handled: false, aborted: false } as const;
197228
const emptyConfig = {} as OpenClawConfig;

0 commit comments

Comments
 (0)