DeepSeek Harness · 세션과 루프

Model-visible ⟺ logged: 눈앞에서 터지는 불변조건

모델이 본 모든 것은 로그에서 재구성할 수 있어야 하고, 요청을 보내기 전에 그 자리에서 다시 검증하며, 검증에 실패하면 바로 크래시합니다.

강의 목표읽고 나면 세 가지를 말할 수 있어요. DSH가 대화의 진실은 하나뿐이라 정한 이유, 모델이 본 모든 것이 세션 로그에서 재구성되어야 한다는 점; 요청을 보내기 전에 그 자리에서 다시 만들고 바이트 단위로 비교하는 방식; 비교에 실패했을 때 경고 한 줄조차 남기지 않고 그 자리에서 크래시하는 이유.
인터랙티브 데모 · 로그 변조 실험실

먼저 해보고 설명해요. 왼쪽은 뒤에만 이어 붙는 이벤트 로그, 오른쪽은 로그에서 다시 만든 메시지 배열과 모델로 곧 보낼 요청이에요. 「재생」을 눌러 이벤트가 흘러드는 걸 보세요. 끝나면 한 번 악당이 되어 보세요: 로그 한 줄을 지우거나, 로그를 건너뛰고 요청을 직접 바꾼 뒤 「다음 요청 보내기」를 누르면, 비교가 한 줄씩 체크하고 갈라진 곳에 빨간 엑스, 그리고 눈앞에서 터지는 걸 볼 수 있어요.

추가만 되는 이벤트 로그session-1
(빈 로그)
로그에서 재구성한 메시지 배열deriveMessages()
(아직 모델에 보여줄 내용이 없어요)
모델로 곧 보낼 요청
(요청이 아직 만들어지지 않았어요)
「재생」을 눌러 이벤트가 로그로 한 줄씩 흘러드는 걸 보세요.

실험대의 비교 순서는 소스와 같고, 빨간 띠의 오류는 소스 원문을 유지합니다: 비교 로직은 packages/core/agent-loop/src/invariant.ts 31–42행, 오류 접두사 조립은 packages/runtime-diagnostics/invariants/src/index.ts 62행. 확인일 2026-08-13.

아이디어 1 · 진실은 하나

어떤 문제를 푸는가.대부분의 채팅 프로그램은 대화를 두 벌로 둡니다. 메모리 배열 하나, 디스크 아카이브 하나, 각자 따로 씁니다. 어느 날 프로세스가 죽으면 아카이브에서 세션을 복구하는데, 복구된 기록에는 모델이 실제로 본 도구 결과가 하나 빠져 있어요. 모델의 다음 답이 전부 엇나가고, 두 상태가 서로를 증명하지 못하니 원인조차 찾을 수 없습니다. 진실이 두 곳에 있으면 언젠가 어긋납니다.

아이디어는 무엇인가.DSH는 진실을 한 벌로 압축합니다. 규칙은 저장소 루트 AGENTS.md 107행에 있어요:

Model-visible ⟺ logged: anything that reaches a model request must be reconstructable from the session log; a new model-visible input requires a session event. 출처: deepseek-harness-master 저장소 AGENTS.md 107행, 확인일 2026-08-13

풀어 말하면, 세션 로그는 추가만 되는 이벤트 스트림이고, 모델에게 보이게 하려는 내용은 먼저 이벤트 한 줄로 로그에 써야 합니다. 메시지 기록은 로그에서 파생되며, 공식 문서는 “따로 저장하지 않는다”고 합니다. 그래서 DSH에서 로그가 곧 대화 자체이고, 시스템에 두 번째 대화 상태는 없어요.

출처: docs/subsystems/session.zh.md 5행, 확인일 2026-08-13.

그리고 제목의 양방향 화살표 ⟺. 로그로 요청을 이끌어낼 수 있어야 하고, 요청도 로그로 설명되어야 합니다. 로그만 써서 되는 게 아니에요—요청을 보내는 길목에 지키는 사람이 필요합니다. 요청이 나갈 때마다 DSH는 로그에서 그 자리에 있어야 할 메시지 배열을 다시 파생해, 요청에 실제로 실린 것과 전체 문자열로 비교합니다. System Prompt, 모델 이름, 샘플링 파라미터, 도구 목록도 로그의 요청 헤더 스냅샷과 필드마다 맞아야 합니다. 전부 맞아야 통과해요.

추가만 되는 이벤트 로그 요청 헤더 스냅샷 사용자 메시지 어시스턴트 메시지 도구 호출 · 도구 결과 유일한 진실, 다른 곳에 복사본 없음 그 자리에서 재구성 로그에서 파생한 있어야 할 요청 메시지 배열 + 요청 헤더 스냅샷 루프가 실제로 만든 요청 깊게 동결, 통과 후엔 바꿀 수 없음 출구에서 바이트 단위 비교 일치하면 통과·발송 불일치면 그 자리에서 크래시
수업용 구조도: 노드와 연결은 소스 관계를 설명하며, 내용은 수업에 맞게 정리했습니다.

