Studio Notebook

Claude Code Atlas

Task Model And Lifecycle Basics

Learn the shared task vocabulary and lifecycle rules used by every task kind.

Why this matters

This page introduces the shared task language that the later pages reuse. It is the quickest way to see the common shape before the concrete task kinds split apart.

Task.ts is the common contract every task state starts from. It is not one implementation file for one task kind. It is the shared interface that all the real task implementations inherit from, and the later chapters fill in the specific behavior on top of it.

The shared task vocabulary

The first half of Task.ts names the shared pieces that most task flows depend on, plus one shell-specific input type that shows up later in this subtree:

  • TaskType says what kind of task this is.
  • TaskStatus says where it is in its lifecycle.
  • isTerminalTaskStatus() separates finished tasks from tasks that can still move.
  • TaskHandle, TaskContext, and TaskStateBase describe the shared runtime shape.
  • LocalShellSpawnInput is not universal. It records the inputs that start a local shell task, so it belongs here as an early example of a task-kind- specific helper type.

The isTerminalTaskStatus() helper matters because the rest of the system uses it to avoid updating tasks that are already done. Use this to guard against injecting messages into dead teammates, evicting finished tasks from AppState, and orphan-cleanup paths.

export type TaskType =
  | 'local_bash'
  | 'local_agent'
  | 'remote_agent'
  | 'in_process_teammate'
  | 'local_workflow'
  | 'monitor_mcp'
  | 'dream'

export type TaskStatus =
  | 'pending'
  | 'running'
  | 'completed'
  | 'failed'
  | 'killed'

/**
 * True when a task is in a terminal state and will not transition further.
 * Used to guard against injecting messages into dead teammates, evicting
 * finished tasks from AppState, and orphan-cleanup paths.
 */
export function isTerminalTaskStatus(status: TaskStatus): boolean {
  return status === 'completed' || status === 'failed' || status === 'killed'
}

export type TaskHandle = {
  taskId: string
  cleanup?: () => void
}

export type SetAppState = (f: (prev: AppState) => AppState) => void

export type TaskContext = {
  abortController: AbortController
  getAppState: () => AppState
  setAppState: SetAppState
}

// Base fields shared by all task states
export type TaskStateBase = {
  id: string
  type: TaskType
  status: TaskStatus
  description: string
  toolUseId?: string
  startTime: number
  endTime?: number
  totalPausedMs?: number
  outputFile: string
  outputOffset: number
  notified: boolean
}

export type LocalShellSpawnInput = {
  command: string
  description: string
  timeout?: number
  toolUseId?: string
  agentId?: AgentId
  /** UI display variant: description-as-label, dialog title, status bar pill. */
  kind?: 'bash' | 'monitor'
}

The rest of the file shows how task IDs are made and how a new task state starts with the shared base values.

Task IDs And New Tasks

generateTaskId() gives each task a short prefix plus a random suffix. The prefix helps humans tell task kinds apart when they are looking at logs or file paths.

createTaskStateBase() then fills in the shared starting fields for a new task: pending status, the task description, the output file, and the initial notification flag.

// What getTaskByType dispatches for: kill. spawn/render were never
// called polymorphically (removed in #22546). All six kill implementations
// use only setAppState — getAppState/abortController were dead weight.
export type Task = {
  name: string
  type: TaskType
  kill(taskId: string, setAppState: SetAppState): Promise<void>
}

// Task ID prefixes
const TASK_ID_PREFIXES: Record<string, string> = {
  local_bash: 'b', // Keep as 'b' for backward compatibility
  local_agent: 'a',
  remote_agent: 'r',
  in_process_teammate: 't',
  local_workflow: 'w',
  monitor_mcp: 'm',
  dream: 'd',
}

// Get task ID prefix
function getTaskIdPrefix(type: TaskType): string {
  return TASK_ID_PREFIXES[type] ?? 'x'
}

// Case-insensitive-safe alphabet (digits + lowercase) for task IDs.
// 36^8 ≈ 2.8 trillion combinations, sufficient to resist brute-force symlink attacks.
const TASK_ID_ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz'

export function generateTaskId(type: TaskType): string {
  const prefix = getTaskIdPrefix(type)
  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
}

export function createTaskStateBase(
  id: string,
  type: TaskType,
  description: string,
  toolUseId?: string,
): TaskStateBase {
  return {
    id,
    type,
    status: 'pending',
    description,
    toolUseId,
    startTime: Date.now(),
    outputFile: getTaskOutputPath(id),
    outputOffset: 0,
    notified: false,
  }
}