Skip to content

Commit ce69a5c

Browse files
authored
🤖 fix: auto-correct redundant absolute paths with warning (#503)
## Problem Terminal-bench agents hit "Redundant path prefix" errors on 55% of tasks (44/80) when using absolute paths like `/app/file.txt`. This wastes tool calls as agents retry with relative paths—particularly affecting QEMU (80% fail), Git (83% fail), and Security (75% fail) categories. ## Solution Changed `validateNoRedundantPrefix` to auto-correct paths while keeping the warning. Tools now use the corrected path and include: `"Using relative paths like 'file.txt' instead of '/app/file.txt' saves tokens. The path has been auto-corrected for you."` **Expected impact:** +5-8pp pass rate improvement. Combined with merged 30-min timeout, targeting 60-65% pass rate. _Generated with `cmux`_
1 parent d86727b commit ce69a5c

File tree

8 files changed

+84
-56
lines changed

8 files changed

+84
-56
lines changed

src/services/tools/fileCommon.test.ts

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -147,19 +147,20 @@ describe("fileCommon", () => {
147147
expect(validateNoRedundantPrefix("file.ts", cwd, runtime)).toBeNull();
148148
});
149149

150-
it("should reject absolute paths that contain the cwd prefix", () => {
150+
it("should auto-correct absolute paths that contain the cwd prefix", () => {
151151
const result = validateNoRedundantPrefix("/workspace/project/src/file.ts", cwd, runtime);
152152
expect(result).not.toBeNull();
153-
expect(result?.error).toContain("Redundant path prefix detected");
154-
expect(result?.error).toContain("Please use relative paths to save tokens");
155-
expect(result?.error).toContain("src/file.ts"); // Should suggest the relative path
153+
expect(result?.correctedPath).toBe("src/file.ts");
154+
expect(result?.warning).toContain("Using relative paths");
155+
expect(result?.warning).toContain("saves tokens");
156+
expect(result?.warning).toContain("auto-corrected");
156157
});
157158

158-
it("should reject absolute paths at the cwd root", () => {
159+
it("should auto-correct absolute paths at the cwd root", () => {
159160
const result = validateNoRedundantPrefix("/workspace/project/file.ts", cwd, runtime);
160161
expect(result).not.toBeNull();
161-
expect(result?.error).toContain("Redundant path prefix detected");
162-
expect(result?.error).toContain("file.ts"); // Should suggest the relative path
162+
expect(result?.correctedPath).toBe("file.ts");
163+
expect(result?.warning).toContain("auto-corrected");
163164
});
164165

165166
it("should allow absolute paths outside cwd (they will be caught by validatePathInCwd)", () => {
@@ -182,7 +183,8 @@ describe("fileCommon", () => {
182183
runtime
183184
);
184185
expect(result).not.toBeNull();
185-
expect(result?.error).toContain("src/file.ts");
186+
expect(result?.correctedPath).toBe("src/file.ts");
187+
expect(result?.warning).toContain("auto-corrected");
186188
});
187189

188190
it("should handle nested paths correctly", () => {
@@ -192,14 +194,15 @@ describe("fileCommon", () => {
192194
runtime
193195
);
194196
expect(result).not.toBeNull();
195-
expect(result?.error).toContain("src/components/Button/index.ts");
197+
expect(result?.correctedPath).toBe("src/components/Button/index.ts");
198+
expect(result?.warning).toContain("auto-corrected");
196199
});
197200

198-
it("should reject path that equals cwd exactly", () => {
201+
it("should auto-correct path that equals cwd exactly", () => {
199202
const result = validateNoRedundantPrefix("/workspace/project", cwd, runtime);
200203
expect(result).not.toBeNull();
201-
expect(result?.error).toContain("Redundant path prefix detected");
202-
expect(result?.error).toContain("."); // Should suggest current directory
204+
expect(result?.correctedPath).toBe(".");
205+
expect(result?.warning).toContain("auto-corrected");
203206
});
204207

205208
it("should not match partial directory names", () => {
@@ -217,15 +220,15 @@ describe("fileCommon", () => {
217220
});
218221
const sshCwd = "/home/user/cmux/project/branch";
219222

220-
// Should reject absolute paths with redundant prefix on SSH too
223+
// Should auto-correct absolute paths with redundant prefix on SSH too
221224
const result = validateNoRedundantPrefix(
222225
"/home/user/cmux/project/branch/src/file.ts",
223226
sshCwd,
224227
sshRuntime
225228
);
226229
expect(result).not.toBeNull();
227-
expect(result?.error).toContain("Redundant path prefix detected");
228-
expect(result?.error).toContain("src/file.ts");
230+
expect(result?.correctedPath).toBe("src/file.ts");
231+
expect(result?.warning).toContain("auto-corrected");
229232

230233
// Should allow relative paths on SSH
231234
expect(validateNoRedundantPrefix("src/file.ts", sshCwd, sshRuntime)).toBeNull();

src/services/tools/fileCommon.ts

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export function validateFileSize(stats: FileStat): { error: string } | null {
4848

4949
/**
5050
* Validates that a file path doesn't contain redundant workspace prefix.
51-
* Returns an error object if the path contains the cwd prefix, null if valid.
51+
* If the path contains the cwd prefix, returns the corrected relative path and a warning.
5252
* This helps save tokens by encouraging relative paths.
5353
*
5454
* Works for both local and SSH runtimes by using runtime.normalizePath()
@@ -57,13 +57,13 @@ export function validateFileSize(stats: FileStat): { error: string } | null {
5757
* @param filePath - The file path to validate
5858
* @param cwd - The working directory
5959
* @param runtime - The runtime to use for path normalization
60-
* @returns Error object if redundant prefix found, null if valid
60+
* @returns Object with corrected path and warning if redundant prefix found, null if valid
6161
*/
6262
export function validateNoRedundantPrefix(
6363
filePath: string,
6464
cwd: string,
6565
runtime: Runtime
66-
): { error: string } | null {
66+
): { correctedPath: string; warning: string } | null {
6767
// Only check absolute paths (start with /) - relative paths are fine
6868
// This works for both local and SSH since both use Unix-style paths
6969
if (!filePath.startsWith("/")) {
@@ -87,7 +87,8 @@ export function validateNoRedundantPrefix(
8787
const relativePath =
8888
normalizedPath === cleanCwd ? "." : normalizedPath.substring(cleanCwd.length + 1);
8989
return {
90-
error: `Redundant path prefix detected. The path '${filePath}' contains the workspace directory. Please use relative paths to save tokens: '${relativePath}'`,
90+
correctedPath: relativePath,
91+
warning: `Note: Using relative paths like '${relativePath}' instead of '${filePath}' saves tokens. The path has been auto-corrected for you.`,
9192
};
9293
}
9394

@@ -135,3 +136,30 @@ export function validatePathInCwd(
135136

136137
return null;
137138
}
139+
140+
/**
141+
* Validates and auto-corrects redundant path prefixes in file paths.
142+
* Returns the corrected path and an optional warning message.
143+
*
144+
* This is a convenience wrapper around validateNoRedundantPrefix that handles
145+
* the common pattern of auto-correcting paths and returning warnings.
146+
*
147+
* @param filePath - The file path to validate (may be modified if redundant prefix found)
148+
* @param cwd - The working directory
149+
* @param runtime - The runtime to use for path normalization
150+
* @returns Object with correctedPath and optional warning
151+
*/
152+
export function validateAndCorrectPath(
153+
filePath: string,
154+
cwd: string,
155+
runtime: Runtime
156+
): { correctedPath: string; warning?: string } {
157+
const redundantPrefixValidation = validateNoRedundantPrefix(filePath, cwd, runtime);
158+
if (redundantPrefixValidation) {
159+
return {
160+
correctedPath: redundantPrefixValidation.correctedPath,
161+
warning: redundantPrefixValidation.warning,
162+
};
163+
}
164+
return { correctedPath: filePath };
165+
}

src/services/tools/file_edit_insert.ts

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { tool } from "ai";
22
import type { FileEditInsertToolResult } from "@/types/tools";
33
import type { ToolConfiguration, ToolFactory } from "@/utils/tools/tools";
44
import { TOOL_DEFINITIONS } from "@/utils/tools/toolDefinitions";
5-
import { validatePathInCwd, validateNoRedundantPrefix } from "./fileCommon";
5+
import { validatePathInCwd, validateAndCorrectPath } from "./fileCommon";
66
import { EDIT_FAILED_NOTE_PREFIX, NOTE_READ_FILE_RETRY } from "@/types/tools";
77
import { executeFileEditOperation } from "./file_edit_operation";
88
import { RuntimeError } from "@/runtime/Runtime";
@@ -23,18 +23,13 @@ export const createFileEditInsertTool: ToolFactory = (config: ToolConfiguration)
2323
{ abortSignal }
2424
): Promise<FileEditInsertToolResult> => {
2525
try {
26-
// Validate no redundant path prefix (must come first to catch absolute paths)
27-
const redundantPrefixValidation = validateNoRedundantPrefix(
26+
// Validate and auto-correct redundant path prefix
27+
const { correctedPath: validatedPath, warning: pathWarning } = validateAndCorrectPath(
2828
file_path,
2929
config.cwd,
3030
config.runtime
3131
);
32-
if (redundantPrefixValidation) {
33-
return {
34-
success: false,
35-
error: redundantPrefixValidation.error,
36-
};
37-
}
32+
file_path = validatedPath;
3833

3934
const pathValidation = validatePathInCwd(file_path, config.cwd, config.runtime);
4035
if (pathValidation) {
@@ -81,7 +76,7 @@ export const createFileEditInsertTool: ToolFactory = (config: ToolConfiguration)
8176
}
8277
}
8378

84-
return executeFileEditOperation({
79+
const result = await executeFileEditOperation({
8580
config,
8681
filePath: file_path,
8782
abortSignal,
@@ -119,6 +114,15 @@ export const createFileEditInsertTool: ToolFactory = (config: ToolConfiguration)
119114
};
120115
},
121116
});
117+
118+
// Add path warning if present
119+
if (pathWarning && result.success) {
120+
return {
121+
...result,
122+
warning: pathWarning,
123+
};
124+
}
125+
return result;
122126
} catch (error) {
123127
if (error && typeof error === "object" && "code" in error && error.code === "EACCES") {
124128
return {

src/services/tools/file_edit_operation.ts

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {
44
generateDiff,
55
validateFileSize,
66
validatePathInCwd,
7-
validateNoRedundantPrefix,
7+
validateAndCorrectPath,
88
} from "./fileCommon";
99
import { RuntimeError } from "@/runtime/Runtime";
1010
import { readFileString, writeFileString } from "@/utils/runtime/helpers";
@@ -43,18 +43,13 @@ export async function executeFileEditOperation<TMetadata>({
4343
FileEditErrorResult | (FileEditDiffSuccessBase & TMetadata)
4444
> {
4545
try {
46-
// Validate no redundant path prefix (must come first to catch absolute paths)
47-
const redundantPrefixValidation = validateNoRedundantPrefix(
46+
// Validate and auto-correct redundant path prefix
47+
const { correctedPath: validatedPath, warning: pathWarning } = validateAndCorrectPath(
4848
filePath,
4949
config.cwd,
5050
config.runtime
5151
);
52-
if (redundantPrefixValidation) {
53-
return {
54-
success: false,
55-
error: redundantPrefixValidation.error,
56-
};
57-
}
52+
filePath = validatedPath;
5853

5954
const pathValidation = validatePathInCwd(filePath, config.cwd, config.runtime);
6055
if (pathValidation) {
@@ -139,6 +134,7 @@ export async function executeFileEditOperation<TMetadata>({
139134
success: true,
140135
diff,
141136
...operationResult.metadata,
137+
...(pathWarning && { warning: pathWarning }),
142138
};
143139
} catch (error) {
144140
if (error && typeof error === "object" && "code" in error) {

src/services/tools/file_read.test.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -318,7 +318,7 @@ describe("file_read tool", () => {
318318
}
319319
});
320320

321-
it("should reject absolute paths containing the workspace directory", async () => {
321+
it("should auto-correct absolute paths containing the workspace directory", async () => {
322322
// Setup
323323
const content = "test content";
324324
await fs.writeFile(testFilePath, content);
@@ -332,12 +332,12 @@ describe("file_read tool", () => {
332332
// Execute
333333
const result = (await tool.execute!(args, mockToolCallOptions)) as FileReadToolResult;
334334

335-
// Assert
336-
expect(result.success).toBe(false);
337-
if (!result.success) {
338-
expect(result.error).toContain("Redundant path prefix detected");
339-
expect(result.error).toContain("Please use relative paths to save tokens");
340-
expect(result.error).toContain("test.txt"); // Should suggest the relative path
335+
// Assert - Should succeed with auto-correction warning
336+
expect(result.success).toBe(true);
337+
if (result.success) {
338+
expect(result.warning).toContain("auto-corrected");
339+
expect(result.warning).toContain("test.txt"); // Should mention the relative path
340+
expect(result.content).toContain("test content");
341341
}
342342
});
343343

src/services/tools/file_read.ts

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { tool } from "ai";
22
import type { FileReadToolResult } from "@/types/tools";
33
import type { ToolConfiguration, ToolFactory } from "@/utils/tools/tools";
44
import { TOOL_DEFINITIONS } from "@/utils/tools/toolDefinitions";
5-
import { validatePathInCwd, validateFileSize, validateNoRedundantPrefix } from "./fileCommon";
5+
import { validatePathInCwd, validateFileSize, validateAndCorrectPath } from "./fileCommon";
66
import { RuntimeError } from "@/runtime/Runtime";
77
import { readFileString } from "@/utils/runtime/helpers";
88

@@ -22,18 +22,13 @@ export const createFileReadTool: ToolFactory = (config: ToolConfiguration) => {
2222
// Note: abortSignal available but not used - file reads are fast and complete quickly
2323

2424
try {
25-
// Validate no redundant path prefix (must come first to catch absolute paths)
26-
const redundantPrefixValidation = validateNoRedundantPrefix(
25+
// Validate and auto-correct redundant path prefix
26+
const { correctedPath: validatedPath, warning: pathWarning } = validateAndCorrectPath(
2727
filePath,
2828
config.cwd,
2929
config.runtime
3030
);
31-
if (redundantPrefixValidation) {
32-
return {
33-
success: false,
34-
error: redundantPrefixValidation.error,
35-
};
36-
}
31+
filePath = validatedPath;
3732

3833
// Validate that the path is within the working directory
3934
const pathValidation = validatePathInCwd(filePath, config.cwd, config.runtime);
@@ -172,6 +167,7 @@ export const createFileReadTool: ToolFactory = (config: ToolConfiguration) => {
172167
modifiedTime: fileStat.modifiedTime.toISOString(),
173168
lines_read: numberedLines.length,
174169
content,
170+
...(pathWarning && { warning: pathWarning }),
175171
};
176172
} catch (error) {
177173
// Handle specific errors

src/telemetry/utils.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,8 @@ import { VERSION } from "../version";
99
* Get base telemetry properties included with all events
1010
*/
1111
export function getBaseTelemetryProperties(): BaseTelemetryProperties {
12-
const gitDescribe: string = VERSION.git_describe;
1312
return {
14-
version: gitDescribe,
13+
version: String(VERSION.git_describe),
1514
platform: window.api?.platform || "unknown",
1615
electronVersion: window.api?.versions?.electron || "unknown",
1716
};

src/types/tools.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ export type FileReadToolResult =
5252
modifiedTime: string;
5353
lines_read: number;
5454
content: string;
55+
warning?: string;
5556
}
5657
| {
5758
success: false;
@@ -61,6 +62,7 @@ export type FileReadToolResult =
6162
export interface FileEditDiffSuccessBase {
6263
success: true;
6364
diff: string;
65+
warning?: string;
6466
}
6567

6668
export interface FileEditErrorResult {

0 commit comments

Comments
 (0)