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
This commit is contained in:
Daniel 2025-11-20 17:03:02 -05:00 committed by GitHub
parent 1589cc1849
commit 97cdc41937
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 139 additions and 1 deletions

View file

@ -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

View file

@ -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)
})
})
})
})