Skip to content

Conversation

@hannesrudolph
Copy link
Collaborator

@hannesrudolph hannesrudolph commented Nov 14, 2025

Summary

Closes #8040

Adds an auto-delete task history feature that purges old tasks on extension reload, helping users manage disk space and keep their task history clean.

Changes

New Setting

  • Auto-delete task history dropdown in Settings → About
  • Options: Never (default), 90 days, 60 days, 30 days, 7 days, 3 days
  • Stored in VS Code global configuration as roo-cline.taskHistoryRetention

Purge Logic (src/utils/task-history-retention.ts)

  • Runs once on extension activation
  • Deletes tasks older than the configured retention period based on task_metadata.json timestamp
  • Falls back to directory mtime for legacy tasks without metadata
  • Detects and removes orphan checkpoint-only folders (directories containing only a checkpoints/ subdirectory)
  • Uses ClineProvider.deleteTaskWithId() to ensure consistent cleanup of checkpoints, shadow repositories, and task state
  • Includes retry logic with backoff for stubborn filesystem operations

UI Updates

  • Added retention dropdown to About.tsx settings panel
  • Full i18n support across 17 languages
  • Warning text about permanent deletion

Testing

  • New test suite task-history-retention.spec.ts covering:
    • Retention periods (3, 7, 30 days)
    • Dry run mode
    • Invalid metadata handling
    • Never (disabled) setting

Technical Notes

  • Purge executes sequentially (not in parallel) to avoid race conditions when updating task state
  • Verbose logging available but disabled by default to reduce noise
  • Schema allows string enum values for UI compatibility

Important

Adds auto-delete task history feature with configurable retention settings and UI support, including logic for purging old tasks and comprehensive testing.

  • Behavior:
    • Adds auto-delete task history feature with retention options (Never, 90, 60, 30, 7, 3 days) in task-history-retention.ts.
    • Purge logic runs on extension activation, deletes tasks older than retention period, handles orphan checkpoint-only folders.
    • Uses ClineProvider.deleteTaskWithId() for consistent cleanup.
  • Settings:
    • New setting taskHistoryRetention added to global configuration in global-settings.ts.
    • UI dropdown added in About.tsx for retention settings with i18n support.
  • Testing:
    • New test suite task-history-retention.spec.ts for retention periods, dry run mode, and invalid metadata handling.
  • Misc:
    • Updates package.json and i18n files for new setting descriptions.

This description was created by Ellipsis for 84853c9. You can customize this summary. It will automatically update as commits are pushed.

@dosubot dosubot bot added size:XL This PR changes 500-999 lines, ignoring generated files. Enhancement New feature or request labels Nov 14, 2025
@roomote
Copy link
Contributor

roomote bot commented Nov 14, 2025

Oroocle Clock   See task on Roo Cloud

Re-reviewed latest commits on this PR (cc67f71). No new issues found.

  • Race condition in concurrent task state updates during purge
Previous reviews

Mention @roomote in a comment to request specific changes to this pull request or fix all unresolved issues.

@hannesrudolph hannesrudolph added the Issue/PR - Triage New issue. Needs quick review to confirm validity and assign labels. label Nov 14, 2025
@hannesrudolph hannesrudolph force-pushed the chore/retention-purge-checkpoint-cleanup branch from 784a70d to 67c7629 Compare November 19, 2025 08:57
@hannesrudolph hannesrudolph force-pushed the chore/retention-purge-checkpoint-cleanup branch from d9b68d5 to 42d73fe Compare November 26, 2025 19:47
@hannesrudolph hannesrudolph moved this from Triage to PR [Needs Prelim Review] in Roo Code Roadmap Nov 26, 2025
@hannesrudolph hannesrudolph added PR - Needs Preliminary Review and removed Issue/PR - Triage New issue. Needs quick review to confirm validity and assign labels. labels Nov 26, 2025
- Remove unused fs/promises import in extension.ts
- Fix Turkish translation: replace Cyrillic 'д' with Turkish 'd' in süreden
- Fix Catalan translation: use consistent 'extensió' terminology
- Import and use RetentionSetting type instead of 'as any' in extension.ts
- Use proper type narrowing for metadata parsing instead of 'as any'
…on flow

