태스크 시스템

하나의 인터페이스, 7개 구현 — 백그라운드 태스크의 생명주기, 알림, 출력 관리.

01

개요

Claude Code의 태스크 시스템은 모든 백그라운드 작업을 단일 Task 인터페이스로 추상화합니다. 이 인터페이스는 name, type, kill()이라는 핵심 계약을 정의하며, 7가지 구체 타입이 이를 구현합니다.

7가지 구체 타입

TaskStateBase 공유 필드

모든 태스크 타입은 TaskStateBase를 통해 공통 필드를 공유합니다: 태스크 ID, 상태(status), 생성 시각, 알림 플래그 등. 이 공유 구조 덕분에 UI와 관리 로직이 타입에 무관하게 동작할 수 있습니다.

Task ID 인코딩

태스크 ID는 한 글자 접두사 + 8자 암호학적 랜덤 문자열로 구성됩니다. 접두사는 태스크 타입을 즉시 식별하게 합니다:

// 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는 보안 위험입니다.

02

생명주기

5가지 상태

모든 태스크는 다음 5가지 상태를 거칩니다:

notified 플래그 — 단방향 래치

notified 플래그는 단방향 래치로 동작합니다. 한번 true로 설정되면 다시 false로 돌아가지 않습니다. 이는 compare-and-set 패턴으로 구현되어, 경합 조건에서도 알림이 정확히 한 번만 전달되도록 보장합니다.

퇴거 메커니즘

태스크 상태 관리에는 두 가지 퇴거 경로가 있습니다:

03

7가지 태스크 타입 상세

LocalShellTask

local_bash 타입은 셸 명령을 포그라운드 또는 백그라운드로 spawn합니다.

스톨 감시기 (Stall Watchdog)

장시간 실행되는 셸 명령이 사용자 입력을 기다리며 멈출 수 있습니다. 스톨 감시기는 이를 감지합니다:

// 스톨 감시기 패턴 — 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> 태그를 생략합니다. 일반 태스크 알림에는 <status> 태그가 포함되어 LLM이 태스크 상태를 파싱하지만, 스톨 알림은 "즉시 인간 개입이 필요하다"는 긴급 신호이므로 정규 상태 업데이트와 구분됩니다.

고아 정리: killShellTasksForAgent()

에이전트가 종료될 때 해당 에이전트가 spawn한 셸 태스크가 고아가 될 수 있습니다. killShellTasksForAgent()는 에이전트에 연결된 모든 실행 중 셸 태스크를 정리합니다.

LocalAgentTask

local_agent 타입은 두 개의 토큰 카운터를 관리합니다:

유지/퇴거 게이트는 30초 유예 기간을 제공하여, 완료 직후 참조되는 경우를 대비합니다. 백그라운드 요약은 태스크 완료 후 결과를 요약하여 부모 에이전트에 전달합니다.

RemoteAgentTask

remote_agent 타입은 5가지 하위 타입을 지원합니다. 각 하위 타입은 고유한 완료 검사기를 가지며, 메타데이터는 디스크에 영속화되어 --resume 플래그로 이전 세션을 복원할 수 있습니다.

InProcessTeammateTask

정체성은 agentName@teamName 형식으로 표현됩니다. idlerunning 세부 상태를 추적하여 팀메이트의 현재 활동 상태를 UI에 반영합니다.

프로덕션 인시던트 — 50메시지 UI 캡

UI에 표시되는 메시지는 50개로 제한됩니다. 이 제한이 도입된 배경: 한 프로덕션 인시던트에서 2분 내 292개 에이전트가 생성되어 36.8GB RSS를 기록했습니다. 제한 없는 메시지 목록이 메모리를 소진한 것입니다.

DreamTask

Dream 태스크는 startingupdating 2단계로 진행됩니다.

kill-then-rewind 패턴

실행 중인 Dream 태스크를 kill하면 통합 잠금(consolidation lock)이 유지됩니다. 잠금을 삭제하지 않고 mtime을 이전 값으로 되감아, 다음 세션에서 dream이 다시 시도될 수 있도록 합니다.

04

알림 시스템

태스크 완료 시 알림은 정규 XML 봉투 형식으로 전달됩니다:

알림 우선순위

알림에는 두 가지 우선순위가 있습니다:

05

출력 관리

DiskTaskOutput

디스크 기반 출력은 쓰기 큐를 통해 직렬화됩니다. 5GB 캡으로 디스크 사용량을 제한합니다.

TaskOutput — 메모리 버퍼 + 디스크 스필

TaskOutput8MB 인메모리 버퍼를 유지하다가 초과 시 디스크로 스필합니다. 두 가지 모드로 동작합니다:

보안: O_NOFOLLOW

출력 파일 생성 시 O_NOFOLLOW 플래그를 사용하여 심볼릭 링크를 따르지 않습니다. 이는 공격자가 심볼릭 링크를 배치하여 임의 파일에 쓰기를 유도하는 심볼릭 링크 공격을 방지합니다.

API 소비를 위한 잘라내기

API로 전달되는 출력은 32,000자로 잘라냅니다. 전체 출력은 디스크에 보존되지만, LLM 컨텍스트 윈도우를 보호하기 위해 API 전송 시 잘라냅니다.

