DeepSeek Harness · 컨텍스트 엔지니어링

Compaction 이중 경로와 replaceGeneration

컨텍스트가 거의 차면 먼저 정리하고, 진짜로 터지면 정리한 뒤 재시도합니다. 재시도 전에 세대 번호를 대조하고, 정리가 먹히지 않으면 재시도하지 않습니다.

강의 목표읽고 나면 두 가지를 말할 수 있어요. DSH가 왜 자동 압축을 능동·수동 트리거로 나눠 각각 한 구간을 맡기고 겹치지 않게 하는지, 그리고 오버플로 재시도 증빙이 왜 플러그인 반환값이 아니라 단조 증가하는 replaceGeneration 세대 번호인지.
인터랙티브 데모 · 여행 가방 정리 시뮬레이터

컨텍스트 윈도우를 여행 가방으로 생각하세요. 메시지마다 옷 한 벌, 점선은 80% 경고선. 왼쪽 아래 정리 횟수가 replaceGeneration(세대 번호)이고 줄지 않습니다. 시나리오 셋이 압축의 세 운명을 보여 주고, 매 단계에 자막이 있습니다.

80% 선(임계값 0.8)
요청 거부 · CONTEXT_WINDOW_EXCEEDED
정리 횟수 replaceGeneration3
시나리오를 고른 뒤 「재생」을 누르거나, 여기로 스크롤하면 시나리오 A가 자동 재생됩니다.
로직 궤적(행 번호는 compaction-basic/src/index.ts에 대응, 애니메이션이 그 단계에 오면 해당 행이 밝아짐)
PRESSURE 경로
on('agent/pre-step')L147
measure().totalTokens ≥ thresholdTokens ?L304
가지치기 → 요약 → surface 교체L308-323
CONTEXT-OVERFLOW 경로
on('agent/request-error')L179
code ≠ CONTEXT_WINDOW_EXCEEDED → next()L183
generation = surface.replaceGenerationL191
compactIfNeeded('context-overflow')L194
replaceGeneration > generation ?L218-219
예 → return { kind: 'retry' }L222
아니오 → return next(), 원본 오류 유지L219
데모는 수업용 시뮬레이션입니다. 용량 바·옷 블록·세대 번호는 강의용 추상화예요. 시나리오 C의 거짓 성공 압축 백엔드는 수업 가정이고, 실제 compaction-basic은 거짓말하지 않습니다. 다만 compaction은 열린 seam이라 서드파티 백엔드가 붙으면 무엇이든 가능하고, 세대 번호 대조가 그걸 막는 장치입니다. 출처:packages/compaction/compaction-basic/src/index.ts 147–223행.
설계 아이디어 1 · 정리는 트리거 둘이 필요해요

어떤 문제를 푸는가

능동 임계값 경로만 있다고 가정해 보세요. 요청마다 재서 80%를 넘으면 정리. 충분해 보이지만 부족합니다. 토큰 수는 추정치라 provider 실측과 항상 어긋납니다. 거대한 도구 결과가 갑자기 들어오면 측정은 임계 전인데 요청은 이미 한도를 넘겨 provider가 거부합니다. 복구 로직이 없으면 turn이 오류로 끝나고, 사용자는 알 수 없는 실패만 봅니다.

한 번 잘못 세면 바로 벽에 부딪히고 두 번째 방어선이 없습니다. 이게 단일 트리거의 문제입니다.

아이디어는 무엇인가

DSH는 이를 독립 트리거 둘로 나눕니다. 거의 차면 능동 정리, 벽에 부딪히면 수동 구출. 서로 다른 이벤트·조건·실패 의미를 씁니다.

pressure 경로는 각 Step(모델 요청 한 번) 시작 전에 걸립니다. 총 토큰을 재서 용량의 0.8을 넘으면 정리하고, 최근 대화 원문의 16% 꼬리를 남깁니다. 실패 의미는 느슨해요. 정리 중 오류면 로그만 남기고 계속 가고, 미리 정리에 실패해도 세상이 무너지진 않습니다.

