mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(chat): tool titles corrupt boundary emoji in long inputs (#104464)
* fix(chat): tool titles corrupt boundary emoji in long inputs * test(chat): simplify UTF-16 title boundary coverage * test(chat): satisfy title boundary lint --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
@@ -133,6 +133,25 @@ describe("generateToolCallTitles", () => {
|
||||
expect(content).not.toContain(token.slice(0, 12));
|
||||
});
|
||||
|
||||
it("keeps bounded tool input valid before utility-model prompt construction", async () => {
|
||||
mockPreparedModel();
|
||||
mockCompletionTitles({ "0": "Inspected boundary input" });
|
||||
|
||||
await generateToolCallTitles({
|
||||
cfg: {} satisfies OpenClawConfig,
|
||||
agentId: AGENT_ID,
|
||||
items: [{ id: "item-1", name: "bash", input: `${"a".repeat(1_999)}😀tail` }],
|
||||
});
|
||||
|
||||
const call = completeWithPreparedSimpleCompletionModel.mock.calls[0]?.[0] as {
|
||||
context: { messages: Array<{ content: string }> };
|
||||
};
|
||||
const promptPayload = JSON.parse(call.context.messages[0]?.content ?? "{}") as {
|
||||
items?: Array<{ input?: string }>;
|
||||
};
|
||||
expect(promptPayload.items?.[0]?.input).toBe("a".repeat(1_999));
|
||||
});
|
||||
|
||||
it("serves repeated items from the SQLite cache without a second completion", async () => {
|
||||
mockPreparedModel();
|
||||
mockCompletionTitles({ "0": "Checked repo status" });
|
||||
|
||||
@@ -74,7 +74,7 @@ function normalizeItems(items: readonly ToolTitleRequestItem[]): ToolTitleReques
|
||||
// secret egress path. Redaction runs on the full schema-bounded input and
|
||||
// only then truncates — slicing first could bisect a secret so its
|
||||
// fragment no longer matches any redaction pattern.
|
||||
const input = redactToolPayloadText(item.input).slice(0, TOOL_TITLE_INPUT_MAX_CHARS);
|
||||
const input = truncateUtf16Safe(redactToolPayloadText(item.input), TOOL_TITLE_INPUT_MAX_CHARS);
|
||||
if (!id || !name || !input.trim() || seen.has(id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -52,6 +52,19 @@ describe("resolveToolTitleRequest", () => {
|
||||
|
||||
expect(first?.key).toBe(second?.key);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["command", "bash", { command: `${"a".repeat(1_999)}😀tail` }, "a".repeat(1_999)],
|
||||
["string args", "mcp__linear__create_issue", `${"a".repeat(1_999)}😀tail`, "a".repeat(1_999)],
|
||||
[
|
||||
"serialized object args",
|
||||
"mcp__linear__create_issue",
|
||||
{ value: `${"a".repeat(1_989)}😀tail` },
|
||||
'{"value":"' + "a".repeat(1_989),
|
||||
],
|
||||
])("keeps bounded %s on a valid UTF-16 boundary", (_label, name, args, expected) => {
|
||||
expect(resolveToolTitleRequest(name, args)?.input).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getToolCallTitle", () => {
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
* labels.
|
||||
*/
|
||||
|
||||
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
|
||||
import type { GatewayBrowserClient } from "../../api/gateway.ts";
|
||||
import { resolveToolCallKind, unwrapShellWrapperCommand } from "../../lib/chat/tool-call-view.ts";
|
||||
|
||||
@@ -68,11 +69,11 @@ function serializeArgs(args: unknown): string | null {
|
||||
return null;
|
||||
}
|
||||
if (typeof args === "string") {
|
||||
return args.slice(0, MAX_TITLE_INPUT_CHARS);
|
||||
return truncateUtf16Safe(args, MAX_TITLE_INPUT_CHARS);
|
||||
}
|
||||
try {
|
||||
const encoded = JSON.stringify(args);
|
||||
return typeof encoded === "string" ? encoded.slice(0, MAX_TITLE_INPUT_CHARS) : null;
|
||||
return typeof encoded === "string" ? truncateUtf16Safe(encoded, MAX_TITLE_INPUT_CHARS) : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
@@ -98,7 +99,7 @@ export function resolveToolTitleRequest(
|
||||
if (command.length < MIN_COMMAND_CHARS_FOR_TITLE) {
|
||||
return null;
|
||||
}
|
||||
const input = command.slice(0, MAX_TITLE_INPUT_CHARS);
|
||||
const input = truncateUtf16Safe(command, MAX_TITLE_INPUT_CHARS);
|
||||
return { key: digest("command", input), input };
|
||||
}
|
||||
if (kind !== "generic") {
|
||||
|
||||
Reference in New Issue
Block a user