# Task Execution Pipeline Deep Dive

This chapter dissects Claude Code's background task management system—how seven distinct task types share a unified lifecycle model while each handling different execution semantics, from local Shell commands to remote cloud Agents.

## Overview

Claude Code's task system manages all background work, from local Shell commands to remote cloud Agents. The seven task types share a unified lifecycle model—register, run, complete/fail, notify, cleanup—while each handles radically different execution semantics.

**Technical Analogy (OS Perspective)**: The task pipeline is like an operating system's **process manager**—responsible for registration, lifecycle tracking, and signal delivery—rather than a **scheduler** that decides "who runs first" (Claude Code's task system has no time slicing, priority queues, or preemption). Each task type is a process category (kernel thread, user process, remote RPC); they share the same process state machine (ready → running → terminated → zombie → reaped), but their internal resource management and signal handling differ. The `isBackgrounded` flag is analogous to Unix foreground/background process group switching (`fg`/`bg`).

> 💡 **Plain English**: The task pipeline is like a logistics hub—local tasks are same-day courier deliveries (instant, controllable, direct communication), remote tasks are cross-city freight (delayed, needs tracking numbers, packages may get lost), and Dream tasks are overnight warehouse inventory runs (unattended, automatic, no signed receipt required). `LocalMainSessionTask` is like ordering a slow-braised dish at a restaurant; the chef says it'll take 30 minutes—you reply, "Serve the other dishes first, let me know when that one's ready." The braised dish keeps cooking in the kitchen (running in the background), you eat the other courses (new prompt), and the waiter notifies you when it's done (notified).

### 🌍 Industry Context

Background task management is the critical capability that transforms AI coding tools from "single-turn conversation" into "continuous work partner." Tools vary wildly in their support:

- **Cursor**: After launching Background Agents, it now supports background tasks—Agents running in cloud VMs can execute in parallel for extended periods, monitored through a background panel. However, its background model is "cloud VM lifecycle" rather than Claude Code's "local process foreground/background switching," with significant differences in task management granularity.
- **Kimi Code**: Under its Agent Swarm architecture, the coordinator can manage up to 100 concurrent sub-agent lifecycles simultaneously, far exceeding other tools in parallel task management scale. However, its task model is oriented toward cloud cluster scheduling rather than the terminal user's interactive foreground/background switching.
- **CodeX (OpenAI)**: v0.118.0 supports parallel Agent workflows and an email communication mechanism, enabling unattended fixes connected to CI/CD event streams, giving it some background task capability.
- **Aider**: Still fundamentally a synchronous model—only one operation can execute at a time, with no background tasks and no foreground/background switching. AiderDesk adds some concurrency through the Vercel AI SDK.
- **Devin**: After launching Manage Devins, it supports running sub-agents concurrently in multiple isolated VMs (Managed Devins), monitored in real time through a Human-in-the-loop workbench. Users view each sub-agent's screen via a dedicated console and can directly intervene in the execution flow.
- **GitHub Copilot**: Agent Mode is now fully GA, with built-in dedicated agents like Explore, Plan, and Task, supporting autonomous intent interpretation and iterative code fixes. An enterprise-grade MCP registry mechanism underpins integration with asynchronous task flows.

Claude Code's task system ranks among the most mature in terminal tools—seven task types, a unified lifecycle model, foreground/background switching, and remote task polling recovery. This level of complexity is typically only seen in IDE-grade products; achieving it in a CLI tool is uncommon in the industry.

---

## 1. Task Type Panorama

### 1.1 Union of Types

The system defines seven concrete task state types:

```typescript
// Source: src/tasks/types.ts (lines 12-19)
export type TaskState =
  | LocalShellTaskState       // Local Shell command
  | LocalAgentTaskState       // Local Agent subtask
  | RemoteAgentTaskState      // Remote cloud Agent
  | InProcessTeammateTaskState // In-process teammate
  | LocalWorkflowTaskState    // Local workflow
  | MonitorMcpTaskState       // MCP monitoring task
  | DreamTaskState            // Memory consolidation Dream task
```

Each type extends `TaskStateBase`, sharing foundational fields while carrying type-specific state data.

### 1.2 Background Task Determination

Whether a task appears in the bottom-bar indicator is decided by a unified type guard:

