마지막 JSONL 라인은 상태 머신(State Machine)이 아니다
요약
Claude Code의 JSONL 로그를 파싱하여 로컬 상태 모니터를 구축할 때 발생하는 기술적 난제와 해결 방법을 다룹니다. 단순한 파일 수정 시간이나 타임스탬프가 아닌, 이벤트 순서와 의미론적 맥락을 기반으로 상태를 결정해야 함을 강조합니다.
핵심 포인트
- JSONL의 마지막 라인이 반드시 세션의 종료를 의미하지는 않음
- 파일 수정 시간(Modification time)을 상태 매핑의 기준으로 삼는 것은 위험함
- 정확한 상태 추론을 위해 이벤트 순서와 유예 기간(Grace window) 설정이 필요함
- 사이드체인 이벤트와 메인 스레드 이벤트를 구분하여 처리해야 함
어시스턴트 메시지로 트랜스크립트(Transcript)가 끝났습니다. Claude Code가 종료된 것일까요?
반드시 그렇지는 않습니다.
그 라인은 현재 턴(Turn)의 끝일 수도 있습니다. 오래된 세션에 속해 있을 수도 있습니다. Claude Desktop이 그 이후에 장부 기록용 메타데이터(Bookkeeping metadata)를 작성했을 수도 있습니다. 사용자가 이미 응답했을 수도 있습니다. 또는 어시스턴트 메시지가 실제로는 종료 이유(Stop reason)를 담고 있는 API 에러이며, 그 이유가 기만적으로 최종적인 것처럼 보일 수도 있습니다.
이것이 로컬 상태 모니터(Local status monitor)를 구축할 때 까다로운 부분입니다. JSONL을 파싱(Parsing)하는 것은 쉽습니다. 하지만 현재 그 증거가 무엇을 의미하는지 결정하는 것이 바로 엔지니어링 작업입니다.
파일 활동은 증거이지, 상태가 아니다
Claude Code와 Claude Desktop은 이미 기기에 유용한 기록을 남깁니다. 로컬 컴패니언(Local companion)은 트랜스크립트를 호스팅 서비스로 전송하지 않고도 이를 검사할 수 있습니다. 이는 강력한 프라이버시 경계를 제공하지만, 서버에 단일 권위 있는 상태를 물어볼 수 있는 편리함은 제거합니다.
첫 번째 구현 시의 유혹은 수정 시간(Modification time)을 상태에 직접 매핑하는 것입니다:
최근 수정됨 -> 작업 중 (working)
수정되지 않음 -> 유휴 상태 (idle)
이는 양방향 모두에서 실패합니다. 세션이 작업 중일 때 쓰기(Write) 작업이 폭발적으로 발생할 수 있습니다. 메타데이터 쓰기가 실제 턴이 끝난 후에 파일을 건드릴 수도 있습니다. 조용한 파일은 모델이 생각 중이거나, 도구(Tool)를 기다리거나, 정체되었거나, 혹은 단순히 방치된 상태임을 의미할 수도 있습니다.
타임스탬프(Timestamp)는 유용하지만, 결정의 전부를 담을 수는 없습니다.
완료(Completion)는 새로운 노이즈를 견뎌내야 한다
Claude의 경우, 터미널 어시스턴트 이벤트는 '사용자 차례(Your-turn)' 결정을 위한 하나의 입력값입니다. 스캐너(Scanner)는 이벤트 식별자와 의미론적 타임스탬프(Semantic timestamp)도 추적합니다. 이는 Claude Desktop이 마지막 어시스턴트 이벤트가 발생한 후 몇 초 뒤에 활동 기록을 업데이트할 수 있기 때문에 중요합니다.
만약 모든 새로운 데스크톱 타임스탬프가 "사용자가 돌아왔음"을 의미한다면, 해당 장부 기록 쓰기 작업은 알림이 울리기 전에 완료 상태를 지워버릴 것입니다. 우리는 완료 직후의 짧은 쓰기 작업에 대해 유예 기간(Grace window)을 둡니다. 유예 기간이 훨씬 지난 후의 활동은 다릅니다. 그것은 사용자가 아마도 돌아왔다는 증거이므로, 이전의 완료 상태는 억제되어야 합니다.
이러한 구분은 관찰된 이벤트 순서(event ordering)에서 직접적으로 도출되었습니다. 이는 일반적인 파일 와처 (file watcher)가 추론할 수 있는 것이 아닙니다.
새로운 턴은 이전 답변을 취소합니다
파서 (parser)는 단순히 마지막 물리적 라인이 아니라, 가장 최근의 의미 있는 메인 스레드 (main-thread) 이벤트를 기준으로 작동합니다. 어시스턴트 (assistant) 완료 후 발생하는 사용자 이벤트는 이전의 핸드오프 (handoff)가 종료되었음을 의미합니다. 터미널 스톱 (terminal stop)이 없는 스트리밍 어시스턴트 (streaming assistant) 이벤트는 계속 작동 중인 상태로 남습니다.
사이드체인 (sidechain) 이벤트는 별도의 처리가 필요합니다. 도구 (tool) 및 서브에이전트 (subagent) 트래픽은 메인 어시스턴트 이벤트 이후에 나타날 수 있습니다. 사이드체인 완료가 메인 턴을 종료하도록 두면 노이즈가 섞인 알림이 생성되고, 뒤따르는 사이드체인 라인이 실제 메인 스레드 완료를 가리게 두면 알림을 놓치게 됩니다. 따라서 파서는 먼저 가장 관련 있는 최신 메인 스레드 이벤트를 식별합니다.
API 에러는 또 다른 함정입니다. 속도 제한 (rate-limit) 응답은 중단 사유 (stop reason)를 포함한 어시스턴트 라인으로 표현될 수 있습니다. 그것은 어텐션 (attention)이지만, 성공적인 사용자 턴 (your-turn) 완료는 아닙니다. 에러 마커 (error marker)가 우선권을 가져야 합니다.
상태에는 만료 정책이 필요합니다
정확하게 감지된 완료 상태라 할지라도 영원히 최신 상태로 유지되지는 않습니다.
우리의 스캐너 (scanner)는 작동 중 (working), 사용자 턴 (your turn), 정체됨 (stalled), 어텐션 (attention) 상태를 위해 제한된 윈도우 (bounded windows)를 유지합니다. 이전에 작동 중으로 관찰되었던 세션은 지속적인 침묵 후에 정체됨 상태가 될 수 있습니다. 완료된 턴은 결국 시간이 지나 유휴 (idle) 상태로 돌아갑니다. 어텐션 윈도우 외부의 증거는 무시됩니다.
정확한 임계값 (thresholds)은 변경될 수 있는 구현 세부 사항입니다. 더 중요한 것은 불변의 법칙입니다:
이전의 증거가 새로운 방해 (interruption)를 만들어내서는 안 된다
이 규칙은 앱 실행 시에도 적용됩니다. 기존에 완료된 턴은 기준 히스토리 (baseline history)가 됩니다. 모니터가 시작되기 전에 종료된 모든 세션에 대해 알람을 울리지는 않습니다.
여러 파일은 여전히 하나의 신호로 통합되어야 합니다
실제 환경에는 중복된 Claude Desktop 기록, 아카이브된 스레드 (archived threads), 메인 트랜스크립트 (main transcripts), 그리고 서브에이전트 트랜스크립트 (subagent transcripts)가 존재할 수 있습니다. 상태 표시줄 (status bar)은 그러한 가공되지 않은 인벤토리 (raw inventory)를 그대로 렌더링할 수 없습니다.
모니터는 아카이브된 세션과 기계 주도 세션 (machine-driven sessions)을 필터링하고, 중복된 세션 ID (session IDs)를 제거한 다음, 제공자 (provider)를 위해 가장 시급한 현재 상태를 선택합니다. 서브에이전트 (Subagent) 알람은 선택 사항 (opt-in)으로 유지되는데, 이는 오케스트레이션 (orchestration)이 여러 자식 턴 (child turns)을 한꺼번에 처리할 수 있는 반면, 인간은 오직 부모 워크플로 (parent workflow)에만 관심을 두기 때문입니다.
이는 "실시간 AI 관측성 (real-time AI observability)"이라고 주장하는 것보다는 덜 인상적일지 모르지만, 훨씬 더 유용합니다. 사용자는 방어 가능한 단 하나의 답변, 즉 '작동 중', '당신의 차례', 또는 '주의 필요'가 필요합니다.
테스트는 단순히 피스처 (fixtures)가 아니라 이벤트 순서를 인코딩해야 합니다
중요한 회귀 테스트 (regression cases)는 시퀀스 (sequences)입니다:
- 어시스턴트 (assistant) 완료 후 사용자 답변;
- 완료 후 짧은 장부 기록 (bookkeeping) 쓰기;
- 완료 후 실제 이후의 데스크톱 활동;
- 완료되지 않은 메인 출력 이후의 사이드체인 (sidechain) 완료;
- 중단 사유 (stop reason)가 포함된 API 오류 어시스턴트 라인;
- 방금 수정된 파일과 함께 나타나는 오래된 의미론적 활동 (semantic activity).
각 시퀀스는 상태 전이 (state transition)를 보호합니다. 단지 하나의 JSON 객체가 디코딩될 수 있는지만 확인하는 파서 (parser) 테스트는 실제 실패 사례를 놓칠 것입니다.
경계 (The boundary)
로컬 모니터는 Claude가 출력을 생성하고 있는지, 턴을 종료했는지, 또는 주의가 필요한 것처럼 보이는지를 알려줄 수 있습니다. 하지만 요청된 작업이 올바른지 또는 생성된 코드가 안전한지는 증명할 수 없습니다. "당신의 차례"라는 것은 답변이 필요하다는 의미이지, 엔지니어링 작업이 완료되었다는 의미가 아닙니다.
Agent Island는 Claude Code 및 Codex 상태 표면 (status surface)을 위해 이 로컬 모델을 사용합니다. 최신성 경계 (freshness boundaries)와 공개된 범위를 포함한 더 자세한 엔지니어링 가이드는 여기에서 확인할 수 있습니다: Claude Code status monitor.
이 프로젝트는 오픈 소스입니다: GitHub에서 스캐너와 테스트를 확인하세요.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기