[BUG] Concurrent task history updates from parallel tabs may cause lost entries
Problem
updateTaskHistory() has serialization within a single ClineProvider instance (see ClineProvider.taskHistory.spec.ts), but when two parallel tabs (different ClineProvider instances) call updateTaskHistory() concurrently, there may be a race condition that causes task history entries to be overwritten or lost.
Evidence
Code locations:
Reproduction:
1. Open Tab A (sidebar) → start Task X
2. Open Tab B (editor) → start Task Y
3. Both tabs call updateTaskHistory() concurrently
4. Check if both entries are persisted correctly, or if one is lost/overwritten
5. Repeat with 5+ concurrent updates from different tabs
Current test coverage:
- ✅ Single instance:
ClineProvider.taskHistory.spec.ts — serializes 5 concurrent calls on same instance
- ❌ Cross-instance: No test for parallel tabs calling updateTaskHistory simultaneously
Root Cause
PR #909 (view-local state isolation) created an independent viewLocalState buffer for each ClineProvider instance, but the serialization logic in updateTaskHistory() only protects concurrent calls within the same instance. If two parallel tabs (different instances) call updateTaskHistory() simultaneously:
- Tab A:
updateTaskHistory(taskX) → reads current state
- Tab B:
updateTaskHistory(taskY) → reads same stale state (before Tab A's write)
- Tab A: writes taskX to history
- Tab B: writes taskY to history — may overwrite Tab A's entry
This issue is similar to the atomicReadAndUpdate pattern in #357 [Epic 2] Task Lifecycle Fixes, but #357 only handles single-instance scenarios.
Proposed Fix
Option A: Cross-instance serialization via shared lock (recommended)
// In ClineProvider.ts or a new TaskHistoryLock service
class TaskHistoryLock {
private queue = Promise.resolve()
async withLock<T>(fn: () => Promise<T>): Promise<T> {
const prev = this.queue
let result: T
this.queue = prev.then(() => (result = fn()))
return result!
}
}
// Usage in updateTaskHistory():
await taskHistoryLock.withLock(async () => {
await this.updateTaskHistoryInternal(task)
})
Option B: Optimistic concurrency with version counter
interface TaskHistoryEntry {
id: string
version: number // Increment on each write
// ... other fields
}
// On write: check version matches, retry if stale
async updateTaskHistory(task: Task): Promise<void> {
let retries = 0
while (retries < MAX_RETRIES) {
const currentVersion = await this.getHistoryVersion()
const result = await this.writeWithVersionCheck(task, currentVersion)
if (result.success) return
retries++
}
}
Option C: Append-only log + merge on read
- Each tab's updates are appended to a log (no overwrites)
- Read phase merges and deduplicates
- Suitable for task history use case, but increases storage size
Scope
- Impact: All multi-tab users — any concurrent task operations across tabs may lose history entries
- Risk: Low-Medium — serialization adds slight latency, but task history is not real-time critical
- Priority: High (data integrity issue)
Related Issues
Testing Requirements
-
Add test in TaskHistoryStore.crossInstance.spec.ts:
it("serializes concurrent updateTaskHistory from two parallel instances", async () => {
// Create 2 ClineProvider instances (simulating 2 tabs)
// Both call updateTaskHistory concurrently
// Verify all entries are persisted correctly
})
-
Test with 5+ concurrent updates from different tabs
-
Test concurrent updateTaskHistory + deleteTaskFromState across tabs
[BUG] Concurrent task history updates from parallel tabs may cause lost entries
Problem
updateTaskHistory()has serialization within a single ClineProvider instance (seeClineProvider.taskHistory.spec.ts), but when two parallel tabs (different ClineProvider instances) callupdateTaskHistory()concurrently, there may be a race condition that causes task history entries to be overwritten or lost.Evidence
Code locations:
ClineProvider.taskHistory.spec.ts:it("serializes concurrent updateTaskHistory calls so no entries are lost")— only tests concurrency within the same instanceTaskHistoryStore.crossInstance.spec.ts:it("concurrent writes to different tasks from two instances produce correct final state")— implies cross-instance scenario needs verificationClineProvider.ts:showTaskWithId(parentTaskId)comment mentions concurrent navigation riskReproduction:
Current test coverage:
ClineProvider.taskHistory.spec.ts— serializes 5 concurrent calls on same instanceRoot Cause
PR #909 (view-local state isolation) created an independent
viewLocalStatebuffer for each ClineProvider instance, but the serialization logic inupdateTaskHistory()only protects concurrent calls within the same instance. If two parallel tabs (different instances) callupdateTaskHistory()simultaneously:updateTaskHistory(taskX)→ reads current stateupdateTaskHistory(taskY)→ reads same stale state (before Tab A's write)This issue is similar to the
atomicReadAndUpdatepattern in #357 [Epic 2] Task Lifecycle Fixes, but #357 only handles single-instance scenarios.Proposed Fix
Option A: Cross-instance serialization via shared lock (recommended)
Option B: Optimistic concurrency with version counter
Option C: Append-only log + merge on read
Scope
Related Issues
fix(webview): isolate parallel provider view stateTesting Requirements
Add test in
TaskHistoryStore.crossInstance.spec.ts:Test with 5+ concurrent updates from different tabs
Test concurrent
updateTaskHistory+deleteTaskFromStateacross tabs