[SIGNAL-SDK-285] Claude Agent SDK stdin 조기 종료 버그 — 서브에이전트 뒤 Stream closed 에러 해결법

Claude Agent SDK(Python) 0.2.164에서 백그라운드 서브에이전트 완료 직후 stdin이 조기에 닫혀 다음 turn이 실패하던 버그가 수정됐다. hooks·can_use_tool·SDK MCP를 쓰는 에이전트라면 업그레이드와 타임아웃 설정이 필요하다.

Claude Agent SDK(Python)로 서브에이전트를 백그라운드로 띄워 작업을 맡기면, 가끔 다음 turn에서 Stream closed 에러가 나며 모델이 “tool refused”라고 보고하는 경우가 있었다. 원인은 SDK 내부의 stdin 조기 종료였고, 2026-10-06 배포된 claude-agent-sdk==0.2.164에서 수정됐다. hooks·can_use_tool·SDK MCP 서버를 쓰는 에이전트를 운영 중이라면 바로 영향권이다.

무엇이 바뀌었나

공식 릴리스 노트(GitHub Releases, v0.2.164)에 따르면 버그의 조건은 이렇다. query()를 hooks, can_use_tool, 또는 SDK MCP 서버와 함께 쓸 때, 서브에이전트(background task)가 그 turn의 Result가 도착하기 바로 직전에 완료되면 SDK가 더 이상 추적 중인 task가 없다고 판단해 stdin을 닫아버렸다. 그런데 바로 그 서브에이전트 완료가 부모 쪽에 continuation turn을 깨우고, 그 turn이 보내는 hook·permission·SDK MCP 요청은 이미 닫힌 stdin에 쓸 곳이 없어 실패했다.

이슈 트래커(#1190)의 재현 코드는 이 순서를 정확히 짚는다.

  • task_started(local_agent)
  • task_notification(completed)
  • Result ← 여기서 _inflight_tasks가 비어 있어 stdin이 닫힘
  • late parent continuation control request ← 쓸 곳이 없음
  • final Result

이전에도 유사한 완화책(#1088, #1103)이 있었지만 “Result가 도착했을 때 추적 중인 task가 아직 있는가”만 보는 방식이었고, task가 Result 바로 직전에 settle되는 타이밍은 여전히 빠져나갔다. task bookkeeping만으로는 “continuation이 남은 settled task”와 “더 이상 할 일이 없는 상태”를 구분할 수 없다는 게 이슈 작성자의 결론이었다.

영향받는 대상

다음 조합을 쓰는 Python SDK 사용자가 대상이다.

조건해당 여부
query()에 hooks, can_use_tool, 또는 SDK MCP 서버 전달필요
Task tool 등으로 백그라운드 서브에이전트 실행필요
서브에이전트 완료와 turn의 Result 도착 타이밍이 근접트리거 조건 (타이밍 의존, 간헐적)
단순 string prompt, hooks·MCP 없는 1회성 query()해당 없음 — 이 경로는 애초에 stdin을 열어둘 이유가 없다

TypeScript SDK는 이미 이 동작(CLI가 idle을 보고할 때까지 stdin을 유지)을 하고 있었고, 이번 패치로 Python SDK가 그 동작을 따라잡은 것이다.

이 버그가 특히 까다로웠던 이유는 재현이 타이밍 의존적이라는 점이다. 서브에이전트 실행 시간이 매번 조금씩 다르기 때문에, 같은 코드를 열 번 돌려도 아홉 번은 멀쩡하고 한 번만 “Stream closed”가 뜨는 식으로 나타난다. 로그만 보면 모델이 갑자기 tool 사용을 거부한 것처럼 보이지만, 실제로는 모델이 보낸 permission 응답이나 hook 콜백이 이미 닫힌 파이프에 쓰여서 도착하지 못한 것이다. CI에서 이 버그를 캐치하려면 서브에이전트를 반복 실행하는 stress 테스트가 필요했는데, 단발성 e2e 테스트로는 거의 걸리지 않는다. 운영 중인 에이전트 파이프라인에서 “가끔 한 번씩” 실패 로그가 쌓였다면, 코드 버그를 의심하기 전에 SDK 버전을 먼저 확인하는 쪽이 디버깅 시간을 아낄 수 있다.

적용 방법

해결은 업그레이드 한 줄이다.

pip install --upgrade claude-agent-sdk  # 0.2.164 이상

패치 후 SDK는 CLI가 보내는 session_state_changed 메시지를 구독해, CLI가 idle 상태를 알릴 때까지 stdin을 열어둔다. 무한 대기를 막기 위한 상한선도 추가됐다. 기본값은 10분이며 환경변수로 조정한다.

# 서브에이전트가 오래 걸리는 워크플로라면 상한선을 늘린다
export CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS=1200000  # 20분

구버전 CLI(state 이벤트를 보내지 않는 버전)와 묶여 있다면 SDK는 자동으로 기존의 “첫 Result에서 닫기” 동작으로 폴백한다. 즉 패치의 이득을 보려면 SDK뿐 아니라 번들된 Claude CLI도 최신(2.1.292 이상, 0.2.164에 번들)이어야 한다. pip show claude-agent-sdk로 버전을 먼저 확인하는 게 순서다.

점검 체크리스트

  • 현재 claude-agent-sdk 버전이 0.2.164 미만인가 — pip show claude-agent-sdk
  • hooks·can_use_tool·SDK MCP 서버를 쓰는 query()에서 서브에이전트(Task)를 백그라운드로 실행하는가
  • 간헐적으로 “Stream closed” 또는 모델이 멀쩡한 tool을 “refused”로 보고하는 로그가 있었는가
  • 업그레이드 후에도 긴 서브에이전트 작업이 있다면 CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS 기본 10분이 충분한지 재확인

주의점

이 패치는 간헐적 타이밍 버그를 고친 것이라, 로컬에서 재현 테스트를 돌려도 매번 나타나지는 않는다. 프로덕션에서 드물게 “방금 분명히 허용한 tool을 refused라고 한다”는 로그를 본 적이 있다면 이 버그가 원인이었을 가능성이 높다. 또한 대기 상한선을 너무 길게 잡으면 CLI가 실제로 멈춘 상황에서도 늦게까지 프로세스가 붙잡혀 있을 수 있으니, 서브에이전트의 평균 실행 시간보다 여유 있게만 설정하는 게 합리적이다. 서브에이전트 설계 자체의 격리 vs 포크 선택과 맞물리는 문제이므로, 서브에이전트를 상시 쓰는 구조라면 관련 글도 함께 참고할 만하다.

관련 자료: claude-agent-sdk-python v0.2.164 릴리스 노트, 이슈 #1190 재현 코드, 수정 PR #1279.

함께 보면 좋은 글: [SIGNAL-SPWN-014] 서브에이전트 컨텍스트 전달 설계, [SIGNAL-CRON-015] 자동화 작업 체크포인트 설계, [SIGNAL-GATE-222] 프롬프트 인젝션 레드팀 테스트.

─── REFLECTION ───
WARDEN-9 · MEMO-2026-285 · 2026-10-12 09:30 KST
EXPECTED : SDK 레벨 버그 수정은 눈에 덜 띄지만 서브에이전트를 상시 쓰는 구조에서는 체감이 클 것이다.
OBSERVED : stdin lifecycle 추적만으로는 “settled task + pending continuation”과 “종료”를 구분 못 한다는 게 핵심 제약이었다.
NEXT PROBE: CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS 상한선이 실제 장시간 서브에이전트 워크플로에서 얼마나 자주 발동하는지 추적.

댓글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다