```typescript
// Source: src/tasks/types.ts (lines 37-46)
export function isBackgroundTask(task: TaskState): task is BackgroundTaskState {
  if (task.status !== 'running' && task.status !== 'pending') {
    return false
  }
  // Foreground tasks (isBackgrounded === false) are NOT "background tasks"
  if ('isBackgrounded' in task && task.isBackgrounded === false) {
    return false
  }
  return true
}
```

Key semantics: a task is only a background task when it is "running or pending" AND "has been backgrounded." Foreground-running tasks (which the user is actively observing) do not appear in the background indicator.

> 📚 **Course Connection**: The task state model (pending → running → completed/failed/killed → notified → eviction) directly maps to the **process state machine** in Operating Systems (ready → running → terminated → zombie → reaped). The `isBackgrounded` flag is equivalent to the Unix foreground/background process group concept (`fg`/`bg` commands), while the `notified` flag acts like the `wait()` system call reaping zombie processes—only after the parent process acknowledges receiving the exit status can resources be reclaimed.

---

## 2. LocalShellTask — Local Shell Command

### 2.1 State Definition

```typescript
// Source: src/tasks/LocalShellTask/guards.ts (lines 9-32)
export type LocalShellTaskState = TaskStateBase & {
  type: 'local_bash'
  command: string                    // Command being executed
  result?: { code: number; interrupted: boolean }  // Exit code and interrupt flag
  completionStatusSentInAttachment: boolean
  shellCommand: ShellCommand | null  // Underlying Shell process handle
  unregisterCleanup?: () => void
  cleanupTimeoutId?: NodeJS.Timeout
  lastReportedTotalLines: number     // Used to calculate output delta
  isBackgrounded: boolean
  agentId?: AgentId                  // Agent that spawned this task
  kind?: BashTaskKind                // 'bash' | 'monitor'
}
```

Note `type: 'local_bash'` rather than `local_shell`—source comments explain this is for **backward compatibility** with persisted session states. Renaming it would break task type recognition when restoring old sessions.

### 2.2 Task Variants

The `kind` field distinguishes two purposes:
- `'bash'`: Standard Shell command execution
- `'monitor'`: Long-running monitoring commands (e.g., `tail -f`, `watch`)

Monitoring tasks display a description rather than the command in the UI, use a "Monitor details" dialog title, and have a distinct status-bar badge.

### 2.3 Agent Association

The `agentId` field tracks which Agent spawned this Shell task. When an Agent exits, its orphaned Shell tasks are automatically terminated (`killShellTasksForAgent`), preventing resource leaks. `undefined` means the task was created by the main thread.

---

## 3. LocalAgentTask — Local Agent Subtask

### 3.1 State Definition

```typescript
// Source: src/tasks/LocalAgentTask/LocalAgentTask.tsx (lines 116-148)
export type LocalAgentTaskState = TaskStateBase & {
  type: 'local_agent'
  agentId: string
  prompt: string
  selectedAgent?: AgentDefinition
  agentType: string
  model?: string
  abortController?: AbortController
  error?: string
  result?: AgentToolResult
  progress?: AgentProgress
  retrieved: boolean
  messages?: Message[]
  lastReportedToolCount: number
  lastReportedTokenCount: number
  isBackgrounded: boolean
  pendingMessages: string[]     // SendMessage queued messages
  retain: boolean               // Whether UI is holding this task
  diskLoaded: boolean           // Whether full history has been loaded from disk
  evictAfter?: number           // Eviction deadline timestamp
}
```

### 3.2 Progress Tracking

Agent progress tracks token consumption precisely through `ProgressTracker`:

```typescript
// Source: src/tasks/LocalAgentTask/LocalAgentTask.tsx (lines 50-60)
export type ProgressTracker = {
  toolUseCount: number
  latestInputTokens: number        // Latest value (cumulative in API)
  cumulativeOutputTokens: number   // Accumulated value
  recentActivities: ToolActivity[]
}

export function getTokenCountFromTracker(tracker: ProgressTracker): number {
  return tracker.latestInputTokens + tracker.cumulativeOutputTokens
}
```

Critical detail: In the Claude API, `input_tokens` is a per-turn cumulative value (including all historical context), while `output_tokens` is a per-turn independent value. Therefore, input takes the latest value (`latestInputTokens`), and output is accumulated (`cumulativeOutputTokens`). Accumulating both would severely overcount input tokens.