context-overflow 경로는 요청 오류 뒤에 걸리고, 어댑터가 정규화한 CONTEXT_WINDOW_EXCEEDED만 인정합니다. 다른 오류는 통과. 임계값은 보지 않고 예비 예산을 0으로 만든 뒤 진짜 축소를 강제합니다. 실패 의미는 엄격해요. 재시도할지, 원본 오류를 보고할지 반드시 결정합니다. 재시도 상한 기본 1회, 성공한 모델 응답마다 카운터를 리셋해 정상 세션이 막히지 않습니다.

같은 일, 트리거 둘, 각자 한 구간 거의 참(요청 전 측정) 총 토큰이 용량의 80% 초과 능동 정리 최근 대화 원문 16% 유지 여정 계속 실패는 로그만 한 줄 벽에 부딪힘(요청 거부) CONTEXT_WINDOW_EXCEEDED 강제 정리 임계값 무시, 가능한 만큼 전부 압축 대조 후 결정 재시도, 또는 원본 오류 보고
위는 예방, 아래는 최후 수단. 두 경로는 독립이라 하나가 실패해도 다른 쪽은 그대로 동작합니다.

출처:pressure 리스너는 packages/compaction/compaction-basic/src/index.ts 147–165행, overflow 리스너는 179–223행; 기본값 0.8·0.16은 같은 패키지 config.ts 20·23행, 재시도 상한 기본 1은 93행 — provider+model 조합별로 덮어쓸 수 있습니다.

왜 오래가는가

예방과 최후 수단을 나누는 것은 신뢰성 공학의 통칙입니다. 백업과 복구는 두 시스템, 레이트 리밋과 서킷 브레이커는 두 게이트 — 같은 이치예요. 예방은 싸고 자주 돌며 실패해도 괜찮고, 최후 수단은 드물지만 믿을 만해야 하며 실패에 설명이 필요합니다. 한 로직에 넣으면 서로 타협합니다. 언어를 바꿔 harness를 다시 써도, 모델에 컨텍스트 한도가 있고 토큰이 추정이면 이 트리거 둘은 그대로 필요합니다.

설계 아이디어 2 · 재시도는 증거를 내야 해요

어떤 문제를 푸는가

벽에 부딪힌 뒤 한 번 정리하고 요청을 다시 보냅니다. 문제는: 정리가 진짜 먹혔는지 어떻게 아나? Compaction은 열린 seam이라 서드파티가 커스텀 백엔드를 붙일 수 있습니다. 매번 성공을 보고하면서 모델이 보는 내용은 안 바꾸는 백엔드를 상상해 보세요. 반환값만 보고 재시도하면 요청은 그대로 한도 초과 → 오류 → 압축 → 재시도, 한 바퀴마다 API 비용만 태우고 새벽까지 돕니다.

아이디어는 무엇인가

DSH의 답은 세대 번호입니다. 배경: surface는 세션 로그에서 모델이 보는 이벤트의 실시간 프로젝션, 즉 모델 눈의 대화입니다. replaceGeneration은 그 대화가 몇 번 교체됐는지 세는 읽기 전용 카운터입니다. 코드베이스 전체에서 올리는 곳은 한곳뿐 — 옛 메시지가 요약으로 진짜 교체되고 디스크에 쓰인 순간. 줄이거나 리셋하는 API는 없습니다. 세대가 앞으로 가면 수학적으로 최소 한 번의 실제·영속 교체가 일어난 것과 같습니다.

오버플로 복구는 세 단계입니다. 정리 전에 현재값을 스냅샷, 정리, 새 값과 스냅샷을 비교. 엄격히 커져야 재시도, 아니면 통과시켜 원본 오류를 그대로 보고. 플러그인이 뭐라고 하냐보다 장부 숫자가 변했느냐가 중요합니다.

재시도 전에 정리 횟수가 변했는지 확인 실행 전 스냅샷 정리 횟수 = 3 한 번 정리 압축 백엔드에 위임 카운트를 다시 확인 지금은 몇? 4가 됨 — 가방이 정말 움직였음 재시도 허용 여전히 3 — 정리가 먹히지 않음 재시도 거부, 원본 오류 보고
카운터는 줄지 않고, 교체가 진짜로 디스크에 쓰인 그 한곳에서만 올라갑니다. 숫자가 커진 것이 곧 단단한 증거입니다.

