Просмотр исходного кода

fix: prevent duplicate environment_details when resuming cancelled tasks (#9442)

- 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
Daniel 1 месяц назад
Родитель
Сommit
97cdc41937
2 измененных файлов с 139 добавлено и 1 удалено
  1. 18 1
      src/core/task/Task.ts
  2. 121 0
      src/core/task/__tests__/task-tool-history.spec.ts

+ 18 - 1
src/core/task/Task.ts

@@ -2024,9 +2024,26 @@ export class Task extends EventEmitter<TaskEvents> 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("<environment_details>") &&
+						block.text.trim().endsWith("</environment_details>")
+					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

+ 121 - 0
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: "<environment_details>\n# Old Environment Details\nCurrent time: 2024-01-01\n</environment_details>",
+					},
+				]
+
+				// 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("<environment_details>") &&
+							block.text.trim().endsWith("</environment_details>")
+						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 =
+					"<environment_details>\n# Fresh Environment Details\nCurrent time: 2024-01-02\n</environment_details>"
+				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("<environment_details>") &&
+							block.text.trim().endsWith("</environment_details>")
+						)
+					}
+					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 <environment_details> work in this system",
+					},
+					{
+						type: "text" as const,
+						text: "The closing tag is </environment_details>",
+					},
+					{
+						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("<environment_details>") &&
+							block.text.trim().endsWith("</environment_details>")
+						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 <task> 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("<environment_details>") &&
+							block.text.trim().endsWith("</environment_details>")
+						return !isEnvironmentDetailsBlock
+					}
+					return true
+				})
+
+				// All blocks should be preserved
+				expect(contentWithoutEnvDetails).toHaveLength(3)
+				expect(contentWithoutEnvDetails).toEqual(userContent)
+			})
+		})
 	})
 })