CH 10 · 세션 로그가 곧 진실의 원천
본 장 목표
CH 09에서 "장부"(세션)에 대해 다루었습니다. 이번 장에서는 그것을 풀어헤쳐 봅니다. 왜 그것이 기계 전체의 진실의 원천인지 — 모델이 기억하는 것, 사용자가 보는 Trajectory, 내보낼 수 있는 대화 기록, 새로 갈라 만들 수 있는 fork까지 모두 이 단 하나의 로그로부터 파생되며, 그 로그는 오직 증가만 하고 결코 수정되지 않습니다.
한 줄 정리: 세션 로그 = 에이전트의 장부, 기억, 기록 보관소
Session의 공식 정의는 단단합니다. 세 가지로 나뉩니다.
- 추가 전용(append-only): 끝에 기록만 더하며, 기존 항목을 수정하거나 삭제하지 않습니다.
- 타입이 지정된 이벤트(typed events): 각 줄은 자유 형식의 텍스트가 아니라 "이벤트" 한 종류입니다.
turn/start,user/message,assistant/message,tool/result,turn/end등. - 단일 진실의 원천(single source of truth): 에이전트 상호작용의 전체 히스토리에서, 진짜인 것은 이것뿐이며 나머지는 모두 이로부터의 투영입니다.
CH 09와 이어 붙이면 매우 자연스럽습니다. 지난 장에서 본 "메시지 흐름"의 각 단계는 사실 이 로그에 이벤트를 쓰는 행위입니다 — turn/start가 turn을 열고, step/start가 step을 시작하며, user/message가 사용자가 보낸 것을 기록하고, assistant/message가 모델의 응답을 기록하며, tool/result가 실행된 도구를 기록하고, turn/end가 turn을 닫습니다.
기억법: 흐름은 "무슨 일이 일어나고 있는지", 로그는 "일어난 일의 기록", 둘은 1대1로 대응합니다.
왜 "진실의 원천"인가
공식 문서의 표현은 다음과 같습니다.
모델 메시지 히스토리는 로그로부터 파생되며, 별도로 저장되지 않습니다.
의미는 이렇습니다. dsh에는 대화 기록의 두 번째 사본이 없습니다. "모델이 앞에서 한 말을 기억한다"고 느끼지만, 모델이 보는 히스토리는 로그로부터 투영됩니다. 보이는 Trajectory 뷰, 내보낼 수 있는 대화 기록, 열 수 있는 fork — 모두 동일한 로그로부터 렌더링됩니다.
- 모델 대화 히스토리:
deriveMessages()가 로그로부터 모델이 보는Message[]를 투영합니다 — 따라서 "모델이 기억하는 것" = "로그에 있는 것"; - Trajectory 뷰: CH 04에서 본
ASSISTANT / TOOL타임라인은 그저 로그의 시각화일 뿐입니다; - 대화 기록 / 내보내기: 전체 대화 텍스트는 로그로부터 재구성됩니다;
- fork 가지치기: 특정 히스토리 노드에서 새 세션을 시작합니다;
- 텔레메트리 / 통계: 토큰 사용량, 소요 시간 등이 로그로부터 계산됩니다;
- 영속화 파일:
$DSH_HOME/sessions의session.jsonl.zstd.
왜 이렇게 설계되어야 할까요? 진실의 원천이 하나여야만 "UI에 보이는 것, 모델이 기억하는 것, 내보내는 것 — 세 사본이 서로 어긋나는 상황"이 절대 생기지 않기 때문입니다. 다른 모든 뷰는 동일한 로그의 투영이며, 규칙이 같으므로 항상 일관됩니다.
"모델이 보는 것이 곧 기록된 것": 설계의 축
이것은 dsh의 엄격한 규칙이며, 앞 장에서 언급하고 여기서 펼칩니다.
모델 요청에 도달하는 모든 것은 로그로부터 재구성 가능해야 하며, 런타임은 이를 불변식(invariant)으로 검사합니다.
여기서 두 가지 직접적인 결과가 따라 나옵니다.
- 모델이 볼 새로운 것을 더하려면, 새로운 이벤트 타입을 더해야 합니다. 예를 들어 Agent에게 주입한 컨텍스트를 보여 주고 싶다면, 로그를 우회해 요청에 직접 끼워 넣을 수 없습니다. 대신 새 세션 이벤트를 정의해 로그에 쓰고, 로그로부터 투영해 내보내야 합니다. 이렇게 하면 모든 단계가 추적 가능해지며, 홈페이지의 "모든 실행은 추적 가능"이라는 문장의 근본적 보장이 됩니다.
- 로그는 무손실입니다. 모델이 반환한 원시 스트리밍 청크까지 그대로 보존되므로(
assistant/chunk), 재생을 토큰 단위로 충실하게 할 수 있고 UI도 똑같이 복원할 수 있습니다.
한 줄로 요약하면: 이 설계는 "추적 가능"이라는 슬로건이 아니라, 아키텍처적으로 필연적인 결과로 만들어 줍니다.
CH 04와의 간단한 교차 참조: 컨텍스트를 압축할 때 쓰던 /compact 명령은, 내부적으로 그저 "압축했다"는 행위를 로그에 기록하는 것일 뿐이며(compaction/* 이벤트), 로그로부터 모델에 더 다듬어진 형태를 투영합니다. 히스토리를 다시 쓰지 않습니다 — 원래 이벤트는 로그에 그대로 있고, 모델이 보는 투영만 재정렬될 뿐입니다. 그래서 압축 후에도 Agent가 "더 다듬어진 버전"을 기억하듯 보이지만, 원래 기록은 온전히 남아 있습니다.
실제로는 어떤 모습인가: 로컬 세션 로그
자신의 컴퓨터로 돌아옵니다. 이 컴퓨터를 예로 들면(CH 05에서 headless를 돌렸다면 여기에 이미 존재합니다).
C:\Users\mortal\.dsh\
├─ profiles\ ← 프로필 목록(CH 08의 "메뉴판")
├─ sessions\ ← 세션 로그가 여기 있습니다
│ ├─ --E-software-workspace-DeepSeek~0020harness~0020demo--\
│ │ └─ session-307edce2-...\session.jsonl.zstd ← CH 05 headless 실행 기록
│ └─ --E-software-workspace-doubaowork-DeepSeekHarnessGuide--\
│ └─ session-f417b4dd-...\session.jsonl.zstd ← 이 프로젝트에서 사용한 세션들
├─ storages\
├─ settings.yaml
└─ .credentials.yaml몇 가지 짚어둘 점입니다.
- 작업 영역별 디렉터리: 디렉터리 이름은 작업 영역 경로를 escape한 형태입니다(공백은
~0020이 됨). 따라서 어디서 작업하며 어떤 세션이 만들어졌는지 한눈에 볼 수 있습니다. - 세션마다 폴더 하나, 그 안에
session.jsonl.zstd가 있습니다 — JSONL 줄 단위 추가 + zstd 압축 영속화 파일입니다. - CH 05의 "Ungrouped"와 맞물려: 이 파일들은 작업 영역별로 저장되지만, Web UI의 세션 목록은 headless 실행을 Ungrouped로 묶습니다 — 두 차원을 혼동하지 마십시오.
- 이 파일들은 세션의 "기록 보관소"이므로 함부로 지우지 마십시오. 지우면 그 세션의 "기억"이 정말로 사라집니다.
fork: 로그로부터 새 세션을 자라게 하기
로그를 완전히 재생할 수 있으므로 자연스럽게 "어떤 지점에서 다시 자라게 하기"가 가능합니다. 공식 팀이 명명한 이름은 fork입니다. 안정된 위치 이전의 모든 이벤트를 복제해 새 세션의 시작점으로 삼고, 그 뒤로 갈라지는 방식입니다. 용도는 직관적입니다. 원래 세션을 건드리지 않고, 특정 히스토리 노드에서 새 길을 시험해 보고 싶을 때. 구체적인 사용법은 후반 실습 장에서 다루며, 여기서는 "그런 것이 존재하며, 그것이 가능하기는 바로 로그를 완전히 재생할 수 있기 때문"이라는 사실만 기억하면 됩니다.
이번 장에서 배운 것
아래 항목들을 스스로 점검할 수 있으면 합격입니다.
- [ ] 세션 로그의 세 가지 특성(추가 전용 / 타입이 지정된 이벤트 / 단일 진실의 원천)을 말할 수 있다
- [ ] CH 09의 메시지 흐름을 로그 이벤트와 1대1로 대응시킬 수 있다(
turn/start,user/message,assistant/message,tool/result,turn/end) - [ ] "모델 히스토리는 로그로부터 파생되며 별도로 저장되지 않는다"를 설명하고, 왜 이것이 어긋나는 세 사본을 막는지 말할 수 있다
- [ ] "모델이 보는 것이 곧 기록된 것"의 두 가지 결과(새로 더하기 = 새 이벤트 타입 추가; 무손실 로그는 토큰 단위 재생 가능)를 말할 수 있다
- [ ] 로컬 세션 로그의 위치를 알고 있으며(
$DSH_HOME/sessions의 작업 영역별 디렉터리,session.jsonl.zstd), Web UI의 "Ungrouped"와의 차이를 안다