- Replace generic 'number' with explicit RetentionDays union type to prevent accidental misconfiguration
- Skip aggressive fs cleanup when deleteTaskById callback successfully removes the directory
- This avoids redundant filesystem work and log noise on stubborn paths
@hannesrudolph hannesrudolph force-pushed the chore/retention-purge-checkpoint-cleanup branch from 84853c9 to cc67f71 Compare January 7, 2026 16:38
Copy link
Contributor

@heyseth heyseth left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Executive Summary

This PR introduces an auto-delete task history feature that purges old tasks on extension reload. The implementation is well-architected, type-safe, and thoroughly tested. The code demonstrates solid engineering practices with proper error handling, i18n support, and integration with existing deletion logic.

Strengths

  • Consistency: Excellent reuse of existing deleteTaskWithId() logic ensures checkpoints, shadow repos, and state are cleaned up alongside files.
  • Reliability: Sequential deletion prevents race conditions, and robust retry logic handles stubborn filesystem operations.
  • Quality: Full i18n support across 17 languages and type-safe schema validation with Zod.

Suggested Enhancements (Code Included)

I have included specific code snippets in the comments to address performance and user trust:

1. Performance: Non-blocking Activation (Critical)
The current implementation awaits the purge process during the activate phase. On machines with large histories or slow disks, this will block the extension startup.

  • Fix: I provided a snippet to wrap the logic in a background "fire-and-forget" task (void (async ...)), ensuring the extension activates immediately.
  • Optimization: Added an early return check for retention === "never" to skip the logic entirely for the majority of users.

2. UX: Visibility & Trust
Silent deletion of user data can be alarming if a user expects to find a task.

  • Fix: I added a Toast Notification logic to the background task. It only triggers if files were actually deleted and provides a direct link to Settings.

3. Testing: Edge Case Coverage
Since we are permanently deleting files, I identified a gap in testing regarding "Orphan Checkpoints" and "Legacy Mtime Fallback."

  • Fix: I drafted 4 specific test cases to verify we strictly target garbage data and do not accidentally delete recent, valid tasks that lack metadata.

Conclusion

This is a high-value maintenance feature. Once the activation blocking issue is resolved (using the background task pattern I suggested), this logic is solid and ready for production!

Comment on lines +120 to +151

