Skip to content

[ENHANCEMENT] Task Tree Abstraction: Unified Subtask Management with Auto-Nesting Limit #856

Description

@easonLiangWorldedtech

Task Tree Abstraction: Unified Subtask Management with Auto-Nesting Limit

Problem Statement

Zoo Code's current delegation system has three major problems:

  1. Linear chain, not treeparentTaskId + childTaskId only supports a single path (A→B→C), but actual workflows need tree structure (A→{B,C}→D)
  2. Unlimited tab nesting — each delegation opens a new tab, users have no way to control depth
  3. Context / Checkpoint / Inline mode are siloedManual Context Control: Manual Control Over Context Compression #849, Inline Subtask Mode: Continue Work in the Same Conversation Without Opening a New Tab #850, [ENHANCEMENT] Task as Checkpoint: Review History and Decide Next Move #854 are three separate issues, but they're all symptoms of the same core problem

Current Architecture Analysis

Existing Data Model (Linear Chain)

// Task.ts line ~169-170
interface Task {
    readonly parentTaskId?: string      // single parent only
    childTaskId?: string                // single child only
}

// ClineProvider.ts — delegation metadata
interface HistoryItem {
    status: "delegated" | "active" | ...
    delegatedToId: string               // single child
    awaitingChildId: string             // single child
    childIds: string[]                  // array but only used for linear chain
}

Current Delegation Flow (Linear)

delegateParentAndOpenChild():
  1. Flush parent tool results to history
  2. removeClineFromStack(parent)          ← parent removed from stack
  3. createTask(child, parent=parent)      ← child created with parent reference
  4. persist metadata: status="delegated", awaitingChildId=child.taskId
  5. child.start()

reopenParentFromDelegation():
  1. Load parent from history
  2. Validate delegation state (status === "delegated")
  3. Call resumeAfterDelegation() on parent
  4. Parent continues task loop

Limitation: This is a stack-based LIFO model. Only ONE child can be active at a time. No parallelism, no tree structure.

Existing Fields That Support Tree (But Unused)

// Task.ts line ~169-170 — these ALREADY support tree!
readonly parentTaskId?: string    // ← single parent ✓
childTaskId?: string              // ← could be childIds: string[] for multiple children

// ClineProvider.ts line 3533
const childIds = Array.from(new Set([...(historyItem.childIds ?? []), child.taskId]))
// ← childIds IS an array! But only used to track lineage, not execution order

Key Insight: The data model ALREADY supports tree structure. We just need to:

  1. Change childTaskIdchildIds: string[] in Task interface
  2. Modify delegation flow to support multiple children (parallel + sequential)
  3. Add nesting depth tracking + auto-flatten

Proposed Design: Task Tree Abstraction

1. New Data Model

// Task.ts — NEW fields
interface Task {
    // Existing (keep for backward compat)
    readonly parentTaskId?: string
    
    // NEW: Tree structure
    childIds: string[]              // ← replaces single childTaskId
    siblingIds: string[]            // ← children of same parent
    depth: number                   // ← nesting level (root = 0)
    
    // NEW: Execution mode
    executionMode: "sequential" | "parallel" | "inline"
    maxDepth: number                // ← per-task nesting limit
    
    // NEW: Checkpoint support
    checkpointEnabled: boolean      // ← replaces #854
    lastCheckpointId?: string       // ← checkpoint reference
}

// ClineProvider.ts — NEW tree state
interface TaskTree {
    rootTaskId: string              // ← always the user's original task
    activeTasks: Map<string, Task>  // ← all active tasks (not just one)
    completedTasks: Set<string>     // ← finished tasks
    
    // Tree operations
    addChild(parentId: string, child: Task): void
    removeChild(parentId: string, childId: string): void
    getChildren(taskId: string): Task[]
    getDepth(taskId: string): number
    flattenExcess(taskId: string, maxDepth: number): void
}

2. Execution Modes

Mode A: Sequential (Current Default)

Parent → Child1 → Child2 → ... (LIFO stack, current behavior)
  • Backward compatible with existing flow
  • No changes needed for existing users

