Skip to content

[BUG] Switching mode/apiConfig while parallel tools are running may corrupt task state #922

Description

@easonLiangWorldedtech

[BUG] Switching mode/apiConfig while parallel tools are running may corrupt task state

Problem

When a tab is running parallel tools (PR #359 [Epic 4] Parallel Tool Execution) and another tab switches mode or apiConfig, it may affect shared resources (e.g., API provider, tool execution context). The current handleModeSwitch() + activateProviderProfile() logic does not account for state isolation during parallel tool execution.

Evidence

Code locations:

  • ClineProvider.ts:
    • handleModeSwitch() — updates viewLocalState then global state
    • activateProviderProfile() — updates apiConfig in viewLocalState + global state
    • Comment: "Broadcast reset to all other live instances so parallel tabs don't keep stale state"
  • ClineProvider.parallelMode.spec.ts: Tests view-local isolation, but NOT during parallel tool execution

Reproduction:

1. Open Tab A (sidebar) → start task with parallel tools enabled (#359)
2. Parallel tools are running: Tool 1 (read file), Tool 2 (execute command), Tool 3 (write file)
3. In Tab B (editor): switch mode from "code" to "architect" via handleModeSwitch()
4. What happens?
   - Tab A's parallel tools still running — do they use old mode or new mode?
   - If they use global state, they see Tab B's mode change
   - If they use viewLocalState, they keep old mode (correct)
   - BUT: shared resources (API provider, tool execution context) may be affected
5. Check if parallel tools complete correctly with original mode/apiConfig

Current test coverage:

  • ✅ View-local isolation: ClineProvider.parallelMode.spec.ts — tests state isolation during normal operation
  • ❌ Parallel execution: No test for mode switch during parallel tool execution

Root Cause

PR #909's view-local state isolation design:

// ClineProvider.ts (simplified)
async handleModeSwitch(newMode: string): Promise<void> {
    // Step 1: Update viewLocalState (local-first pattern)
    await this.saveViewState("mode", newMode)
    
    // Step 2: Update global state (broadcast to all tabs)
    await this.updateGlobalState("mode", newMode)
}

async activateProviderProfile(profileName: string): Promise<void> {
    // Similar pattern: viewLocalState first, then global state
    await this.saveViewState("currentApiConfigName", profileName)
    await this.activateProviderInContext(profileName)
}

When parallel tools are running:

  1. Tab A: parallelTools.execute() → starts Tool 1, Tool 2, Tool 3 concurrently
  2. Tools use shared resources (API provider, tool execution context) from Tab A's viewLocalState
  3. Tab B: handleModeSwitch("architect") → updates global state
  4. Shared resources may be affected by Tab B's mode change
  5. Problem: parallel tools in Tab A may see inconsistent state (some use old mode, some use new mode)

This issue is related to the DispatchState machine in #359 [Epic 4] Parallel Tool Execution, but #359 does not account for multi-tab scenarios.

Proposed Fix

Option A: Capture tool execution context at start (recommended)

// In ClineProvider.ts — capture state when parallel tools start, not during execution
async executeParallelTools(tasks: ToolTask[]): Promise<ToolResult[]> {
    // Capture snapshot of viewLocalState at execution time
    const executionContext = {
        mode: this.viewLocalState.mode ?? this.globalState.mode,
        apiConfigName: this.viewLocalState.currentApiConfigName ?? this.globalState.currentApiConfigName,
        apiConfiguration: this.viewLocalState.apiConfiguration ?? this.globalState.apiConfiguration,
        // ... other fields needed for tool execution
    }
    
    // Pass executionContext to each parallel task
    const results = await Promise.all(
        tasks.map(task => this.executeToolWithContext(task, executionContext))
    )
    
    return results
}

async executeToolWithContext(tool: ToolTask, context: ExecutionContext): Promise<ToolResult> {
    // Use captured context, not current viewLocalState (which may change during execution)
    const provider = await this.getProviderForMode(context.mode, context.apiConfigName)
    return tool.execute(provider, context)
}

Option B: Lock mode/apiConfig during parallel execution

// In ClineProvider.ts — prevent mode switch while parallel tools are running
class ParallelExecutionLock {
    private isExecuting = false
    
    async withParallelExecution<T>(fn: () => Promise<T>): Promise<T> {
        if (this.isExecuting) {
            throw new Error("Cannot change mode/apiConfig during parallel tool execution")
        }
        
        this.isExecuting = true
        try {
            return await fn()
        } finally {
            this.isExecuting = false
        }
    }
}

// Usage in handleModeSwitch():
async handleModeSwitch(newMode: string): Promise<void> {
    if (this.parallelExecutionLock.isExecuting) {
        // Option 1: Block mode switch until parallel execution completes
        await this.parallelExecutionLock.waitForCompletion()
        
        // Option 2: Queue the mode switch for after completion
        this.pendingModeSwitch = newMode
        return
    }
    
    await this.saveViewState("mode", newMode)
    await this.updateGlobalState("mode", newMode)
}

Option C: Per-tool state isolation

  • Each parallel tool captures its own viewLocalState snapshot at start time
  • Tool execution uses the snapshot, not global state
  • Suitable for fine-grained control, but increases complexity

Scope

  • Impact: Users who run parallel tools ([Epic 4] Parallel Tool Execution #359) in multi-tab mode — mode/apiConfig switches may corrupt tool execution
  • Risk: Low-Medium — Option A is safest (explicit context), Option B adds UX constraint (block mode switch)
  • Priority: Medium-High (parallel tools are core feature, state corruption causes silent failures)

Related Issues

Testing Requirements

  1. Add test in ClineProvider.parallelMode.spec.ts:

    it("parallel tools complete correctly when another tab switches mode during execution", async () => {
        // Create 2 ClineProvider instances (Tab A, Tab B)
        // Tab A: start parallel tool execution (3 concurrent tools)
        // Tab B: switch mode/apiConfig while Tab A's tools are running
        // Verify Tab A's tools use original state, not Tab B's new state
    })
    
    it("parallel tools handle apiConfig change during execution", async () => {
        // Tab A: start parallel tools with profile "A"
        // Tab B: switch to profile "B"
        // Verify Tab A's tools still use profile "A" (not corrupted)
    })
  2. Test with 5+ concurrent parallel tools across different tabs

  3. Test concurrent handleModeSwitch + activateProviderProfile during parallel execution

  4. Test that mode switch is blocked/queued if parallel execution is in progress (Option B)

Notes

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