내부 구조: JSONL 세션 해석이 에이전트 라우팅을 어떻게 살리는가
MadoHub가 Claude Code의 세션 로그를 읽어, 지능적인 라우팅을 위한 깨끗하고 구조화된 응답을 추출하는 방식을 깊이 있게 설명합니다.
에이전트 라우팅에서 가장 어려운 문제는 대상 에이전트에게 보낼 프롬프트를 만드는 것이 아닙니다. 소스 에이전트에서 깨끗한 응답을 추출하는 것입니다.
터미널 출력은 지저분합니다. ANSI 이스케이프 코드, 진행 스피너, 줄바꿈, 색상 시퀀스 때문에 원시 터미널 스크래핑은 소음 덩어리만 줍니다. 제어 코드를 지워도 포맷 흔적, 부분 줄, 섞인 내용이 남습니다.
MadoHub은 다른 방식으로 해결합니다. 터미널 출력을 긁지 않고, Claude Code의 구조화된 세션 로그를 읽습니다. 이 JSONL 파일에는 모든 대화 턴의 깨끗하고 의미론적인 표현이 들어 있습니다.
이 글은 그 작동 방식을 설명합니다.
터미널 스크래핑의 문제
Claude Code가 터미널에서 실행될 때 출력에는 다음이 포함됩니다.
- ANSI 이스케이프 시퀀스 - 색상, 커서 위치, 포맷용
- 유니코드 마커 - ❯(프롬프트), ✽(생각 중), ⏺(도구 사용)
- 진행 표시기 - 이전 줄을 덮어씀
- 권한 대화상자 - 상호작용 요소 포함
- 멀티라인 코드 블록 - 문법 강조 이스케이프 코드 포함
ANSI 코드를 제거하면 깨끗한 텍스트에 가까워지지만, 아직 충분하지 않습니다. 진행 표시기는 부분 줄을 남기고, 코드 블록은 구조를 잃고, "에이전트 출력"과 "셸 잡음"의 경계가 모호합니다.
단순한 라우팅(예: "마지막 10줄 보내기")에는 터미널 스크래핑으로 충분합니다. 하지만 AI 모델이 에이전트가 실제로 무엇을 했는지 이해하고 문맥 있는 프롬프트를 생성해야 하는 지능적 라우팅에는 깨끗하고 구조화된 데이터가 필요합니다.
Claude Code는 세션을 어떻게 저장할까
Claude Code는 JSONL 형식(한 줄에 JSON 객체 1개)으로 구조화된 세션 로그를 씁니다. 해석 경로는 다음과 같습니다.
tmux pane → ps process-tree BFS
→ agent PID (first descendant whose command matches claude/codex/amp/opencode)
→ ~/.claude/sessions/{PID}.json
→ sessionId field
→ ~/.claude/projects/*/{sessionId}.jsonl
→ conversation turns (user/assistant messages)각 대화 턴은 메시지 역할, 내용, 메타데이터를 포함하지만 터미널 포맷은 없습니다. 이것이 Claude Code가 내부적으로 대화 문맥을 유지할 때 사용하는 데이터입니다.
해석 체인
MadoHub은 JSONL 파일을 다단계 체인으로 해석합니다.
1단계: PTY 프로세스 ID 가져오기
각 터미널 타일은 tmux 페인을 기반으로 하며, MadoHub은 터미널이 생성되는 순간부터 그 페인의 PID를 추적합니다. 이 PID는 실제 에이전트 프로세스(보통 페인 셸의 하위 프로세스로 실행됨)를 찾기 위한 출발점입니다.
2단계: 에이전트 PID와 세션 파일 찾기
Claude Code는 ~/.claude/sessions/에 세션 디렉터리를 유지하지만, 각 파일은 세션 ID가 아니라 에이전트의 프로세스 ID로 이름이 지정됩니다. 이 PID를 찾기 위해 MadoHub은 tmux 페인에서부터 프로세스 트리를 순회합니다(하위 프로세스에 대한 ps -eo pid=,ppid=,comm= 너비 우선 탐색). 명령어 이름이 알려진 에이전트(claude, codex, amp, opencode)와 일치하는 첫 번째 프로세스를 찾을 때까지 탐색합니다. 그런 다음 MadoHub은 해당 PID의 ~/.claude/sessions/{PID}.json을 읽습니다. 이 파일에는 실제 대화 로그를 가리키는 sessionId 필드가 들어 있습니다.
3단계: JSONL 찾아서 읽기
MadoHub은 세션 파일에서 sessionId를 가져와 ~/.claude/projects/의 모든 하위 디렉터리를 스캔해서 {sessionId}.jsonl이라는 파일을 찾습니다 - 그 파일이 트랜스크립트입니다. MadoHub은 이를 파싱해서 마지막 assistant 메시지, 즉 Claude Code의 가장 최근 응답을 추출합니다.
이 메시지는 깨끗한 텍스트입니다. ANSI 코드도 없고, 터미널 아티팩트도 없고, 진행 표시기도 없습니다. Claude Code가 의도한 형식 그대로 "말한" 내용입니다.
4단계: 해석에 실패했을 때
프로세스 트리 탐색으로 일치하는 에이전트 프로세스를 찾지 못하거나, 세션 파일이 이미 사라진 경우(예: 프로세스가 종료됨), MadoHub은 해당 턴에 사용할 수 있는 JSONL 경로가 없습니다. 이 경우 터미널 출력 추출로 fallback합니다 - 세션 로그를 전혀 생성하지 않는 에이전트에 쓰는 것과 동일한 ANSI 제거 방식입니다.
깨끗한 응답이 가능하게 하는 것
깨끗하고 구조화된 응답이 있으면 라우팅 변환은 다음을 할 수 있습니다.
문맥 이해: AI 라우팅 모델은 실제 의미 내용 - "세션 쿠키 대신 JWT 토큰을 사용하도록 인증 미들웨어를 리팩터링했습니다. src/auth/middleware.ts, src/auth/tokens.ts, src/config/auth.ts의 3개 파일을 변경했습니다." - 을 받습니다. 색깔만 섞인 터미널 출력이 아닙니다.
정확한 프롬프트 생성: "src/auth/middleware.ts, src/auth/tokens.ts, src/config/auth.ts의 JWT 토큰 마이그레이션을 검토하세요. 토큰 갱신 흐름이 만료를 올바르게 처리하는지, 미들웨어가 토큰 서명을 제대로 검증하는지 확인하세요."
코드 블록 보존: 에이전트 응답에 코드 조각이 포함되어 있으면, 터미널 줄바꿈과 ANSI 색상 코드에 의해 망가지지 않고 원래 포맷이 유지됩니다.
캐싱과 성능
JSONL 파일은 긴 세션에서 매우 커질 수 있습니다. MadoHub은 타일 ID별 마지막 해석 응답을 캐시해서 매 라우팅 트리거마다 전체 파일을 다시 파싱하지 않도록 합니다. 캐시는 새로운 완료가 감지될 때 무효화됩니다.
해석 체인도 흔한 경우에 맞춰 최적화되어 있습니다. 프로세스 트리 탐색과 세션 파일 읽기는 빠르며, 대부분의 경우 해석에 성공합니다. ~/.claude/projects/를 스캔해서 일치하는 {sessionId}.jsonl 파일을 찾는 것은 모든 프로젝트 하위 디렉터리를 검색해야 하므로 더 비용이 큰 단계입니다.
한계
JSONL 기반 상태 감지는 더 이상 Claude Code에만 국한되지 않습니다 - Codex도 이제 자체 JSONL 기반 상태 파이프라인을 갖추고 있습니다(task_complete → completed, turn_aborted/error → failed, user_message/task_started/agent_message → working, ri_message → stopped). 에이전트 간에 여전히 다른 부분은 응답 텍스트 추출입니다. 이 글에서 설명한, 트랜스크립트에서 마지막 assistant 메시지를 그대로 읽어오는 깨끗하고 구조화된 세션 로그 형식은 Claude Code에 특화되어 있습니다. Codex와 셸 명령의 경우, MadoHub은 실제 응답 텍스트를 얻기 위해 여전히 ANSI 제거가 포함된 터미널 출력 추출로 fallback합니다.
raw와 full-output 변환 타입은 터미널 출력을 직접 사용하기 때문에 어떤 에이전트와도 동작합니다. ai-routing과 summary 변환은 깨끗한 JSONL 내용을 받는 Claude Code에서 가장 좋은 결과를 냅니다.
왜 중요한가
라우팅 출력의 품질은 추출된 응답의 품질에 정비례합니다. Garbage in, garbage out. 터미널 원시 출력이 아니라 구조화된 세션 로그를 읽으면, MadoHub은 소스 에이전트가 실제로 무엇을 생성했는지 가장 충실하게 표현한 데이터를 얻습니다.
이것은 복리 효과를 내는 설계 결정입니다. 더 나은 추출 → 더 나은 변환 프롬프트 → 더 정확한 대상 에이전트 지시 → 낭비되는 사이클 감소 → 더 빠른 워크플로 수렴. 깨끗한 추출에 대한 투자는 이후 라우팅 파이프라인의 모든 단계에서 보상을 줍니다.