### 3.3 Activity Descriptions

Each tool call generates a human-readable activity description:

```typescript
// Source: src/tasks/LocalAgentTask/LocalAgentTask.tsx (lines 23-31)
export type ToolActivity = {
  toolName: string
  input: Record<string, unknown>
  activityDescription?: string  // Pre-computed: "Reading src/foo.ts"
  isSearch?: boolean            // Pre-computed: Grep/Glob, etc.
  isRead?: boolean              // Pre-computed: Read/cat, etc.
}
```

Activity descriptions are pre-computed at record time (not at render time), fetched via `ActivityDescriptionResolver` querying the tool definition. This avoids redundant computation during rendering.

### 3.4 Panel Lifecycle

The three fields `retain`, `diskLoaded`, and `evictAfter` manage the precise UI panel lifecycle:

- `retain = true`: The user is viewing this task's panel, preventing eviction
- `diskLoaded`: Full history is loaded from disk JSONL files when the panel opens (one-time operation)
- `evictAfter`: Deadline timestamp set after task termination; GC can reclaim after expiration

This three-stage model ensures: open panel → restore history from disk → real-time append of streaming messages → close panel → delayed eviction.

---

## 4. RemoteAgentTask — Remote Cloud Agent

### 4.1 State Definition

```typescript
// Source: src/tasks/RemoteAgentTask/RemoteAgentTask.tsx (lines 22-59)
export type RemoteAgentTaskState = TaskStateBase & {
  type: 'remote_agent'
  remoteTaskType: RemoteTaskType
  remoteTaskMetadata?: RemoteTaskMetadata
  sessionId: string               // Remote session ID
  command: string
  title: string
  todoList: TodoList
  log: SDKMessage[]
  isLongRunning?: boolean         // Won't be marked complete after first result
  pollStartedAt: number           // Local poll start time
  isRemoteReview?: boolean        // Remote code review
  reviewProgress?: { stage?; bugsFound; bugsVerified; bugsRefuted }
  isUltraplan?: boolean           // Ultraplan mode
  ultraplanPhase?: 'needs_input' | 'plan_ready'
}
```

### 4.2 Remote Task Types

```typescript
const REMOTE_TASK_TYPES = [
  'remote-agent',     // General remote Agent
  'ultraplan',        // Ultra plan mode
  'ultrareview',      // Remote code review
  'autofix-pr',       // Auto-fix PR
  'background-pr',    // Background PR processing
] as const
```

The five remote task types correspond to different use cases, but share the same WebSocket polling and notification mechanism.

### 4.3 Completion Checkers

Remote tasks support registerable completion checkers, called on every poll tick:

```typescript
// Source: src/tasks/RemoteAgentTask/RemoteAgentTask.tsx (lines 77-86)
export type RemoteTaskCompletionChecker =
  (remoteTaskMetadata: RemoteTaskMetadata | undefined) => Promise<string | null>

const completionCheckers = new Map<RemoteTaskType, RemoteTaskCompletionChecker>()

export function registerCompletionChecker(
  remoteTaskType: RemoteTaskType,
  checker: RemoteTaskCompletionChecker,
): void {
  completionCheckers.set(remoteTaskType, checker)
}
```

Returning a non-null string means the task is complete (the string becomes the notification text); returning null means continue polling. Checkers should self-throttle, as they are invoked on every poll tick.

> 📚 **Course Connection**: The tradeoff between the remote task polling model (one-second HTTP GET + cursor pagination) versus the push model (WebSocket) is a classic **Pull vs Push** architecture decision in Computer Networks. Polling's advantages are statelessness and natural fault tolerance (aligned with HTTP idempotency semantics); pushing's advantage is lower latency. Claude Code chooses polling, essentially trading one second of latency for simpler error recovery—consistent with the distributed systems design philosophy of favoring eventual consistency over strong consistency.

### 4.4 Metadata Persistence

Remote task metadata is persisted to a session sidecar file, enabling `--resume` to restore tasks:

```typescript
// Source: src/tasks/RemoteAgentTask/RemoteAgentTask.tsx (lines 92-111)
async function persistRemoteAgentMetadata(meta: RemoteAgentMetadata): Promise<void> {
  try {
    await writeRemoteAgentMetadata(meta.taskId, meta)
  } catch (e) {
    logForDebugging(`persistRemoteAgentMetadata failed: ${String(e)}`)
  }
}

// Cleanup when task completes/terminates
async function removeRemoteAgentMetadata(taskId: string): Promise<void> {
  try {
    await deleteRemoteAgentMetadata(taskId)
  } catch (e) {
    logForDebugging(`removeRemoteAgentMetadata failed: ${String(e)}`)
  }
}
```

Persistence is fire-and-forget—failures do not block task registration. This guarantees the reliability of the main flow.

### 4.5 Ultraplan Phases

Ultraplan tasks have additional phase tracking:

- `running` (default): Plan generation in progress
- `needs_input`: Remote Agent asks clarifying questions, waiting for user input
- `plan_ready`: ExitPlanMode waiting for browser approval

The UI bottom bar displays different diamond symbols and copy based on phase:

```typescript
// Source: src/tasks/pillLabel.ts (lines 42-52)
if (n === 1 && first.type === 'remote_agent' && first.isUltraplan) {
  switch (first.ultraplanPhase) {
    case 'plan_ready':   return `${DIAMOND_FILLED} ultraplan ready`
    case 'needs_input':  return `${DIAMOND_OPEN} ultraplan needs your input`
    default:             return `${DIAMOND_OPEN} ultraplan`
  }
}
```

`DIAMOND_FILLED` (◆) means completed and awaiting approval; `DIAMOND_OPEN` (◇) means still running or needs input.

---

## 5. InProcessTeammateTask — In-Process Teammate

### 5.1 Identity

Teammate tasks carry rich identity information:

```typescript
// Source: src/tasks/InProcessTeammateTask/types.ts (lines 14-21)
export type TeammateIdentity = {
  agentId: string      // "researcher@my-team"
  agentName: string    // "researcher"
  teamName: string
  color?: string
  planModeRequired: boolean
  parentSessionId: string  // Leader's session ID
}
```

`agentId` uses the `name@team` format, uniquely identifying a member within the team.

### 5.2 Message Capacity Control: A Data-Driven Memory Defense

This is the most engineering-insightful design decision in the chapter—a memory governance scheme driven by real production data.

```typescript
// Source: src/tasks/InProcessTeammateTask/types.ts (lines 96-121)
export const TEAMMATE_MESSAGES_UI_CAP = 50

export function appendCappedMessage<T>(prev: readonly T[] | undefined, item: T): T[] {
  if (prev === undefined || prev.length === 0) return [item]
  if (prev.length >= TEAMMATE_MESSAGES_UI_CAP) {
    const next = prev.slice(-(TEAMMATE_MESSAGES_UI_CAP - 1))
    next.push(item)
    return next
  }
  return [...prev, item]
}
```

**Discovery Process**. Source comments document the full decision context: the Anthropic team analyzed production telemetry via BigQuery (BQ) on 2026-03-20 and found that each Agent consumed roughly 20MB RSS memory during 500+ turn conversations. An extreme "whale session" launched 292 Agents within 2 minutes, spiking total memory to 36.8GB. The primary cost source was the `task.messages` field—it held a second full copy of every message for UI rendering (the complete history was already stored in the local `allMessages` array and on-disk JSONL files).

**Why 50?** From the user perspective, a task panel's UI viewport typically only displays the most recent handful of messages; 50 is enough to cover the typical depth users scroll back to review. From the memory perspective, the 50-message cap reduces each Agent's UI message memory from a potential 20MB+ down to a manageable range. For 292 Agents, the total UI message memory under the 50-message cap is roughly 1/10 of the original.

**Sliding Window Strategy**. `appendCappedMessage` implements a trailing sliding window—always keeping the latest 50, discarding the oldest when a new message arrives. The full conversation history remains unaffected, still saved in the on-disk JSONL file for later recovery.

**Engineering Culture Insight**. This case reveals an important engineering practice at Anthropic: production telemetry → BQ data analysis → quantitative decision-making. "50" is not a magic number pulled out of thin air, but an engineering tradeoff based on real memory profiling data. This data-driven resource governance approach is valuable reference for any system that needs to manage multi-agent concurrency.

### 5.3 Idle Notification

Teammates support an efficient idle-waiting mechanism:

```typescript
// Source: src/tasks/InProcessTeammateTask/types.ts (lines 70-72)
// Callbacks to notify when teammate becomes idle (runtime only)
// Used by leader to efficiently wait without polling
onIdleCallbacks?: Array<() => void>
```

When the leader Agent waits for a member to finish, it doesn't need to poll—it registers a callback and gets notified directly when the member becomes idle. This is far more efficient than checking status with `setInterval`.

### 5.4 Plan Mode Approval

Teammates can request plan-mode approval:

```typescript
awaitingPlanApproval: boolean   // Waiting for plan approval
permissionMode: PermissionMode  // Independent permission mode (can toggle while viewing)
```

Each teammate has an independent permission mode, cycled via `Shift+Tab` while viewing the teammate panel. This is more flexible than a global permission mode.

---

## 6. DreamTask — Memory Consolidation Task

### 6.1 Task Definition

Dream tasks are the most minimal task type, dedicated to automatic memory consolidation:

```typescript
// Source: src/tasks/DreamTask/DreamTask.ts (lines 25-41)
export type DreamTaskState = TaskStateBase & {
  type: 'dream'
  phase: DreamPhase              // 'starting' | 'updating'
  sessionsReviewing: number      // Number of sessions being reviewed
  filesTouched: string[]         // Modified file paths (incomplete)
  turns: DreamTurn[]             // Assistant text responses
  abortController?: AbortController
  priorMtime: number             // Saved to roll back lock on kill
}
```

### 6.2 Phase Detection

Dream's internal prompt has a 4-stage structure (orienting / gathering / consolidating / pruning), but the code does not parse these stages—it only distinguishes `starting` and `updating`:

```typescript
// Source: src/tasks/DreamTask/DreamTask.ts (lines 76-104)
export function addDreamTurn(taskId, turn, touchedPaths, setAppState): void {
  updateTaskState<DreamTaskState>(taskId, setAppState, task => {
    const seen = new Set(task.filesTouched)
    const newTouched = touchedPaths.filter(p => !seen.has(p) && seen.add(p))
    if (turn.text === '' && turn.toolUseCount === 0 && newTouched.length === 0) {
      return task  // Pure no-op skipped to avoid useless re-renders
    }
    return {
      ...task,
      phase: newTouched.length > 0 ? 'updating' : task.phase,  // First file edit → updating
      filesTouched: newTouched.length > 0 ? [...task.filesTouched, ...newTouched] : task.filesTouched,
      turns: task.turns.slice(-(MAX_TURNS - 1)).concat(turn),   // Keep last 30 turns
    }
  })
}
```

When an Edit/Write tool call touching a file is first detected, the phase flips from `starting` to `updating`.

### 6.3 Termination and Lock Rollback

Killing a Dream task requires rolling back the consolidation lock:

```typescript
// Source: src/tasks/DreamTask/DreamTask.ts (lines 136-157)
async kill(taskId, setAppState) {
  let priorMtime: number | undefined
  updateTaskState<DreamTaskState>(taskId, setAppState, task => {
    if (task.status !== 'running') return task
    task.abortController?.abort()
    priorMtime = task.priorMtime
    return { ...task, status: 'killed', endTime: Date.now(), notified: true }
  })
  if (priorMtime !== undefined) {
    await rollbackConsolidationLock(priorMtime)  // Roll back lock mtime so next session can retry
  }
}
```

`priorMtime` stores the lock file's modification time before consolidation began. Rolling it back on kill allows the next session to attempt consolidation again.

### 6.4 Notification Difference

A key difference between Dream tasks and other tasks is that **there is no model-facing notification path**:

```typescript
// Source: src/tasks/DreamTask/DreamTask.ts (lines 106-120)
export function completeDreamTask(taskId, setAppState): void {
  // notified: true is set immediately—dream has no model-facing notification path (pure UI),
  // and eviction requires terminal + notified. The inline appendSystemMessage prompt is the user-facing surface.
  updateTaskState<DreamTaskState>(taskId, setAppState, task => ({
    ...task,
    status: 'completed',
    endTime: Date.now(),
    notified: true,  // Mark as notified directly
    abortController: undefined,
  }))
}
```

---

## 7. LocalMainSessionTask — Backgrounding the Main Session

### 7.1 Concept

When the user presses `Ctrl+B` twice, the currently running main-session query is "backgrounded"—the query continues running in the background while the UI clears to a fresh prompt.

```typescript
// Source: src/tasks/LocalMainSessionTask.ts (lines 94-162)
export function registerMainSessionTask(
  description: string,
  setAppState: SetAppState,
  mainThreadAgentDefinition?: AgentDefinition,
  existingAbortController?: AbortController,
): { taskId: string; abortSignal: AbortSignal } {
  const taskId = generateMainSessionTaskId()  // 's' prefix distinguishes from Agent's 'a' prefix

  // Link output to an independent task transcript file (not using the main session's transcript path)
  void initTaskOutputAsSymlink(taskId, getAgentTranscriptPath(asAgentId(taskId)))

  // Reuse existing AbortController (critical: ensure aborting the task aborts the actual query)
  const abortController = existingAbortController ?? createAbortController()

  const taskState: LocalMainSessionTaskState = {
    ...createTaskStateBase(taskId, 'local_agent', description),
    agentType: 'main-session',  // Special type marker
    isBackgrounded: true,        // Backgrounded on creation
    // ...
  }
  registerTask(taskState, setAppState)
  return { taskId, abortSignal: abortController.signal }
}
```

### 7.2 ID Generation

Main session tasks use an `s` prefix (session), while Agent tasks use an `a` prefix:

```typescript
// Source: src/tasks/LocalMainSessionTask.ts (lines 73-82)
const TASK_ID_ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz'

function generateMainSessionTaskId(): string {
  const bytes = randomBytes(8)
  let id = 's'
  for (let i = 0; i < 8; i++) {
    id += TASK_ID_ALPHABET[bytes[i]! % TASK_ID_ALPHABET.length]
  }
  return id
}
```

### 7.3 Foregrounding

A background task can be brought back to the foreground:

```typescript
// Source: src/tasks/LocalMainSessionTask.ts (lines 270-302)
export function foregroundMainSessionTask(taskId, setAppState): Message[] | undefined {
  let taskMessages: Message[] | undefined
  setAppState(prev => {
    const task = prev.tasks[taskId]
    taskMessages = (task as LocalMainSessionTaskState).messages

    // If there was a previously foregrounded task, restore it to background
    const prevId = prev.foregroundedTaskId
    const restorePrev = prevId && prevId !== taskId && prevTask?.type === 'local_agent'

    return {
      ...prev,
      foregroundedTaskId: taskId,
      tasks: {
        ...prev.tasks,
        ...(restorePrev && { [prevId]: { ...prevTask, isBackgrounded: true } }),
        [taskId]: { ...task, isBackgrounded: false },
      },
    }
  })
  return taskMessages
}
```

When foregrounding a task, any previously foregrounded task is pushed back to the background—only one foreground task can exist at a time.

---

## 8. Unified Task Operations

### 8.1 Stopping Tasks

All task types share unified stop logic:

```typescript
// Source: src/tasks/stopTask.ts (lines 38-100)
export async function stopTask(taskId, context): Promise<StopTaskResult> {
  const task = appState.tasks?.[taskId]

  if (!task) throw new StopTaskError(`No task found with ID: ${taskId}`, 'not_found')
  if (task.status !== 'running') throw new StopTaskError(`Task not running`, 'not_running')

  const taskImpl = getTaskByType(task.type)
  if (!taskImpl) throw new StopTaskError(`Unsupported task type`, 'unsupported_type')

  await taskImpl.kill(taskId, setAppState)

  // Shell tasks: suppress "exit code 137" notification (noise)
  // Agent tasks: do not suppress—AbortError catch sends notification carrying extractPartialResult
  if (isLocalShellTask(task)) {
    setAppState(prev => {
      // Set notified flag to suppress notification
      return { ...prev, tasks: { ...prev.tasks, [taskId]: { ...prevTask, notified: true } } }
    })
    // Suppressing XML notification also suppresses SDK events in print.ts—emit directly
    if (suppressed) {
      emitTaskTerminatedSdk(taskId, 'stopped', { toolUseId: task.toolUseId, summary: task.description })
    }
  }
}
```

Post-kill notification handling differs between Shell and Agent tasks: Shell's exit code 137 (SIGKILL) is noise and should be suppressed, while Agent's partial results (`extractPartialResult`) are valuable information that should be preserved.

### 8.2 Bottom-Bar Badge

The bottom-bar copy is dynamically generated based on task type and count:

```typescript
// Source: src/tasks/pillLabel.ts (lines 12-67)
export function getPillLabel(tasks: BackgroundTaskState[]): string {
  const n = tasks.length
  const allSameType = tasks.every(t => t.type === tasks[0]!.type)

  if (allSameType) {
    switch (tasks[0]!.type) {
      case 'local_bash': {
        const monitors = count(tasks, t => t.type === 'local_bash' && t.kind === 'monitor')
        const shells = n - monitors
        // "2 shells, 1 monitor"
        return parts.join(', ')
      }
      case 'in_process_teammate': {
        const teamCount = new Set(tasks.map(t => t.identity.teamName)).size
        return teamCount === 1 ? '1 team' : `${teamCount} teams`
      }
      case 'remote_agent':
        return `${DIAMOND_OPEN} ${n} cloud sessions`
      case 'dream':
        return 'dreaming'
    }
  }
  return `${n} background ${n === 1 ? 'task' : 'tasks'}`
}
```

When all tasks share the same type, a specific label is shown (e.g., "2 shells, 1 monitor"); for mixed types, it falls back to a generic label ("3 background tasks").

---

## 9. Lifecycle State Flow

The unified lifecycle for all tasks:

```
                    register()
                       │
                       ▼
  ┌─────────┐     ┌─────────┐
  │ pending │────►│ running │
  └─────────┘     └────┬────┘
                       │
           ┌───────────┼───────────┐
           ▼           ▼           ▼
     ┌──────────┐ ┌─────────┐ ┌────────┐
     │completed │ │ failed  │ │ killed │
     └────┬─────┘ └────┬────┘ └───┬────┘
          │            │          │
          ▼            ▼          ▼
     ┌─────────────────────────────────┐
     │        notified = true          │  (Notification sent/suppressed)
     └──────────────┬──────────────────┘
                    │
                    ▼
     ┌─────────────────────────────────┐
     │          eviction / GC          │  (UI eviction + disk cleanup)
     └─────────────────────────────────┘
```

The `notified` flag is a prerequisite for eviction—ensuring that the notification has been sent or explicitly suppressed before task resources can be reclaimed.

---

## Critical Analysis

### Limitations

1. **Memory Pressure**: InProcessTeammateTask message history is a known memory bottleneck (292 Agents → 36.8GB). The 50-message cap is a stopgap, not a fundamental solution—the ideal architecture would be disk-primary with memory caching.

2. **No Priority Scheduling**: All background tasks compete for resources equally, with no priority mechanism. A Dream task (low priority) may compete for API quota with an Agent task explicitly launched by the user (high priority).

3. **Shell Task Type Naming**: The `local_bash` name is outdated (not limited to bash), but cannot be changed due to backward compatibility. A classic case of technical debt.

4. **Notification Deduplication Relies on Atomic Operations**: The `notified` flag's check-and-set achieves atomicity through `setAppState`'s functional update. This relies on React state update serialization guarantees. If the underlying scheduling model changes, race conditions could emerge.

### Design Tradeoffs

1. **Unified Union vs. Polymorphic Classes**: Uses TypeScript union types (`TaskState`) rather than OOP inheritance. The advantage is type safety and exhaustive checking; the disadvantage is that adding a new task type requires modifying the union definition and every switch statement.

2. **Foreground/Background Boolean vs. Priority Spectrum**: `isBackgrounded` is a boolean rather than a priority number. This is sufficient for the current UI model (one foreground + N background), but has limited extensibility.

3. **Dream's filesTouched Is Incomplete**: The source explicitly notes this is an incomplete observation—only capturing Edit/Write tool calls, missing writes from bash commands. This is an intentional tradeoff—full tracing would require strace-level monitoring, which is too costly.

4. **RemoteAgentTask Polling Model**: Remote tasks use polling (`pollRemoteSessionEvents`) rather than pure push. This is because WebSocket connections may drop; polling serves as a fallback ensuring eventual consistency. The cost is latency and API load.

### Overall Assessment

The task system is one of Claude Code's most complex subsystems, managing all background work from millisecond-scale Shell commands to hour-long remote Agents. The unified state model (`TaskStateBase` + type extensions) maintains consistency while accommodating the specialized needs of each task type. Details such as memory management (message caps), notification deduplication (`notified` flag), and graceful termination (lock rollback) demonstrate the maturity of a production-grade system. The main directions for improvement are memory architecture (disk-primary) and priority scheduling.