06

TaskCreateTool & TaskUpdateTool

태스크 목록은 백그라운드 태스크와 별개 개념입니다. 태스크 목록은 UI에서 사용자에게 보여지는 상위 수준 작업 항목이고, 백그라운드 태스크는 시스템이 실제로 실행하는 프로세스입니다.

TaskCreated 훅 파이프라인

태스크 생성 시 TaskCreated 훅이 파이프라인을 따라 실행됩니다. 훅은 태스크 메타데이터를 수정하거나 생성을 거부할 수 있습니다.

상태 워크플로우

태스크 상태 전이는 정의된 워크플로우를 따릅니다. updateTaskStatesetAppState 호출 전 참조 동등성 체크를 수행합니다 — 상태가 실제로 변경되지 않았다면 React 재렌더링을 건너뛰어 성능을 보호합니다.

팀메이트 자동 소유권

팀메이트가 생성한 태스크는 해당 팀메이트에 자동으로 소유권이 할당됩니다.

검증 넛지

3개 이상의 태스크가 완료된 후, 사용자에게 결과를 검증하도록 넛지 메시지가 표시됩니다.

07

핵심 요약

핵심 포인트

  • 태스크 시스템은 단일 Task 인터페이스 아래 7가지 구체 타입을 통합합니다 — 다형성으로 UI와 관리 로직을 타입에 무관하게 유지
  • 태스크 ID는 한 글자 접두사 + 8자 암호학적 랜덤으로 구성되어, 타입 식별과 경로 탐색 공격 방지를 동시에 달성
  • notified 플래그는 단방향 래치(compare-and-set)로 알림이 정확히 한 번만 전달되도록 보장
  • 스톨 감시기는 5초 폴링 + 45초 임계값 + 7개 패턴 매칭으로 멈춘 셸 프로세스를 감지하며, 스톨 알림은 의도적으로 <status> 태그를 생략
  • output_tokens는 턴별이라 합산하고, input_tokens는 API가 누적 반환하므로 최신 값만 사용 — 이중 계산 방지
  • DreamTask의 kill-then-rewind 패턴은 통합 잠금을 유지하면서 mtime을 되감아 재시도를 가능하게 함
  • InProcessTeammateTask의 50메시지 UI 캡은 2분 내 292 에이전트(36.8GB RSS) 프로덕션 인시던트에서 학습한 안전장치
  • O_NOFOLLOW 플래그와 암호학적 랜덤 ID는 파일 시스템 수준의 보안을 제공
08

지식 확인

퀴즈 — 5문제

Q1. 태스크 ID t3a9bx2f의 타입은?

  • A) bash
  • B) agent
  • C) in-process teammate
  • D) remote
접두사 tin_process_teammate를 의미합니다. 접두사 매핑: b=bash, a=agent, r=remote, t=teammate, w=workflow, m=monitor, d=dream.

Q2. bash 명령이 60초 실행된 후 마지막 줄이 Continue? [y/N]이면 어떻게 되나요?

  • A) 자동 응답
  • B) 타임아웃 후 종료
  • C) <status> 태그 없는 스톨 알림 전달
  • D) 강제 종료
스톨 감시기가 45초 무성장을 감지하고 프롬프트 패턴을 매칭하여 스톨 알림을 전달합니다. 이 알림은 의도적으로 <status> 태그를 생략하여 긴급 신호임을 나타냅니다.

Q3. updateTaskStatesetAppState 전에 참조 동등성 체크를 하는 이유는?

  • A) TypeScript 타입 시스템이 요구
  • B) 상태가 미변경 시 React 재렌더링을 건너뛰기 위해
  • C) 동시성 충돌 방지
  • D) 분석 이벤트 정확도를 위해
상태 객체가 실제로 변경되지 않았다면 setAppState를 호출하지 않아 불필요한 React 재렌더링을 방지합니다. 태스크 수가 많을 때 성능에 중요한 최적화입니다.

Q4. DreamTask 실행 중 kill 시 통합 잠금은?

  • A) 삭제
  • B) 유지
  • C) mtime을 이전 값으로 되감아 다음 세션에서 dream 가능
  • D) 영구 잠금
kill-then-rewind 패턴은 통합 잠금을 유지하면서 mtime을 이전 값으로 되감습니다. 이렇게 하면 다음 세션에서 잠금의 mtime을 확인할 때 dream이 아직 수행되지 않은 것으로 판단하여 재시도합니다.

Q5. output_tokens를 합산하면서 input_tokens는 최신 값을 사용하는 이유는?

  • A) API 구현 방식의 제약
  • B) API가 input_tokens를 누적 반환하므로 합산 시 이중 계산, output_tokens는 턴별
  • C) 메모리 절약
  • D) 정확도가 중요하지 않아서
Anthropic API는 input_tokens를 누적 값으로 반환합니다(컨텍스트 전체가 매번 포함되므로). 이를 합산하면 이중 계산이 됩니다. 반면 output_tokens는 해당 턴에서 새로 생성된 토큰만 반환하므로 합산해야 총량을 얻습니다.
0 / 5