// Task history retention purge (runs only on activation)
try {
const config = vscode.workspace.getConfiguration(Package.name)
const retention = config.get<string>("taskHistoryRetention", "never") ?? "never"

outputChannel.appendLine(`[Retention] Startup purge: setting=${retention}`)

const result = await purgeOldTasks(
retention as RetentionSetting,
contextProxy.globalStorageUri.fsPath,
(m) => {
outputChannel.appendLine(m)
console.log(m)
},
false,
async (taskId: string, _taskDirPath: string) => {
// Reuse the same internal deletion logic as the History view so that
// checkpoints, shadow repositories, and task state are cleaned up consistently.
await provider.deleteTaskWithId(taskId)
},
)

outputChannel.appendLine(
`[Retention] Startup purge complete: purged=${result.purgedCount}, cutoff=${result.cutoff ?? "none"}`,
)
} catch (error) {
outputChannel.appendLine(
`[Retention] Failed during startup purge: ${error instanceof Error ? error.message : String(error)}`,
)
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Critical Performance Optimization: Non-blocking Activation

Currently, await purgeOldTasks() blocks the extension activation. If a user has a massive history or a slow disk, this could hang their extension startup.

I strongly recommend moving this to a background "fire-and-forget" execution. By removing the await and handling errors internally, we allow VS Code to finish activating immediately while the cleanup happens asynchronously.

Suggested change
// Task history retention purge (runs only on activation)
try {
const config = vscode.workspace.getConfiguration(Package.name)
const retention = config.get<string>("taskHistoryRetention", "never") ?? "never"
outputChannel.appendLine(`[Retention] Startup purge: setting=${retention}`)
const result = await purgeOldTasks(
retention as RetentionSetting,
contextProxy.globalStorageUri.fsPath,
(m) => {
outputChannel.appendLine(m)
console.log(m)
},
false,
async (taskId: string, _taskDirPath: string) => {
// Reuse the same internal deletion logic as the History view so that
// checkpoints, shadow repositories, and task state are cleaned up consistently.
await provider.deleteTaskWithId(taskId)
},
)
outputChannel.appendLine(
`[Retention] Startup purge complete: purged=${result.purgedCount}, cutoff=${result.cutoff ?? "none"}`,
)
} catch (error) {
outputChannel.appendLine(
`[Retention] Failed during startup purge: ${error instanceof Error ? error.message : String(error)}`,
)
}

Implementation Note: GitHub collapsed the lines at the end of the activate function, so I cannot make this an inline suggestion. Please insert the following logic manually at the very end of activate, immediately after the activationCompleted command.

	// Task history retention purge (runs in background after activation)
	// By this point, provider is fully initialized and ready to handle deletions
	void (async () => {
		try {
			const config = vscode.workspace.getConfiguration(Package.name)
			const retention = config.get<string>("taskHistoryRetention", "never") ?? "never"

			// Skip if retention is disabled
			if (retention === "never") {
				outputChannel.appendLine("[Retention] Background purge skipped: retention is set to 'never'")
				return
			}

			outputChannel.appendLine(`[Retention] Starting background purge: setting=${retention}`)

			const result = await purgeOldTasks(
				retention as RetentionSetting,
				contextProxy.globalStorageUri.fsPath,
				(m) => {
					outputChannel.appendLine(m)
					console.log(m)
				},
				false,
				async (taskId: string, _taskDirPath: string) => {
					// Reuse the same internal deletion logic as the History view so that
					// checkpoints, shadow repositories, and task state are cleaned up consistently.
					await provider.deleteTaskWithId(taskId)
				},
			)

			outputChannel.appendLine(
				`[Retention] Background purge complete: purged=${result.purgedCount}, cutoff=${result.cutoff ?? "none"}`,
			)

			// Show user notification if tasks were deleted
			if (result.purgedCount > 0) {
				const retentionDays = retention === "never" ? "" : retention
				const message = `Roo Code deleted ${result.purgedCount} task${result.purgedCount === 1 ? "" : "s"} older than ${retentionDays} day${retentionDays === "1" ? "" : "s"}`

				vscode.window.showInformationMessage(message, "View Settings", "Dismiss").then((action) => {
					if (action === "View Settings") {
						vscode.commands.executeCommand("workbench.action.openSettings", "roo-cline.taskHistoryRetention")
					}
				})
			}
		} catch (error) {
			outputChannel.appendLine(
				`[Retention] Failed during background purge: ${error instanceof Error ? error.message : String(error)}`,
			)
		}
	})()

UX Note: I bundled a toast notification into this snippet. Silent file deletion can be alarming to users (or confusing if they check history and it's gone), so explicit feedback here builds trust. Feel free to omit that specific if block if you prefer a strictly silent background process, but I personally recommend keeping it.

} finally {
await fs.rm(base, { recursive: true, force: true })
}
})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Robust Edge Case Coverage

Since this feature involves permanent file deletion, I wanted to be absolutely sure we cover the "danger zones." I drafted these 4 test cases to verify the logic handles edge cases safely:

  1. Orphan Cleanup: Ensures we delete directories that only contain checkpoints (garbage).
  2. Safety Check: Ensures we don't delete directories that look like orphans but actually contain user files.
  3. Legacy Fallback: Verifies that tasks without metadata still get cleaned up based on file modification time.
  4. Priority: Confirms that if metadata exists, we trust it over the file system timestamp (crucial for accurate retention).

You should be able to drop these directly into the suite.