Mode B: Parallel (task-level — complements #355)

Parent
├── Child1 (parallel)
└── Child2 (parallel)
     ↓
    Both complete → Parent resumes
  • Multiple children execute concurrently
  • Parent waits for ALL children to complete
  • Uses childIds: string[] + parallel task management

Note on #355: Issue #355 focuses on tool-level parallelism (parallel tool calls within ONE task). Task Tree's Parallel mode is task-level delegation (parent spawns multiple concurrent child tasks, each with its own conversation). These are different scopes and can coexist.

Mode C: Inline (supersedes #850)

Parent conversation
  └── [subtask executes inline]
      → tool_result injected into same conversation
  → Parent continues naturally
  • No tab switch, no new Task instance
  • Subtask runs in parent's conversation as a "virtual child"
  • Results injected as tool_results

Inline Mode Execution Mechanism (How It Actually Works)

Inline mode executes subtasks within the parent's existing conversation without creating a new Task instance or tab switch. But how does this actually work? The implementation needs to answer: when depth exceeds maxDepth and auto-flatten triggers, what mechanism runs the subtask?

Decision: Inline mode uses AI-driven execution (not direct tool calls).

Inline subtask flow:

1. Parent receives delegation request (depth > maxDepth → auto-flatten)
2. System constructs inline prompt:
   ┌─────────────────────────────────────────────┐
   │ [System Prompt]                             │
   │ [Parent's recent conversation history]      │  ← Limited to last N messages
   │                                             │
   │ [INLINE SUBTASK CONTEXT]                    │
   │ "You are now executing a subtask inline."   │
   │ Original goal: "{subtask message}"          │
   │ Parent context: {relevant parent state}     │
   │                                             │
   │ Instructions:                               │
   │ - Execute the subtask using available tools  │
   │ - When done, call attempt_completion with    │
   │   your result (no approval popup needed)     │
   │ - Your result will be injected into parent's │
   │   conversation automatically                 │
   └─────────────────────────────────────────────┘
3. AI agent processes the inline prompt → generates tool calls → executes
4. When AI calls attempt_completion:
   - Result is captured (no approval popup)
   - Result injected as tool_result into parent's conversation
   - Parent resumes via resumeAfterDelegation()

Why AI-driven, not direct tool execution?

Approach Pros Cons
AI-driven (chosen) Full reasoning capability; handles complex subtasks; consistent with existing delegation flow Slightly more tokens (prompt overhead); one extra API call
Direct tool calls Faster, fewer tokens; no prompt overhead Loses AI reasoning; can't handle conditional logic; limited to simple operations

AI-driven is the right choice because: Inline subtasks are meant for complex work that needs AI reasoning (e.g., "fix the auth bug in middleware"). Direct tool execution would only work for trivial operations, which defeats the purpose of inline mode. The token overhead (~500-1000 tokens for prompt) is negligible compared to the actual subtask work.

Context management for inline prompts:

// Build inline prompt with bounded context
async function buildInlinePrompt(parent: Task, subtaskMessage: string): Promise<string> {
    // Include parent's recent history (bounded — don't flood the prompt)
    const recentHistory = parent.apiConversationHistory.slice(-15)  // Last ~15 messages
    
    // Extract relevant state from parent (not full conversation)
    const parentState = await extractParentContext(parent, {
        includeTodos: true,           // Current todo list
        includeRecentFiles: true,     // Files recently edited by parent
        excludeToolOutputs: true,     // Skip verbose tool outputs to save tokens
    })
    
    return `
[INLINE SUBTASK]
You are executing a subtask inline within the parent conversation.

Parent's current goal: ${parentState.currentGoal}
Subtask instruction: ${subtaskMessage}
Current todo status: ${parentState.todoStatus}
Recently edited files: ${parentState.recentFiles.join(", ")}

Execute this subtask using available tools. When complete, call attempt_completion with your result.
Your result will be automatically injected into the parent conversation — no approval needed.`
}

Error handling in inline mode:

// Inline error scenarios and how they're handled:

// Scenario 1: Subtask AI fails to produce a valid result
// → attempt_completion called with empty/error message
// → Parent receives tool_result: "Subtask failed: {error}"
// → Parent's resumeAfterDelegation() continues — parent can decide next steps

// Scenario 2: Subtask AI enters infinite loop (tool call without completion)
// → Same as regular task: API timeout or max turns limit kicks in
// → Result injected as error tool_result to parent
// → Parent resumes and handles the failure

// Scenario 3: Inline subtask needs more context than available
// → AI calls compress_context() first (same tools available as regular tasks)
// → If still stuck, fails with "insufficient context" error
// → Parent can decide whether to retry with different approach

// Key point: Inline mode reuses the same error handling infrastructure as tab mode.
// The only difference is no approval popup — errors are silently injected into parent.

ResumeAfterDelegation compatibility:

// resumeAfterDelegation() works identically for inline and tab modes:
async function resumeAfterDelegation(task: Task): Promise<void> {
    // 1. Clear ask states (already done by inline completion)
    task.clearAskStates()
    
    // 2. Reset abort flags
    task.abortFlag = false
    
    // 3. Load conversation history (inline mode: same conversation, already loaded)
    await task.loadHistory()
    
    // 4. Continue task loop — parent sees the tool_result and proceeds naturally
    await task.continueTaskLoop()
}

// The only difference: in tab mode, loadHistory() loads from storage.
// In inline mode, history is already in memory (same conversation).
// No state change needed — resumeAfterDelegation() still works correctly.

Inline Mode Escalation: Upgrading Back to Tab Mode

Problem: The current design makes inline mode permanent once triggered. But what if an inline subtask grows unexpectedly complex? For example, user says "fix the auth bug" (simple), but during execution the AI discovers it needs to update 3 different modules, add tests, and update docs — suddenly this is a large task that deserves its own tab for clarity.

Solution: Allow inline mode to escalate back to tab mode when conditions warrant it. This is one-way escalation (inline → tab), not bidirectional oscillation.

// Inline execution with optional escalation
async function executeInlineSubtask(parent: Task, message: string): Promise<void> {
    // 1. Start inline (no new tab)
    const result = await runInlineExecution(parent, message)
    
    // 2. Check if we should escalate back to tab mode
    if (shouldEscalateToTab(result)) {
        // Create a NEW tab with the inline work so far as context
        const child = await createTaskFromInline(parent, result)
        
        // Continue in new tab — depth stays same but now has its own conversation space
        await child.start()
        return
    }
    
    // 3. Normal path: inject result into parent (no escalation needed)
    parent.say({ type: "tool_result", text: `Subtask completed: ${result}` })
}

// Heuristics for when inline is no longer sufficient
function shouldEscalateToTab(result: InlineResult): boolean {
    // 1. Result contains multiple distinct sub-goals (e.g., "fix auth" turned into 
    //    "update middleware + add tests + update docs")
    if (countDistinctGoals(result) > config.inlineEscalation.minGoals) return true
    
    // 2. Inline conversation grew too large — context is getting unwieldy
    if (result.conversationTokens > config.inlineEscalation.tokenThreshold) return true
    
    // 3. User explicitly requests new tab during inline execution
    if (result.userRequestedTabSwitch) return true
    
    // 4. AI detects need for parallel work within this subtask
    if (result.detectedParallelNeeds) return true
    
    return false
}

// Create a new task from inline execution results
async function createTaskFromInline(parent: Task, result: InlineResult): Promise<Task> {
    const child = await provider.createTask(result.summary, undefined, parent as any, {
        initialTodos: extractTodosFromResult(result),
        mode: "sequential",
        startTask: false,
        depth: parent.depth + 1,  // Same depth as inline would have been
        executionMode: "sequential",  // Now in tab mode, not inline
    })
    
    // Inject the inline work so far as context for the new task
    child.apiConversationHistory = [
        { role: "user", content: `Continuing from inline subtask:\n${result.fullContext}` },
        ...child.apiConversationHistory,
    ]
    
    return child
}

When escalation triggers:

Scenario Escalation? Why
Subtask = "fix auth bug" (simple, 5 turns) ❌ No Stays inline, inject result to parent
Subtask = "migrate DB + update API + fix tests" (complex, 30+ turns) ✅ Yes Multiple distinct goals → needs own tab for clarity
User says "this is getting big, open a new task" during inline ✅ Yes Explicit user request
Inline AI detects it needs to spawn parallel sub-subtasks ✅ Yes Parallel work better in dedicated tab

VSCode settings:

{
    "zooCode.inlineEscalation.enabled": true,           // Allow inline → tab upgrade
    "zooCode.inlineEscalation.tokenThreshold": 30000,   // Escalate if tokens > this
    "zooCode.inlineEscalation.minGoals": 3,             // Escalate if > N distinct goals
}

Key constraints:

  • One-way only: Once escalated to tab mode, the task follows normal depth tracking. It cannot go back to inline unless it hits maxDepth again (which would trigger a new inline subtask). This prevents oscillation between modes.
  • No data loss: The inline conversation context is preserved and injected into the new tab as starting context. User sees what happened during inline execution.
  • Depth stays same: Escalation doesn't increment depth — it just moves existing work to a tab. If the task was at depth 3 (inline), the new tab also starts at depth 3, preserving the nesting limit logic.

3. Auto-Nesting Limit (supersedes #850 + adds depth control)

Configuration

// VSCode settings
{
    "zooCode.maxNestingDepth": 2,        // ← default: 2 levels
    "zooCode.autoFlattenOnLimit": true,   // ← auto-switch to inline when limit reached
    "zooCode.parallelByDefault": false,   // ← sequential by default for safety
}

Auto-Flatten Logic

function shouldAutoFlatten(task: Task): boolean {
    return task.depth >= task.maxDepth && config.autoFlattenOnLimit
}

// When auto-flatten triggers:
// 1. Instead of creating new tab, execute inline
// 2. Inject result as tool_result into parent conversation
// 3. No state change needed — resumeAfterDelegation() still works

How it's triggered: The AI agent decides to delegate based on system prompt guidance (encouraging flat structure over deep nesting). When maxDepth is reached, the system enforces inline execution regardless of what the AI tries. This means even if the AI calls new_task, the runtime checks depth first and falls back to inline mode automatically.

Example:

User: "Do X, then Y, then Z" (depth=0)
AI:   [delegates to subtask for X] → depth=1 ✓
      [subtask delegates to sub-subtask for X.1] → depth=2 ✓
      [sub-subtask delegates again] → depth=3 > maxDepth(2) → AUTO-FLATTEN!
         → Execute inline, inject result as tool_result

Key difference from #850: #850 requires the user/AI to manually choose subtaskMode: "inline". Task Tree's auto-flatten is enforced at runtime — no manual decision needed.

4. Checkpoint System (supersedes #854)

What "Checkpoint" Means in Task Tree Context

Important distinction: Zoo Code's existing checkpoint system (src/core/checkpoints/) snapshots workspace files via shadow git repo. That's a different feature.

Task Tree's checkpoint is about conversation-level state management: saving the AI's reasoning context so it can be compressed, analyzed, and used to generate systematic next moves with clean context for inline continuation.

Core Flow: Compress → Analyze → Continue Inline

User: "Do X, then Y, then Z" (depth=0)
AI:   [delegates to subtask for X] → depth=1 ✓
      [subtask delegates again] → depth=2 ✓
      
      ┌─── CHECKPOINT TRIGGERED (depth >= maxDepth - 1) ─────────────┐
      │                                                                │
      │  1. Save conversation snapshot (checkpoint)                    │
      │     → Full history stored, not lost                            │
      │                                                                │
      │  2. Compress conversation history                              │
      │     → compress_context("balanced") reduces token count         │
      │     → Keeps reasoning chain intact, removes redundant details  │
      │                                                                │
      │  3. Feed compressed context to AI for analysis                 │
      │     → "Based on current progress, what are the next steps?"    │
      │     → AI generates systematic next move suggestions            │
      │                                                                │
      │  4. User reviews suggestions + adjusts context if needed       │
      │     → Can add/remove/modify suggestions                        │
      │     → Can choose different compression level                   │
      │     → Helps avoid weak models that struggle with long context  │
      │                                                                │
      │  5. Create inline task with clean, focused context             │
      │     → No need to carry full conversation history               │
      │     → AI continues work with clear direction                   │
      └────────────────────────────────────────────────────────────────┘
      
      [inline subtask executes] → depth=3 > maxDepth(2) → inline mode

Checkpoint Data Structure (Conversation-Level)

interface ConversationCheckpoint {
    id: string                      // Unique checkpoint ID
    taskId: string                  // Which task this belongs to
    timestamp: number               // When created
    
    // Full conversation history (preserved, not lost)
    fullHistory: Anthropic.Messages.MessageParam[]
    
    // Compressed version (for AI analysis)
    compressedSummary: {
        tokensOriginal: number      // Original token count
        tokensCompressed: number    // After compression
        strategy: "minimal" | "balanced" | "aggressive"  // Compression level used
        
        // Structured summary of what happened
        completedTasks: string[]    // What's been done
        currentProgress: string     // Where we are now
        pendingWork: string[]       // What remains
        blockers?: string[]         // Any issues encountered
    }
    
    // AI-generated next move suggestions (from compressed context)
    suggestedNextMoves: {
        id: string
        description: string
        priority: "high" | "medium" | "low"
        estimatedEffort: string     // e.g., "quick fix", "requires investigation"
        requiresContext?: string[]  // What context this move needs
    }[]
    
    // User adjustments (if any)
    userAdjustedMoves?: {
        selectedMoveIds: string[]   // Which suggestions to follow
        customInstructions?: string // User's own additions/modifications
        compressionOverride?: "minimal" | "balanced" | "aggressive"  // Override auto-level
    }
}

Integration with Context Compression Tools (#849)

Checkpoint uses the context compression tools from #849:

// When checkpoint is triggered (auto or manual):
async function createConversationCheckpoint(task: Task): Promise<ConversationCheckpoint> {
    // 1. Save full history
    const fullHistory = [...task.apiConversationHistory]
    
    // 2. Compress using #849 tools
    const compressionStrategy = task.depth >= task.maxDepth - 1 ? "balanced" : "minimal"
    const compressed = await compressContext(task, compressionStrategy)
    
    // 3. Feed to AI for analysis — generate next move suggestions
    const suggestedMoves = await analyzeCompressedContext(compressed, fullHistory)
    
    return {
        id: crypto.randomUUID(),
        taskId: task.taskId,
        timestamp: Date.now(),
        fullHistory,
        compressedSummary: compressed.summary,
        suggestedNextMoves: suggestedMoves,
    }
}

// AI analysis prompt (uses compressed context):
const analysisPrompt = `
Based on the following compressed conversation history:
- Completed: ${completedTasks.join(", ")}
- Current progress: ${currentProgress}
- Pending work: ${pendingWork.join("; ")}

Generate systematic next move suggestions. For each suggestion include:
1. Description of what to do
2. Priority (high/medium/low)
3. Estimated effort
4. Any context needed

Consider the full workflow goal and suggest moves that make sense together, not just the immediate next step.`

User Control Points (Key Design Principle)

The checkpoint system gives users explicit control at critical moments:

When What User Sees User Can Do
Auto-trigger (depth >= maxDepth - 1) "Checkpoint created. AI suggests next moves:" + list of suggestions Accept all / Pick some / Modify / Reject all and write custom instructions
Manual trigger (create_checkpoint tool) Same as above, but user chose when Same options
After compression Shows token reduction stats (e.g., "Compressed from 45K to 12K tokens") Choose different compression level if current one loses too much info
Before inline task starts Shows clean context summary + selected next moves Edit the context, add/remove moves, adjust instructions

Why this matters for weak models: Some models struggle with long conversation contexts (e.g., older models, smaller models). By compressing and presenting structured suggestions:

  • User can see what info is being preserved vs. discarded
  • Can choose "minimal" compression if model handles short context well but needs dense information
  • Can choose "aggressive" compression if model prefers focused prompts
  • Can manually add/remove context before the inline task starts

Checkpoint Auto-Trigger Conditions

// Auto-create checkpoint when:
// 1. Task depth reaches maxDepth - 1 (one level before auto-flatten)
//    → Gives user a chance to review and adjust BEFORE inline execution kicks in
// 2. User explicitly calls create_checkpoint tool (manual, always available)
// 3. After each completed subtask (configurable — saves progress snapshot for parent's context)

// VSCode settings:
{
    "zooCode.autoCheckpointOnNesting": true,      // Auto-trigger at maxDepth - 1
    "zooCode.checkpointCompressionDefault": "balanced", // Default compression level
    "zooCode.showSuggestionsBeforeInline": true,   // Show AI suggestions before inline task starts
}

Change Detection for Checkpoint Triggers (Enhancement)

Problem: Depth-based triggers alone can create unnecessary checkpoints. If a subtask has been running for 20 turns but only made minor edits to the same file, creating a new checkpoint each time wastes tokens and creates noise. Users don't want spam — they want checkpoints when something meaningful changed.

Solution: Add change detection logic that compares current conversation state with the last checkpoint before deciding whether to create a new one:

// Enhanced checkpoint trigger with change detection
async function shouldCreateCheckpoint(task: Task): Promise<boolean> {
    // 1. Depth-based trigger (existing — always fires)
    if (task.depth >= task.maxDepth - 1) return true
    
    // 2. Manual trigger (existing — always fires)
    if (userRequestedCheckpoint) return true
    
    // 3. After subtask completion (existing — saves progress for parent)
    if (task.isSubtaskCompleted) return true
    
    // 4. NEW: Change detection — compare with last checkpoint
    const lastCheckpoint = await loadLastCheckpoint(task.taskId)
    if (!lastCheckpoint) return false  // No previous checkpoint, skip
    
    const hasMeaningfulChange = detectSemanticDifference(
        task.apiConversationHistory.slice(-10),
        lastCheckpoint.conversationHistory.slice(-10)
    )
    
    // 5. NEW: Stuck task detection — N turns with no progress → auto-trigger for fresh analysis
    const isStuck = await detectStuckTask(task)
    
    return hasMeaningfulChange || isStuck
}

// Detect whether the conversation has changed meaningfully since last checkpoint
function detectSemanticDifference(currentMessages: MessageParam[], previousMessages: MessageParam[]): boolean {
    // Check for new tool calls (different files edited, different commands run)
    const currentTools = extractToolCalls(currentMessages)
    const previousTools = extractToolCalls(previousMessages)
    
    if (!arraysOverlap(currentTools, previousTools)) return true
    
    // Check for status changes in todo list
    const currentTodos = extractTodoStatus(currentMessages)
    const previousTodos = extractTodoStatus(previousMessages)
    if (todosChanged(currentTodos, previousTodos)) return true
    
    // Check for new errors or blockers mentioned
    if (hasNewErrors(currentMessages, previousMessages)) return true
    
    return false  // No meaningful change — skip checkpoint
}

// Detect when a task seems stuck (no progress for N turns)
async function detectStuckTask(task: Task): Promise<boolean> {
    const recentMessages = task.apiConversationHistory.slice(-20)
    
    // Check if last N messages are all the same tool call pattern
    const toolCallPattern = extractToolCallPatterns(recentMessages)
    const isRepetitive = isPatternRepeating(toolCallPattern, threshold: 5)
    
    // Check if todo status hasn't changed in recent turns
    const todos = extractTodoStatus(recentMessages)
    const hasNoProgress = todos.every(t => t.status === 'pending' || t.status === 'in_progress')
    
    return isRepetitive && hasNoProgress
}

When change detection kicks in (non-depth-based scenarios):

Scenario Depth Trigger? Change Detection Result Checkpoint Created?
Subtask just started, first few turns No (depth < maxDepth-1) First run → no previous checkpoint ❌ Skip
Subtask running 20 turns, same file edits No detectSemanticDifference = false ❌ Skip — avoids spam
Subtask running 20 turns, new files edited No detectSemanticDifference = true ✅ Create — meaningful change
Task stuck (repetitive tool calls, no progress) No detectStuckTask = true ✅ Create — fresh analysis needed
Depth reaches maxDepth - 1 Yes Always fires regardless of changes ✅ Create — safety net

VSCode settings for change detection:

{
    "zooCode.checkpointChangeDetection.enabled": true,      // Enable semantic diff check
    "zooCode.checkpointChangeDetection.minTurnsBeforeCheck": 5,  // Don't check until N turns after last checkpoint
    "zooCode.checkpointStuckDetection.turnThreshold": 10,     // Flag as stuck after 10 turns with no progress
    "zooCode.checkpointStuckDetection.forceCheckpoint": true, // Auto-create checkpoint when stuck
}

Why this matters: Without change detection, a long-running subtask that's iterating on the same file (e.g., fixing bugs in a function) would create checkpoints every time depth-based conditions are met — even though nothing meaningful changed. Change detection ensures checkpoints are only created when there's actually new information to preserve, reducing token waste and keeping the checkpoint history clean and useful.

Relationship to Existing Checkpoint System (Shadow Git)

Aspect Existing Checkpoint (shadow git) Task Tree Checkpoint (conversation)
What it snapshots Workspace files Conversation history + AI reasoning
Storage Shadow git repo per task In-memory + persisted checkpoint records
Restore mechanism git reset --hard to commit hash Restore conversation history from saved snapshot
User interaction Diff/restore workspace changes at specific point Review compressed context, adjust suggestions, choose compression level
Purpose Recover from bad file edits Manage context size, get systematic next moves, continue with clean focus
Integration Independent feature Integrated into Task Tree flow (auto-trigger on nesting depth)

Both can coexist: shadow git for workspace recovery, conversation checkpoint for context management. They serve different purposes and don't conflict.

AI-Native Tools for Shadow Git Checkpoints (New)

Problem: The existing shadow git checkpoint system (src/core/checkpoints/) creates git commits per task, but the AI has to interact with it using raw git CLI commands. Weak models struggle with this:

  • Don't know which commit hash to restore from
  • May use wrong flags (--hard vs --soft vs --mixed)
  • Can accidentally restore wrong files or overwrite current work
  • No way to preview what will change before restoring

Solution: Provide AI-native tools that wrap shadow git operations, so the AI can safely interact with checkpoints without needing deep git knowledge:

// Native tools for AI to interact with shadow git checkpoints
interface ShadowGitCheckpointTools {
    // List available checkpoints for current task (with file diff preview)
    list_available_checkpoints(): CheckpointListResult
    
    // Restore workspace files from a specific checkpoint
    restore_checkpoint_files(checkpointId: string, options?: {
        mode: 'hard' | 'soft' | 'mixed',  // git reset --hard/--soft/--mixed
        targetFiles?: string[]             // Only restore specific files (not full reset)
        dryRun?: boolean                   // Preview changes before applying
    }): RestoreResult
    
    // Compare current workspace with a checkpoint (what would change on restore)
    diff_with_checkpoint(checkpointId: string): FileDiffResult
    
    // Create a new shadow git checkpoint manually (in addition to auto-triggers)
    create_shadow_git_checkpoint(message?: string): CheckpointInfo
    
    // Get status of current task's shadow git repo
    get_shadow_git_status(): ShadowGitStatus
}

interface CheckpointListResult {
    checkpoints: Array<{
        id: string              // Commit hash
        timestamp: number       // When created
        message: string         // Git commit message (auto-generated from task state)
        fileCount: number       // How many files changed in this checkpoint
        diffPreview?: FileDiffResult  // Preview of what changed (if dryRun=true)
    }>
}

interface RestoreResult {
    success: boolean
    restoredFiles: string[]     // Files that were restored
    unchangedFiles: string[]    // Files not affected by restore
    modeUsed: 'hard' | 'soft' | 'mixed'
    message?: string            // Error or info message
}

interface FileDiffResult {
    added: string[]           // Files added since checkpoint
    modified: Array<{         // Files modified (with diff summary)
        path: string
        oldContentPreview: string  // First 200 chars of original
        newContentPreview: string  // First 200 chars of current
        hunks?: number             // Number of changed sections
    }>
    deleted: string[]         // Files deleted since checkpoint
}

interface ShadowGitStatus {
    hasShadowRepo: boolean    // Does this task have a shadow git repo?
    lastCheckpointId?: string // Most recent checkpoint commit hash
    uncommittedChanges: number  // Number of unstaged changes
    branchName: string        // Current branch in shadow repo
}

How the AI uses these tools (practical examples):

// Example 1: AI detects bad file edits, wants to restore from checkpoint
async function recoverFromBadEdits(): Promise<void> {
    // Step 1: List available checkpoints with diff preview
    const checkpoints = await list_available_checkpoints()
    
    // Step 2: Show user the most recent checkpoint (before bad edits)
    const bestCheckpoint = checkpoints.checkpoints[checkpoints.checkpoints.length - 1]
    
    // Step 3: Dry run to see what will change
    const diff = await diff_with_checkpoint(bestCheckpoint.id, { dryRun: true })
    
    // Step 4: If user approves (or auto-approve for critical files), restore
    await restore_checkpoint_files(bestCheckpoint.id, {
        mode: 'hard',
        targetFiles: diff.modified.map(f => f.path)  // Only restore modified files
    })
}

// Example 2: AI wants to check shadow git status before checkpointing
async function prepareForCheckpoint(): Promise<void> {
    const status = await get_shadow_git_status()
    
    if (!status.hasShadowRepo) {
        // Create one if it doesn't exist (first time use)
        await create_shadow_git_checkpoint('Initial workspace state')
    }
    
    // Now safe to proceed — shadow git is ready for checkpointing
}

// Example 3: Weak model scenario — AI uses simple, guided restore
async function weakModelSafeRestore(): Promise<void> {
    // Step 1: Get list of checkpoints (AI just sees IDs and timestamps)
    const checkpoints = await list_available_checkpoints()
    
    // Step 2: Pick the most recent one (simple logic, no git knowledge needed)
    const latestId = checkpoints.checkpoints[checkpoints.checkpoints.length - 1].id
    
    // Step 3: Restore with default mode (--mixed keeps working tree changes staged)
    await restore_checkpoint_files(latestId, { mode: 'mixed' })
    
    // No need to know commit hashes, git flags, or branch names
}

VSCode settings for shadow git checkpoint tools:

{
    "zooCode.shadowGit.autoCreateOnFirstEdit": true,   // Create shadow repo on first file edit
    "zooCode.shadowGit.defaultRestoreMode": "mixed",    // Default: --mixed (keeps changes staged)
    "zooCode.shadowGit.showDiffPreviewBeforeRestore": true,  // Show diff before applying restore
    "zooCode.shadowGit.maxCheckpointsToKeep": 10,       // Limit checkpoint history to avoid bloat
}

Why this matters for weak models:

Scenario Without Native Tools (raw git CLI) With Native Tools
Restore from checkpoint AI must know: commit hash, git reset --hard <hash>, correct branch AI calls restore_checkpoint_files(id) — no git knowledge needed
Preview changes AI runs git diff HEAD~1 (may not understand output format) AI calls diff_with_checkpoint(id) → structured JSON with file previews
Selective restore AI must know: git checkout <hash> -- file1 file2 syntax AI passes targetFiles: ['file1', 'file2'] — simple array
Safe recovery AI might use --hard and lose current work accidentally Default mode is --mixed (keeps changes staged, safer)
Status check AI runs git status, parses messy output AI calls get_shadow_git_status() → clean structured data

Integration with conversation checkpoint system:

Shadow git checkpoints and conversation checkpoints serve different purposes but can work together:

// When a conversation checkpoint is created, also create shadow git snapshot
async function createConversationCheckpoint(task: Task): Promise<ConversationCheckpoint> {
    // 1. Save conversation history (existing logic)
    const conversationCheckpoint = await saveConversationSnapshot(task)
    
    // 2. Also create shadow git checkpoint for workspace state
    if (config.zooCode.shadowGit.autoCreateOnFirstEdit) {
        const gitStatus = await get_shadow_git_status()
        if (gitStatus.uncommittedChanges > 0) {
            await create_shadow_git_checkpoint(
                `Checkpoint: ${conversationCheckpoint.id}`
            )
        }
    }
    
    return conversationCheckpoint
}

// When restoring from a checkpoint, restore BOTH conversation AND workspace files
async function restoreFullCheckpoint(checkpointId: string): Promise<void> {
    // 1. Restore conversation history (existing logic)
    await loadConversationSnapshot(checkpointId)
    
    // 2. Find corresponding shadow git commit and restore workspace
    const gitStatus = await get_shadow_git_status()
    if (gitStatus.lastCheckpointId === checkpointId) {
        await restore_checkpoint_files(gitStatus.lastCheckpointId, { mode: 'mixed' })
    }
}

This means users get complete recovery — both conversation context AND workspace files are restored together. The AI doesn't need to coordinate between the two systems manually; it just calls restore_full_checkpoint(id) and everything is restored consistently.

Key Difference from #854

#854's original intent was "review history and decide next move." This design fulfills that by:

  1. Compressing the full conversation (not just viewing raw history)
  2. Feeding compressed context to AI for systematic analysis (generating structured suggestions, not just showing history)
  3. Giving user control over compression level and which suggestions to follow
  4. Creating inline task with clean context — focused on selected next moves, no need to carry full conversation

This addresses the real pain point: users don't want to manually read through hundreds of messages to figure out what to do next. They want AI to analyze progress systematically and suggest clear next steps, while giving them control over how much context is preserved (important for models that struggle with long contexts).

5. Context Management (supersedes #849)

AI-Aware Context Tools (Key Addition)

Problem: Current design has compress_context() as a manual tool, but the AI doesn't know:

  • How much context it currently has (token count)
  • What the model's context limit is
  • Whether compression is needed or when to trigger it

Solution: Provide tools that let the AI itself manage context, not just the user:

// Tools for AI to understand and manage its own context:
interface ContextAwareTools {
    // READ: Get current context state (for AI decision making)
    get_context_status(): {
        currentTokens: number           // How many tokens currently in context
        modelContextLimit: number       // Max tokens the model supports (e.g., 128K, 200K)
        usagePercent: number            // currentTokens / modelContextLimit * 100
        autoCompressRate?: number       // Configured compression rate from zoo_code config
        estimatedCompressionRatio?: {   // Expected reduction with each strategy
            minimal: number              // e.g., 0.7 (30% reduction)
            balanced: number             // e.g., 0.4 (60% reduction)
            aggressive: number           // e.g., 0.2 (80% reduction)
        }
    }
    
    // WRITE: Compress context with strategy chosen by AI (or user override)
    compress_context(strategy: "minimal" | "balanced" | "aggressive"): {
        tokensBefore: number
        tokensAfter: number
        compressionRatio: number
        preservedInfo: string[]          // What info was kept
        discardedInfo: string[]          // What info was removed (for transparency)
    }
    
    // READ: View compressed history in structured format
    view_compressed_history(): CompressedHistoryView
    
    // WRITE: Restore to a checkpoint
    restore_checkpoint(checkpointId: string): void
}

// Zoo Code config for auto-compression behavior:
interface ZooCodeContextConfig {
    autoCompressRate?: number           // e.g., 0.8 = compress when context > 80% of limit
    autoCompressStrategy?: "minimal" | "balanced" | "aggressive"  // Default strategy
    preserveReasoningChain: boolean     // Keep AI's reasoning intact during compression
    maxContextTokens?: number           // Override model default (e.g., force 64K for weak models)
}

How AI Uses These Tools (Autonomous Context Management)

// Example: AI decides when to compress based on context status
async function aiDecidesToCompress(): Promise<boolean> {
    const status = await get_context_status()
    
    // Check if compression is needed (configurable threshold from zoo_code config)
    const autoCompressRate = config.zooCode.autoCompressRate ?? 0.8  // Default: compress at 80%
    if (status.usagePercent < autoCompressRate * 100) {
        return false  // Not yet time to compress
    }
    
    // Choose strategy based on current situation
    let strategy: "minimal" | "balanced" | "aggressive" = "balanced"
    
    if (status.usagePercent > 95) {
        strategy = "aggressive"  // Critical — need maximum reduction
    } else if (status.currentTokens < 20000) {
        strategy = "minimal"     // Small context, preserve as much as possible
    }
    
    // Compress and report results
    const result = await compress_context(strategy)
    console.log(`Compressed: ${result.tokensBefore}${result.tokensAfter} (${(result.compressionRatio * 100).toFixed(0)}% remaining)`)
    
    return true
}

// AI can also proactively check before delegating to subtask:
async function delegateToSubtask(message: string): Promise<void> {
    // Check context BEFORE creating new task (new task adds more tokens)
    const status = await get_context_status()
    
    if (status.usagePercent > 70) {
        // Compress first to make room for delegation overhead
        await compress_context("balanced")
    }
    
    // Now safe to delegate — context has room
    await new_task({ message, mode: "sequential" })
}

Manual Control Tools (from #849, now integrated)

These tools are still available for user-initiated actions:

interface ContextTools {
    // User can manually trigger compression with specific strategy
    compress_context(strategy: "minimal" | "balanced" | "aggressive"): void
    
    // View what's currently in context (for debugging/inspection)
    view_compressed_history(): CompressedHistoryView
    
    // Restore to a checkpoint
    restore_checkpoint(checkpointId: string): void
}

// User can also override AI's compression choice:
// 1. "Use minimal compression" → AI uses "minimal" instead of auto-chosen strategy
// 2. "Compress now" → Triggers immediate compression (AI decides when to do it)
// 3. "Show me context usage" → Calls get_context_status() for user inspection

Tree-Aware Context Pruning

When parent task needs context from completed children:

// When a subtask completes:
async function onSubtaskComplete(childTask: Task, parentTask: Task): Promise<void> {
    // 1. Compress child's conversation history (AI decides strategy based on context status)
    const compressed = await compressConversationHistory(
        childTask.apiConversationHistory, 
        "balanced"  // Default for completed subtasks
    )
    
    // 2. Inject summary into parent (not full history!)
    const summary = formatCompressionSummary(compressed)
    await parentTask.say({ type: "text", text: `Subtask completed:\n${summary}` })
    
    // 3. Remove child's full history from active context
    //    (keep only in storage for reference)
}

// When parent task needs context:
async function getContextForTask(task: Task): Promise<Anthropic.Messages.MessageParam[]> {
    // 1. Include own conversation history (full)
    const ownHistory = task.apiConversationHistory
    
    // 2. Include summaries of completed siblings (compressed)
    const siblingSummaries = await Promise.all(
        task.siblingIds.map(id => loadCompressedSummary(id))
    )
    
    // 3. Include summary of parent's context (not full history!)
    const parentContext = await loadParentContext(task.parentTaskId)
    
    return [...parentContext, ...ownHistory, ...siblingSummaries]
}

Integration with Checkpoint System

Checkpoint uses these same tools:

// When checkpoint is triggered (auto or manual):
async function createConversationCheckpoint(task: Task): Promise<ConversationCheckpoint> {
    // 1. Save full history first (before compression)
    const fullHistory = [...task.apiConversationHistory]
    
    // 2. Get context status to inform compression strategy
    const status = await get_context_status()
    const compressionStrategy = task.depth >= task.maxDepth - 1 ? "balanced" : "minimal"
    
    // 3. Compress using #849 tools (AI can override if needed)
    const compressed = await compressContext(task, compressionStrategy)
    
    // 4. Feed to AI for analysis — generate next move suggestions
    const suggestedMoves = await analyzeCompressedContext(compressed, fullHistory)
    
    return {
        id: crypto.randomUUID(),
        taskId: task.taskId,
        timestamp: Date.now(),
        fullHistory,
        compressedSummary: compressed.summary,
        suggestedNextMoves: suggestedMoves,
    }
}

VSCode Settings for Context Management

{
    // Auto-compression triggers
    "zooCode.autoCompressRate": 0.8,              // Compress when context > 80% of limit
    "zooCode.autoCompressStrategy": "balanced",   // Default strategy (minimal/balanced/aggressive)
    
    // AI autonomy
    "zooCode.allowAIContextManagement": true,     // Let AI decide when/how to compress
    "zooCode.showCompressionStats": true,         // Show token reduction stats after compression
    
    // Model-specific overrides
    "zooCode.maxContextTokens": undefined,        // Override model default (e.g., 64000 for weak models)
    
    // Checkpoint integration
    "zooCode.autoCheckpointOnNesting": true,      // Auto-trigger at maxDepth - 1
    "zooCode.showSuggestionsBeforeInline": true,   // Show AI suggestions before inline task starts
}

Why This Matters for Weak Models

Some models struggle with long conversation contexts (e.g., older models, smaller models). These tools help:

Scenario How Tools Help
Model has small context limit User sets maxContextTokens to match model's actual capacity
Model loses info in long context AI monitors usagePercent and compresses proactively before degradation
User wants more control Can manually choose "minimal" compression if model handles short context well but needs dense information
AI needs to decide when to compress get_context_status() gives AI the data it needs (current tokens, limit, usage %)

This addresses the real pain point: users don't want to manually track token counts or guess when context is getting too long. The AI can monitor its own context and compress automatically based on configurable thresholds — while still giving users control over strategy and compression level.

Cross-Model Context Analysis (New)

Problem: Some models are weak at understanding their own progress and generating good next move suggestions, even with compressed context. They might miss important details or generate poor recommendations.

Solution: Let the current model compress its context and delegate analysis to a different model provider. The target model returns better structured suggestions that the original model can follow.

// Tools for cross-model context analysis:
interface CrossModelAnalysisTools {
    // Current model generates compression prompt based on its understanding
    generate_compression_prompt(): CompressionExportData
    
    // Delegate compressed context to another model provider for analysis
    delegate_context_analysis(
        targetProvider: string,      // e.g., "anthropic", "openai"
        targetModel: string,         // e.g., "claude-sonnet-4", "gpt-4o"
        exportData: CompressionExportData,
        analysisConfig?: {
            maxTokens?: number       // How many tokens the target model can use
            focusAreas?: ("progress" | "decisions" | "errors" | "all")[]  // What to emphasize
            outputFormat?: "structured" | "narrative"  // Return format preference
        }
    ): AnalysisResult
    
    // Apply suggestions from external analysis back to current task
    apply_external_analysis(
        taskId: string,
        analysisId: string,         // Reference to the external analysis
        sourceProvider: string,     // Which provider generated this analysis
        confidenceThreshold?: number  // Only accept if confidence > threshold
    ): void
}

// Data structure for context export (what gets sent to target model)
interface CompressionExportData {
    taskId: string
    timestamp: number
    
    // What the current model thinks is important to preserve
    keyDecisions: string[]           // Major decisions made so far
    currentGoal: string              // What we're trying to achieve now
    blockers: string[]               // Any issues encountered
    pendingActions: string[]         // Actions still needed
    
    // Compression instructions for the target model
    compressionInstructions: {
        preserveReasoningChain: boolean  // Keep AI's thought process intact
        focusOn: "progress" | "decisions" | "errors" | "all"  // What to emphasize
        excludePatterns?: string[]       // e.g., ["file diffs", "tool outputs"]
    }
    
    // Metadata for the target model
    contextStats: {
        totalTokens: number
        messageCount: number
        toolCallsCount: number
        lastCheckpointHash?: string
    }
}

// Result from external analysis (what comes back)
interface AnalysisResult {
    analysisId: string              // Unique ID for this analysis session
    timestamp: number
    
    suggestedNextMoves: NextMoveSuggestion[]
    confidenceScore: number         // 0.0 - 1.0, how confident is the target model
    reasoningSummary: string        // Brief explanation of why these moves were chosen
    alternativeApproaches?: string[]  // Other valid strategies to consider
    
    // Metadata for tracking and auditing
    provider: string                // Which provider was used (e.g., "anthropic")
    model: string                   // Which model was used (e.g., "claude-sonnet-4")
    tokensUsed: number              // How many tokens the target model consumed
}

interface NextMoveSuggestion {
    id: string
    description: string             // What to do next
    priority: "high" | "medium" | "low"
    estimatedEffort: string         // e.g., "quick fix", "requires investigation"
    requiresContext?: string[]      // What context this move needs
    reasoning: string               // Why this is the right next step (from target model)
}

How it works in practice:

// Example flow when AI decides to delegate analysis to another provider:
async function getBetterNextMoves(task: Task): Promise<void> {
    // 1. Current model generates compression prompt
    const exportData = await generate_compression_prompt()
    
    // 2. Compress context using current strategy
    const compressed = await compress_context("balanced")
    
    // 3. Delegate to different provider for analysis
    const analysisResult = await delegate_context_analysis(
        "anthropic",              // Target provider
        "claude-sonnet-4",        // Target model
        exportData,               // Compressed context data
        {                         // Analysis configuration
            maxTokens: 8000,
            focusAreas: ["progress", "decisions"],
            outputFormat: "structured"
        }
    )
    
    // 4. Check confidence threshold (configurable)
    if (analysisResult.confidenceScore < 0.7) {
        console.log("Target model not confident, falling back to local analysis")
        return  // Use current model's own suggestions instead
    }
    
    // 5. Apply external analysis results
    await apply_external_analysis(
        task.taskId,
        analysisResult.analysisId,
        "anthropic",
        0.7  // Minimum confidence threshold
    )
}

// User can also manually trigger cross-model analysis:
// "Ask another model to analyze my progress and suggest next steps"
// → AI calls generate_compression_prompt() + delegate_context_analysis()

VSCode settings for cross-model analysis:

{
    // Cross-model context analysis
    "zooCode.crossModelAnalysis.enabled": true,           // Enable feature
    "zooCode.crossModelAnalysis.defaultTargetProvider": "anthropic",  // Default target provider
    "zooCode.crossModelAnalysis.defaultTargetModel": "claude-sonnet-4",  // Default target model
    "zooCode.crossModelAnalysis.confidenceThreshold": 0.7,  // Min confidence to accept suggestions
    
    // When to auto-trigger cross-model analysis
    "zooCode.crossModelAnalysis.autoTriggerOnNesting": true,      // Trigger at maxDepth - 1
    "zooCode.crossModelAnalysis.autoTriggerOnHighComplexity": true,  // Trigger when context > 90% usage
    
    // Cost control (cross-model calls can be expensive)
    "zooCode.crossModelAnalysis.maxCallsPerSession": 5,     // Limit per task session
    "zooCode.crossModelAnalysis.showCostEstimate": true,    // Show estimated cost before calling
}

Use cases:

Scenario How it helps
Weak model doing complex task Current model generates compression prompt → target provider analyzes → better suggestions returned
User wants expert opinion User manually triggers "ask another model" → gets structured next moves from different reasoning capability
Checkpoint with high confidence At checkpoint trigger, auto-delegate to target provider if context is large and complex
Debugging stuck tasks When task seems stuck (no progress for N turns), delegate to target provider for fresh perspective

Cost considerations:

  • Cross-model calls are more expensive than local compression
  • AI should only use this when necessary (high complexity, low confidence in own suggestions)
  • User can set maxCallsPerSession to control costs
  • Target model's reasoning summary is included so user understands why those moves were suggested

Key difference from regular context compression:

  • Regular: Current model compresses its own context → analyzes it itself (fast, cheap, but may miss details)
  • Cross-model: Current model generates export data + compressed context → delegates to different provider → gets better analysis (slower, more expensive, but higher quality suggestions)

Both can coexist: use regular compression for routine tasks, cross-model for complex decisions or when the current model's confidence is low.


Unified UI: Task Tree View

Replace Tab Switching with Tree View

Current: Each subtask opens a new tab → hard to manage deep nesting

New: Single tree view showing all active tasks + status

┌─────────────────────────────────────┐
│  Task Tree                          │
├─────────────────────────────────────┤
│ 🟢 Main Task (depth=0)              │
│ ├─ 🟡 Subtask A (depth=1, running)  │
│ │   └─ ⏸️ Sub-subtask A.1 (paused)  │
│ ├─ 🔵 Subtask B (depth=1, pending)  │
│ └─ ✅ Subtask C (depth=1, completed)│
├─────────────────────────────────────┤
│ [Collapse All] [Expand All]         │
│ [Auto-Flatten: ON] [Max Depth: 2]   │
└─────────────────────────────────────┘

Features:

  • Click any node to view its conversation (inline, no tab switch)
  • Status indicators: 🟢 active, 🟡 running, ⏸️ paused, 🔵 pending, ✅ completed
  • Auto-flatten indicator when depth limit reached
  • Checkpoint buttons on each node

Implementation Requirements (For PR Stage)

The following requirements should be addressed during implementation but are not part of this design spec:

  • Task Tree PersistencechildIds, siblingIds, and depth fields must survive VSCode restart. Current persistence only saves individual task history; tree topology (parent → child relationships) needs to be persisted alongside each task so the tree structure can be reconstructed on reload.
  • Cancellation cascade — When a parent task is cancelled, decide whether active children are also cancelled or allowed to finish. This is a UX detail best handled in PR stage with user testing.
  • Circular reference protection — Add validation to prevent A → B → A cycles when modifying tree edges. Edge case that can be caught during code review.

Migration Path from Existing Issues

Existing Issue How Task Tree Replaces It
#849 (Context Control) Integrated into Phase 4 — auto-compression + manual tools
#850 (Inline Mode) Integrated into Phase 2 — executionMode: "inline" + auto-flatten
#854 (Checkpoint) Integrated into Phase 3 — checkpoint as tree node property

Recommendation: Close #849, #850, #854 as duplicates of this new issue. Reference them in the new issue for context.


Benefits Over Separate Issues

Aspect 3 Separate Issues Task Tree (Unified)
Nesting #850 only switches mode, no limit Auto-limit + auto-flatten
Context #849 manual compression, passive Auto-compression based on tree structure
Checkpoint #854 separate feature Tree node property, integrated with execution
UI Tab switching (#850) + tools (#849) Single tree view, no tab switching needed
Complexity 3 independent features, potential conflicts 1 abstraction, internal coordination
Performance Each feature manages context separately Tree-aware context pruning (shared optimization)

Risk Assessment

Low Risk Changes

  • Adding childIds, depth fields to Task interface (backward compatible)
  • Inline execution mode (no tab switch, same conversation flow)
  • Checkpoint as tree node property (doesn't change existing checkpoint logic)

Medium Risk Changes

  • Parallel execution (requires careful state management)
  • Auto-flatten logic (needs thorough testing with various workflows)
  • Tree-aware context compression (performance impact unknown)

Mitigation Strategies

  1. Feature flags: Each new feature has a VSCode setting to enable/disable
  2. Backward compat: Existing delegation flow unchanged unless user opts in
  3. Gradual rollout: Phase 1 → test → Phase 2 → test → etc.
  4. Monitoring: Track nesting depth, context size, checkpoint frequency

Manual Usage Flow & Permission Handling

When a user manually works with the AI (not auto-task), the delegation flow involves two permission checkpoints:

Current Flow (Tab Mode)

User: "Do X, then Y"
AI:   [calls new_task for X] → VSCode approval popup appears
      → User clicks YES → parent pauses, child opens in NEW TAB
      → Child works independently
      
      AI:   [subtask calls attempt_completion] → approval popup appears
            → User clicks YES → parent resumes with result injected as tool_result

Two permission points:

  • askApproval on new_task — user approves creating a new tab
  • askFinishSubTaskApproval on attempt_completion — user approves finishing the subtask and resuming parent

Both are VSCode notification popups. The existing flow is unchanged for sequential/tab mode.

New Flow: Auto-Flatten (Inline Mode)

User: "Do X, then Y" (depth=0)
AI:   [calls new_task for X] → depth=1 ✓ → user approves (same as current)
      → Child opens in NEW TAB
      
      AI:   [subtask calls new_task for X.1] → depth=2 ✓ → user approves (same as current)
            → Grandchild opens in NEW TAB
            
      AI:   [grandchild calls new_task again] → depth=3 > maxDepth(2)
            → AUTO-FLATTEN! Execute inline, no tab switch

Three Permission Changes Needed

1. Auto-Flatten Bypasses new_task Approval Popup

Problem: If AI calls new_task but runtime auto-flattens (depth > maxDepth), showing an approval popup that does nothing is confusing.

Solution: Check depth in NewTaskTool.ts BEFORE showing the approval popup:

// NewTaskTool.ts — NEW logic at line ~113
if (task.depth >= task.maxDepth && config.autoFlattenOnLimit) {
    // Execute inline, no approval popup needed
    await this.executeInline(task, unescapedMessage, mode)
    pushToolResult(`Executed inline (auto-flattened at depth ${task.depth})`)
    return
}

// Existing flow — unchanged for tab/sequential mode
const child = await provider.delegateParentAndOpenChild({ ... })

User experience: No popup appears. The tool result shows Executed inline (auto-flattened at depth 3). User sees the inline execution happen in the same conversation.

2. Inline Mode Bypasses askFinishSubTaskApproval

Problem: In inline mode, the subtask executes within the parent's conversation. The user already sees the result directly. If attempt_completion still triggers an approval popup, it creates double friction — user has already seen and accepted the result visually.

Solution: Check executionMode in AttemptCompletionTool.ts:

// AttemptCompletionTool.ts — NEW logic at line ~85
if (task.executionMode === "inline") {
    // Inject result directly, no approval popup needed
    parent.say({ type: "tool_result", text: `Subtask completed: ${result}` })
    await parent.resumeAfterDelegation()  // ← same state reset logic
    return
}

// Existing flow for tab mode — unchanged
if (task.parentTaskId) {
    const didApprove = await askFinishSubTaskApproval()
    if (!didApprove) return "denied"
    await provider.reopenParentFromDelegation(...)
}

User experience: Inline subtask completes → result appears in conversation → parent continues naturally. No popup interruption.

3. Parallel Mode: Multiple Children Completing Simultaneously (Future)

Problem: If parent spawns multiple children in parallel, they may call attempt_completion at the same time. Current code uses awaitingChildId: string (single child), which would cause a race condition.

Solution (Phase 2): Change state model to support multiple awaiting children:

// ClineProvider.ts — NEW fields for parallel mode
interface HistoryItem {
    awaitingChildrenIds: string[]   // ← replaces awaitingChildId
    completedChildrenIds: string[]  // ← track which ones finished
}

Note: This is a medium-risk change deferred to Phase 2. Sequential and inline modes are unaffected.

Summary of Permission Changes

Scenario new_task Approval askFinishSubTaskApproval Risk Level
Tab mode (existing) ✅ Show popup ✅ Show popup Unchanged
Sequential mode ✅ Show popup ✅ Show popup Unchanged
Inline mode (auto-flatten) Bypassed (no tab) Bypassed (user sees result) Low — just condition check
Parallel mode ✅ Show popup per child Race condition fix needed Medium — Phase 2

Key takeaway: The permission changes are all simple conditional checks (if depth >= maxDepth / if executionMode === "inline"). No new permission types, no new UI components. Existing tab/sequential flow is completely unchanged.


Appendix: Code Examples

Example 1: NewTaskTool with executionMode

// NEW: NewTaskTool.ts — add executionMode parameter
interface NewTaskParams {
    mode: string
    message: string
    todos?: string
    executionMode?: "sequential" | "parallel" | "inline"  // ← NEW
}

async execute(params: NewTaskParams, task: Task, callbacks: ToolCallbacks): Promise<void> {
    const { mode, message, todos, executionMode = "sequential" } = params
    
    if (executionMode === "inline") {
        // Execute inline — no tab switch
        await this.executeInline(task, message, mode)
    } else {
        // Existing delegation flow (unchanged)
        const child = await provider.delegateParentAndOpenChild({
            parentTaskId: task.taskId,
            message,
            initialTodos: todos,
            mode,
            executionMode,  // ← pass to child
        })
    }
}

// NEW: Inline execution (no tab switch)
async executeInline(parent: Task, message: string, mode: string): Promise<void> {
    // Create virtual subtask in same conversation
    const result = await this.runSubtaskInConversation(parent, message, mode)
    
    // Inject result as tool_result (same flow as resumeAfterDelegation)
    parent.say({ type: "tool_result", text: `Subtask completed: ${result}` })
}

Example 2: Auto-Flatten in delegateParentAndOpenChild

// NEW: ClineProvider.ts — auto-flatten logic
public async delegateParentAndOpenChild(params): Promise<Task> {
    const parent = this.getCurrentTask()
    
    // Check if we should auto-flatten
    if (parent.depth >= parent.maxDepth && config.autoFlattenOnLimit) {
        // Execute inline instead of creating new tab
        await this.executeInlineSubtask(parent, params.message, params.mode)
        return null  // No new task created
    }
    
    // Existing flow (unchanged) — create child with incremented depth
    const child = await this.createTask(params.message, undefined, parent as any, {
        initialTodos: params.initialTodos,
        mode: params.mode,
        startTask: false,
        depth: parent.depth + 1,  // ← NEW: track depth
    })
    
    // ... rest of existing flow
}

Example 3: Tree-Aware Context Loading

// NEW: Task.ts — tree-aware context loading
private async getContextForApiCall(): Promise<Anthropic.Messages.MessageParam[]> {
    const ownHistory = this.apiConversationHistory
    
    if (!this.parentTaskId) {
        return ownHistory  // Root task — full history
    }
    
    // Load parent's compressed context (not full history!)
    const parentContext = await this.loadCompressedParentContext(this.parentTaskId)
    
    // Include summaries of completed siblings
    const siblingSummaries = await Promise.all(
        this.siblingIds
            .filter(id => this.isCompleted(id))
            .map(id => this.loadCompressedSummary(id))
    )
    
    return [...parentContext, ...ownHistory, ...siblingSummaries]
}

Conclusion

Task Tree abstraction solves ALL THREE problems (#849, #850, #854) with ONE unified design:

  1. Tree structure replaces linear chain → enables parallel + sequential execution at task level (complements, not replaces, [Tracking] Parallelizable Tasks — Implementation Roadmap #355's tool-level parallelism)
  2. Auto-nesting limit prevents infinite tab growth → enforced inline mode when depth exceeded (no manual decision needed)
  3. Checkpoint as tree node property integrates with execution flow instead of being a separate feature
  4. Tree-aware context compression optimizes memory usage based on structure

This is primarily a data model + delegation flow change, not just a UI change. The existing data model ALREADY supports this (childIds array, parentTaskId), we just need to use it properly.

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