Skip to content

Commit 97cdc41

Browse files
authored
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
1 parent 1589cc1 commit 97cdc41

File tree

2 files changed

+139
-1
lines changed

2 files changed

+139
-1
lines changed

src/core/task/Task.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2024,9 +2024,26 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
20242024

20252025
const environmentDetails = await getEnvironmentDetails(this, currentIncludeFileDetails)
20262026

2027+
// Remove any existing environment_details blocks before adding fresh ones.
2028+
// This prevents duplicate environment details when resuming tasks with XML tool calls,
2029+
// where the old user message content may already contain environment details from the previous session.
2030+
// We check for both opening and closing tags to ensure we're matching complete environment detail blocks,
2031+
// not just mentions of the tag in regular content.
2032+
const contentWithoutEnvDetails = parsedUserContent.filter((block) => {
2033+
if (block.type === "text" && typeof block.text === "string") {
2034+
// Check if this text block is a complete environment_details block
2035+
// by verifying it starts with the opening tag and ends with the closing tag
2036+
const isEnvironmentDetailsBlock =
2037+
block.text.trim().startsWith("<environment_details>") &&
2038+
block.text.trim().endsWith("</environment_details>")
2039+
return !isEnvironmentDetailsBlock
2040+
}
2041+
return true
2042+
})
2043+
20272044
// Add environment details as its own text block, separate from tool
20282045
// results.
2029-
const finalUserContent = [...parsedUserContent, { type: "text" as const, text: environmentDetails }]
2046+
const finalUserContent = [...contentWithoutEnvDetails, { type: "text" as const, text: environmentDetails }]
20302047

20312048
// Only add user message to conversation history if:
20322049
// 1. This is the first attempt (retryAttempt === 0), OR

src/core/task/__tests__/task-tool-history.spec.ts

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,5 +196,126 @@ describe("Task Tool History Handling", () => {
196196
content: '{"setting": "value"}',
197197
})
198198
})
199+
200+
describe("environment details deduplication", () => {
201+
it("should filter out existing environment_details blocks before adding new ones", () => {
202+
// Simulate user content that already contains environment details from a previous session
203+
const userContentWithOldEnvDetails = [
204+
{
205+
type: "text" as const,
206+
text: "Some user message",
207+
},
208+
{
209+
type: "text" as const,
210+
text: "<environment_details>\n# Old Environment Details\nCurrent time: 2024-01-01\n</environment_details>",
211+
},
212+
]
213+
214+
// Filter out existing environment_details blocks using the same logic as Task.ts
215+
const contentWithoutEnvDetails = userContentWithOldEnvDetails.filter((block) => {
216+
if (block.type === "text" && typeof block.text === "string") {
217+
// Check if this text block is a complete environment_details block
218+
const isEnvironmentDetailsBlock =
219+
block.text.trim().startsWith("<environment_details>") &&
220+
block.text.trim().endsWith("</environment_details>")
221+
return !isEnvironmentDetailsBlock
222+
}
223+
return true
224+
})
225+
226+
// Verify old environment details were removed
227+
expect(contentWithoutEnvDetails).toHaveLength(1)
228+
expect(contentWithoutEnvDetails[0].text).toBe("Some user message")
229+
230+
// Simulate adding fresh environment details
231+
const newEnvironmentDetails =
232+
"<environment_details>\n# Fresh Environment Details\nCurrent time: 2024-01-02\n</environment_details>"
233+
const finalUserContent = [
234+
...contentWithoutEnvDetails,
235+
{ type: "text" as const, text: newEnvironmentDetails },
236+
]
237+
238+
// Verify we have exactly one environment_details block (the new one)
239+
const envDetailsBlocks = finalUserContent.filter((block) => {
240+
if (block.type === "text" && typeof block.text === "string") {
241+
return (
242+
block.text.trim().startsWith("<environment_details>") &&
243+
block.text.trim().endsWith("</environment_details>")
244+
)
245+
}
246+
return false
247+
})
248+
expect(envDetailsBlocks).toHaveLength(1)
249+
expect(envDetailsBlocks[0].text).toContain("2024-01-02")
250+
expect(envDetailsBlocks[0].text).not.toContain("2024-01-01")
251+
})
252+
253+
it("should not filter out text that mentions environment_details tags in content", () => {
254+
// User content that mentions the tags but isn't an environment_details block
255+
const userContent = [
256+
{
257+
type: "text" as const,
258+
text: "Let me explain how <environment_details> work in this system",
259+
},
260+
{
261+
type: "text" as const,
262+
text: "The closing tag is </environment_details>",
263+
},
264+
{
265+
type: "text" as const,
266+
text: "Regular message",
267+
},
268+
]
269+
270+
// Filter using the same logic as Task.ts
271+
const contentWithoutEnvDetails = userContent.filter((block) => {
272+
if (block.type === "text" && typeof block.text === "string") {
273+
const isEnvironmentDetailsBlock =
274+
block.text.trim().startsWith("<environment_details>") &&
275+
block.text.trim().endsWith("</environment_details>")
276+
return !isEnvironmentDetailsBlock
277+
}
278+
return true
279+
})
280+
281+
// All blocks should be preserved since none are complete environment_details blocks
282+
expect(contentWithoutEnvDetails).toHaveLength(3)
283+
expect(contentWithoutEnvDetails).toEqual(userContent)
284+
})
285+
286+
it("should not filter out regular text blocks", () => {
287+
// User content with various blocks but no environment details
288+
const userContent = [
289+
{
290+
type: "text" as const,
291+
text: "Regular message",
292+
},
293+
{
294+
type: "text" as const,
295+
text: "Another message with <task> tags",
296+
},
297+
{
298+
type: "tool_result" as const,
299+
tool_use_id: "tool_123",
300+
content: "Tool result",
301+
},
302+
]
303+
304+
// Filter using the same logic as Task.ts
305+
const contentWithoutEnvDetails = userContent.filter((block) => {
306+
if (block.type === "text" && typeof block.text === "string") {
307+
const isEnvironmentDetailsBlock =
308+
block.text.trim().startsWith("<environment_details>") &&
309+
block.text.trim().endsWith("</environment_details>")
310+
return !isEnvironmentDetailsBlock
311+
}
312+
return true
313+
})
314+
315+
// All blocks should be preserved
316+
expect(contentWithoutEnvDetails).toHaveLength(3)
317+
expect(contentWithoutEnvDetails).toEqual(userContent)
318+
})
319+
})
199320
})
200321
})

0 commit comments

Comments
 (0)