From b6cfbe1d0943a6c9bb5f7ce62c40ac57c4ac6650 Mon Sep 17 00:00:00 2001 From: daniel-lxs Date: Thu, 20 Nov 2025 13:18:16 -0500 Subject: [PATCH] fix: prevent duplicate environment_details when resuming cancelled tasks - Filter out complete environment_details blocks before appending fresh ones - Check for both opening and closing tags to ensure we're matching complete blocks - Prevents stale environment data from being kept during task resume - Add tests to verify deduplication logic and edge cases --- src/core/task/Task.ts | 19 ++- .../task/__tests__/task-tool-history.spec.ts | 121 ++++++++++++++++++ 2 files changed, 139 insertions(+), 1 deletion(-) diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 7c0355e498..b340091dde 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -2024,9 +2024,26 @@ export class Task extends EventEmitter implements TaskLike { const environmentDetails = await getEnvironmentDetails(this, currentIncludeFileDetails) + // Remove any existing environment_details blocks before adding fresh ones. + // This prevents duplicate environment details when resuming tasks with XML tool calls, + // where the old user message content may already contain environment details from the previous session. + // We check for both opening and closing tags to ensure we're matching complete environment detail blocks, + // not just mentions of the tag in regular content. + const contentWithoutEnvDetails = parsedUserContent.filter((block) => { + if (block.type === "text" && typeof block.text === "string") { + // Check if this text block is a complete environment_details block + // by verifying it starts with the opening tag and ends with the closing tag + const isEnvironmentDetailsBlock = + block.text.trim().startsWith("") && + block.text.trim().endsWith("") + return !isEnvironmentDetailsBlock + } + return true + }) + // Add environment details as its own text block, separate from tool // results. - const finalUserContent = [...parsedUserContent, { type: "text" as const, text: environmentDetails }] + const finalUserContent = [...contentWithoutEnvDetails, { type: "text" as const, text: environmentDetails }] // Only add user message to conversation history if: // 1. This is the first attempt (retryAttempt === 0), OR diff --git a/src/core/task/__tests__/task-tool-history.spec.ts b/src/core/task/__tests__/task-tool-history.spec.ts index 0ab087c7a2..832e81c37b 100644 --- a/src/core/task/__tests__/task-tool-history.spec.ts +++ b/src/core/task/__tests__/task-tool-history.spec.ts @@ -196,5 +196,126 @@ describe("Task Tool History Handling", () => { content: '{"setting": "value"}', }) }) + + describe("environment details deduplication", () => { + it("should filter out existing environment_details blocks before adding new ones", () => { + // Simulate user content that already contains environment details from a previous session + const userContentWithOldEnvDetails = [ + { + type: "text" as const, + text: "Some user message", + }, + { + type: "text" as const, + text: "\n# Old Environment Details\nCurrent time: 2024-01-01\n", + }, + ] + + // Filter out existing environment_details blocks using the same logic as Task.ts + const contentWithoutEnvDetails = userContentWithOldEnvDetails.filter((block) => { + if (block.type === "text" && typeof block.text === "string") { + // Check if this text block is a complete environment_details block + const isEnvironmentDetailsBlock = + block.text.trim().startsWith("") && + block.text.trim().endsWith("") + return !isEnvironmentDetailsBlock + } + return true + }) + + // Verify old environment details were removed + expect(contentWithoutEnvDetails).toHaveLength(1) + expect(contentWithoutEnvDetails[0].text).toBe("Some user message") + + // Simulate adding fresh environment details + const newEnvironmentDetails = + "\n# Fresh Environment Details\nCurrent time: 2024-01-02\n" + const finalUserContent = [ + ...contentWithoutEnvDetails, + { type: "text" as const, text: newEnvironmentDetails }, + ] + + // Verify we have exactly one environment_details block (the new one) + const envDetailsBlocks = finalUserContent.filter((block) => { + if (block.type === "text" && typeof block.text === "string") { + return ( + block.text.trim().startsWith("") && + block.text.trim().endsWith("") + ) + } + return false + }) + expect(envDetailsBlocks).toHaveLength(1) + expect(envDetailsBlocks[0].text).toContain("2024-01-02") + expect(envDetailsBlocks[0].text).not.toContain("2024-01-01") + }) + + it("should not filter out text that mentions environment_details tags in content", () => { + // User content that mentions the tags but isn't an environment_details block + const userContent = [ + { + type: "text" as const, + text: "Let me explain how work in this system", + }, + { + type: "text" as const, + text: "The closing tag is ", + }, + { + type: "text" as const, + text: "Regular message", + }, + ] + + // Filter using the same logic as Task.ts + const contentWithoutEnvDetails = userContent.filter((block) => { + if (block.type === "text" && typeof block.text === "string") { + const isEnvironmentDetailsBlock = + block.text.trim().startsWith("") && + block.text.trim().endsWith("") + return !isEnvironmentDetailsBlock + } + return true + }) + + // All blocks should be preserved since none are complete environment_details blocks + expect(contentWithoutEnvDetails).toHaveLength(3) + expect(contentWithoutEnvDetails).toEqual(userContent) + }) + + it("should not filter out regular text blocks", () => { + // User content with various blocks but no environment details + const userContent = [ + { + type: "text" as const, + text: "Regular message", + }, + { + type: "text" as const, + text: "Another message with tags", + }, + { + type: "tool_result" as const, + tool_use_id: "tool_123", + content: "Tool result", + }, + ] + + // Filter using the same logic as Task.ts + const contentWithoutEnvDetails = userContent.filter((block) => { + if (block.type === "text" && typeof block.text === "string") { + const isEnvironmentDetailsBlock = + block.text.trim().startsWith("") && + block.text.trim().endsWith("") + return !isEnvironmentDetailsBlock + } + return true + }) + + // All blocks should be preserved + expect(contentWithoutEnvDetails).toHaveLength(3) + expect(contentWithoutEnvDetails).toEqual(userContent) + }) + }) }) })