
동일한 버그를 반복해서 해결하지 않기 위해 3.3 GB의 코딩 에이전트 로그를 인덱싱했습니다
요약
코딩 에이전트의 세션 로그를 활용해 과거의 디버깅 기록을 효율적으로 찾는 방법을 다룹니다. Claude Code, Codex 등 다양한 에이전트의 로그 형식이 다르기 때문에 이를 통합적으로 인덱싱하는 과정의 필요성을 설명합니다.
핵심 포인트
- 코딩 에이전트의 로그는 훌륭한 엔지니어링 노트 역할을 함
- Claude Code 등 에이전트마다 로그 저장 방식과 형식이 상이함
- 단순 grep 검색은 도구 출력값 때문에 효율이 떨어질 수 있음
- 다양한 에이전트 로그를 통합 관리하기 위한 인덱싱 전략이 필요함
몇 주 전, 저는 이미 디버깅했다고 확신했던 postgres 연결 누수(connection leak)를 디버깅하며 저녁 시간 대부분을 보냈습니다. 비슷한 문제가 아니었습니다. 동일한 서비스에서, 동일한 pgxpool 증상을 보이는 바로 그 문제였습니다. 자정쯤 되었을 때 저는 예전의 해결책을 찾아냈습니다. git 히스토리에도 없었고, 메모에도 없었습니다. 당연히 아무것도 적어두지 않았으니까요. 그것은 4월의 Claude Code 트랜스크립트(transcript) 안에 있었고, ~/.claude/projects/ 아래의 JSONL 파일에 내내 자리 잡고 있었습니다.
이 파일은 모든 코딩 에이전트가 전체 세션 트랜스크립트를 디스크에 저장하기 때문에 존재합니다. 전부 다 말이죠. 아무도 이 파일들을 읽지 않고, 대부분의 사람들은 존재조차 모르지만, 에이전트를 집중적으로 사용하는 작업을 몇 달간 하고 나면 이 파일들은 당신이 쓰지 않은 최고의 엔지니어링 노트가 됩니다. 저의 경우 세 개의 에이전트에 걸쳐 3.3 GB, 대략 5만 개의 메시지가 쌓여 있었습니다.
그래서 당연히 첫 번째로 할 수 있는 행동은 grep을 사용하는 것이었습니다.
grep -r "connection pool" ~/.claude/projects/ | head
이것은 기술적으로는 작동하지만 실질적으로는 작동하지 않습니다. Claude Code 트랜스크립트는 텍스트가 .message.content에 위치하는 JSONL 형식인데, 이것은 때로는 문자열(string)이고 때로는 타입이 지정된 블록(typed blocks)의 배열(array)입니다. 도구 출력(Tool outputs)이 바이트 수를 차지하는 대부분을 차지하기 때문에, 대부분의 검색 결과는 사람이 말한 내용이 아니라 40 KB 크기의 cat 결과물 내부에 있습니다. 그리고 그것은 제가 실제로 사용하는 여러 에이전트 중 단 하나일 뿐입니다. Codex는 다른 레이아웃(~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl 및 이를 부분적으로 중복하는 별도의 history.jsonl)을 가집니다. opencode는 모든 것을 SQLite에 보관합니다. 이 중 하나를 위해 jq 파이프라인을 구축하는 데는 10분이 걸리겠지만, 8개를 위한 jq 파이프라인을 유지 관리하는 것은 불가능할 것입니다.
제가 8개라는 것을 아는 이유는 결국 그것들을 목록화했기 때문입니다:
이 중 어느 것도 문서화되어 있지 않습니다. 일부는 적극적으로 이상하기까지 합니다. Claude Code 프로젝트 디렉토리는 /를 -로 대체하여 프로젝트 경로를 인코딩하는데, 이는 -Users-me-my-app이 my/app이었는지 아니면 문자 그대로 my-app이라는 이름의 디렉토리였는지 모호하다는 것을 의미합니다. 문자열만으로는 알 수 없으며, 파일 시스템을 탐색하여 무엇이 존재하는지 확인해야 합니다. Gemini는 동일한 세션을 체크포인트 .json과 이벤트 로그 .jsonl 두 가지로 작성하며, .jsonl에는 $rewindTo 이벤트가 포함되어 있어 이를 읽는다는 것은 단순히 줄을 파싱하는 것이 아니라 편집 내용을 다시 재생(replaying)하는 것을 의미합니다.
그 결과물이 바로 deja-vu입니다. 이는 이 모든 것을 하나의 로컬 인덱스로 파싱하는 단일 Go 바이너리입니다:
curl -fsSL https://raw.githubusercontent.com/vshulcz/deja-vu/main/install.sh | sh
deja install --auto
deja "connection pool exhausted"
스크린샷은 제가 실제로 원했던 속성을 보여줍니다. Claude Code, Codex, Antigravity 세션의 히트(hits)가 한꺼번에 반환됩니다. 4월의 수정 사항은 제가 4월에 어떤 에이전트와 대화하고 있었는지와 상관없이 찾아낼 수 있습니다. 또한 이미 디스크에 있는 것을 인덱싱하기 때문에 콜드 스타트(cold-start) 문제가 없습니다. 설치할 프록시도, 캡처 훅(capture hook)도, "한 달 뒤면 유용해질 거야"라는 식의 기다림도 필요 없습니다. 히스토리는 이미 그곳에 있었고, 단지 쿼리(query)할 수 없었을 뿐입니다.
왜 임베딩(embeddings)을 사용하지 않는가
모든 에이전트 메모리 프로젝트가 벡터 스토어(vector store)를 지향하기 때문에 타당한 질문입니다. 저도 고려해 보았지만, 지루한 이유로 사용하지 않기로 결정했습니다. 이 로그들을 검색할 때 저는 에러 문자열, 함수 이름, 또는 플래그를 검색하기 때문입니다. "SSL_ERROR_SYSCALL", resolveEncodedPath, --force-with-lease. 정확한 토큰(tokens)들 말입니다. 여기서는 의미론적 유사성(Semantic similarity)이 오히려 방해가 됩니다. 저는 제 에러와 유사한 것을 원하는 것이 아니라, 그 문자열이 문자 그대로 나타난 세션을 원합니다. 왜냐하면 바로 그곳에 해결책이 있기 때문입니다.
따라서 이 인덱스는 매우 고전적인 방식입니다. 레코드 파일, varint로 인코딩된 토큰 포스팅(token postings), 그리고 작은 매니페스트(manifest)로 구성됩니다. 제 코퍼스(corpus)에서 약 14초 만에 처음부터 다시 빌드되며, 웜 쿼리(warm queries)에는 한 자릿수 밀리초(ms) 내로 응답합니다. 데몬(daemon)은 없습니다. CLI가 실행될 때마다 파일 크기와 수정 시간(mtime)을 확인하여 변경된 사항을 점진적으로 수집(ingest)합니다. 전체 시스템은 하나의 정적 바이너리(static binary)로 되어 있는데, 이는 원래 머신 간에 scp로 주고받던 도구로 시작되었고, 그 기능을 그대로 유지하고 싶었기 때문입니다.
나를 괴롭혔던 부분들
다른 프로세스가 활발하게 쓰고 있는 파일들을 점진적으로 인덱싱(incremental indexing)하는 과정이 실제 엔지니어링의 핵심이라는 사실이 드러났습니다.
찢어진 꼬리(Torn tails). 인덱서가 실행되는 도중에 에이전트가 쓰기 작업을 수행 중일 수 있으며, 이로 인해 파일이 JSON 라인의 절반만 작성된 상태로 끝날 수 있습니다. 깨진 라인을 건너뛰는 것은 쉽습니다. 진짜 버그는 다음 실행 시에 발생합니다. 만약 새로운 파일 끝(end-of-file)부터 다시 시작한다면, 지난번에 절반만 작성되었던 메시지가 이제는 완전한 형태가 되어 나타나게 되는데, 결과적으로 그 메시지를 영원히 건너뛰게 됩니다. 해결책은 인덱싱 시점에 마지막 완전한 줄바꿈(newline)의 오프셋(offset)을 저장해 두었다가, 파일이 끝난 지점이 아니라 그 지점부터 다시 시작하는 것입니다.
취소된 기록(History that un-happens). 저는 트랜스크립트(transcripts)가 추가 전용(append-only)이라고 가정했습니다. 하지만 Gemini와 Grok은 모두 이 가정을 위반합니다. 되감기(rewind) 기능이 파일을 잘라내고(truncate) 다른 내용으로 다시 키우기 때문입니다. 크기 기반의 변경 감지 방식은
filepath.Dir이 루트에서 작동할 때. dir != / 조건으로 종료되는 순회(walk-up) 루프는 Windows에서 무한히 실행됩니다. 그 이유는 `filepath.Dir(
이것이 하지 못하는 것
이 도구는 "우리가 논의했던 그 인증 문제"를 찾아내지는 못합니다. 이는 시맨틱 (semantic) 검색이 아닌 토큰 (token) 검색이기 때문입니다. Cursor IDE 채팅을 사용하려면 sqlite3 CLI가 PATH에 설정되어 있어야 합니다. 단 하나의 하네스 (harness)를 위해 C 의존성 (dependency)을 추가하는 것을 거부했기 때문입니다. 또한, 문서화되지 않은 8가지 형식이 있다는 것은 업스트림 (upstream)에서 언제든 깨뜨릴 수 있는 8개의 파서 (parser)가 존재함을 의미합니다. 테스트 스위트 (test suite)에는 모든 형식에 대한 피스처 (fixtures)와 CI 내의 교차 하네스 스위트 (cross-harness suite)가 포함되어 있지만, 현실적으로 현장 보고 (field reports)는 유지보수 모델의 일부입니다. 만약 사용 중인 에이전트가 지원되지 않거나 형식이 변경되었다면, 개인정보를 삭제한 샘플을 포함하여 이슈 (issue)를 제기하는 것이 가장 빠른 방법입니다. Grok Build 파서는 이슈 제기부터 PR (Pull Request) 병합까지 단 하루 만에 완료되었으며, 그 내용의 대부분은 이슈를 제기한 본인이 작성했습니다.
github.com/vshulcz/deja-vu — MIT, 런타임 의존성 (runtime dependencies) 없음.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기