Suggested change
})
})
it("deletes orphan checkpoint-only directories regardless of age", async () => {
const base = await mkTempBase()
try {
const now = Date.now()
const days = (n: number) => n * 24 * 60 * 60 * 1000
// Create a normal task that is recent (should be kept)
const normalTask = await createTask(base, "task-normal", now - days(1))
// Create an orphan checkpoint-only directory (only has checkpoints/ subdirectory, no metadata)
const orphanDir = path.join(base, "tasks", "task-orphan-checkpoints")
await fs.mkdir(orphanDir, { recursive: true })
const checkpointsDir = path.join(orphanDir, "checkpoints")
await fs.mkdir(checkpointsDir, { recursive: true })
// Add a dummy file inside checkpoints to make it realistic
await fs.writeFile(path.join(checkpointsDir, "checkpoint-1.json"), "{}", "utf8")
// Create another orphan with just checkpoints (no other files)
const orphanDir2 = path.join(base, "tasks", "task-orphan-empty")
await fs.mkdir(orphanDir2, { recursive: true })
const checkpointsDir2 = path.join(orphanDir2, "checkpoints")
await fs.mkdir(checkpointsDir2, { recursive: true })
// Run purge with 7 day retention - orphans should be deleted regardless of age
const { purgedCount } = await purgeOldTasks("7", base, () => {})
// Orphan directories should be deleted even though they're "recent"
expect(await exists(orphanDir)).toBe(false)
expect(await exists(orphanDir2)).toBe(false)
// Normal task should still exist (it's recent)
expect(await exists(normalTask)).toBe(true)
// Should have deleted 2 orphan directories
expect(purgedCount).toBe(2)
} finally {
await fs.rm(base, { recursive: true, force: true })
}
})
it("does not delete directories with checkpoints AND other content", async () => {
const base = await mkTempBase()
try {
const now = Date.now()
const days = (n: number) => n * 24 * 60 * 60 * 1000
// Create a task directory with both checkpoints and other files (but recent, so should be kept)
const taskDir = path.join(base, "tasks", "task-with-content")
await fs.mkdir(taskDir, { recursive: true })
const checkpointsDir = path.join(taskDir, "checkpoints")
await fs.mkdir(checkpointsDir, { recursive: true })
await fs.writeFile(path.join(checkpointsDir, "checkpoint-1.json"), "{}", "utf8")
// Add other files (not just checkpoints)
await fs.writeFile(path.join(taskDir, "some-file.txt"), "content", "utf8")
// Note: No metadata file, so it's technically invalid but has content
const { purgedCount } = await purgeOldTasks("7", base, () => {})
// Should NOT be deleted because it has content besides checkpoints
expect(await exists(taskDir)).toBe(true)
expect(purgedCount).toBe(0)
} finally {
await fs.rm(base, { recursive: true, force: true })
}
})
it("falls back to directory mtime for legacy tasks without metadata", async () => {
const base = await mkTempBase()
try {
const now = Date.now()
const days = (n: number) => n * 24 * 60 * 60 * 1000
// Create a legacy task directory without any metadata file
const oldLegacyDir = path.join(base, "tasks", "task-legacy-old")
await fs.mkdir(oldLegacyDir, { recursive: true })
// Add some content file
await fs.writeFile(path.join(oldLegacyDir, "content.txt"), "old task", "utf8")
// Manually set mtime to 10 days ago by touching the directory
const oldTime = new Date(now - days(10))
await fs.utimes(oldLegacyDir, oldTime, oldTime)
// Create another legacy task that is recent
const recentLegacyDir = path.join(base, "tasks", "task-legacy-recent")
await fs.mkdir(recentLegacyDir, { recursive: true })
await fs.writeFile(path.join(recentLegacyDir, "content.txt"), "recent task", "utf8")
// This one has recent mtime (now)
// Run purge with 7 day retention
const { purgedCount } = await purgeOldTasks("7", base, () => {})
// Old legacy task should be deleted based on mtime
expect(await exists(oldLegacyDir)).toBe(false)
// Recent legacy task should be kept
expect(await exists(recentLegacyDir)).toBe(true)
expect(purgedCount).toBe(1)
} finally {
await fs.rm(base, { recursive: true, force: true })
}
})
it("prioritizes metadata timestamp over mtime when both exist", async () => {
const base = await mkTempBase()
try {
const now = Date.now()
const days = (n: number) => n * 24 * 60 * 60 * 1000
// Create task with old metadata ts but recent mtime
const taskDir = path.join(base, "tasks", "task-priority-test")
await fs.mkdir(taskDir, { recursive: true })
const metadataPath = path.join(taskDir, GlobalFileNames.taskMetadata)
// Metadata says it's 10 days old (should be deleted with 7 day retention)
const metadata = JSON.stringify({ ts: now - days(10) }, null, 2)
await fs.writeFile(metadataPath, metadata, "utf8")
// But directory mtime is recent (could happen after editing)
// (Directory mtime is automatically recent from mkdir/writeFile)
const { purgedCount } = await purgeOldTasks("7", base, () => {})
// Should be deleted based on metadata ts, not mtime
expect(await exists(taskDir)).toBe(false)
expect(purgedCount).toBe(1)
} finally {
await fs.rm(base, { recursive: true, force: true })
}
})

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Enhancement New feature or request PR - Needs Preliminary Review size:XL This PR changes 500-999 lines, ignoring generated files.

Projects

Status: PR [Needs Prelim Review]

Development

Successfully merging this pull request may close these issues.

[ENHANCEMENT] Checkpoints: respect .rooignore and add cleanup/size limits

3 participants