Skip to content

Commit 71c6c92

Browse files
zerone0xclaude
andcommitted
fix(tools): strip </arg_value>> suffix from tool call args
Some models (e.g. Qwen via DashScope) emit tool call arguments with trailing XML fragments like `</arg_value>>` appended to string values. This corrupts exec commands and file paths, blocking all file operations and command execution on affected platforms. Add `stripXmlArgValueSuffix` to strip these artifacts from tool call argument string values in both the parameter normalization layer (for read/write/edit tools) and the exec tool's command parameter. Fixes #48780 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 80a2af1 commit 71c6c92

3 files changed

Lines changed: 94 additions & 0 deletions

File tree

src/agents/bash-tools.exec.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import {
4343
resolveWorkdir,
4444
truncateMiddle,
4545
} from "./bash-tools.shared.js";
46+
import { stripXmlArgValueSuffix } from "./pi-tools.params.js";
4647
import { assertSandboxPath } from "./sandbox-paths.js";
4748

4849
export type { BashSandboxConfig } from "./bash-tools.shared.js";
@@ -226,6 +227,10 @@ export function createExecTool(
226227
throw new Error("Provide a command to start.");
227228
}
228229

230+
// Strip malformed XML closing-tag suffixes leaked by some providers (e.g. Qwen/DashScope).
231+
// See https://github.com/openclaw/openclaw/issues/48780
232+
params.command = stripXmlArgValueSuffix(params.command);
233+
229234
const maxOutput = DEFAULT_MAX_OUTPUT;
230235
const pendingMaxOutput = DEFAULT_PENDING_MAX_OUTPUT;
231236
const warnings: string[] = [];

src/agents/pi-tools.params.test.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { describe, expect, it } from "vitest";
2+
import { normalizeToolParams, stripXmlArgValueSuffix } from "./pi-tools.params.js";
3+
4+
describe("stripXmlArgValueSuffix", () => {
5+
it("strips </arg_value>> suffix", () => {
6+
expect(stripXmlArgValueSuffix('echo "test</arg_value>>')).toBe('echo "test');
7+
});
8+
9+
it("strips </arg_value> suffix (single >)", () => {
10+
expect(stripXmlArgValueSuffix('echo "test</arg_value>')).toBe('echo "test');
11+
});
12+
13+
it("strips </arg_value>>> suffix (triple >)", () => {
14+
expect(stripXmlArgValueSuffix('echo "test</arg_value>>>')).toBe('echo "test');
15+
});
16+
17+
it("leaves clean strings unchanged", () => {
18+
expect(stripXmlArgValueSuffix('echo "hello world"')).toBe('echo "hello world"');
19+
});
20+
21+
it("leaves empty string unchanged", () => {
22+
expect(stripXmlArgValueSuffix("")).toBe("");
23+
});
24+
25+
it("handles file paths with suffix", () => {
26+
expect(stripXmlArgValueSuffix("/home/user/test.txt</arg_value>>")).toBe("/home/user/test.txt");
27+
});
28+
});
29+
30+
describe("normalizeToolParams strips XML arg_value suffixes", () => {
31+
it("strips </arg_value>> from command param", () => {
32+
const result = normalizeToolParams({ command: 'echo "test</arg_value>>' });
33+
expect(result?.command).toBe('echo "test');
34+
});
35+
36+
it("strips </arg_value>> from path param", () => {
37+
const result = normalizeToolParams({ path: "/home/user/test.txt</arg_value>>" });
38+
expect(result?.path).toBe("/home/user/test.txt");
39+
});
40+
41+
it("strips </arg_value>> from file_path param (normalizes to path)", () => {
42+
const result = normalizeToolParams({ file_path: "/home/user/test.txt</arg_value>>" });
43+
expect(result?.path).toBe("/home/user/test.txt");
44+
});
45+
46+
it("leaves clean params unchanged", () => {
47+
const result = normalizeToolParams({ command: "ls -la", path: "/tmp" });
48+
expect(result?.command).toBe("ls -la");
49+
expect(result?.path).toBe("/tmp");
50+
});
51+
52+
it("does not affect non-string values", () => {
53+
const result = normalizeToolParams({ timeout: 5000, background: true });
54+
expect(result?.timeout).toBe(5000);
55+
expect(result?.background).toBe(true);
56+
});
57+
});

src/agents/pi-tools.params.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,35 @@ function normalizeTextLikeParam(record: Record<string, unknown>, key: string) {
8282
}
8383
}
8484

85+
/**
86+
* Strip malformed XML closing-tag suffixes from tool call argument values.
87+
*
88+
* Some models (e.g. Qwen via DashScope) emit tool call arguments with
89+
* trailing XML fragments like `</arg_value>>` or `</arg_value>` appended
90+
* to string values. This corrupts exec commands and file paths on Windows
91+
* (and elsewhere). Strip these artifacts so the actual argument value is
92+
* preserved.
93+
*
94+
* @see https://github.com/openclaw/openclaw/issues/48780
95+
*/
96+
const XML_ARG_VALUE_SUFFIX_RE = /<\/arg_value>>{0,2}$/;
97+
98+
export function stripXmlArgValueSuffix(value: string): string {
99+
if (!value || !value.includes("</arg_value>")) {
100+
return value;
101+
}
102+
return value.replace(XML_ARG_VALUE_SUFFIX_RE, "");
103+
}
104+
105+
function stripXmlArgValueSuffixFromRecord(record: Record<string, unknown>): void {
106+
for (const key of Object.keys(record)) {
107+
const value = record[key];
108+
if (typeof value === "string" && value.includes("</arg_value>")) {
109+
record[key] = stripXmlArgValueSuffix(value);
110+
}
111+
}
112+
}
113+
85114
// Normalize tool parameters from Claude Code conventions to pi-coding-agent conventions.
86115
// Claude Code uses file_path/old_string/new_string while pi-coding-agent uses path/oldText/newText.
87116
// This prevents models trained on Claude Code from getting stuck in tool-call loops.
@@ -111,6 +140,9 @@ export function normalizeToolParams(params: unknown): Record<string, unknown> |
111140
normalizeTextLikeParam(normalized, "content");
112141
normalizeTextLikeParam(normalized, "oldText");
113142
normalizeTextLikeParam(normalized, "newText");
143+
// Strip malformed XML closing-tag suffixes leaked by some providers (e.g. Qwen/DashScope).
144+
// See https://github.com/openclaw/openclaw/issues/48780
145+
stripXmlArgValueSuffixFromRecord(normalized);
114146
return normalized;
115147
}
116148

0 commit comments

Comments
 (0)