Skip to content

Commit eee7307

Browse files
committed
perf(core): trim reply helper churn
1 parent 468c6a0 commit eee7307

11 files changed

Lines changed: 329 additions & 215 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -521,6 +521,7 @@ Docs: https://docs.openclaw.ai
521521

522522
- Channels/streaming: add unified `streaming.mode: "progress"` drafts with auto single-word status labels and shared progress configuration across Discord, Telegram, Matrix, Slack, and Microsoft Teams.
523523
- Agents/commands: add `/steer <message>` for queue-independent steering of the active current-session run without starting a new turn when the session is idle. (#76934)
524+
- Core/performance: trim reply payload routing, heartbeat filtering, tool display, core tool assembly, channel directory, task status, and Slack approval formatting helper chains with direct bounded scans. Thanks @vincentkoc.
524525
- Tools/BTW: add `/side` as a text and native slash-command alias for `/btw` side questions.
525526
- Doctor/config: `doctor --fix` now commits safe legacy migrations even when unrelated validation issues (e.g. a missing plugin) prevent full validation from passing, so `agents.defaults.llm` and other known-legacy keys are always cleaned up by `doctor --fix` regardless of other config problems. Fixes #76798. (#76800) Thanks @hclsys.
526527
- Agents/tools: skip optional media and PDF tool factories when the effective tool denylist already blocks them, avoiding unnecessary hot-path setup for tools that will be filtered out before model use. (#76773) Thanks @dorukardahan.

extensions/slack/src/approval-handler.runtime.ts

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -82,22 +82,35 @@ function formatSlackMetadataLine(label: string, value: string): string {
8282
}
8383

8484
function buildSlackMetadataLines(metadata: readonly { label: string; value: string }[]): string[] {
85-
return metadata.map((item) => formatSlackMetadataLine(item.label, item.value));
85+
const lines: string[] = [];
86+
for (const item of metadata) {
87+
lines.push(formatSlackMetadataLine(item.label, item.value));
88+
}
89+
return lines;
8690
}
8791

8892
function buildSlackMetadataContextElements(metadata: readonly { label: string; value: string }[]) {
8993
const lines = buildSlackMetadataLines(metadata);
90-
const visibleLines =
91-
lines.length > SLACK_CONTEXT_ELEMENTS_MAX
92-
? [
93-
...lines.slice(0, SLACK_CONTEXT_ELEMENTS_MAX - 1),
94-
`…+${lines.length - (SLACK_CONTEXT_ELEMENTS_MAX - 1)} more`,
95-
]
96-
: lines;
97-
return visibleLines.map((line) => ({
98-
type: "mrkdwn" as const,
99-
text: truncateSlackMrkdwn(line, SLACK_TEXT_OBJECT_MAX),
100-
}));
94+
const visibleLineCount =
95+
lines.length > SLACK_CONTEXT_ELEMENTS_MAX ? SLACK_CONTEXT_ELEMENTS_MAX - 1 : lines.length;
96+
const elements: Array<{ type: "mrkdwn"; text: string }> = [];
97+
for (let index = 0; index < visibleLineCount; index += 1) {
98+
const line = lines[index];
99+
if (line === undefined) {
100+
continue;
101+
}
102+
elements.push({
103+
type: "mrkdwn",
104+
text: truncateSlackMrkdwn(line, SLACK_TEXT_OBJECT_MAX),
105+
});
106+
}
107+
if (lines.length > SLACK_CONTEXT_ELEMENTS_MAX) {
108+
elements.push({
109+
type: "mrkdwn",
110+
text: `…+${lines.length - visibleLineCount} more`,
111+
});
112+
}
113+
return elements;
101114
}
102115

103116
function resolveSlackApprovalDecisionLabel(
@@ -120,7 +133,7 @@ function buildSlackPendingApprovalText(view: ExecApprovalPendingView): string {
120133
buildSlackCodeBlock(view.commandText),
121134
...metadataLines,
122135
];
123-
return lines.filter(Boolean).join("\n");
136+
return lines.join("\n");
124137
}
125138

126139
function buildSlackPendingApprovalBlocks(view: ExecApprovalPendingView): SlackBlock[] {

src/agents/pi-tools.ts

Lines changed: 67 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -508,51 +508,56 @@ export function createOpenClawCodingTools(options?: {
508508
const imageSanitization = resolveImageSanitizationLimits(options?.config);
509509
options?.recordToolPrepStage?.("workspace-policy");
510510

511-
const base = includeBaseCodingTools
512-
? (createCodingTools(workspaceRoot) as unknown as AnyAgentTool[]).flatMap((tool) => {
513-
if (tool.name === "read") {
514-
if (sandboxRoot) {
515-
const sandboxed = createSandboxedReadTool({
516-
root: sandboxRoot,
517-
bridge: sandboxFsBridge!,
518-
modelContextWindowTokens: options?.modelContextWindowTokens,
519-
imageSanitization,
520-
});
521-
return [
522-
workspaceOnly
523-
? wrapToolWorkspaceRootGuardWithOptions(sandboxed, sandboxRoot, {
524-
containerWorkdir: sandbox.containerWorkdir,
525-
})
526-
: sandboxed,
527-
];
528-
}
529-
const freshReadTool = createReadTool(workspaceRoot);
530-
const wrapped = createOpenClawReadTool(freshReadTool, {
511+
const base: AnyAgentTool[] = [];
512+
if (includeBaseCodingTools) {
513+
for (const tool of createCodingTools(workspaceRoot) as unknown as AnyAgentTool[]) {
514+
if (tool.name === "read") {
515+
if (sandboxRoot) {
516+
const sandboxed = createSandboxedReadTool({
517+
root: sandboxRoot,
518+
bridge: sandboxFsBridge!,
531519
modelContextWindowTokens: options?.modelContextWindowTokens,
532520
imageSanitization,
533521
});
534-
return [workspaceOnly ? wrapToolWorkspaceRootGuard(wrapped, workspaceRoot) : wrapped];
535-
}
536-
if (tool.name === "bash" || tool.name === execToolName) {
537-
return [];
522+
base.push(
523+
workspaceOnly
524+
? wrapToolWorkspaceRootGuardWithOptions(sandboxed, sandboxRoot, {
525+
containerWorkdir: sandbox.containerWorkdir,
526+
})
527+
: sandboxed,
528+
);
529+
continue;
538530
}
539-
if (tool.name === "write") {
540-
if (sandboxRoot) {
541-
return [];
542-
}
543-
const wrapped = createHostWorkspaceWriteTool(workspaceRoot, { workspaceOnly });
544-
return [workspaceOnly ? wrapToolWorkspaceRootGuard(wrapped, workspaceRoot) : wrapped];
531+
const freshReadTool = createReadTool(workspaceRoot);
532+
const wrapped = createOpenClawReadTool(freshReadTool, {
533+
modelContextWindowTokens: options?.modelContextWindowTokens,
534+
imageSanitization,
535+
});
536+
base.push(workspaceOnly ? wrapToolWorkspaceRootGuard(wrapped, workspaceRoot) : wrapped);
537+
continue;
538+
}
539+
if (tool.name === "bash" || tool.name === execToolName) {
540+
continue;
541+
}
542+
if (tool.name === "write") {
543+
if (sandboxRoot) {
544+
continue;
545545
}
546-
if (tool.name === "edit") {
547-
if (sandboxRoot) {
548-
return [];
549-
}
550-
const wrapped = createHostWorkspaceEditTool(workspaceRoot, { workspaceOnly });
551-
return [workspaceOnly ? wrapToolWorkspaceRootGuard(wrapped, workspaceRoot) : wrapped];
546+
const wrapped = createHostWorkspaceWriteTool(workspaceRoot, { workspaceOnly });
547+
base.push(workspaceOnly ? wrapToolWorkspaceRootGuard(wrapped, workspaceRoot) : wrapped);
548+
continue;
549+
}
550+
if (tool.name === "edit") {
551+
if (sandboxRoot) {
552+
continue;
552553
}
553-
return [tool];
554-
})
555-
: [];
554+
const wrapped = createHostWorkspaceEditTool(workspaceRoot, { workspaceOnly });
555+
base.push(workspaceOnly ? wrapToolWorkspaceRootGuard(wrapped, workspaceRoot) : wrapped);
556+
continue;
557+
}
558+
base.push(tool);
559+
}
560+
}
556561
options?.recordToolPrepStage?.("base-coding-tools");
557562
const { cleanupMs: cleanupMsOverride, ...execDefaults } = options?.exec ?? {};
558563
const execTool = includeShellTools
@@ -754,28 +759,29 @@ export function createOpenClawCodingTools(options?: {
754759
: pluginToolsOnly),
755760
];
756761
options?.recordToolPrepStage?.("openclaw-tools");
757-
const toolsForMemoryFlush =
758-
isMemoryFlushRun && memoryFlushWritePath
759-
? tools.flatMap((tool) => {
760-
if (!MEMORY_FLUSH_ALLOWED_TOOL_NAMES.has(tool.name)) {
761-
return [];
762-
}
763-
if (tool.name === "write") {
764-
return [
765-
wrapToolMemoryFlushAppendOnlyWrite(tool, {
766-
root: sandboxRoot ?? workspaceRoot,
767-
relativePath: memoryFlushWritePath,
768-
containerWorkdir: sandbox?.containerWorkdir,
769-
sandbox:
770-
sandboxRoot && sandboxFsBridge
771-
? { root: sandboxRoot, bridge: sandboxFsBridge }
772-
: undefined,
773-
}),
774-
];
775-
}
776-
return [tool];
777-
})
778-
: tools;
762+
const toolsForMemoryFlush: AnyAgentTool[] = isMemoryFlushRun && memoryFlushWritePath ? [] : tools;
763+
if (isMemoryFlushRun && memoryFlushWritePath) {
764+
for (const tool of tools) {
765+
if (!MEMORY_FLUSH_ALLOWED_TOOL_NAMES.has(tool.name)) {
766+
continue;
767+
}
768+
if (tool.name === "write") {
769+
toolsForMemoryFlush.push(
770+
wrapToolMemoryFlushAppendOnlyWrite(tool, {
771+
root: sandboxRoot ?? workspaceRoot,
772+
relativePath: memoryFlushWritePath,
773+
containerWorkdir: sandbox?.containerWorkdir,
774+
sandbox:
775+
sandboxRoot && sandboxFsBridge
776+
? { root: sandboxRoot, bridge: sandboxFsBridge }
777+
: undefined,
778+
}),
779+
);
780+
continue;
781+
}
782+
toolsForMemoryFlush.push(tool);
783+
}
784+
}
779785
const toolsForMessageProvider = filterToolsByMessageProvider(
780786
toolsForMemoryFlush,
781787
options?.messageProvider,

src/agents/tool-display-common.ts

Lines changed: 54 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,15 @@ export function defaultTitle(name: string): string {
3434
if (!cleaned) {
3535
return "Tool";
3636
}
37-
return cleaned
38-
.split(/\s+/)
39-
.map((part) =>
37+
const parts: string[] = [];
38+
for (const part of cleaned.split(/\s+/)) {
39+
parts.push(
4040
part.length <= 2 && part.toUpperCase() === part
4141
? part
4242
: `${part.at(0)?.toUpperCase() ?? ""}${part.slice(1)}`,
43-
)
44-
.join(" ");
43+
);
44+
}
45+
return parts.join(" ");
4546
}
4647

4748
function normalizeVerb(value?: string): string | undefined {
@@ -131,14 +132,23 @@ function coerceDisplayValue(
131132
return String(value);
132133
}
133134
if (Array.isArray(value)) {
134-
const values = value
135-
.map((item) => coerceDisplayValue(item, opts))
136-
.filter((item): item is string => Boolean(item));
137-
if (values.length === 0) {
135+
const values: string[] = [];
136+
let displayValueCount = 0;
137+
for (const item of value) {
138+
const display = coerceDisplayValue(item, opts);
139+
if (!display) {
140+
continue;
141+
}
142+
displayValueCount += 1;
143+
if (values.length < maxArrayEntries) {
144+
values.push(display);
145+
}
146+
}
147+
if (displayValueCount === 0) {
138148
return undefined;
139149
}
140-
const preview = values.slice(0, maxArrayEntries).join(", ");
141-
return values.length > maxArrayEntries ? `${preview}…` : preview;
150+
const preview = values.join(", ");
151+
return displayValueCount > maxArrayEntries ? `${preview}…` : preview;
142152
}
143153
return undefined;
144154
}
@@ -162,8 +172,13 @@ function lookupValueByPath(args: unknown, path: string): unknown {
162172
}
163173

164174
export function formatDetailKey(raw: string, overrides: Record<string, string> = {}): string {
165-
const segments = raw.split(".").filter(Boolean);
166-
const last = segments.at(-1) ?? raw;
175+
let last = "";
176+
for (const segment of raw.split(".")) {
177+
if (segment) {
178+
last = segment;
179+
}
180+
}
181+
last ||= raw;
167182
const override = overrides[last];
168183
if (override) {
169184
return override;
@@ -295,12 +310,13 @@ function resolveWebFetchDetail(args: unknown): string | undefined {
295310
? Math.floor(record.maxChars)
296311
: undefined;
297312

298-
const suffix = [
299-
mode ? `mode ${mode}` : undefined,
300-
maxChars !== undefined ? `max ${maxChars} chars` : undefined,
301-
]
302-
.filter((value): value is string => Boolean(value))
303-
.join(", ");
313+
let suffix = "";
314+
if (mode) {
315+
suffix = `mode ${mode}`;
316+
}
317+
if (maxChars !== undefined) {
318+
suffix = suffix ? `${suffix}, max ${maxChars} chars` : `max ${maxChars} chars`;
319+
}
304320

305321
return suffix ? `from ${url} (${suffix})` : `from ${url}`;
306322
}
@@ -366,10 +382,15 @@ function resolveDetailFromKeys(
366382
return undefined;
367383
}
368384

369-
return unique
370-
.slice(0, opts.maxEntries ?? 8)
371-
.map((entry) => `${entry.label} ${entry.value}`)
372-
.join(" · ");
385+
const maxEntries = opts.maxEntries ?? 8;
386+
const parts: string[] = [];
387+
for (let index = 0; index < unique.length && index < maxEntries; index += 1) {
388+
const entry = unique[index];
389+
if (entry) {
390+
parts.push(`${entry.label} ${entry.value}`);
391+
}
392+
}
393+
return parts.join(" · ");
373394
}
374395

375396
function resolveToolVerbAndDetail(params: {
@@ -438,11 +459,16 @@ export function formatToolDetailText(
438459
return undefined;
439460
}
440461
const normalized = detail.includes(" · ")
441-
? detail
442-
.split(" · ")
443-
.map((part) => part.trim())
444-
.filter((part) => part.length > 0)
445-
.join(", ")
462+
? (() => {
463+
const parts: string[] = [];
464+
for (const part of detail.split(" · ")) {
465+
const trimmed = part.trim();
466+
if (trimmed) {
467+
parts.push(trimmed);
468+
}
469+
}
470+
return parts.join(", ");
471+
})()
446472
: detail;
447473
if (!normalized) {
448474
return undefined;

src/auto-reply/commands-registry.shared.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -70,11 +70,13 @@ function registerAlias(commands: ChatCommandDefinition[], key: string, ...aliase
7070
if (!command) {
7171
throw new Error(`registerAlias: unknown command key: ${key}`);
7272
}
73-
const existing = new Set(
74-
command.textAliases
75-
.map((alias) => normalizeOptionalLowercaseString(alias))
76-
.filter((alias): alias is string => Boolean(alias)),
77-
);
73+
const existing = new Set<string>();
74+
for (const alias of command.textAliases) {
75+
const lowered = normalizeOptionalLowercaseString(alias);
76+
if (lowered) {
77+
existing.add(lowered);
78+
}
79+
}
7880
for (const alias of aliases) {
7981
const trimmed = alias.trim();
8082
if (!trimmed) {

0 commit comments

Comments
 (0)