Skip to content

Commit dacc1df

Browse files
authored
🤖 fix: truncate status_set messages at 60 chars instead of failing (#487)
The `status_set` tool previously validated message length at 40 characters via Zod schema, causing tool failures when agents provided longer messages. This changes the behavior to gracefully truncate at 60 characters with an ellipsis. **Changes:** - Added `STATUS_MESSAGE_MAX_LENGTH` constant (60 chars) to `toolLimits.ts` - Removed `.max(40)` Zod validation from schema - Added `truncateMessage()` helper that truncates to 59 chars + "…" - Updated tests to verify truncation behavior **Behavior:** - Messages ≤ 60 characters: unchanged - Messages > 60 characters: truncated to 59 chars + ellipsis This eliminates validation errors for long status messages while maintaining a reasonable display limit. _Generated with `cmux`_
1 parent ca16308 commit dacc1df

File tree

6 files changed

+99
-8
lines changed

6 files changed

+99
-8
lines changed

‎src/constants/toolLimits.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,5 @@ export const BASH_TRUNCATE_MAX_FILE_BYTES = 1024 * 1024; // 1MB file limit (same
1515
export const BASH_MAX_LINE_BYTES = 1024; // 1KB per line for AI agent
1616

1717
export const MAX_TODOS = 7; // Maximum number of TODO items in a list
18+
19+
export const STATUS_MESSAGE_MAX_LENGTH = 60; // Maximum length for status messages (auto-truncated)

‎src/services/tools/status_set.test.ts‎

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { createStatusSetTool } from "./status_set";
33
import type { ToolConfiguration } from "@/utils/tools/tools";
44
import { createRuntime } from "@/runtime/runtimeFactory";
55
import type { ToolCallOptions } from "ai";
6+
import { STATUS_MESSAGE_MAX_LENGTH } from "@/constants/toolLimits";
67

78
describe("status_set tool validation", () => {
89
const mockConfig: ToolConfiguration = {
@@ -140,14 +141,15 @@ describe("status_set tool validation", () => {
140141
});
141142

142143
describe("message validation", () => {
143-
it("should accept messages up to 40 characters", async () => {
144+
it(`should accept messages up to ${STATUS_MESSAGE_MAX_LENGTH} characters`, async () => {
144145
const tool = createStatusSetTool(mockConfig);
145146

146147
const result1 = (await tool.execute!(
147-
{ emoji: "✅", message: "a".repeat(40) },
148+
{ emoji: "✅", message: "a".repeat(STATUS_MESSAGE_MAX_LENGTH) },
148149
mockToolCallOptions
149-
)) as { success: boolean };
150+
)) as { success: boolean; message: string };
150151
expect(result1.success).toBe(true);
152+
expect(result1.message).toBe("a".repeat(STATUS_MESSAGE_MAX_LENGTH));
151153

152154
const result2 = (await tool.execute!(
153155
{ emoji: "✅", message: "Analyzing code structure" },
@@ -156,6 +158,30 @@ describe("status_set tool validation", () => {
156158
expect(result2.success).toBe(true);
157159
});
158160

161+
it(`should truncate messages longer than ${STATUS_MESSAGE_MAX_LENGTH} characters with ellipsis`, async () => {
162+
const tool = createStatusSetTool(mockConfig);
163+
164+
// Test with MAX_LENGTH + 1 characters
165+
const result1 = (await tool.execute!(
166+
{ emoji: "✅", message: "a".repeat(STATUS_MESSAGE_MAX_LENGTH + 1) },
167+
mockToolCallOptions
168+
)) as { success: boolean; message: string };
169+
expect(result1.success).toBe(true);
170+
expect(result1.message).toBe("a".repeat(STATUS_MESSAGE_MAX_LENGTH - 1) + "…");
171+
expect(result1.message.length).toBe(STATUS_MESSAGE_MAX_LENGTH);
172+
173+
// Test with longer message
174+
const longMessage =
175+
"This is a very long message that exceeds the 60 character limit and should be truncated";
176+
const result2 = (await tool.execute!(
177+
{ emoji: "✅", message: longMessage },
178+
mockToolCallOptions
179+
)) as { success: boolean; message: string };
180+
expect(result2.success).toBe(true);
181+
expect(result2.message).toBe(longMessage.slice(0, STATUS_MESSAGE_MAX_LENGTH - 1) + "…");
182+
expect(result2.message.length).toBe(STATUS_MESSAGE_MAX_LENGTH);
183+
});
184+
159185
it("should accept empty message", async () => {
160186
const tool = createStatusSetTool(mockConfig);
161187

‎src/services/tools/status_set.ts‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { tool } from "ai";
22
import type { ToolFactory } from "@/utils/tools/tools";
33
import { TOOL_DEFINITIONS } from "@/utils/tools/toolDefinitions";
4+
import { STATUS_MESSAGE_MAX_LENGTH } from "@/constants/toolLimits";
45

56
/**
67
* Result type for status_set tool
@@ -38,6 +39,17 @@ function isValidEmoji(str: string): boolean {
3839
return emojiRegex.test(segments[0].segment);
3940
}
4041

42+
/**
43+
* Truncates a message to a maximum length, adding an ellipsis if truncated
44+
*/
45+
function truncateMessage(message: string, maxLength: number): string {
46+
if (message.length <= maxLength) {
47+
return message;
48+
}
49+
// Truncate to maxLength-1 and add ellipsis (total = maxLength)
50+
return message.slice(0, maxLength - 1) + "…";
51+
}
52+
4153
/**
4254
* Status set tool factory for AI assistant
4355
* Creates a tool that allows the AI to set status indicator showing current activity
@@ -62,12 +74,15 @@ export const createStatusSetTool: ToolFactory = () => {
6274
});
6375
}
6476

77+
// Truncate message if necessary
78+
const truncatedMessage = truncateMessage(message, STATUS_MESSAGE_MAX_LENGTH);
79+
6580
// Tool execution is a no-op on the backend
6681
// The status is tracked by StreamingMessageAggregator and displayed in the frontend
6782
return Promise.resolve({
6883
success: true,
6984
emoji,
70-
message,
85+
message: truncatedMessage,
7186
});
7287
},
7388
});

‎src/utils/messages/StreamingMessageAggregator.status.test.ts‎

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -494,4 +494,49 @@ describe("StreamingMessageAggregator - Agent Status", () => {
494494
// Status should remain undefined (failed validation)
495495
expect(aggregator.getAgentStatus()).toBeUndefined();
496496
});
497+
498+
it("should use truncated message from output, not original input", () => {
499+
const aggregator = new StreamingMessageAggregator(new Date().toISOString());
500+
501+
const messageId = "msg1";
502+
const toolCallId = "tool1";
503+
504+
// Start stream
505+
aggregator.handleStreamStart({
506+
type: "stream-start",
507+
workspaceId: "workspace1",
508+
messageId,
509+
model: "test-model",
510+
historySequence: 1,
511+
});
512+
513+
// Status_set with long message (would be truncated by backend)
514+
const longMessage = "a".repeat(100); // 100 chars, exceeds 60 char limit
515+
const truncatedMessage = "a".repeat(59) + "…"; // What backend returns
516+
517+
aggregator.handleToolCallStart({
518+
type: "tool-call-start",
519+
workspaceId: "workspace1",
520+
messageId,
521+
toolCallId,
522+
toolName: "status_set",
523+
args: { emoji: "✅", message: longMessage },
524+
tokens: 10,
525+
timestamp: Date.now(),
526+
});
527+
528+
aggregator.handleToolCallEnd({
529+
type: "tool-call-end",
530+
workspaceId: "workspace1",
531+
messageId,
532+
toolCallId,
533+
toolName: "status_set",
534+
result: { success: true, emoji: "✅", message: truncatedMessage },
535+
});
536+
537+
// Should use truncated message from output, not the original input
538+
const status = aggregator.getAgentStatus();
539+
expect(status).toEqual({ emoji: "✅", message: truncatedMessage });
540+
expect(status?.message.length).toBe(60);
541+
});
497542
});

‎src/utils/messages/StreamingMessageAggregator.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -520,9 +520,10 @@ export class StreamingMessageAggregator {
520520
}
521521

522522
// Update agent status if this was a successful status_set
523+
// Use output instead of input to get the truncated message
523524
if (toolName === "status_set" && hasSuccessResult(output)) {
524-
const args = input as { emoji: string; message: string };
525-
this.agentStatus = { emoji: args.emoji, message: args.message };
525+
const result = output as { success: true; emoji: string; message: string };
526+
this.agentStatus = { emoji: result.emoji, message: result.message };
526527
}
527528
}
528529

‎src/utils/tools/toolDefinitions.ts‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
BASH_HARD_MAX_LINES,
1212
BASH_MAX_LINE_BYTES,
1313
BASH_MAX_TOTAL_BYTES,
14+
STATUS_MESSAGE_MAX_LENGTH,
1415
} from "@/constants/toolLimits";
1516

1617
import { zodToJsonSchema } from "zod-to-json-schema";
@@ -193,8 +194,9 @@ export const TOOL_DEFINITIONS = {
193194
emoji: z.string().describe("A single emoji character representing the current activity"),
194195
message: z
195196
.string()
196-
.max(40)
197-
.describe("A brief description of the current activity (max 40 characters)"),
197+
.describe(
198+
`A brief description of the current activity (auto-truncated to ${STATUS_MESSAGE_MAX_LENGTH} chars with ellipsis if needed)`
199+
),
198200
})
199201
.strict(),
200202
},

0 commit comments

Comments
 (0)