반대 방향 디테일도 있어요. 정리 중 예외가 나도, 앞선 무료 가지치기가 이미 디스크에 쓰이고 세대가 전진했다면 그 진전만으로도 재시도를 허용합니다. 증거로 통과, 증거로 거부 — 같은 기준입니다.

세대가 전진하지 않으면 재시도는 한 번도 안 됩니다.

출처:스냅샷·비교는 packages/compaction/compaction-basic/src/index.ts 191행과 218–222행, 예외 후 이미 영속된 진전으로 재시도는 195–208행; replaceGeneration 정의는 packages/core/session/src/surface.ts 136–142행, 전체에서 유일한 증가 지점은 361–371행. 프로젝트 Agent Note(.agents/notes/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.zh.md)는 반환값만 보는 방식을 명시적으로 기각했습니다 — 커스텀 백엔드가 성공을 보고하면서 모델 가시 상태를 안 바꿀 수 있기 때문입니다.

왜 오래가는가

단조 증가 버전으로 상태가 변했음을 증명하는 패턴은 DB 낙관적 잠금이 수십 년 써 온 것이고, Git 커밋 체인·분산 시스템의 epoch도 변형입니다. 이득은 신뢰 문제를 산수로 바꾸는 것 — 실행자는 거짓말할 수 있어도 장부는 못 합니다. 신뢰할 수 없는 확장점이 있는 한, 재시도 전에 고칠 수 없는 카운터를 확인하는 것이 가장 싼 방어입니다.

가로 비교 · 세 곳이 돈을 태우는 걸 막는 법
Grok Build

능동 임계값 경로는 DSH pressure와 동형입니다. 기본 85% 트리거, 기본 꺼진 two-pass 사전 요약 — 자세한 내용은 사이트 Compaction: 85% 임계값과 선택적 two-pass. 요청 오류 후 수동 복구+세대 대조는 확인한 Grok Build 자료에 등가 메커니즘이 없습니다. 공개 증거 기준이며, 미지 항목은 남겨 둡니다.

Claude Code

능동 쪽이 가장 두껍습니다. API 호출마다 트리밍·마이크로 압축·접기·전체 요약 네 공정을 거칩니다. 비용 방어는 카운트 서킷 브레이커: 자동 압축이 연속 3회 실패하면 멈춤. 이 3은 실제 사고에서 왔고, 소스 주석에 1279개 세션이 50회 이상 연속 실패해 전 세계 하루 약 25만 API 호출을 낭비했다는 기록이 있습니다. 출처: claude-code-sourcemap-main/study/chapters/03-context-management.md 78–81, 121–124행.

비교 초점은 하나: 압축 실패가 루프로 돈을 태우느냐. Claude Code는 실패를 세어 3에서 서킷 브레이커 — 사고 데이터로 맞춘 카운터 손절입니다. DSH는 횟수를 안 세고, 재시도마다 세대 전진 증거를 요구해 무효 재시도를 한 번도 안 냅니다 — 구조적 증명입니다. 전자의 3은 사고가 먹여 주고, 후자의 0은 유도됩니다. 이중 경로 직교까지 더하면, 세 곳 비교에서 DSH만의 설계입니다.

수업 실습
01

거짓 성공 백엔드를 손으로 밀어 보기

재시도 상한을 1로 두고, 매번 정리 성공을 보고하면서 모델 가시 내용은 절대 바꾸지 않는 커스텀 압축 백엔드를 설치합니다. 이제 첫 오버플로 오류가 납니다.

문제 1: DSH가 두 번째 압축을 시도할까요? 힌트: 대조 실패 후 원본 오류가 바로 보고되고 turn이 끝나, 재시도 카운터가 늘 기회가 없습니다.

문제 2: 판정 기준을 백엔드 반환값만으로 바꾸면, 같은 장면에서 한 바퀴 5초일 때 첫 1분에 실패가 정해진 요청이 몇 번? Claude Code의 3회 서킷 브레이커는 몇 번째 요청 뒤에 손절할까요?

Takeaway:거의 차면 능동 정리, 벽에 부딪히면 수동 구출 — 예방과 최후 수단은 각자 트리거. 오버플로 재시도 증빙은 replaceGeneration의 단조 전진이고, 플러그인 반환값은 안 칩니다. 증거를 요구하세요. 구두 보고를 믿지 마세요.