Skip to content

[BUG] Concurrent task history updates from parallel tabs may cause lost entries #920

Description

@easonLiangWorldedtech

[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:

  • ClineProvider.taskHistory.spec.ts: it("serializes concurrent updateTaskHistory calls so no entries are lost") — only tests concurrency within the same instance
  • TaskHistoryStore.crossInstance.spec.ts: it("concurrent writes to different tasks from two instances produce correct final state") — implies cross-instance scenario needs verification
  • ClineProvider.ts: showTaskWithId(parentTaskId) comment mentions concurrent navigation risk

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:

  1. Tab A: updateTaskHistory(taskX) → reads current state
  2. Tab B: updateTaskHistory(taskY) → reads same stale state (before Tab A's write)
  3. Tab A: writes taskX to history
  4. 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

  1. 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
    })
  2. Test with 5+ concurrent updates from different tabs

  3. Test concurrent updateTaskHistory + deleteTaskFromState across tabs

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions