Claude Code Task Runtime: Background Work as First-Class Objects

2 min readAI Agents

How five task types (LocalShell, LocalAgent, RemoteAgent, InProcessTeammate, DreamTask) promote background work into observable, cancellable runtime objects.

ai-agentsclaude-codetasksruntimebackground-jobssubagents

One of the easiest ways to underestimate Claude Code is to assume its runtime only really exists inside the active foreground turn.

That stops being plausible once you read src/Task.ts, src/tasks.ts, and the concrete task implementations under src/tasks/.

The useful mental model is this:

  • tasks are Claude Code's lifecycle abstraction for long-lived work
  • the abstraction is thin at the interface layer
  • the abstraction is rich at the state, notification, and observability layer

File structure

Files covered in this post7 files
src/
├── Task.ts
├── tasks.ts
├── tasks/
│   ├── DreamTask/
│   │   └── DreamTask.ts
│   └── RemoteAgentTask/
│       └── RemoteAgentTask.tsx

src/Task.ts

Shared task lifecycle vocabulary

Critical

Defines task types, task statuses, shared state fields, ID generation, and the intentionally small Task interface whose main polymorphic operation is kill().

Module
Task Contract

Key Exports

  • TaskType
  • TaskStatus
  • isTerminalTaskStatus
  • generateTaskId
  • createTaskStateBase

Why It Matters

  • Claude Code does not use a heavy OO task framework.
  • The shared contract is mostly about lifecycle state and IDs.
  • The true common boundary is control and observability, not execution strategy.

src/tasks/RemoteAgentTask/RemoteAgentTask.tsx

Polling and resume lifecycle for cloud-backed runs

Critical

Registers remote sessions as tasks, persists resumable metadata, polls remote logs and status, extracts special completion signals, and archives sessions when killed.

Module
Remote Task Runtime

Key Exports

  • RemoteAgentTask
  • registerRemoteAgentTask
  • restoreRemoteAgentTasks
  • registerCompletionChecker

Why It Matters

  • Remote work is normalized into the same task surface as local work.
  • Resume works by persisting task identity and refetching live status, not by serializing runtime objects.
  • The task layer is one of Claude Code's bridges between local and remote runtime.

The core idea

Claude Code tasks exist for work that cannot be reduced to "the current foreground turn is still in progress."

That includes:

  1. background shell commands
  2. background local subagents
  3. remote Claude.ai sessions
  4. same-process teammates
  5. the main session itself after backgrounding

So tasks are not just a helper for subagents.

They are the runtime's way to give long-lived work a stable lifecycle surface.

Task runtime pipeline

The stable pattern shared across very different task types.

  1. 1

    Create shared task identity

    generateTaskId / createTaskStateBase

    Task.ts allocates an ID, base state, and output path metadata.

  2. 2

    Register into AppState

    registerTask

    registerTask() inserts the task into the unified task map and emits task_started for SDK consumers.

  3. 3

    Run a task-specific engine

    spawnShellTask / registerAsyncAgent / registerRemoteAgentTask / startBackgroundSession

    Shell tasks spawn processes, local agents run subagent logic, remote tasks poll cloud sessions, and main-session tasks keep query() running in the background.

  4. 4

    Project status through shared surfaces

    diskOutput.ts / framework.ts / enqueue*Notification

    Tasks expose output files, AppState updates, and task notifications so UI, model, and SDK consumers can all observe them.

  5. 5

    Stop and evict consistently

    stopTask / kill / evictTerminalTask

    stopTask() dispatches to type-specific kill logic, then the framework evicts terminal tasks once they are safely consumed.

Why the small Task interface is the right design

The most revealing part of Task.ts is what it does not require.

The Task interface is basically:

  • name
  • type
  • kill()

That is a strong signal that the designers did not try to force every task into the same internal execution model.

Instead, Claude Code standardizes:

  • lifecycle vocabulary
  • state shape
  • registration
  • stop semantics
  • notifications

That is the right tradeoff when shell processes, local subagents, and remote sessions all need to live under one UI and one state store.

Why LocalMainSessionTask is especially important

The most surprising task is probably src/tasks/LocalMainSessionTask.ts.

It backgrounds the main session query itself.

That tells you the task system is not just "child jobs spawned by tools."

It is the runtime's general mechanism for detaching conversational work from the active foreground shell and making it observable as its own lifecycle object.

That is a much more powerful role than a normal job queue.

Why RemoteAgentTask matters architecturally

RemoteAgentTask shows the abstraction really works.

The actual execution is somewhere else, but the local runtime still gets:

  • a task ID
  • a status
  • output
  • notifications
  • stop behavior
  • resume support

That means the task layer is one of the places where Claude Code turns distributed execution back into a local, inspectable runtime surface.

The real lesson

Claude Code tasks are not generic background jobs and not just subagent wrappers.

They are the runtime abstraction for work that needs to remain visible, controllable, and resumable after it stops fitting inside one foreground turn.

That is why the interface is thin and the lifecycle state is rich.

Task lifecycle across runtimes

The shared task contract: register → run → observe → stop. JS shows the actual pattern; Python and Go show the same lifecycle shape.

typescripttasks.ts (simplified)

The shared registration and stop pattern from learning-claude-code

// Task contract — thin interface, rich lifecycle state
interface Task {
name: string
type: TaskType  // 'shell' | 'local_agent' | 'remote_agent' | 'local_main_session' | 'teammate'
kill(): Promise<void>
}

// Shared base state created for every task
function createTaskStateBase(id: string, name: string, type: TaskType): TaskStateBase {
return {
  id,
  name,
  type,
  status: 'running',
  startedAt: Date.now(),
  outputFilePath: getTaskOutputPath(id),
  notificationFilePath: getTaskNotificationPath(id),
}
}

// Register a task into AppState — emits task_started for SDK consumers
function registerTask(task: Task, state: TaskStateBase, context: ToolUseContext): void {
context.setAppStateForTasks(prev => ({
  ...prev,
  tasks: { ...prev.tasks, [state.id]: { task, state } },
}))
emitTaskStartedNotification(state)
}

// Stop a task — dispatches to type-specific kill logic, then evicts on terminal status
async function stopTask(taskId: string, context: ToolUseContext): Promise<void> {
const { task } = context.getAppState().tasks[taskId] ?? {}
if (task) await task.kill()
context.setAppStateForTasks(prev => ({
  ...prev,
  tasks: { ...prev.tasks, [taskId]: { ...prev.tasks[taskId], state: { ...prev.tasks[taskId].state, status: 'stopped' } } },
}))
}

Why does LocalMainSessionTask exist in Claude Code's task system?

medium

Claude Code has task types for shell processes, local agents, remote agents, and teammates. LocalMainSessionTask wraps something different.

  • AIt manages the startup lifecycle of the main REPL session
    Incorrect.The main REPL startup is handled by main.tsx and replLauncher.tsx, not by LocalMainSessionTask.
  • BIt backgrounds the main session query loop itself, making the active foreground turn observable as a lifecycle object
    Correct!Correct. LocalMainSessionTask wraps the main session's query() so that when the session is backgrounded, it becomes visible in the task registry with status, output, and stop semantics — the same surface as any other task.
  • CIt handles the /resume command for restoring interrupted sessions
    Incorrect.Session resume is handled by the boot pipeline (main.tsx) and transcript restoration, not by LocalMainSessionTask.
  • DIt provides the task ID for the main agent to pass to subagents for parent-child linking
    Incorrect.Subagent linking uses agentId and queryTracking.chainId, not LocalMainSessionTask.