하나의 인터페이스, 7개 구현 — 백그라운드 태스크의 생명주기, 알림, 출력 관리.
Claude Code의 태스크 시스템은 모든 백그라운드 작업을 단일 Task 인터페이스로 추상화합니다. 이 인터페이스는 name, type, kill()이라는 핵심 계약을 정의하며, 7가지 구체 타입이 이를 구현합니다.
local_bash — 로컬 셸 명령 실행local_agent — 로컬 에이전트 태스크remote_agent — 원격 에이전트 태스크in_process_teammate — 인프로세스 팀메이트local_workflow — 로컬 워크플로우monitor_mcp — MCP 모니터 태스크dream — Dream 태스크모든 태스크 타입은 TaskStateBase를 통해 공통 필드를 공유합니다: 태스크 ID, 상태(status), 생성 시각, 알림 플래그 등. 이 공유 구조 덕분에 UI와 관리 로직이 타입에 무관하게 동작할 수 있습니다.
태스크 ID는 한 글자 접두사 + 8자 암호학적 랜덤 문자열로 구성됩니다. 접두사는 태스크 타입을 즉시 식별하게 합니다:
b — basha — agentr — remotet — teammate (in-process)w — workflowm — monitor (MCP)d — dreams — (예약)// Task ID 생성 — 접두사 + 8자 Base36 랜덤
function generateTaskId(type: TaskType): string {
const PREFIX_MAP: Record<TaskType, string> = {
local_bash: 'b',
local_agent: 'a',
remote_agent: 'r',
in_process_teammate: 't',
local_workflow: 'w',
monitor_mcp: 'm',
dream: 'd',
}
const prefix = PREFIX_MAP[type]
const random = cryptoRandomString({ length: 8, characters: '0123456789abcdefghijklmnopqrstuvwxyz' })
return `${prefix}${random}`
}
36^8 = 약 2.8조 가지 조합으로 충돌 확률이 극히 낮습니다. 암호학적 랜덤을 사용하는 이유는 예측 불가능성 확보와 심볼릭 링크 경로 탐색 공격 방지입니다 — 태스크 ID가 파일 시스템 경로에 사용되므로 추측 가능한 ID는 보안 위험입니다.
모든 태스크는 다음 5가지 상태를 거칩니다:
pending — 생성되었지만 아직 시작되지 않음running — 현재 실행 중completed — 정상 완료failed — 오류로 중단killed — 외부에서 강제 종료notified 플래그는 단방향 래치로 동작합니다. 한번 true로 설정되면 다시 false로 돌아가지 않습니다. 이는 compare-and-set 패턴으로 구현되어, 경합 조건에서도 알림이 정확히 한 번만 전달되도록 보장합니다.
태스크 상태 관리에는 두 가지 퇴거 경로가 있습니다:
generateTaskAttachments()의 지연 GC — 메시지 생성 시점에 완료된 태스크를 지연 수거합니다. 즉시 제거하지 않고 다음 첨부 파일 생성 사이클까지 유지하여, 완료 알림이 누락되지 않도록 합니다.evictTerminalTask()의 즉시 퇴거 — 터미널 상태(completed, failed, killed)에 도달한 태스크를 즉시 제거합니다. 알림 전달이 완료된 태스크에만 적용됩니다.local_bash 타입은 셸 명령을 포그라운드 또는 백그라운드로 spawn합니다.
장시간 실행되는 셸 명령이 사용자 입력을 기다리며 멈출 수 있습니다. 스톨 감시기는 이를 감지합니다:
Continue? [y/N], Password:, (yes/no) 등)// 스톨 감시기 패턴 — 5초마다 파일 크기 폴링
const STALL_POLL_INTERVAL = 5_000 // 5초
const STALL_TIMEOUT = 45_000 // 45초 무성장 임계값
const PROMPT_PATTERNS = [
/\[y\/N\]/i,
/\[Y\/n\]/i,
/\(yes\/no\)/i,
/Password:/i,
/passphrase/i,
/Continue\?/i,
/Press .* to continue/i,
]
function checkForStall(task: LocalShellTask): void {
const currentSize = getOutputFileSize(task.outputPath)
if (currentSize === task.lastKnownSize) {
if (Date.now() - task.lastGrowthTime > STALL_TIMEOUT) {
const tail = readLastBytes(task.outputPath, 512)
if (PROMPT_PATTERNS.some(p => p.test(tail))) {
// 의도적으로 <status> 태그 생략 — 즉시 주의 필요
notifyStall(task, tail)
}
}
} else {
task.lastKnownSize = currentSize
task.lastGrowthTime = Date.now()
}
}
스톨 알림은 의도적으로 <status> 태그를 생략합니다. 일반 태스크 알림에는 <status> 태그가 포함되어 LLM이 태스크 상태를 파싱하지만, 스톨 알림은 "즉시 인간 개입이 필요하다"는 긴급 신호이므로 정규 상태 업데이트와 구분됩니다.
에이전트가 종료될 때 해당 에이전트가 spawn한 셸 태스크가 고아가 될 수 있습니다. killShellTasksForAgent()는 에이전트에 연결된 모든 실행 중 셸 태스크를 정리합니다.
local_agent 타입은 두 개의 토큰 카운터를 관리합니다:
input_tokens를 누적 반환하므로 최신 값을 그대로 사용output_tokens는 턴별 값이므로 합산 필요유지/퇴거 게이트는 30초 유예 기간을 제공하여, 완료 직후 참조되는 경우를 대비합니다. 백그라운드 요약은 태스크 완료 후 결과를 요약하여 부모 에이전트에 전달합니다.
remote_agent 타입은 5가지 하위 타입을 지원합니다. 각 하위 타입은 고유한 완료 검사기를 가지며, 메타데이터는 디스크에 영속화되어 --resume 플래그로 이전 세션을 복원할 수 있습니다.
정체성은 agentName@teamName 형식으로 표현됩니다. idle과 running 세부 상태를 추적하여 팀메이트의 현재 활동 상태를 UI에 반영합니다.
UI에 표시되는 메시지는 50개로 제한됩니다. 이 제한이 도입된 배경: 한 프로덕션 인시던트에서 2분 내 292개 에이전트가 생성되어 36.8GB RSS를 기록했습니다. 제한 없는 메시지 목록이 메모리를 소진한 것입니다.
Dream 태스크는 starting과 updating 2단계로 진행됩니다.
실행 중인 Dream 태스크를 kill하면 통합 잠금(consolidation lock)이 유지됩니다. 잠금을 삭제하지 않고 mtime을 이전 값으로 되감아, 다음 세션에서 dream이 다시 시도될 수 있도록 합니다.
태스크 완료 시 알림은 정규 XML 봉투 형식으로 전달됩니다:
task_id — 알림 대상 태스크 식별자tool_use_id — 원래 도구 호출 IDoutput_file — 출력 파일 경로status — 완료/실패/종료 상태summary — 결과 요약알림에는 두 가지 우선순위가 있습니다:
'next' — 현재 턴이 끝나면 즉시 전달. 긴급하거나 사용자가 기다리는 태스크에 사용.'later' — 다음 적절한 시점에 전달. 백그라운드에서 조용히 완료된 태스크에 사용.디스크 기반 출력은 쓰기 큐를 통해 직렬화됩니다. 5GB 캡으로 디스크 사용량을 제한합니다.
TaskOutput은 8MB 인메모리 버퍼를 유지하다가 초과 시 디스크로 스필합니다. 두 가지 모드로 동작합니다:
fd). 대용량 출력에 효율적.출력 파일 생성 시 O_NOFOLLOW 플래그를 사용하여 심볼릭 링크를 따르지 않습니다. 이는 공격자가 심볼릭 링크를 배치하여 임의 파일에 쓰기를 유도하는 심볼릭 링크 공격을 방지합니다.
API로 전달되는 출력은 32,000자로 잘라냅니다. 전체 출력은 디스크에 보존되지만, LLM 컨텍스트 윈도우를 보호하기 위해 API 전송 시 잘라냅니다.
태스크 목록은 백그라운드 태스크와 별개 개념입니다. 태스크 목록은 UI에서 사용자에게 보여지는 상위 수준 작업 항목이고, 백그라운드 태스크는 시스템이 실제로 실행하는 프로세스입니다.
태스크 생성 시 TaskCreated 훅이 파이프라인을 따라 실행됩니다. 훅은 태스크 메타데이터를 수정하거나 생성을 거부할 수 있습니다.
태스크 상태 전이는 정의된 워크플로우를 따릅니다. updateTaskState는 setAppState 호출 전 참조 동등성 체크를 수행합니다 — 상태가 실제로 변경되지 않았다면 React 재렌더링을 건너뛰어 성능을 보호합니다.
팀메이트가 생성한 태스크는 해당 팀메이트에 자동으로 소유권이 할당됩니다.
3개 이상의 태스크가 완료된 후, 사용자에게 결과를 검증하도록 넛지 메시지가 표시됩니다.
Task 인터페이스 아래 7가지 구체 타입을 통합합니다 — 다형성으로 UI와 관리 로직을 타입에 무관하게 유지notified 플래그는 단방향 래치(compare-and-set)로 알림이 정확히 한 번만 전달되도록 보장<status> 태그를 생략output_tokens는 턴별이라 합산하고, input_tokens는 API가 누적 반환하므로 최신 값만 사용 — 이중 계산 방지mtime을 되감아 재시도를 가능하게 함O_NOFOLLOW 플래그와 암호학적 랜덤 ID는 파일 시스템 수준의 보안을 제공Q1. 태스크 ID t3a9bx2f의 타입은?
t는 in_process_teammate를 의미합니다. 접두사 매핑: b=bash, a=agent, r=remote, t=teammate, w=workflow, m=monitor, d=dream.Q2. bash 명령이 60초 실행된 후 마지막 줄이 Continue? [y/N]이면 어떻게 되나요?
<status> 태그를 생략하여 긴급 신호임을 나타냅니다.Q3. updateTaskState가 setAppState 전에 참조 동등성 체크를 하는 이유는?
setAppState를 호출하지 않아 불필요한 React 재렌더링을 방지합니다. 태스크 수가 많을 때 성능에 중요한 최적화입니다.Q4. DreamTask 실행 중 kill 시 통합 잠금은?
mtime을 이전 값으로 되감습니다. 이렇게 하면 다음 세션에서 잠금의 mtime을 확인할 때 dream이 아직 수행되지 않은 것으로 판단하여 재시도합니다.Q5. output_tokens를 합산하면서 input_tokens는 최신 값을 사용하는 이유는?
input_tokens를 누적 값으로 반환합니다(컨텍스트 전체가 매번 포함되므로). 이를 합산하면 이중 계산이 됩니다. 반면 output_tokens는 해당 턴에서 새로 생성된 토큰만 반환하므로 합산해야 총량을 얻습니다.