Skip to content

[ENHANCEMENT] Support Fork-Join parallel modes with per-view state isolation #923

Description

@easonLiangWorldedtech

[ENHANCEMENT] Support Fork-Join parallel modes with per-view state isolation

Problem

Issue #568's Fork-Join Agent Orchestration requires multiple modes to run simultaneously (fork phase), then aggregate results (join phase). The current view-local state isolation (PR #909) may affect fork-join phase data sharing, because each mode instance has its own independent viewLocalState buffer.

Evidence

Code locations:

  • ClineProvider.ts:
    • viewLocalState: Partial<ExtensionState> — per-instance state buffer
    • getState(): merges viewLocalState on top of global state
    • Comment: "parallel tabs within the same extension host share memory by design"
  • ClineProvider.parallelMode.spec.ts: Tests view-local isolation, but NOT fork-join scenario

Fork-Join workflow (from #568):

1. Fork Phase: [Research Mode] + [Ask Mode] + [Debug Mode] run SIMULTANEOUSLY
   - Each mode gathers independent data/context
2. Join Phase: [Architect Mode] receives all 3 datasets
   - Aggregates results to build comprehensive plan

Current test coverage:

  • ✅ View-local isolation: ClineProvider.parallelMode.spec.ts — tests state isolation between parallel tabs
  • ❌ Fork-Join: No design/test for multi-mode orchestration with view-local state

Root Cause

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

// ClineProvider.ts (simplified)
class ClineProvider {
    private viewLocalState: Partial<ExtensionState> = {}
    
    getState() {
        return { ...globalState, ...this.viewLocalState } // Local overrides global
    }
}

When fork-join orchestration occurs:

  1. Fork Phase: 3 modes run simultaneously (Research, Ask, Debug)
    • Each mode is a separate ClineProvider instance (different tabs or sub-agents)
    • Each has independent viewLocalState buffer
  2. Join Phase: Architect Mode aggregates results from all 3 modes
    • But each mode's output may be affected by its own viewLocalState
    • Example: Research mode uses "code" mode settings, Ask mode uses "architect" settings
    • Results are inconsistent because different modes have different configurations

Problem: Fork-join phase needs consistent state across all forked modes, but PR #909's view-local isolation makes each mode independent.

Proposed Fix

Option A: Fork-Join aware orchestration (recommended)

// In Orchestrator.ts — detect fork-join pattern and use shared state
class Orchestrator {
    async executeForkJoinPhase(
        forkModes: Mode[],  // [Research, Ask, Debug]
        joinMode: Mode      // Architect
    ): Promise<Plan> {
        // Step 1: Fork — run all modes with SHARED state (not view-local)
        const sharedState = this.captureGlobalState()  // Use global state, not viewLocalState
        
        const forkResults = await Promise.all(
            forkModes.map(mode => 
                mode.execute({ ...sharedState, isForkPhase: true })
            )
        )
        
        // Step 2: Join — aggregate results with joinMode
        return joinMode.aggregate(forkResults, sharedState)
    }
}

// In ClineProvider.ts — detect fork-join and use global state
async executeWithForkJoinAwareness(): Promise<void> {
    if (this.isInForkPhase()) {
        // Use global state for consistency across forked modes
        const state = this.globalState  // Not viewLocalState!
        await this.executeMode(state)
    } else {
        // Normal mode: use view-local isolation
        const state = this.getState()  // Merged (viewLocal + global)
        await this.executeMode(state)
    }
}

Option B: Explicit state sharing via message passing

// In Orchestrator.ts — pass shared context to all forked modes
async executeForkJoinPhase(forkModes: Mode[]): Promise<Plan> {
    // Create shared context (not view-local)
    const sharedContext = {
        mode: this.globalState.mode,  // Global, not viewLocal
        apiConfigName: this.globalState.currentApiConfigName,
        // ... other fields needed for consistency
    }
    
    // Pass sharedContext to all forked modes via message passing
    const results = await Promise.all(
        forkModes.map(mode => 
            mode.execute({ context: sharedContext })
        )
    )
    
    return this.joinPhase(results)
}

// In ClineProvider.ts — handle shared context from orchestrator
case 'forkJoinExecute': {
    const { context } = payload
    // Override viewLocalState with shared context for fork phase
    await this.setForkPhaseContext(context)
    break
}

Option C: Hybrid approach (view-local + shared override)

  • Fork phase uses global state (consistent across all modes)
  • Join phase uses view-local state (Architect mode's own settings)
  • Simple but limits flexibility: forked modes can't have custom configurations

Scope

Related Issues

Testing Requirements

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

    it("fork-join orchestration uses consistent state across forked modes", async () => {
        // Create 3 ClineProvider instances (Research, Ask, Debug)
        // Each has different viewLocalState (different modes/apiConfigs)
        // Execute fork-join phase with shared context
        // Verify all forked modes use SAME state (not their individual viewLocalState)
    })
    
    it("join mode uses its own view-local settings", async () => {
        // Fork phase: 3 modes run with shared global state
        // Join phase: Architect mode runs with its own viewLocalState
        // Verify join mode can have custom configuration (not forced to use fork's shared state)
    })
  2. Test fork-join with different viewLocalState configurations across forked modes

  3. Test concurrent fork-join execution from multiple tabs

  4. Test that fork phase uses global state, join phase uses view-local state (Option A)

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