지키는 핵심은 네 줄뿐이라 벽에 붙여 둘 만큼 짧아요. 증명하는 건 하나: 매번 디스패치 전에 DSH가 정말 로그에서 메시지를 다시 파생해 실제 요청과 전체 문자열로 비교하고, 안 맞으면 그 자리에서 실패한다는 것:

packages/core/agent-loop/src/invariant.ts39–42행
    const expected = session.deriveMessages()
    if (JSON.stringify(options.messages) !== JSON.stringify(expected)) {
      fail(`llm request for session "${String(session.id)}" diverges from the dispatch-time durable derivation (log-reconstruction desync)`)
    }
소스 스냅샷 안내: 로컬 저장소 deepseek-harness-master 기준, 확인 파일 packages/core/agent-loop/src/invariant.ts, 확인일 2026-08-13. 코드 블록은 소스 원문을 유지합니다.

세부 하나가 더 있어요. 검사용 파생 함수는 복구·리플레이와 같은 공개 함수 세트입니다. 검사하는 쪽과 검사받는 쪽이 같은 재구성 규칙을 공유하고, 누구도 비공개 경로가 없어요. 요청 헤더 재구성도 이벤트만 훑어 마지막 스냅샷을 취하는 일곱 줄짜리 순수 함수일 뿐입니다.

출처: 전체 검사 순서는 packages/core/agent-loop/src/invariant.ts 22–52행; 요청 헤더 재구성은 packages/core/session/src/request-header.ts 65–71행. 확인일 2026-08-13.

왜 오래가는가.single source of truth(단일 진실 원천)는 데이터베이스 분야에서 오십 년 쓴 오래된 원칙입니다. 장부는 한 권뿐이고 나머지는 전부 장부의 뷰이며, 뷰는 버려도 다시 만들어도 됩니다. DSH는 그 규율을 agent 대화 관리로 옮겼을 뿐이에요. 소스를 Rust로 다시 쓰고 이벤트 타입이 세 배가 되어도, 장부는 한 권이라는 사실은 그대로입니다.

로그가 곧 대화 자체이고, 나머지는 전부 뷰예요.
아이디어 2 · 검증 실패면 바로 크래시, 병을 안고 실행하지 마세요

어떤 문제를 푸는가.지키는 쪽이 어긋남을 발견하고 경고 한 줄만 남긴다고 상상해 보세요. 경고는 이미 잘못된 요청이 모델에 갔다는 뜻입니다. 어떤 플러그인이 로그를 건너뛰고 메시지를 몰래 바꿨고, 그 순간부터 모델이 본 것과 로그가 적은 것은 다른 이야기예요. 로그는 계속 쓰이지만 전부 틀린 장부입니다. 사흘 뒤 누군가 그 로그로 리플레이해 보면 온라인의 이상한 행동을 재현할 수 없어요. 조용한 어긋남이 크래시보다 무섭습니다—조사 비용을 미래로 몰래 넘기니까요.

아이디어는 무엇인가.그래서 DSH는 가장 딱딱한 길을 골랐어요. 비교에 실패하면 예외를 던지고, 이번 요청은 그 자리에서 무효—아예 나가지 않습니다. 설계 노트는 부드러운 안을 분명히 기각했고, “연속 요청을 비교하고 발산하면 경고”라는 대안에 대한 판결은 “위반은 인터페이스 수준에서 표현 불가능해야 하므로 기각”이었어요.

출처: .agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.zh.md 「검토했던 대안」절, 확인일 2026-08-13.

곁에 작은 장치 두 개도 있어요. 검사기는 이벤트 리스너 큐의 맨 앞에 끼워져, 다른 리스너가 먼저 단락해 검사를 조용히 건너뛰지 못하게 합니다—지키는 사람이 요청을 먼저 봐야 해요. 그리고 요청 객체와 메시지 배열은 깊게 동결되어, 검사 통과 뒤 내용을 바꾸는 뒷문을 막습니다.

출처: 큐 선두 등록은 invariant.ts 20–21행과 54행, 동결과 세션 검사는 22–29행. 확인일 2026-08-13.

경고 방안(설계 노트에서 기각) 요청과 로그가 어긋남을 발견 경고 한 줄 잘못된 요청은 그대로 발송 이후 로그는 틀린 장부 리플레이·감사 전부 왜곡 DSH의 선택 요청과 로그가 어긋남을 발견 예외를 던지고 그 자리에서 크래시 요청 무효, 나갈 수 없음 로그는 깨끗하게 유지 버그 고치고 다시 돌리면 됨
같은 어긋남, 두 처리 방식의 결과 비교. 수업용 도식.

