MCP 서버
90af14e 기준입니다.
context-mode는 AI 코딩 에이전트와 도구 출력 사이에 놓이는 실행·검색 계층입니다. 에이전트가 긴 출력을 그대로 받는 대신, 별도 프로세스에서 데이터를 가공하거나 SQLite에 색인합니다. 핵심은 모델의 기억 용량을 늘리는 것이 아니라, 지금 읽을 정보와 나중에 찾을 정보를 나누는 데 있습니다.
책상은 컨텍스트 윈도, 서랍은 SQLite, 찾아보기는 전문 검색입니다. 로그 1만 줄을 책상에 올리는 대신 “실패 3건, 모두 같은 파일”이라는 결과만 올릴 수 있습니다.
다만 서랍에 보관했다고 모델이 저절로 아는 것은 아닙니다. 필요한 순간에 검색 도구를 호출하고, 검색 결과가 충분한지 판단하는 과정이 필요합니다.
| 흐름 | 어떻게 처리하나 | 대표 진입점 |
|---|---|---|
| 계산 | 코드를 실행해 개수·합계·오류 목록처럼 필요한 결과만 출력 | ctx_execute, ctx_execute_file |
| 검색 | 원문을 작은 조각으로 저장하고 질의에 맞는 부분을 반환 | ctx_index, ctx_search |
| 복구 | 파일 수정·오류·결정 등을 이벤트로 남기고 대화 압축 후 참조 | PostToolUse, PreCompact, SessionStart |
src/server.ts는 MCP 도구를 등록합니다.
src/executor.ts의 PolyglotExecutor는 자식 프로세스를 만들고,
src/store.ts의 ContentStore는 색인과 검색을 담당합니다.
세션 이벤트는 src/session/db.ts의 SessionDB가 별도로 관리합니다.
다음은 MCP 도구 호출의 인자를 설명하는 예시입니다. 터미널 명령이 아니며, 도구 이름 앞의 접두사는 클라이언트마다 달라질 수 있습니다.
// 긴 출력 대신, 코드가 계산한 결과만 모델에게 전달
ctx_execute({
language: "javascript",
code: "const values = [10, 20, 30]; console.log(values.reduce((a, b) => a + b, 0));"
});
// 정확한 문장을 다시 읽고 싶다면 원문을 색인한 뒤 검색
ctx_index({ content: "## 재시도\n실패하면 최대 3번 재시도한다.", source: "연습문서" });
ctx_search({ queries: ["재시도"], source: "연습문서" });
이 저장소의 핵심은 모델 학습 코드가 아닙니다. 도구 계약, 데이터 이동, 저장소 경계, 오류 처리, 플랫폼 호환성을 읽으면 “AI 기능을 실제 개발 도구로 만드는 법”을 배울 수 있습니다.
브라우저 스냅샷, 이슈 목록, 테스트 로그는 각각 유용하지만 한 대화에 계속 쌓이면 부담이 됩니다. context-mode는 이런 출력의 양을 줄이는 기능과, 대화가 압축된 뒤 작업을 이어 가는 기능을 함께 내세웁니다. GitHub Trending Weekly #15 선정과 스타 31,896개는 관심의 크기를 보여 주지만, 인기의 원인을 증명하는 실험은 아닙니다.
관심을 끌 만한 설계상의 이유는 분명합니다. 도구 출력을 줄이는 경로와 잊어버린 작업을 다시 찾는 경로가 같은 검색 기반 위에서 연결됩니다. 설치할 도구 하나가 절약과 복구를 함께 다룬다는 점이 사용자에게 이해하기 쉬운 제안입니다.
| 자료 | 기록된 결과 | 읽을 때 주의할 점 |
|---|---|---|
README.md | 315 KB → 5.4 KB, 약 98% 감소 | 대표 사례를 요약한 프로젝트의 주장 |
BENCHMARK.md 계산 부분 | 315 KB → 5.5 KB, 98% 절약 | 개수·통계만 출력하는 작업이 포함됨 |
| 같은 문서의 검색 부분 | 60.3 KB → 11.0 KB, 82% 절약 | 코드와 문장을 돌려주므로 더 많은 바이트 필요 |
| 같은 문서의 전체 요약 | 376 KB → 16.5 KB, 96% 절약 | 모든 작업에 98%가 적용된다는 뜻은 아님 |
이 수치들은 프로젝트가 기록한 벤치마크이며 독립 재측정 결과는 아닙니다. 바이트 감소율은 모델별 토큰 감소율, 전체 요금 감소율, 정답률과도 구별해야 합니다. 예를 들어 오류 개수만 물었다면 한 줄 통계로 충분하지만, 수정 코드를 작성하려면 오류의 위치와 원문이 더 필요합니다.
“책 300쪽을 1쪽으로 줄였다”는 말만으로는 좋은 요약인지 알 수 없습니다. 독자가 원하는 것이 매출 합계인지, 계약서의 정확한 조항인지에 따라 필요한 분량이 달라집니다.
아래 표는 순위표가 아니라 해결 방식의 비교입니다. Claude Code·Cursor 같은 에이전트는 context-mode를 연결하는 호스트이기도 합니다. 특정 제품의 최신 성능 우열을 주장하지 않고, 이 저장소에서 확인되는 역할 차이를 비교합니다.
| 비교 대상 | 강점 | context-mode의 위치 |
|---|---|---|
| 호스트의 대화 압축 | 대화를 짧게 만들어 계속 진행 | 도구 출력이 들어오기 전부터 양을 줄이고, 이벤트 기반 복구를 보완 |
| 일반 MCP 서버 | 외부 기능을 표준 도구로 노출 | 실행·색인·검색 도구에 라우팅 훅을 더해 사용 경로를 유도 |
| 단순 출력 자르기 | 구현이 간단하고 응답 크기를 제한 | 큰 출력을 색인하면 나중에 필요한 부분을 다시 검색 가능 |
| 벡터 검색 기반 RAG | 임베딩을 이용한 의미 유사도 검색 | 핵심 검색은 SQLite FTS5 기반으로, 별도 임베딩 모델 없이 동작 |
| 메모 파일 | 사람이 읽고 편집하기 쉬운 기록 | 도구 이벤트를 자동 추출하고 검색·복구 흐름으로 연결 |
docs/platform-support.md는 17개 클라이언트와 OpenClaw 게이트웨이 통합을 구분합니다.
src/adapters/types.ts의 PlatformId에는 OpenClaw를 포함한 이름 18개와 unknown이 있습니다.
따라서 “17개 지원”과 “18개 어댑터 이름”은 집계 범위가 다른 수치입니다.
도구 호출을 차단할 수 있는지, 인자를 고칠 수 있는지, 압축 직전에 개입할 수 있는지는 호스트마다 다릅니다. MCP 연결만 있는 구성은 도구를 제공하지만 자동 라우팅과 세션 복구까지 동일하게 보장하지는 않습니다. 이 차이를 인터페이스로 표현한 방식이 오히려 중요한 학습 대상입니다.
package.json의 패키지 버전은 1.0.169, 모듈 형식은 module,
Node.js 요구 버전은 >=22.5.0입니다.
src/server.ts는 McpServer와 StdioServerTransport를 연결합니다.
핵심 실행 경로는 표준 입출력을 사용하는 MCP 서버이며, 별도 웹 검색 화면을 띄우는 구조는 아닙니다.
| 의존성 | package.json 범위 | bun.lock 기록 | 배울 역할 |
|---|---|---|---|
| MCP SDK | ^1.26.0 | 1.26.0 | 도구 등록과 stdio 통신 |
| Zod | ^3.25.0 | 3.25.76 | 외부 도구 인자를 실행 시 검증 |
| better-sqlite3 | ^12.6.2 | 12.9.0 | SQLite 대체 드라이버 경로 |
| Turndown | ^7.2.0 | 7.2.4 | HTML을 Markdown으로 변환 |
| GFM 플러그인 | ^1.0.2 | 1.0.2 | 표 등 Markdown 표현 확장 |
| Domino | ^2.2.0 | 2.2.0 | 서버 측 DOM 처리 |
| Clack / Picocolors | ^1.0.1 / ^1.1.1 | 1.2.0 / 1.1.1 | CLI 질문과 터미널 색상 |
정식 패키지 이름은 @modelcontextprotocol/sdk, @clack/prompts,
@mixmark-io/domino, turndown-plugin-gfm입니다.
버전 앞의 ^는 허용 범위를 뜻합니다. 잠금 파일의 버전은 그 파일에 기록된 해석 결과이며,
npm install을 사용하는 CI가 매번 같은 버전을 설치한다는 보장은 별도 문제입니다.
src/db-base.ts의 loadDatabase()는 Bun 환경이면 bun:sqlite를 고릅니다.
현대 Node 환경에서는 node:sqlite의 FTS5 지원을 검사하고,
사용할 수 없으면 better-sqlite3로 내려갑니다.
의존성 목록에 있는 라이브러리와 실제 실행 때 선택되는 구현은 다를 수 있습니다.
BunSQLiteAdapter와 NodeSQLiteAdapter가 호출 방식을 맞춥니다.
데이터베이스 구현을 바꾸면서 상위의 색인·세션 코드는 유지하는, 어댑터 패턴의 작은 사례입니다.
| 사용자 접점 | 실제 구성 | 역할의 경계 |
|---|---|---|
| 에이전트 화면 | Claude Code·IDE·CLI가 제공 | context-mode는 도구 결과와 훅 메시지를 전달 |
| 소개 웹 | web/index.html, web/context-saving.html, web/insight.html | 정적 HTML·CSS와 일부 JavaScript |
| 웹 라우터 | web/worker.js | Cloudflare Worker가 경로별 정적 자산 제공 |
| 공유 이미지 제작 | web/og/package.json의 playwright ^1.60.0 | OG 이미지 렌더링 도구, 핵심 서버 의존성과 구분 |
루트 의존성에는 React나 Vue가 없습니다. web/worker.js 주석은 별도 플랫폼 앱 배포를 설명하지만,
이 Worker 자체는 소개 페이지 라우팅과 자산 제공만 수행합니다.
소개 페이지가 있다는 이유로 저장소 전체를 프론트엔드 앱으로 이해하면 핵심을 놓칩니다.
| 항목 | 설정과 버전 | 읽어 볼 파일 |
|---|---|---|
| 타입 검사 | TypeScript ^5.7.0 → 잠금 5.9.3 | tsconfig.json: strict, ES2022, NodeNext |
| 번들링 | esbuild ^0.27.3 → 잠금 0.27.7 | package.json: ESM, --target=node18, 일부 패키지 외부화 |
| 개발 실행 도구 | tsx ^4.21.0 → 잠금 4.21.0 | dev, doctor 스크립트 |
| 테스트 | Vitest ^4.0.18 → 잠금 4.1.5 | vitest.config.ts: forks, 로컬 작업자 3개·CI 2개 |
| CI 행렬 | Ubuntu·macOS·Windows, Node 22.5 | .github/workflows/ci.yml |
| 보조 런타임 | Python 3.12, Elixir 1.17·OTP 27, Go stable | 같은 CI 설정, 일부 OS는 설치 방식이 다름 |
| 웹 배포 | Worker의 compatibility_date: 2025-01-01 | web/wrangler.jsonc |
packageManager는 pnpm@10.23.0을 선언하지만 저장소에는 bun.lock이 있고,
CI는 npm install을 사용합니다. 선언·잠금·CI 명령을 각각 확인해야 재현 조건을 이해할 수 있습니다.
또한 번들의 node18 대상은 출력 문법의 기준이며, 제품의 Node.js 최소 버전 22.5를 낮추는 설정이 아닙니다.
두 갈래를 함께 보세요. MCP 도구 경로는 실행과 검색을 맡고, 훅 경로는 호출 전후와 세션 경계에 개입합니다. 모든 호스트가 아래의 훅 전체를 제공하는 것은 아닙니다.
ctx_execute의 처리기는 PolyglotExecutor.execute()를 호출합니다.
실행기는 임시 디렉터리에 스크립트를 쓰고 child_process.spawn()으로 런타임을 실행합니다.
Rust는 먼저 컴파일하는 별도 분기가 있습니다. 일반 실행의 현재 디렉터리는 프로젝트 루트 또는 호출의 cwd입니다.
완료 후 src/server.ts가 출력 크기를 검사합니다.
큰 문자열 자체 대신 “어느 출처에 몇 조각을 색인했는지”와 검색 방법을 반환하는 것이 핵심입니다.
분산 시스템에서 큰 파일을 직접 보내는 대신 저장 위치와 식별자를 보내는 방식과 닮았습니다.
// src/server.ts의 실제 상수
const INTENT_SEARCH_THRESHOLD = 5_000;
const LARGE_OUTPUT_THRESHOLD = 102_400;
// 성공한 일반 stdout 경로를 설명하기 위한 축약 의사코드
if (intent?.trim() && Buffer.byteLength(stdout) > INTENT_SEARCH_THRESHOLD) {
return intentSearch(stdout, intent, source);
}
if (Buffer.byteLength(stdout) > LARGE_OUTPUT_THRESHOLD) {
return indexStdout(stdout, source);
}
return stdout;
intentSearch()는 원문을 영속 저장소에 색인한 다음 의도에 맞는 부분을 찾습니다.
indexStdout()는 색인 메타데이터와 후속 검색 안내를 만듭니다.
실제 응답에는 실행 코드의 일부를 보여 주는 내용과 오류·타임아웃 처리도 있으므로, 위 코드는 전체 구현을 대체하지 않습니다.
출력 감소와 정보 삭제는 같은 일이 아니지만, 검색 가능한 저장에도 크기와 보존 기간의 한계는 있습니다.
실행기는 stdout과 stderr를 합쳐 기본 100 * 1024 * 1024바이트를 넘으면 프로세스를 종료합니다.
이 제한 너머의 출력까지 모두 보관된다고 이해하면 안 됩니다.
src/executor.ts는 임시 스크립트와 자식 프로세스를 사용하고 위험한 환경변수 일부를 제거합니다.
반면 실제 홈 경로와 많은 환경변수는 유지하며, 핵심 실행기에 컨테이너나 가상머신을 만드는 단계는 없습니다.
따라서 이 계층은 주로 출력과 실행 수명을 관리합니다. 파일·네트워크 권한의 강한 격리는 호스트와 운영체제의 정책까지 함께 봐야 합니다.
ContentStore.index()는 Markdown 제목과 코드 펜스를 읽어 문서 조각을 만듭니다.
sources에는 출처와 파일 기반 색인의 해시를, chunks와 chunks_trigram에는 서로 다른 방식의 전문 색인을 둡니다.
첫 번째는 porter unicode61, 두 번째는 trigram 토크나이저를 사용합니다.
#rrfSearch()의 상수 K는 60입니다.
한 검색 목록에서 순위가 r이면 1 / (60 + r)을 더합니다.
서로 다른 점수의 절댓값을 억지로 비교하지 않고 순위로 합치는 방식입니다.
// RRF를 손으로 계산하는 독립 예제: 순위는 1부터 시작
const score = ranks => ranks.reduce((sum, rank) => sum + 1 / (60 + rank), 0);
const both = score([1, 2]); // 두 검색에서 1위, 2위
const one = score([1]); // 한 검색에서만 1위
console.log({ both, one });
#applyProximityReranking()은 RRF 상위 결과를 요청한 개수만큼 추린 뒤 제목 일치, 본문의 단어 간 거리,
인접한 단어 쌍의 반복 횟수로 순서를 다시 정합니다.
“BM25를 쓴다”는 소개를 넘어, 후보 검색과 순위 조정의 책임이 어떻게 분리되는지 읽어 보세요.
오타 보정은 fuzzyCorrect()와 편집 거리 계산을 사용합니다.
의미를 이해하는 임베딩 검색은 아니므로, “접속 실패”와 “connection refused”처럼 표현이 다른 질의는 결과를 직접 확인해야 합니다.
한글 처리 역시 영어 어간 추출과 똑같은 효과를 기대하면 안 됩니다.
src/adapters/types.ts의 HookAdapter는 입력 파싱, 응답 형식, 설정 경로, 지원 능력의 계약입니다.
hooks/core/routing.mjs의 routePreToolUse()는 플랫폼 전용 JSON 대신 공통 결정 객체를 만듭니다.
// hooks/core/routing.mjs가 사용하는 정규화된 결정 형태
{ action: "deny", reason: "..." }
{ action: "modify", updatedInput: { ... } }
{ action: "context", additionalContext: "..." }
null // 그대로 통과
여러 형식을 각각 구현하되 중심 정책을 공유하면 “curl 출력은 실행·색인 경로로 보내자”라는 규칙을 한곳에서 고칠 수 있습니다.
mcpRedirect()는 MCP 도구가 준비되지 않으면 우회 결정을 통과시켜, 대체 도구가 없어 작업이 막히는 상황을 피합니다.
| 소스의 어댑터 | 관찰할 설정 | 학습 포인트 |
|---|---|---|
src/adapters/claude-code/index.ts | canModifyArgs: true | 인자 변경이 가능한 호스트 |
src/adapters/codex/index.ts | canModifyArgs: false, canModifyOutput: false | 차단과 인자 변경을 별도 능력으로 구분 |
src/adapters/zed/index.ts | 훅 관련 능력이 모두 false | MCP 도구 제공과 훅 제공은 별개 |
위 표의 파일은 모두 src/adapters/ 아래에 있습니다.
읽기·명령 차단 정책은 src/security.ts의 evaluateFilePath(),
evaluateCommandDenyOnly() 등과 연결됩니다.
라우팅을 사용한다고 임의의 코드가 실행 시 완전히 검증되는 것은 아닙니다.
hooks/posttooluse.mjs는 extractEvents()로 파일·오류·Git·작업 등의 사실을 추출합니다.
hooks/session-loaders.mjs의 attributeAndInsertEvents()가 프로젝트 귀속을 계산하고 DB에 넣습니다.
원시 도구 응답을 매번 대화에 다시 넣는 대신, 이벤트 기록과 읽기용 안내를 나눕니다.
src/session/extract.ts가 도구 입력·출력을 여러 추출기에 통과시킵니다.SessionDB는 최근 5개 이벤트 범위에서 중복을 확인하고, 세션당 1,000개 상한에서 중요도가 낮은 이벤트부터, 같은 중요도에서는 오래된 이벤트부터 밀어냅니다.hooks/precompact.mjs가 buildResumeSnapshot()을 호출하고 upsertResume()으로 보관합니다.hooks/sessionstart.mjs가 이벤트 문서와 안내를 만들며, 상황에 따라 저장된 스냅샷도 복구합니다.hooks/session-directive.mjs는 session-events 출처를 검색하도록 안내합니다.buildResumeSnapshot()은 목표·파일·오류·결정·작업·최근 사용자 메시지 등을 조합합니다.
최근 사용자 메시지는 최대 3개, 각 400자 제한이 있고, 파일 목록은 최대 10개를 보여 줍니다.
과거 이벤트 전체를 영구히 보존하거나, 당시 프로세스를 완전히 재생하는 시스템으로 이해해서는 안 됩니다.
| 제한 | 커밋 기준 구현 | 왜 필요한가 |
|---|---|---|
| 검색 결과 수 | 스키마 기본값은 3, 처리기 평상시 최대는 2 | 선언만 읽으면 실제 응답 수를 오해할 수 있음 |
| 검색 호출 빈도 | 기본 60초 창, 4번째부터 질의당 1건, 9번째부터 차단 | 검색 결과 자체가 컨텍스트를 채우는 것을 방지 |
| 빈도 집계 범위 | FloodGuard가 에이전트 문맥 키별로 집계하며, 식별자를 구하지 못하면 공통 키 사용 | 식별된 에이전트 사이에서 호출 예산을 분리 |
| 검색 응답 예산 | MAX_TOTAL = 40 * 1024 | 실제로는 문자열 길이를 누적해 다음 질의 전에 검사 |
| 배치 실행 | 도구 스키마의 동시성 1~8, 기본 1 | 동시에 생성되는 프로세스와 출력량 제어 |
검색 응답의 예산은 엄격한 40KiB 바이트 상한이 아닙니다.
실제 누적은 formatted.length를 사용하므로 한글·이모지의 UTF-8 바이트 수와 다르고,
질의를 처리한 뒤 목표값을 넘어갈 수도 있습니다. 상수의 주석뿐 아니라 비교 시점과 계산 단위까지 확인해야 합니다.
src/runPool.ts의 runPool()은 실패를 작업별로 보관하고, 결과는 입력 순서에 맞춥니다.
runBatchCommands()에서 동시성 1은 전체 명령이 시간 예산을 공유하고,
동시성 2 이상은 명령별로 같은 타임아웃을 받습니다. 숫자 하나를 바꿨는데 시간 정책까지 달라지는 중요한 지점입니다.
src/types.ts로 결과와 이벤트의 모양을 익힌 다음,
src/executor.ts와 src/store.ts에서 두 핵심 부품을 읽어 보세요.
그다음 src/server.ts에서 ctx_execute 처리기를 찾아 부품이 연결되는 과정을 봅니다.
4,991줄짜리 서버를 첫 줄부터 외우는 것보다 한 요청의 경로를 따라가는 편이 낫습니다.
| 묶음 | 공부할 책임 | 함께 읽을 자료 |
|---|---|---|
src/adapters/ | 같은 정책을 다른 호스트 형식에 연결 | tests/adapters/, docs/platform-support.md |
src/session/ | 관찰한 사건과 복구 시 보여 줄 내용을 분리 | tests/session/session-snapshot.test.ts |
hooks/core/ | 도구 실행 전 결정과 호스트별 출력 형식 | tests/hooks/core-routing.test.ts |
docs/adr/ | 설계가 바뀐 이유와 되돌리지 말아야 할 조건 | 다중 작성자·통계 산식에 관한 ADR |
| 배포 번들 | 컴파일·묶기·외부 의존성이 제품에 미치는 영향 | .github/workflows/bundle.yml |
코드 규모를 말할 때 생성 번들과 테스트를 나누어야, 실제로 공부할 구현의 크기가 보입니다.
아래는 줄바꿈 수를 센 대략적인 물리적 줄 수입니다. 주석과 빈 줄을 포함하며,
.git·node_modules·build·dist와 *.bundle.mjs는 제외했습니다.
| 범위 | 파일 수 | 대략 줄 수 |
|---|---|---|
| TypeScript 전체 | 320개 | 126,566줄 |
그중 구현 src/ | 95개 | 41,617줄 |
그중 tests/ | 221개 | 84,756줄, 테스트 외 보조 파일 포함 |
| 그 밖의 TypeScript | 4개 | 193줄 |
| JavaScript | .mjs 91개 + .js 1개 | 13,975줄 + 31줄 |
| Shell / HTML | 4개 / 7개 | 1,652줄 / 2,598줄 |
*.test.ts 파일은 210개입니다. 파일 수는 테스트 실행 결과나 커버리지를 뜻하지 않습니다.
주요 구현은 src/store.ts 2,071줄, src/executor.ts 785줄,
src/session/extract.ts 2,960줄, src/session/db.ts 1,726줄입니다.
src/search/ctx-search-schema.ts의 buildCtxSearchInputSchema()는 공유 DB 모드일 때만
project 필드를 스키마에 넣습니다. 사용할 수 없는 선택지를 설명문으로만 금지하지 않고,
도구가 보여 주는 계약 자체에서 제외하는 설계입니다.
같은 파일의 coerceJsonArray()는 일부 클라이언트가 문자열로 보내는 배열 인자를 보정합니다.
학습할 부분은 “무조건 엄격하게 거절할까”와 “어떤 입력을 호환 목적으로 받아 줄까”의 경계입니다.
// resolveProjectScope()의 의미를 표로 옮긴 의사코드
공유 모드 꺼짐 => undefined // 별도 필터 없음
공유 모드 켜짐 + 생략 => 현재 프로젝트 경로
공유 모드 켜짐 + global => null // 전체 프로젝트 조회
공유 모드 켜짐 + 경로 => 지정 경로
실습 아이디어: undefined, null, 빈 문자열, global을 구분하는 입력표를 만드세요.
각 값이 “생략”, “전체”, “잘못된 경로” 중 무엇인지 먼저 정하고 구현을 설계합니다.
PolyglotExecutor는 stdout·stderr 스트림을 모으고, 크기 상한과 시간 제한을 적용합니다.
Unix에서는 프로세스 그룹을 만들고 killTree()가 하위 프로세스 종료를 돕습니다.
백그라운드 실행은 타임아웃 시 연결을 떼고 일부 결과를 반환하므로 일반 실행과 수명 관리가 다릅니다.
실습 아이디어: “명령이 실패함”, “실행 파일이 없음”, “시간이 지남”, “출력 상한 초과”를 구분해
ExecResult의 stdout, stderr, exitCode, timedOut에 무엇을 담을지 써 보세요.
applyWALPragmas()는 journal_mode = WAL, synchronous = NORMAL을 설정합니다.
읽기와 쓰기의 공존을 돕지만, 쓰기 경합은 여전히 처리해야 합니다.
생성 시 30,000ms 대기 설정과 withRetry()의 100·500·2,000ms 재시도가 함께 사용됩니다.
withRetry()는 동기 API를 유지하기 위해 재시도 사이를 바쁜 대기로 구현합니다.
따라서 편한 호출 구조와 프로세스의 이벤트 루프를 막는 비용을 함께 토론할 수 있습니다.
실습 아이디어: docs/adr/0001-sessiondb-multi-writer.md를 읽고,
단일 작성자 잠금이 다중 창 사용을 왜 막았는지 사건·원인·해결책 세 칸으로 정리하세요.
집계 작업은 한 줄로 끝날 수 있지만 문서 검색은 사용할 코드와 조건을 충분히 반환해야 합니다.
ContentStore는 제목·코드 여부·출처를 보관하고,
src/search/unified.ts의 searchAllSources()는 조회 모드에 따라 소스를 합칩니다.
기본 relevance 모드는 ContentStore 결과를 사용합니다.
timeline 모드는 SessionDB와 자동 메모 검색을 추가하고 시간순으로 합칩니다.
모든 검색 모드가 같은 자료와 같은 순서를 제공하는 것은 아닙니다.
실습 아이디어: 짧은 문서 5개에 같은 단어를 다른 제목·위치로 배치하고 예상 순위를 적으세요. 영어 어간, 한글, 오타, 두 글자 질의를 각각 준비하면 검색기의 가정이 보입니다.
runPool()은 정해진 수의 작업자가 공유 인덱스를 가져가며 다음 작업을 실행합니다.
settled[idx]에 결과를 넣어 완료 순서와 출력 순서를 분리합니다.
한 작업의 예외가 다른 작업의 결과를 없애지 않는 점이 핵심입니다.
실습 아이디어: 실행 시간이 30·10·20ms인 세 작업을 동시성 2로 처리하는 시간표를 그리세요. 둘째 작업이 실패해도 셋째 작업이 진행돼야 하며, 반환 배열은 원래 입력 순서여야 합니다.
| 검증 대상 | 읽을 테스트 | 확인하는 행동 |
|---|---|---|
| 검색 저장소 | tests/store.test.ts | FTS5 스키마·색인·검색·마이그레이션 |
| 복구 스냅샷 | tests/session/session-snapshot.test.ts | 빈 이벤트·파일 중복·최근 10개 파일 |
| 검색 호출 제한 | tests/core/search-flood-guard.test.ts | 에이전트별 호출 예산·시간 창 초기화 |
좋은 학습 결과는 코드를 따라 치는 데서 끝나지 않고, 이 설계가 지켜야 할 조건을 자기 말로 설명하는 것입니다.
| 항목 | 확인되는 조건 | 초보자가 이해할 점 |
|---|---|---|
| 운영체제 | CI 대상은 Linux·macOS·Windows | 호스트별 훅 지원과 경로 처리는 별도 확인 |
| 기본 런타임 | package.json: Node.js >=22.5.0 | 번들 대상 node18과 혼동하지 않기 |
| Bun | 별도의 SQLite·실행 경로 지원 | 대안 런타임이며 모든 도구를 대체한다는 뜻은 아님 |
| SQLite | FTS5 지원 필요 | nodeSqliteHasFts5()가 실제 기능을 검사 |
| 언어별 실행기 | 사용할 언어의 런타임 또는 컴파일러 | Python·Rust·Go 등을 모두 설치해야 하는 것은 아님 |
| 호스트 | MCP 또는 지원 플러그인 경로를 갖춘 에이전트 | 훅이 없으면 자동 라우팅 기능이 제한될 수 있음 |
| GPU | 핵심 실행·검색 경로에는 GPU 연산 요구 없음 | 모델 실행 비용과 이 서버의 자원을 구분 |
src/runtime.ts의 Language는 JavaScript, TypeScript, Python, Shell, Ruby,
Go, Rust, PHP, Perl, R, Elixir, C#의 12개를 선언합니다.
detectRuntimes()가 설치 상태를 확인하므로 선언된 언어와 현재 사용할 수 있는 언어는 다릅니다.
C# 경로는 dotnet-script, Rust 경로는 rustc를 찾습니다.
공식적인 최소 RAM·CPU 등급은 루트 패키지와 핵심 설정에 명시돼 있지 않습니다. 실행 하나의 출력 수집 상한이 100MiB라면, 8개가 동시에 상한에 가까워지는 경우 수집 버퍼만 약 800MiB가 될 수 있습니다. 문자열 변환·버퍼 합치기·SQLite 캐시·각 자식 프로세스의 메모리는 여기에 추가됩니다.
이 계산은 실제 사용량 측정이나 최소 사양이 아닙니다.
hardCapBytes는 출력 버퍼의 기준이며 자식 프로세스 전체의 RAM 한도를 설정하는 기능도 아닙니다.
대형 빌드를 실행하면 빌드 도구 자체의 요구사항이 더 클 수 있습니다.
동시성은 계산대 수와 같습니다. 계산대를 늘리면 대기는 줄어들지만, 각 계산대 앞의 장바구니와 직원이 차지하는 공간도 함께 늘어납니다.
CONTEXT_MODE_DIR는 세션·콘텐츠 저장 루트 재지정에 사용됩니다.ctx_fetch_and_index는 외부 자료를 요청하며, 자식 코드도 네트워크를 사용할 수 있습니다.hooks/platform-bridge.mjs는 유효한 플랫폼 설정이 있을 때 이벤트 전달 경로를 엽니다.resolveExecTimeout()에서 기본 120,000ms 예외가 있습니다.src/lifecycle.ts는 부모 프로세스의 생존과 종료 신호를 살펴 남겨진 서버를 정리합니다.
이 저장소에서는 검색 성능뿐 아니라 “도구가 언제 끝나야 하는가”도 시스템 요구사항의 일부입니다.
아래 과제는 소스를 읽거나 별도의 연습용 코드로 해결할 수 있습니다. 실제 에이전트 계정, 운영 데이터, 외부 API는 필요하지 않습니다. 각 과제는 완성물과 통과 조건이 있으므로 스스로 이해도를 점검하기 좋습니다.
할 일: ctx_execute 등록부에서 시작해 PolyglotExecutor.execute(),
출력 검사, indexStdout(), ContentStore.index()로 이어지는 화살표 5개를 그립니다.
완성물: 함수명·입력·반환값을 적은 한 장짜리 순서도.
통과 조건: 4,000바이트, 6,000바이트, 110,000바이트 stdout이 각각 어떻게 처리되는지 설명합니다.
6,000바이트는 intent의 유무에 따라 경로가 달라져야 합니다.
힌트: 상수는 글자 수가 아니라 Buffer.byteLength()로 비교됩니다.
할 일: ASCII, 한글, 이모지를 포함한 문자열의 길이를 예상하고 아래 독립 예제와 비교합니다.
완성물: 문자열·UTF-16 길이·UTF-8 바이트 수를 비교한 표.
통과 조건: “5000자면 항상 5000바이트”가 왜 틀린지 설명하고,
src/truncate.ts가 문자 경계를 보존하려는 이유를 적습니다.
// 저장소 모듈을 불러오지 않는 독립 JavaScript 예제
for (const text of ["abc", "한글", "🙂"]) {
console.log({ text, units: text.length, bytes: Buffer.byteLength(text, "utf8") });
}
// 예상: abc = 3 / 3, 한글 = 2 / 6, 🙂 = 2 / 4
할 일: 문서 A~E에 대해 “단어 검색 순위”와 “부분 문자열 순위”를 직접 정합니다.
1 / (60 + 순위)를 더하는 함수를 독립적으로 만들고 결과를 정렬합니다.
완성물: 순위표 두 개, RRF 계산표, 예상 상위 3건.
통과 조건: 두 목록에 모두 등장하는 문서가 한쪽에만 등장하는 문서보다 높아지는 예를 설명합니다. 같은 점수일 때 정렬 기준도 명시합니다.
확장: 제목 일치 보정을 더한 뒤 원래 RRF 점수와 최종 순위를 구분해 보세요.
할 일: src/runPool.ts를 참고해 지연 시간과 실패 여부만 가진 가짜 작업 5개를 처리하는 함수를 작성합니다.
완성물: 동시성 제한, 작업별 성공·실패 결과, 시작·종료 시각표.
통과 조건: 동시성 2일 때 실행 중인 작업이 2개를 넘지 않고, 한 작업이 실패해도 다른 작업은 완료되며 결과 배열은 입력 순서를 유지합니다.
확장: 전체 시간 예산과 작업별 시간 예산을 별도 옵션으로 설계하고 둘의 차이를 예제로 보여 주세요.
할 일: 가짜 이벤트로 파일 수정, 작업 완료, 미해결 오류, 사용자 결정을 표현하고 최신 상태를 요약하는 순수 함수를 만듭니다. 이벤트를 실제 DB나 호스트에 넣을 필요는 없습니다.
완성물: 이벤트 JSON 10건, 복구 안내문, 실패 사례 4개와 기대 결과.
통과 조건: 완료된 작업을 다시 미완료로 보여 주지 않고, 다른 프로젝트의 이벤트를 섞지 않으며, 중복 이벤트가 상태를 왜곡하지 않아야 합니다. 1,000개 제한으로 오래된 결정이 사라질 때의 처리 방침도 설명합니다.
함께 읽기: src/session/snapshot.ts, src/session/project-attribution.ts,
tests/session/session-pipeline.test.ts.
과제의 정답은 실행 결과 하나보다, 어떤 정보가 사라져도 되고 어떤 정보는 다시 찾을 수 있어야 하는지 정한 기준에 있습니다.
JavaScript의 배열·객체·함수와 async/await를 알고 있다는 기준입니다.
매주 읽을 범위를 좁히고, 마지막에 설명 가능한 산출물을 하나씩 남깁니다.
주당 시간은 학습 계획용 제안이며 성능이나 숙련도에 대한 보장은 아닙니다.
| 주차 | 핵심 주제 | 읽을 범위 | 산출물 |
|---|---|---|---|
| 1주 · 3~4시간 | TypeScript 타입·모듈·입력 계약 | package.json, tsconfig.json, src/types.ts | 입력과 결과의 타입 지도 |
| 2주 · 4~5시간 | 자식 프로세스·스트림·시간 제한 | src/executor.ts, src/runtime.ts | 정상·실패·초과 출력 순서도 |
| 3주 · 4~6시간 | SQLite·트랜잭션·WAL·FTS5 | src/db-base.ts, src/store.ts 스키마 부분 | 출처·조각·이벤트 관계도 |
| 4주 · 4~6시간 | 검색 후보·RRF·재정렬 | searchWithFallback(), src/search/unified.ts | 작은 검색 평가표와 실패 질의 |
| 5주 · 4~5시간 | MCP·어댑터·라우팅 정책 | src/server.ts 도구 한 개, hooks/core/routing.mjs | 호스트 두 개의 능력 비교표 |
| 6주 · 5~6시간 | 이벤트 기록·복구·회귀 테스트 | src/session/, hooks/session-directive.mjs, docs/adr/ | 복구 시나리오와 설계 결정문 |
로그 분석기에는 “집계 후 출력”, 문서 도구에는 “출처를 가진 조각 검색”, 긴 작업 도우미에는 “이벤트와 복구 상태 분리”를 적용할 수 있습니다. 세 기능을 한 번에 복제하기보다 해결하려는 문제에 맞는 한 가지부터 옮겨 보세요.
이 저장소를 다 읽는 것보다, 입력·저장·출력·복구의 경계를 다른 문제에도 그릴 수 있게 되는 것이 목표입니다.
| 용어 | 쉬운 설명 |
|---|---|
| 컨텍스트 윈도 | 모델이 이번 응답을 만들 때 펼쳐 볼 수 있는 입력 공간. |
| 토큰 | 모델이 글을 처리하는 단위. 글자 수나 UTF-8 바이트 수와 같지 않습니다. |
| MCP | 호스트와 도구 서버가 기능과 데이터를 주고받는 표준 프로토콜. |
| 호스트 | 대화와 도구 호출을 운영하는 코딩 에이전트 앱 또는 CLI. |
| stdio | 프로세스의 표준 입력·출력. 이 서버와 호스트가 메시지를 주고받는 통로. |
| 훅 (hook) | 도구 호출 전후처럼 정해진 순간에 실행되는 연결 지점. |
| 라우팅 | 어떤 작업을 어떤 도구 경로로 보낼지 결정하는 것. |
| 어댑터 | 다른 입력 형식과 설정을 공통 인터페이스에 맞춰 주는 변환 부품. |
| 스키마 | JSON에 어떤 필드와 값이 들어올 수 있는지 정한 모양과 규칙. |
| 자식 프로세스 | 부모 프로그램이 시작한 별도 실행 단위. 메모리는 분리돼도 파일·네트워크 권한은 별도 문제입니다. |
| stdout / stderr | 일반 출력과 진단 출력의 통로. 코드에 따라 오류가 일반 출력으로 섞일 수도 있습니다. |
| sandbox | 실행을 분리·관리하는 공간이라는 표현. 이 프로젝트의 실제 격리 범위는 프로세스·환경·호스트 정책을 함께 봐야 합니다. |
| SQLite | 파일에 데이터를 저장하는 내장형 관계형 데이터베이스. |
| FTS5 | SQLite에서 문서 본문의 단어를 빠르게 찾는 전문 검색 확장. |
| 역색인 | 단어마다 그 단어가 등장한 문서를 가리키는 찾아보기. |
| 청크 (chunk) | 저장·검색·전달하기 좋게 나눈 문서 조각. |
| BM25 | 검색어의 희소성과 문서 내 빈도 등을 반영하는 검색 점수 방식. |
| Porter | 영어 단어의 어형 차이를 줄이는 어간 추출 방식. |
| trigram | 문자열을 연속한 세 글자 단위로 나누는 방식. |
| RRF | 검색기들의 결과 순위를 점수로 바꾸어 합치는 순위 융합 방식. |
| 편집 거리 | 글자를 삽입·삭제·교체해 한 단어를 다른 단어로 바꾸는 데 필요한 횟수. |
| WAL | 변경 내용을 별도 로그에 먼저 기록하는 SQLite 모드. 읽기·쓰기 공존을 돕지만 쓰기 경합이 사라지지는 않습니다. |
| 트랜잭션 | 여러 데이터 변경을 하나의 성공·실패 묶음으로 처리하는 단위. |
| 중복 제거 | 같은 사실을 반복 저장하거나 반복 집계하지 않도록 거르는 처리. |
| 대화 압축 | 길어진 대화를 줄여 다음 작업에 필요한 공간을 확보하는 과정. |
| 복구 스냅샷 | 나중에 작업 상태를 되찾기 위한 요약. 실행 중이던 컴퓨터 전체의 복사본은 아닙니다. |
| 유한 동시성 | 전체 작업이 많아도 동시에 진행하는 개수는 상한 안에 두는 실행 방식. |
| 번들 / 잠금 파일 | 번들은 배포용으로 묶은 코드, 잠금 파일은 해석된 의존성 버전의 기록입니다. |
| ADR | 중요한 설계 결정의 이유·대안·결과를 남기는 문서. |
| 항목 | 상태 | 기준과 해석 |
|---|---|---|
| 선정 | GitHub Trending Weekly #15 | 분석일 2026-10-23 |
| 스타 / 포크 | 31,896 / 2,195 | 2026-10-23 GitHub 기준 |
| 주 언어 | TypeScript | GitHub 언어 분류 |
| 생성일 | 2026-02-23 | GitHub 저장소 메타데이터 |
| 최근 push | 2026-10-23 | GitHub의 저장소 갱신 시각, 커밋 날짜와 별개 |
| 최신 릴리스 | v1.0.169 · 2026-06-29 | 2026-10-23 GitHub 기준 |
| 분석 커밋 | 90af14e · 2026-10-22 | ci: update install stats · 12:07:06 UTC |
| 패키지 버전 | 1.0.169 | 커밋 90af14e의 package.json 기준 |
| 구현 규모 | 핵심 TypeScript 41,617줄 | src/ 아래 95개 파일, 주석·빈 줄 포함 |
| 테스트 규모 | TypeScript 84,756줄, 테스트 파일 210개 | tests/의 보조 코드 포함, 통과·커버리지 수치 아님 |
| 기타 코드 | JavaScript 약 14,006줄 · Shell 1,652줄 · HTML 2,598줄 | 생성 번들·빌드 결과 제외 |
| 라이선스 | GitHub: NOASSERTION소스: Elastic-2.0 | LICENSE는 Elastic License 2.0 원문 |
| 기본 실행 요구 | Node.js >=22.5.0, SQLite FTS5 | 추가 언어 실행기는 사용하는 기능에 따라 필요 |
절약률: 98%는 프로젝트의 특정 집계 벤치마크를 대표하는 수치입니다.
실제 비용과 정답률은 작업·모델·반환량에 따라 달라집니다. ctx_stats의 바이트 기반 추정과 실제 청구서를 동일시하면 안 됩니다.
보존: README는 새 세션의 이전 자료가 즉시 삭제된다고 설명하지만,
Claude Code의 hooks/sessionstart.mjs 시작 경로에는 cleanupOldSessions(7)과 고아 이벤트 정리가 있습니다.
모든 이전 기록을 매번 즉시 지운다는 설명으로 일반화할 수 없습니다. 세션당 이벤트 상한도 있습니다.
지원표: 문서의 Codex PreCompact 칸은 미지원 표시지만,
src/adapters/codex/index.ts의 capabilities.preCompact는 true입니다.
문서·어댑터 선언·실제 호스트 버전이 같은 상태라고 가정하지 마세요.
청크: Markdown 경로는 기본 4,096바이트를 기준으로 문단을 나누지만, 단일 큰 문단은 그대로 남을 수 있습니다. 코드 펜스 인식도 전체 Markdown 파서나 언어별 구문 트리 분석을 대신하지 않습니다.
개인정보: 로컬 저장과 외부 연결은 별도 기능입니다.
플랫폼 설정이 있으면 maybeForward()가 정제한 이벤트를 전송합니다.
정규식 기반 가림 처리를 모든 비밀정보가 제거된다는 보장으로 읽어서는 안 됩니다.
검증과 라이선스: 테스트 코드의 존재가 모든 환경에서의 통과를 보장하지는 않습니다.
BENCHMARK.md의 일부 실행 명령은 현재 패키지 스크립트와도 다릅니다.
GitHub의 NOASSERTION 대신 LICENSE 원문을 확인하고, 무제한 허용 라이선스로 단정하지 마세요.