From 22695e39b1a0cd5df94873454021cbe2b0c3f200 Mon Sep 17 00:00:00 2001 From: Ammar Date: Wed, 29 Oct 2025 17:00:09 +0000 Subject: [PATCH 1/3] =?UTF-8?q?=F0=9F=A4=96=20refactor:=20truncate=20statu?= =?UTF-8?q?s=5Fset=20messages=20at=2060=20chars=20instead=20of=20failing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add STATUS_MESSAGE_MAX_LENGTH constant (60 chars) - Remove Zod max validation, add truncation logic - Messages >60 chars truncated to 59 chars + ellipsis - Update tests for truncation behavior No more validation errors for long status messages. --- src/constants/toolLimits.ts | 2 ++ src/services/tools/status_set.test.ts | 31 ++++++++++++++++++++++++--- src/services/tools/status_set.ts | 17 ++++++++++++++- src/utils/tools/toolDefinitions.ts | 4 ++-- 4 files changed, 48 insertions(+), 6 deletions(-) diff --git a/src/constants/toolLimits.ts b/src/constants/toolLimits.ts index c9706316b..83760af92 100644 --- a/src/constants/toolLimits.ts +++ b/src/constants/toolLimits.ts @@ -15,3 +15,5 @@ export const BASH_TRUNCATE_MAX_FILE_BYTES = 1024 * 1024; // 1MB file limit (same export const BASH_MAX_LINE_BYTES = 1024; // 1KB per line for AI agent export const MAX_TODOS = 7; // Maximum number of TODO items in a list + +export const STATUS_MESSAGE_MAX_LENGTH = 60; // Maximum length for status messages (auto-truncated) diff --git a/src/services/tools/status_set.test.ts b/src/services/tools/status_set.test.ts index 36e6d7daf..8c1c0290e 100644 --- a/src/services/tools/status_set.test.ts +++ b/src/services/tools/status_set.test.ts @@ -3,6 +3,7 @@ import { createStatusSetTool } from "./status_set"; import type { ToolConfiguration } from "@/utils/tools/tools"; import { createRuntime } from "@/runtime/runtimeFactory"; import type { ToolCallOptions } from "ai"; +import { STATUS_MESSAGE_MAX_LENGTH } from "@/constants/toolLimits"; describe("status_set tool validation", () => { const mockConfig: ToolConfiguration = { @@ -140,14 +141,15 @@ describe("status_set tool validation", () => { }); describe("message validation", () => { - it("should accept messages up to 40 characters", async () => { + it(`should accept messages up to ${STATUS_MESSAGE_MAX_LENGTH} characters`, async () => { const tool = createStatusSetTool(mockConfig); const result1 = (await tool.execute!( - { emoji: "✅", message: "a".repeat(40) }, + { emoji: "✅", message: "a".repeat(STATUS_MESSAGE_MAX_LENGTH) }, mockToolCallOptions - )) as { success: boolean }; + )) as { success: boolean; message: string }; expect(result1.success).toBe(true); + expect(result1.message).toBe("a".repeat(STATUS_MESSAGE_MAX_LENGTH)); const result2 = (await tool.execute!( { emoji: "✅", message: "Analyzing code structure" }, @@ -156,6 +158,29 @@ describe("status_set tool validation", () => { expect(result2.success).toBe(true); }); + it(`should truncate messages longer than ${STATUS_MESSAGE_MAX_LENGTH} characters with ellipsis`, async () => { + const tool = createStatusSetTool(mockConfig); + + // Test with MAX_LENGTH + 1 characters + const result1 = (await tool.execute!( + { emoji: "✅", message: "a".repeat(STATUS_MESSAGE_MAX_LENGTH + 1) }, + mockToolCallOptions + )) as { success: boolean; message: string }; + expect(result1.success).toBe(true); + expect(result1.message).toBe("a".repeat(STATUS_MESSAGE_MAX_LENGTH - 1) + "…"); + expect(result1.message.length).toBe(STATUS_MESSAGE_MAX_LENGTH); + + // Test with longer message + const longMessage = "This is a very long message that exceeds the 60 character limit and should be truncated"; + const result2 = (await tool.execute!( + { emoji: "✅", message: longMessage }, + mockToolCallOptions + )) as { success: boolean; message: string }; + expect(result2.success).toBe(true); + expect(result2.message).toBe(longMessage.slice(0, STATUS_MESSAGE_MAX_LENGTH - 1) + "…"); + expect(result2.message.length).toBe(STATUS_MESSAGE_MAX_LENGTH); + }); + it("should accept empty message", async () => { const tool = createStatusSetTool(mockConfig); diff --git a/src/services/tools/status_set.ts b/src/services/tools/status_set.ts index 14a499402..ac0ca36a8 100644 --- a/src/services/tools/status_set.ts +++ b/src/services/tools/status_set.ts @@ -1,6 +1,7 @@ import { tool } from "ai"; import type { ToolFactory } from "@/utils/tools/tools"; import { TOOL_DEFINITIONS } from "@/utils/tools/toolDefinitions"; +import { STATUS_MESSAGE_MAX_LENGTH } from "@/constants/toolLimits"; /** * Result type for status_set tool @@ -38,6 +39,17 @@ function isValidEmoji(str: string): boolean { return emojiRegex.test(segments[0].segment); } +/** + * Truncates a message to a maximum length, adding an ellipsis if truncated + */ +function truncateMessage(message: string, maxLength: number): string { + if (message.length <= maxLength) { + return message; + } + // Truncate to maxLength-1 and add ellipsis (total = maxLength) + return message.slice(0, maxLength - 1) + "…"; +} + /** * Status set tool factory for AI assistant * Creates a tool that allows the AI to set status indicator showing current activity @@ -62,12 +74,15 @@ export const createStatusSetTool: ToolFactory = () => { }); } + // Truncate message if necessary + const truncatedMessage = truncateMessage(message, STATUS_MESSAGE_MAX_LENGTH); + // Tool execution is a no-op on the backend // The status is tracked by StreamingMessageAggregator and displayed in the frontend return Promise.resolve({ success: true, emoji, - message, + message: truncatedMessage, }); }, }); diff --git a/src/utils/tools/toolDefinitions.ts b/src/utils/tools/toolDefinitions.ts index 2e106a011..4fe588c18 100644 --- a/src/utils/tools/toolDefinitions.ts +++ b/src/utils/tools/toolDefinitions.ts @@ -11,6 +11,7 @@ import { BASH_HARD_MAX_LINES, BASH_MAX_LINE_BYTES, BASH_MAX_TOTAL_BYTES, + STATUS_MESSAGE_MAX_LENGTH, } from "@/constants/toolLimits"; import { zodToJsonSchema } from "zod-to-json-schema"; @@ -193,8 +194,7 @@ export const TOOL_DEFINITIONS = { emoji: z.string().describe("A single emoji character representing the current activity"), message: z .string() - .max(40) - .describe("A brief description of the current activity (max 40 characters)"), + .describe(`A brief description of the current activity (auto-truncated to ${STATUS_MESSAGE_MAX_LENGTH} chars with ellipsis if needed)`), }) .strict(), }, From 9d817f09e9690337c9481a6c78133555c40fe37e Mon Sep 17 00:00:00 2001 From: Ammar Date: Wed, 29 Oct 2025 17:02:20 +0000 Subject: [PATCH 2/3] style: apply prettier formatting --- src/services/tools/status_set.test.ts | 3 ++- src/utils/tools/toolDefinitions.ts | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/services/tools/status_set.test.ts b/src/services/tools/status_set.test.ts index 8c1c0290e..e8ea73b50 100644 --- a/src/services/tools/status_set.test.ts +++ b/src/services/tools/status_set.test.ts @@ -171,7 +171,8 @@ describe("status_set tool validation", () => { expect(result1.message.length).toBe(STATUS_MESSAGE_MAX_LENGTH); // Test with longer message - const longMessage = "This is a very long message that exceeds the 60 character limit and should be truncated"; + const longMessage = + "This is a very long message that exceeds the 60 character limit and should be truncated"; const result2 = (await tool.execute!( { emoji: "✅", message: longMessage }, mockToolCallOptions diff --git a/src/utils/tools/toolDefinitions.ts b/src/utils/tools/toolDefinitions.ts index 4fe588c18..8eea68fe0 100644 --- a/src/utils/tools/toolDefinitions.ts +++ b/src/utils/tools/toolDefinitions.ts @@ -194,7 +194,9 @@ export const TOOL_DEFINITIONS = { emoji: z.string().describe("A single emoji character representing the current activity"), message: z .string() - .describe(`A brief description of the current activity (auto-truncated to ${STATUS_MESSAGE_MAX_LENGTH} chars with ellipsis if needed)`), + .describe( + `A brief description of the current activity (auto-truncated to ${STATUS_MESSAGE_MAX_LENGTH} chars with ellipsis if needed)` + ), }) .strict(), }, From d68720780432d260498998073339e37572c69760 Mon Sep 17 00:00:00 2001 From: Ammar Date: Wed, 29 Oct 2025 17:06:30 +0000 Subject: [PATCH 3/3] fix: use truncated message from output in StreamingMessageAggregator Codex correctly identified that the UI was reading status_set message from tool input instead of output, bypassing truncation. Now reads from the tool result to get the truncated message. Added test to verify truncation propagates to UI status display. --- .../StreamingMessageAggregator.status.test.ts | 45 +++++++++++++++++++ .../messages/StreamingMessageAggregator.ts | 5 ++- 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/src/utils/messages/StreamingMessageAggregator.status.test.ts b/src/utils/messages/StreamingMessageAggregator.status.test.ts index ad1617688..5609c8b2b 100644 --- a/src/utils/messages/StreamingMessageAggregator.status.test.ts +++ b/src/utils/messages/StreamingMessageAggregator.status.test.ts @@ -494,4 +494,49 @@ describe("StreamingMessageAggregator - Agent Status", () => { // Status should remain undefined (failed validation) expect(aggregator.getAgentStatus()).toBeUndefined(); }); + + it("should use truncated message from output, not original input", () => { + const aggregator = new StreamingMessageAggregator(new Date().toISOString()); + + const messageId = "msg1"; + const toolCallId = "tool1"; + + // Start stream + aggregator.handleStreamStart({ + type: "stream-start", + workspaceId: "workspace1", + messageId, + model: "test-model", + historySequence: 1, + }); + + // Status_set with long message (would be truncated by backend) + const longMessage = "a".repeat(100); // 100 chars, exceeds 60 char limit + const truncatedMessage = "a".repeat(59) + "…"; // What backend returns + + aggregator.handleToolCallStart({ + type: "tool-call-start", + workspaceId: "workspace1", + messageId, + toolCallId, + toolName: "status_set", + args: { emoji: "✅", message: longMessage }, + tokens: 10, + timestamp: Date.now(), + }); + + aggregator.handleToolCallEnd({ + type: "tool-call-end", + workspaceId: "workspace1", + messageId, + toolCallId, + toolName: "status_set", + result: { success: true, emoji: "✅", message: truncatedMessage }, + }); + + // Should use truncated message from output, not the original input + const status = aggregator.getAgentStatus(); + expect(status).toEqual({ emoji: "✅", message: truncatedMessage }); + expect(status?.message.length).toBe(60); + }); }); diff --git a/src/utils/messages/StreamingMessageAggregator.ts b/src/utils/messages/StreamingMessageAggregator.ts index 1af90a9ac..9954b8361 100644 --- a/src/utils/messages/StreamingMessageAggregator.ts +++ b/src/utils/messages/StreamingMessageAggregator.ts @@ -520,9 +520,10 @@ export class StreamingMessageAggregator { } // Update agent status if this was a successful status_set + // Use output instead of input to get the truncated message if (toolName === "status_set" && hasSuccessResult(output)) { - const args = input as { emoji: string; message: string }; - this.agentStatus = { emoji: args.emoji, message: args.message }; + const result = output as { success: true; emoji: string; message: string }; + this.agentStatus = { emoji: result.emoji, message: result.message }; } }