Claude Code Task Runtime: Background Work as First-Class Objects
How five task types (LocalShell, LocalAgent, RemoteAgent, InProcessTeammate, DreamTask) promote background work into observable, cancellable runtime objects.
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
src/
├── Task.ts
├── tasks.ts
├── tasks/
│ ├── DreamTask/
│ │ └── DreamTask.ts
│ └── RemoteAgentTask/
│ └── RemoteAgentTask.tsxsrc/Task.ts
Shared task lifecycle vocabulary
Defines task types, task statuses, shared state fields, ID generation, and the intentionally small Task interface whose main polymorphic operation is kill().
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
Registers remote sessions as tasks, persists resumable metadata, polls remote logs and status, extracts special completion signals, and archives sessions when killed.
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:
- background shell commands
- background local subagents
- remote Claude.ai sessions
- same-process teammates
- 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
Create shared task identity
generateTaskId / createTaskStateBaseTask.ts allocates an ID, base state, and output path metadata.
- 2
Register into AppState
registerTaskregisterTask() inserts the task into the unified task map and emits task_started for SDK consumers.
- 3
Run a task-specific engine
spawnShellTask / registerAsyncAgent / registerRemoteAgentTask / startBackgroundSessionShell tasks spawn processes, local agents run subagent logic, remote tasks poll cloud sessions, and main-session tasks keep query() running in the background.
- 4
Project status through shared surfaces
diskOutput.ts / framework.ts / enqueue*NotificationTasks expose output files, AppState updates, and task notifications so UI, model, and SDK consumers can all observe them.
- 5
Stop and evict consistently
stopTask / kill / evictTerminalTaskstopTask() 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:
nametypekill()
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.
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?
mediumClaude 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.