왜 오래가는가.이것이 fail-fast(빠른 실패)예요. 크래시는 손실을 0에 잠급니다. 로그에 오염된 턴이 없으니 버그를 고치고 다시 돌리면 됩니다. 병을 안고 실행하는 비용은 복리라서, 오래 갈수록 나쁜 데이터가 늘고 나중엔 언제부터 망가졌는지조차 찾을 수 없어요. 경계에서 한 번 터지는 편이, 사흘 뒤 이상한 현상 속에서 고고학하는 것보다 훨씬 쌉니다. 이 판단은 언어·프레임워크와 무관해요.

요청이 나가는 순간, 로그는 더 이상 모델을 설명할 수 없어요.
아이디어 3 · 로그 하나로 풀세트

앞의 두 아이디어가 각각 힘을 썼고, 보상이 여기서 함께 정산됩니다. 로그가 유일한 진실이니 그 둘레로 능력 세트를 공짜로 얻어요:

Compaction(압축, 너무 긴 기록을 요약으로 바꿈)조차 이 불변조건 아래예요. 압축으로 생긴 요약도 이벤트로 로그에 쓰이고, 압축 뒤 요청도 출구 비교를 통과해야 하며, Compaction 구현에 버그가 있으면 그 자리에서 크래시—도망갈 수 없어요.

왜 오래가는가.이 방식엔 학명이 있어요—event sourcing(이벤트 소싱). 은행·회계 시스템이 오랫동안 써 온 것처럼, 잔액은 저장하지 않고 거래만 남기며 잔액은 항상 거래에서 계산합니다. DSH는 거래 흐름을 대화 이벤트로 바꿨을 뿐이에요. 이벤트 스트림이 유일한 진실인 한, 이 능력들은 계속 공짜 부산물입니다.

가로 비교 · 다들 로그를 쓰는데, 누가 역방향 검증하나

세션 영속화는 코딩 agent의 거의 기본이고, 차이는 방향에 있어요. Grok Build(xAI 오픈소스 Rust 코딩 agent)의 영속화는 일방통행입니다. 메모리의 대화 상태가 주이고 디스크는 종. 메시지마다 메모리 항목을 복사해 디스크 채널에 넣고, 전송 결과는 버리고, 디스크 실패도 대화를 끊지 않으며, 압축 때는 디스크 기록 전체를 한 번에 바꿀 수도 있어요. 합리적인 제품 취사선택이지만, 영속 계층은 정확성 책임을 지지 않습니다—로그를 역으로 파생해 출구 요청과 비교하는 경로가 없어요.

출처: grok-build-main 저장소 crates/codegen/xai-grok-shell/src/session/chat_persistence.rs 30–38행(persist_messagereplace_history), 확인일 2026-08-13.

Claude Code는 클로즈드 소스이고, 공개된 것은 ~/.claude 아래 .jsonl 세션 로그와 복구 기능입니다. 공개 증거상 사후 기록형 영속화에 가깝고, 디스패치 시점에 로그에서 요청을 재구성해 비교하는 런타임 어설션이 있다는 공개 자료는 없으며, 내부에 동등한 메커니즘이 있는지는 미지수예요.

그래서 여기서는 메커니즘 방향만 비교합니다. Grok Build와 Claude Code는 영속화를 복구 수단으로 쓰고, DSH는 로그를 런타임에서 증명해야 하는 제1원리로 끌어올렸어요.

수업 실습
01

트리거 경로 한 번 추론하기

어떤 플러그인이 요청 출구 이벤트를 듣고, 보내기 전에 메시지 배열에 시스템 프롬프트를 넣고 싶어합니다. 이번 수업의 검사 순서로 두 경우를 추론해 보세요. 동결된 배열을 직접 바꾸면 어떻게 될까요? 요청 전체를 복제해 복제본을 고친 뒤 전달하면, 어느 단계에서 막힐까요? 힌트: 동결 검사와 바이트 단위 비교가 각각 무엇을 지키나요.

Takeaway: DSH의 세션 로그는 대화의 유일한 진실이고, 복구·분기·리플레이·감사가 같은 이벤트 스트림을 공유합니다. 요청이 나갈 때마다 로그에서 그 자리 재구성 후 바이트 단위로 비교하고, 안 맞으면 그 자리에서 예외—요청은 나가지 않아요. 크래시가 경고보다 나은 이유는, 요청이 나가는 순간 로그가 더 이상 모델의 행동을 설명할 수 없기 때문입니다.