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

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions