InkdownInkdown
Start writing

Claude-Code

62 files·4 subfolders

Shared Workspace

Claude-Code
codex

05-tasks-system

Shared from "Claude-Code" on Inkdown

Tasks System Architecture

Overview

The Tasks System manages background work in Claude Code. When you run a long command or spawn a sub-agent, it runs as a task - isolated, trackable, and controllable.

Plain text
┌─────────────────────────────────────────────────────────────────────────────┐
│                          TASKS SYSTEM                                        │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│   ┌─────────────────────────────────────────────────────────────────────┐  │
│   │                       TASK LIFECYCLE                                 │  │
│   │                                                                      │  │
│   │   PENDING ──► RUNNING ──► COMPLETED/FAILED/KILLED                   │  │
│   │      │          │              │                                     │  │
│   │      │          ▼              ▼                                     │  │
│   │      │     Output File      Terminal                                │  │
│   │      │     (on disk)        State                                   │  │
│   │      │                                                            │  │
│   │      ▼                                                            │  │
│   │   Can be killed                                                   │  │
│   │   at any time                                                     │  │
│   └─────────────────────────────────────────────────────────────────────┘  │
│                                                                             │
│   ┌─────────────────────────────────────────────────────────────────────┐  │
│   │                      TASK TYPES                                      │  │
│   │  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐ │  │
│   │  │ LocalShell │  │ LocalAgent │  │ RemoteAgent│  │    Dream    │ │  │
│   │  │   Task     │  │    Task    │  │   Task     │  │    Task     │ │  │
│   │  │  (bash)    │  │ (sub-agent)│  │ (remote)   │  │ (compact)  │ │  │
│   │  └─────────────┘  └─────────────┘  └─────────────┘  └─────────────┘ │  │
│   └─────────────────────────────────────────────────────────────────────┘  │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘
0000_start_here_index_and_recommended_reading_order.md
0100_project_overview_tech_stack_runtime_modes_and_folder_map.md
0200_startup_flow_entry_points_and_cold_start_sequence.md
0300_codebase_modules_layers_state_models_and_schemas.md
0400_system_architecture_and_design_rationale.md
0500_interactive_repl_request_flow_end_to_end.md
0600_headless_sdk_and_print_mode_request_flow_end_to_end.md
0700_mcp_integration_connection_and_tool_call_flow.md
0800_external_services_sdks_storage_and_local_dependencies.md
0900_environment_variables_settings_feature_flags_and_failure_modes.md
1000_non_obvious_patterns_gotchas_and_debugging_traps.md
1100_full_codebase_file_inventory_grouped_by_directory.md
kimi
00-overview.md
01-entrypoints.md
02-state-management.md
03-query-system.md
04-tools-system.md
05-tasks-system.md
06-ui-components.md
07-bridge-remote.md
08-services.md
09-skills-plugins.md
10-commands.md
11-testing-architecture.md
12-permission-system.md
13-build-system.md
14-ink-internals.md
15-git-internals.md
16-context-compaction.md
17-vim-mode.md
18-mailbox-notifications.md
19-session-persistence.md
20-hooks-system.md
21-error-recovery.md
README.md
qwen
00-overview.md
01-entry-points.md
02-query-engine.md
03-tools-and-tasks.md
04-commands-and-skills.md
05-state-management.md
06-ink-rendering.md
07-bridge-remote.md
08-mcp-services.md
09-services-overview.md
10-multi-agent.md
11-system-prompt-constants.md
12-tool-interface.md
13-memory-system.md
14-buddy-companion.md
15-keybindings.md
16-stop-hooks.md
17-vim-mode.md
18-upstreamproxy.md
19-cost-tracking-history.md
20-contexts-styles-onboarding.md
21-hooks.md
22-screens.md
tweets-explain
claude-code-memory-analysis.md
compact
memory-system
agentic-architecture

Core Files

FilePurpose
Task.tsTask type definitions + ID generation
tasks.tsTask registry and getAllTasks()
tasks/[Type]Task/[Type]Task.tsxIndividual task implementations
tasks/stopTask.tsTask termination logic
tasks/types.tsShared task type definitions

Task State Machine

TypeScript
// Task.ts
type TaskStatus =
  | 'pending'   // Created, not started
  | 'running'   // Actively executing
  | 'completed' // Finished successfully
  | 'failed'    // Error during execution
  | 'killed'    // User terminated

// State is terminal (no more transitions)
export function isTerminalTaskStatus(status: TaskStatus): boolean {
  return status === 'completed' || status === 'failed' || status === 'killed'
}
State Transitions
Plain text
┌─────────┐    spawn     ┌─────────┐    finish    ┌───────────┐
│ PENDING │ ────────────►│ RUNNING │ ────────────►│ COMPLETED │
└─────────┘              └────┬────┘              └───────────┘
                             │
                             │ error              ┌───────────┐
                             └───────────────────►│  FAILED   │
                             │                    └───────────┘
                             │
                    kill()   │                    ┌───────────┐
                             └───────────────────►│   KILLED  │
                                                  └───────────┘

Task Types

1. LocalShellTask

Purpose: Execute bash/shell commands in background.

TypeScript
// tasks/LocalShellTask/LocalShellTask.tsx
export const LocalShellTask: Task = {
  name: 'LocalShell',
  type: 'local_bash',

  async kill(taskId: string, setAppState: SetAppState): Promise<void> {
    // Find the process
    const abortController = findAbortController(taskId)

    // Signal cancellation
    abortController.abort()

    // Kill the actual process
    const pid = getTaskPid(taskId)
    if (pid) {
      process.kill(pid, 'SIGTERM')
      await waitForExit(pid, 5000)
      process.kill(pid, 'SIGKILL')  // Force if needed
    }

    // Update state
    setAppState(prev => ({
      ...prev,
      tasks: {
        ...prev.tasks,
        [taskId]: { ...prev.tasks[taskId]!, status: 'killed', endTime: Date.now() },
      },
    }))
  },
}

// Spawn input
export type LocalShellSpawnInput = {
  command: string
  description: string
  timeout?: number
  toolUseId?: string
  agentId?: AgentId
  kind?: 'bash' | 'monitor'  // UI display variant
}
2. LocalAgentTask

Purpose: Spawn a sub-agent (autonomous AI worker).

TypeScript
// tasks/LocalAgentTask/LocalAgentTask.tsx
export const LocalAgentTask: Task = {
  name: 'LocalAgent',
  type: 'local_agent',

  async kill(taskId: string, setAppState: SetAppState): Promise<void> {
    // Agents have their own abort controllers
    const abortController = getAgentAbortController(taskId)
    abortController.abort()

    // Update state
    setAppState(prev => updateTaskStatus(prev, taskId, 'killed'))
  },
}

Key Difference: Agents can use tools autonomously; shells just run commands.

3. RemoteAgentTask

Purpose: Spawn an agent on a remote (cloud) environment.

TypeScript
// tasks/RemoteAgentTask/RemoteAgentTask.tsx
export const RemoteAgentTask: Task = {
  name: 'RemoteAgent',
  type: 'remote_agent',

  async kill(taskId: string, setAppState: SetAppState): Promise<void> {
    // Send kill signal to remote daemon
    await remoteKill(taskId)

    // Update local state
    setAppState(prev => updateTaskStatus(prev, taskId, 'killed'))
  },
}
4. DreamTask

Purpose: Background compaction/summarization.

TypeScript
// tasks/DreamTask/DreamTask.ts
export const DreamTask: Task = {
  name: 'Dream',
  type: 'dream',

  // Dream tasks auto-complete, user doesn't kill them
  async kill(taskId: string, setAppState: SetAppState): Promise<void> {
    // Usually no-op or gentle signal
  },
}

Task State Structure

TypeScript
// tasks/types.ts
type TaskStateBase = {
  // Identity
  id: string                    // Unique task ID (e.g., "b12345678")
  type: TaskType                 // 'local_bash' | 'local_agent' | etc.

  // Status
  status: TaskStatus             // pending | running | completed | failed | killed
  description: string            // Human-readable description

  // Timestamps
  startTime: number              // When task started (epoch ms)
  endTime?: number               // When task finished
  totalPausedMs?: number         // Time spent paused

  // Output
  outputFile: string             // Path to disk file with output
  outputOffset: number           // Bytes read from output file
  notified: boolean              // User notified of completion?

  // Optional
  toolUseId?: string             // Associated tool use (if any)
}

// Task-specific state extensions
export type LocalShellTaskState = TaskStateBase & {
  type: 'local_bash'
  command: string
  exitCode?: number
}

export type LocalAgentTaskState = TaskStateBase & {
  type: 'local_agent'
  agentId: AgentId
  messages: Message[]           // Agent's conversation
}

Task ID Generation

TypeScript
// Task.ts
const TASK_ID_PREFIXES: Record<string, string> = {
  local_bash: 'b',        // b12345678
  local_agent: 'a',       // a87654321
  remote_agent: 'r',      // r1234abcd
  in_process_teammate: 't',
  local_workflow: 'w',
  monitor_mcp: 'm',
  dream: 'd',
}

const TASK_ID_ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz'

export function generateTaskId(type: TaskType): string {
  const prefix = TASK_ID_PREFIXES[type] ?? 'x'
  const bytes = randomBytes(8)
  let id = prefix
  for (let i = 0; i < 8; i++) {
    id += TASK_ID_ALPHABET[bytes[i]! % TASK_ID_ALPHABET.length]
  }
  return id  // e.g., "b12345678"
}

Why 36^8? ~2.8 trillion combinations - resists brute-force symlink attacks.


Task Creation Flow

Plain text
┌─────────────────────────────────────────────────────────────────┐
│                    TASK CREATION FLOW                            │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  1. TOOL CALLS TASK CREATE                                      │
│     e.g., BashTool calls TaskCreateTool                         │
│                                                                 │
│  2. GENERATE TASK ID                                            │
│     const taskId = generateTaskId('local_bash')                 │
│                                                                 │
│  3. CREATE OUTPUT FILE                                          │
│     const outputFile = getTaskOutputPath(taskId)                │
│     fs.writeFile(outputFile, '')                                │
│                                                                 │
│  4. CREATE TASK STATE                                           │
│     const taskState: LocalShellTaskState = {                    │
│       id: taskId,                                               │
│       type: 'local_bash',                                       │
│       status: 'pending',                                        │
│       description: 'Running npm install',                       │
│       startTime: Date.now(),                                    │
│       outputFile,                                               │
│       outputOffset: 0,                                          │
│       notified: false,                                          │
│       command: 'npm install',                                   │
│     }                                                           │
│                                                                 │
│  5. ADD TO APP STATE                                            │
│     setAppState(prev => ({                                      │
│       ...prev,                                                  │
│       tasks: { ...prev.tasks, [taskId]: taskState },            │
│     }))                                                         │
│                                                                 │
│  6. START EXECUTION                                             │
│     await executeTask(taskState)                                │
│                                                                 │
│  7. UPDATE STATUS → 'running'                                   │
│     setAppState(prev => updateTaskStatus(prev, taskId,         │
│       'running'))                                               │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Task Execution: LocalShellTask

TypeScript
// Simplified flow
async function executeLocalShellTask(
  taskState: LocalShellTaskState,
  setAppState: SetAppState
): Promise<void> {
  const abortController = new AbortController()
  registerAbortController(taskState.id, abortController)

  // Spawn the process
  const proc = spawn(taskState.command, {
    signal: abortController.signal,
  })

  // Stream output to file
  const outputStream = createWriteStream(taskState.outputFile)
  proc.stdout.pipe(outputStream)
  proc.stderr.pipe(outputStream)

  // Wait for completion
  try {
    const exitCode = await proc.exited

    // Update to completed/failed
    const finalStatus = exitCode === 0 ? 'completed' : 'failed'
    setAppState(prev => ({
      ...prev,
      tasks: {
        ...prev.tasks,
        [taskState.id]: {
          ...prev.tasks[taskState.id]!,
          status: finalStatus,
          endTime: Date.now(),
          exitCode,
        },
      },
    }))
  } catch (e) {
    // Killed or errored
    setAppState(prev => updateTaskStatus(prev, taskState.id, 'killed'))
  }
}

Reading Task Output

Tasks write to disk files, not memory. Reading is streaming:

TypeScript
// utils/task/diskOutput.ts
export async function readTaskOutput(
  taskId: string,
  offset: number = 0
): Promise<{ content: string; newOffset: number }> {
  const outputPath = getTaskOutputPath(taskId)

  // Read from offset (streaming)
  const content = await readFile(outputPath, 'utf-8')
  const newContent = content.slice(offset)

  return {
    content: newContent,
    newOffset: content.length,
  }
}

// Usage in UI
function TaskOutputDisplay({ taskId }: { taskId: string }) {
  const task = useAppState(s => s.tasks[taskId])
  const [output, setOutput] = useState('')

  useEffect(() => {
    const interval = setInterval(async () => {
      const { content, newOffset } = await readTaskOutput(
        taskId,
        task.outputOffset
      )

      if (content) {
        setOutput(prev => prev + content)
        setAppState(prev => ({
          ...prev,
          tasks: {
            ...prev.tasks,
            [taskId]: { ...prev.tasks[taskId]!, outputOffset: newOffset },
          },
        }))
      }
    }, 100)

    return () => clearInterval(interval)
  }, [taskId])

  return <Text>{output}</Text>
}

Task Killing

TypeScript
// tasks/stopTask.ts
export async function stopTask(
  taskId: string,
  setAppState: SetAppState
): Promise<void> {
  // Find task type
  const task = getAppState().tasks[taskId]
  if (!task) return

  // Get task implementation
  const taskImpl = getTaskByType(task.type)
  if (!taskImpl) return

  // Delegate to task-specific kill logic
  await taskImpl.kill(taskId, setAppState)
}
Kill Strategies by Type
Task TypeKill Strategy
local_bashSIGTERM → wait → SIGKILL
local_agentAbortController + signal agents
remote_agentRemote API call to kill
dreamUsually no-op (background)

TaskGet Tool

The TaskGetTool retrieves task output:

TypeScript
// tools/TaskGetTool/TaskGetTool.ts
export const TaskGetTool: Tool = {
  name: 'TaskGet',
  description: 'Get the output of a background task',

  inputSchema: z.object({
    task_id: z.string(),
    wait?: z.number(),  // Wait up to N seconds for completion
  }),

  async execute(input, context) {
    const { task_id, wait } = input
    const task = context.getAppState().tasks[task_id]

    if (!task) {
      return { content: `Task ${task_id} not found`, is_error: true }
    }

    // Wait if requested
    if (wait && !isTerminalTaskStatus(task.status)) {
      await waitForTaskComplete(task_id, wait * 1000)
    }

    // Read output
    const { content } = await readTaskOutput(task_id, 0)

    return {
      content: `Status: ${task.status}\n\n${content}`,
    }
  },
}

Task UI Components

TypeScript
// components/TaskPanel.tsx
export function TaskPanel() {
  const tasks = useAppState(s => Object.values(s.tasks))

  return (
    <Box flexDirection="column">
      {tasks.map(task => (
        <TaskRow key={task.id} task={task} />
      ))}
    </Box>
  )
}

function TaskRow({ task }: { task: TaskState }) {
  const statusColor = {
    pending: 'yellow',
    running: 'blue',
    completed: 'green',
    failed: 'red',
    killed: 'gray',
  }[task.status]

  return (
    <Box>
      <Text color={statusColor}>●</Text>
      <Text>{task.description}</Text>
      <Text dimColor>{task.id}</Text>
      {task.status === 'running' && <Spinner />}
    </Box>
  )
}

Foregrounding Tasks

Users can "foreground" a task to see its output in the main view:

TypeScript
// State field
foregroundedTaskId?: string  // Which task is in main view

// Foreground action
function foregroundTask(taskId: string) {
  setAppState(prev => ({
    ...prev,
    foregroundedTaskId: taskId,
  }))
}

Key Concepts

1. Disk-Backed Output

Task output goes to files, not memory. This:

  • Prevents memory bloat
  • Survives crashes
  • Allows streaming reads
2. Status-Driven UI

UI reacts to status changes. No direct process management in components.

3. Type-Specific Logic

Each task type handles its own spawning and killing. Common interface, specialized implementations.

4. Observable State

All task state is in AppState. Anyone can observe tasks.

5. Graceful Termination

SIGTERM first, SIGKILL only if needed. Allows cleanup.


Common Patterns

Starting a Background Task
TypeScript
// In a tool implementation
async execute(input, context) {
  // Create task
  const taskId = generateTaskId('local_bash')
  const taskState = createTaskState(taskId, 'local_bash', input.command)

  // Add to state
  context.setAppState(prev => ({
    ...prev,
    tasks: { ...prev.tasks, [taskId]: taskState },
  }))

  // Start (don't await - runs in background)
  executeLocalShellTask(taskState, context.setAppState)

  // Return task ID immediately
  return {
    content: `Started task ${taskId}. Use TaskGet to check output.`,
  }
}
Waiting for Task Completion
TypeScript
async function waitForTask(
  taskId: string,
  timeoutMs: number
): Promise<boolean> {
  const start = Date.now()

  while (Date.now() - start < timeoutMs) {
    const task = getAppState().tasks[taskId]
    if (isTerminalTaskStatus(task.status)) {
      return true
    }
    await sleep(100)
  }

  return false  // Timeout
}

Related Documentation

  • State Management - How task state works
  • Tools System - How tools create tasks
  • Query System - How tasks relate to queries