
AI 코딩 에이전트에게 '기억'을 부여하는 claw-memory 제작기 (아키텍처 해설)
요약
Claude Code의 Auto Memory 한계를 극복하기 위해 개발된 로컬 완결형 장기 메모리 플러그인 claw-memory를 소개합니다. RAG 방식을 활용하여 대화 내용을 벡터화하고, SQLite를 통해 외부 DB 없이 로컬에서 안전하게 기억을 관리하는 아키텍처를 해설합니다.
핵심 포인트
- Claude Code Auto Memory의 컨텍스트 제한 문제 해결
- RAG 기반의 의미론적 유사도 검색을 통한 장기 기억 구현
- SQLite를 활용한 로컬 완결형 데이터 저장 및 보안 강화
- 데몬이나 외부 벡터 DB 없이 Node.js 환경에서 동작
Claude Code에는 2026년 2월부터 Auto Memory가 표준 탑재되어 기본적으로 활성화되었습니다. MEMORY.md에 에이전트가 스스로 메모를 기록해 두면, 다음 세션에서 이를 읽어옵니다. "세션이 바뀌면 전부 잊어버린다"는 시대는 일단 끝나가고 있습니다.
그렇다면 이제 장기 메모리(Long-term Memory) 도구는 필요 없는 걸까요? 저는 한동안 Auto Memory를 사용해 보면서 한 가지 한계를 깨달았습니다. 이력이 늘어날수록, 표준 메모리는 '현재 관계된 과거'를 꺼내기 어려워진다는 점입니다.
Auto Memory가 매번 로드하는 것은 MEMORY.md의 상단 200행(또는 25KB)까지입니다. 프로젝트가 성장할수록 과거의 중요한 대화는 이 천장 너머로 밀려나게 되며, 에이전트가 "그 파일을 읽으러 가야겠다"라고 판단했을 때만 참조됩니다. 판단을 놓치면 그 기억은 사실상 없는 것이나 마찬가지입니다. 상시 사용할 수 있는 기억의 총량이 컨텍스트(Context)의 천장에 막히게 되는 것입니다.
이 "기억의 총량"과 "컨텍스트 소비"를 분리하고 싶어서, claw-memory라는 로컬 완결형 장기 메모리 플러그인을 만들었습니다. 발상은 RAG (검색 증강 생성, Retrieval-Augmented Generation)와 유사하며, 과거의 모든 대화를 임베딩(Embedding)으로 벡터화해 두었다가, 현재의 프롬프트와 의미적으로 가까운 것들만 추출하여 주입합니다. 모수가 아무리 늘어나도 상시 로드되는 양은 늘어나지 않습니다.
본 기사에서는 그 메커니즘을 아키텍처 레벨에서 자세히 해설합니다. 실제로 직접 움직여 보며 테스트할 수 있도록 리포지토리와 설치 절차도 함께 기재했습니다.
- GitHub: https://github.com/nogataka/claw-memory
- npm: https://www.npmjs.com/package/@nogataka/claw-memory
이 기사는 claw-memory v0.1.x 시점의 구현을 바탕으로 하고 있습니다. 프리릴리스(Pre-release) 단계이므로 향후 인터페이스가 변경될 가능성이 있습니다. 최신 정보는 리포지토리를 확인해 주세요.
한마디로 말하면, AI 코딩 에이전트(Claude Code 및 Codex)에게 장기 기억을 부여하는 MCP 서버입니다. 설계상의 핵심은 "로컬 완결"에 두었습니다.
- 상주 데몬(Daemon)이 필요 없습니다.
- Python 런타임이 필요 없습니다.
- 외부 벡터 DB (Pinecone 또는 Weaviate 등)가 필요 없습니다.
- 데이터는 머신 외부로 나가지 않습니다. 예외는 세션을 요약하는 LLM 호출뿐이며, 이 또한 사용자가 선택할 수 있습니다.
동작 환경은 Node.js 20 이상입니다. 기억의 저장소는 ~/.claw-memory/memory.db라는 하나의 SQLite 파일로 통합되어 있으며, 벡터 또한 이 안에 저장합니다.
AI 에이전트의 "기억"을 구현하는 방법은 여러 가지가 있습니다. 예를 들어 CLAUDE.md와 같은 설정 파일에 수동으로 기록해 두는 방법도 있고, 외부 벡터 DB 서비스에 대화를 축적하는 방법도 있습니다. 각각의 장점이 있으며, claw-memory의 방식이 항상 최적이라고 주장하려는 것은 아닙니다.
다만, 저의 경우에는 다음 점들을 중시했습니다.
- 설정 파일의 수동 유지보수는 지속하기 어려우므로, 세션으로부터 자동으로 기억을 추출하고 싶다.
- 업무 코드를 다루는 경우가 많아, 대화 내용을 외부 서비스로 보내고 싶지 않다.
- 셋업 과정에서 Docker나 Python 환경을 요구하면, 여러 머신에 도입하기 번거로워진다.
이 세 가지 점을 충족시키려 노력한 결과, "로컬 SQLite에 전부 넣는다", "임베딩 계산도 로컬에서 완결한다", "외부로 나가는 것은 요약을 위한 LLM 호출뿐이다"라는 설계에 도달했습니다.
Auto Memory가 기본적으로 작동하는 지금, 가장 궁금한 점은 "표준 기능만으로는 부족한가"라는 점일 것입니다. 결론부터 말씀드리면, claw-memory는 Auto Memory를 대체하는 것이 아니라, Auto Memory가 원리적으로 취약한 영역을 메우는 것이라고 생각합니다.
양자의 설계를 비교해 보면, 기억을 "꺼내는 방식"이 근본적으로 다릅니다.
| 관점 | Auto Memory (표준) | claw-memory |
|---|---|---|
| 회상 방식 | MEMORY.md 상단 200행/25KB를 매번 로드 (고정 인덱스) | 프롬프트마다 의미가 유사한 대화를 검색하여 주입 (RAG형) |
| 스케일 | 상시 사용 가능한 기억이 한계에 도달하여 정체됨 | 모수가 늘어나도 상시 로드되는 양은 늘어나지 않음 |
| 검색 | 구조가 없는 Markdown을 통째로 참조 | type / 개념 / 파일 / 날짜로 필터링 가능 |
| 대상 에이전트 | Claude Code 전용 | Claude Code와 Codex 모두 지원 |
| 도입 전 이력 | 대상 제외 | 생(raw) 로그 전체 검색으로 소급 가능 |
| 여기서는 제가 실제로 "표준만으로는 어렵다"고 느꼈던 상황을 구체적인 사례로 설명하겠습니다.
이 부분이 claw-memory의 가장 큰 가치라고 생각하는 지점입니다.
어느 날, SSE 연결에서 ECONNRESET이 산발적으로 발생하는 버그를 시간을 들여 수정했다고 가정해 봅시다. 원인은 "프록시의 유휴 시간 초과(idle timeout)"였고, 해결책은 "하트비트(heartbeat) 송출"이었습니다. Auto Memory는 이를 MEMORY.md 또는 토픽 파일에 기록합니다.
3개월 후, 비슷한 증상을 다시 마주합니다. 이때의 동작 차이가 승부처입니다.
- Auto Memory: 해당 지식이
MEMORY.md상단 200행에 남아 있다면 찾아낼 수 있습니다. 하지만 3개월 치의 메모에 밀려 토픽 파일 쪽으로 옮겨졌다면, 에이전트가 "그 파일을 열어야겠다"고 판단하지 않는 한 참조되지 않습니다. 공식 문서에서는MEMORY.md의 고정 로드와 토픽 파일의 온디맨드(on-demand) 읽기에 대해 설명하고 있으며, 현재 프롬프트와의 의미적 유사성에 따라 정렬하는 인덱스에 대해서는 언급하고 있지 않기 때문입니다. - claw-memory: 현재 증상(
ECONNRESET+ SSE)의 임베딩(embedding)과 유사한 청크(chunk)가 모수의 양과 관계없이 상위에 나타납니다. 축적된 데이터가 6개월 치든 1년 치든, 불러오는 관련도는 저하되지 않습니다.
(프롬프트) SSE에서 가끔 연결이 끊기는데 원인을 알 수 있어?
↓ claw-memory가 코사인 유사도(cosine similarity) 근방에서 과거 대화를 주입
<relevant-past-conversations instruction="reference-only">
...
"기억의 총량"과 "컨텍스트 소비"가 분리되어 있다는 것은 바로 이런 의미입니다.
claw-memory는 증류(distillation) 시 각 대화에 관측 타입(bugfix / feature / decision 등)과 개념 키워드, 참조·편집 파일을 메타데이터로 붙입니다.
따라서 memory_search를 통해 다음과 같은 필터링이 가능합니다.
memory_search(query="인증 관련", type="bugfix", file="src/auth/")
→ 인증 관련 "버그 수정"만, 게다가 src/auth/를 건드린 대화로 한정
Auto Memory의 토픽 파일은 비구조화된 Markdown이므로, "과거 내용 중 결정 사항(decision)만" 또는 "특정 파일을 건드린 대화만"과 같은 기계적인 필터링이 작동하지 않습니다. 게다가 memory_search는 우선 id와 제목, 날짜만 포함된 가벼운 인덱스를 반환하고, 정말 필요한 것만 memory_get으로 본문을 가져오기 때문에 컨텍스트 소비도 최소한으로 억제됩니다.
Auto Memory는 Claude Code 전용이며 리포지토리 단위입니다. 저는 동일한 프로젝트를 Claude Code와 Codex 사이에서 오가며 작업하는 경우가 있는데, 이 점이 은근히 불편했습니다.
claw-memory는 양쪽 세션을 동일한 memory.db로 증류하므로, 기억이 에이전트를 가로질러 공유됩니다.
- 어제 Codex에서 "이 API의 속도 제한(rate limit)은 429 재시도 + 지수 백오프(exponential backoff)로 대응한다"고 결정했다.
- 오늘 Claude Code에서 동일한 API를 다룸 → 그 결정이
memory_recall을 통해 나타남.
"어느 에이전트와 대화했는지"를 기억할 필요가 없어집니다.
memory_search_logs는 ~/.claude/projects와 ~/.codex/sessions의 원본 로그를 직접 전체 검색합니다. 증류되지 않은 세션이나 claw-memory를 도입하기 전의 이력도 모두 대상에 포함됩니다.
memory_search_logs(query="Stripeのwebhook署名検証", sources=["claude-code","codex"])
→ 過去どこかで話した webhook 検証の実装を、前後100文字の文脈付きで列挙
"언젠가 이야기했던 것 같은데, 어떤 세션이었는지 기억이 나지 않아"라는 상황을 구원하는 것이 바로 이 기능입니다. Auto Memory는 기록된 메모를 대상으로 하며, 원본 로그를 거슬러 올라가는 검색이 아닙니다.
한편, "대화에서 자동으로 기억을 추출한다"라는 자동 캡처(Auto Capture) 자체는 더 이상 claw-memory만의 고유한 기능은 아닙니다. Auto Memory도 기본적으로 설정이나 패턴을 자동으로 기록합니다. 이 부분은 기능이 중첩됩니다.
따라서 저는 두 가지를 경쟁 관계가 아닌 역할 분담으로 파악하는 것이 좋다고 생각합니다.
- 명시적 규칙이나 항상 적용하고 싶은 지시 사항 →
CLAUDE.md(인간이 작성) - 가벼운 "최근의 학습 내용"의 상시 로드 → Auto Memory (표준)
- 이력이 늘어나도 유효한 관련도 검색 · 횡단 검색 · 원본 검색 → claw-memory
두 기능을 모두 활성화할 경우, 주입(Injection)이 이중으로 되어 컨텍스트(Context)를 압박하지 않는지라는 관점만 주의하면 안전합니다.
claw-memory에는 성질이 다른 두 가지 기억 소스(Memory Source)가 있습니다. 이를 분리한 점이 설계상의 포인트입니다.
| 소스 | 내용 | 대응 도구 |
|---|---|---|
| 증류 DB (Distilled DB) | LLM으로 요약한 세션. 요약 · 사용자 설정 · 메타데이터가 포함된 대화 청크(Chunk)를 보유하며, 시맨틱 검색 (Semantic Search)이 가능 | memory_recall, memory_search, memory_get |
| 생 로그 검색 (Raw transcript search) | ~/.claude/projects와 ~/.codex/sessions에 있는 실제 로그를 전체 검색. 증류되지 않은 세션도 대상 | memory_search_logs |
증류 DB는 "정리된 빠른 기억"입니다. 반면 생 로그 검색은 "보험"의 위치에 있으며, claw-memory를 도입하기 전의 대화를 포함하여 원본 로그가 남아 있는 것을 대소문자 구분 없는 부분 일치 방식으로 전체 검색할 수 있습니다 (거대한 로그 파일은 대상에서 제외됩니다).
두 가지를 나눈 이유는 단순합니다. 증류(Distillation)에는 LLM 호출 비용이 들기 때문에 모든 세션을 완벽하게 증류할 수 있다고 보장할 수 없기 때문입니다. 증류가 누락되더라도 생 로그라는 원본이 수중에 남아 있다면 놓친 부분을 찾아낼 수 있습니다. 이 "큐레이션된 색인"과 "전체 grep이 가능한 원본"이라는 이중 구조가 실용적인 안심감으로 이어집니다.
먼저 전체상을 그림으로 보여드립니다. 쓰기(기억 생성)와 읽기(기억 사용)의 두 가지 경로, 그리고 독립된 생 로그 검색 경로로 나뉩니다.
각 컴포넌트의 구현은 TypeScript로 되어 있으며, 소스는 src/ 하위에 모여 있습니다. 역할별로 파일이 나누어져 있으며, 주요 파일은 다음과 같습니다.
| 파일 | 역할 |
|---|---|
src/cli.ts | 엔트리 포인트 (Entry Point). 모든 서브 커맨드 정의 |
src/mcp/server.ts | MCP 서버 본체. 8개의 도구(Tool)를 공개 |
src/core/distill.ts | 세션 증류 파이프라인 |
src/core/recall.ts | 기억 블록(Memory Block) 조립 |
src/core/db.ts | SQLite 초기화 및 스키마 정의 |
src/core/vector-memory.ts | 벡터 테이블과 FTS5 조작 |
src/core/embeddings.ts | 로컬 임베딩 (Xenova e5) |
src/core/llm.ts / providers.ts | 플러그인 가능한 LLM 추상화 |
src/core/logsearch/ | 생 로그의 전체 검색 |
src/core/hooks.ts | 라이프사이클 훅 (Lifecycle Hook) 처리 |
src/ui/server.ts / page.ts | 읽기 전용 Web 뷰어 |
이어서 데이터 흐름(Data Flow)에 따라 내용을 살펴보겠습니다.
세션이 종료되면 Claude Code는 Stop 훅을 호출합니다. claw-memory는 여기서 증류 처리를 시작하지만, 사용자의 조작을 차단하지 않도록 비동기(Asynchronous)로 처리한 점이 노하우입니다.
구체적으로는 src/core/hooks.ts의 처리가 spawn
에서 자식 프로세스를 분리하여(detached, stdio: ignore), 백그라운드에서 증류(Distillation)를 실행합니다. 사용자 입장에서는 세션을 닫는 순간 백그라운드에서 기억이 생성되어 가는 형태가 됩니다.
불필요한 실행을 피하는 메커니즘도 포함되어 있습니다. distill_watermarks 테이블에 트랜스크립트(Transcript)의 수정 시간(mtime)을 기록하고, 내용에 변화가 없다면 증류를 건너뜁니다(--if-stale). 이를 통해 증류는 증분(Incremental) 방식으로 동작하며, 동일한 세션을 중복 처리하지 않습니다.
증류의 본체는 src/core/distill.ts입니다. 처리는 크게 4단계로 나뉩니다.
- 전처리
<private>…</private>로 둘러싸인 부분을 제거합니다(src/core/private.ts). 이는 저장 시에도, LLM 전송 시에도 포함되지 않습니다.- 메시지 수가 2개 미만이거나 본문이 100자 미만인 세션은 노이즈로 간주하여 제외합니다.
- LLM 증류 (tier:
summary)- 단 1회의 LLM 호출로 요약, 관측 타입(Observation type), 개념 키워드, 참조/편집 파일, 감지된 설정을 한꺼번에 추출합니다.
- 저장 (요약·설정)
- 요약은
addSessionSummary(), 설정은setPreference()를 통해 각각의 테이블에 먼저 저장(upsert)합니다. 이들은 임베딩(Embedding)을 거치지 않습니다.
- 요약은
- 요약은
- 벡터화 및 청크(Chunk) 저장
- User/Assistant 쌍마다 로컬에서 임베딩을 계산합니다. 관측 타입, 개념, 파일 등의 메타데이터는 이 청크에 부여됩니다.
chunkExists()로 기존 청크와의 중복을 판정하여, 동일한 내용은 건너뜁니다.- 청크 쓰기(
vec_chunks+conversation_chunks+chunks_fts)는 하나의 트랜잭션(Transaction)으로 묶어서 수행하여 세 테이블의 정합성을 유지합니다. 요약 및 설정 저장은 이 트랜잭션 외부에서 이루어집니다.
LLM이 반환하는 구조는 다음과 같습니다. 요약은 정해진 헤더 구성으로 만들어 나중에 읽기 쉽게 했습니다.
summary: 구조화된 Markdown (### 의뢰 / 조사·판명 / 완료 / 다음 단계)
obs_type: discovery | bugfix | feature | decision | change | other
concepts: 기술 키워드 배열 (3~8개)
...
obs_type(관측 타입)과 concepts(개념)를 붙이는 이유는 후속 검색 단계에서 필터를 걸 수 있도록 하기 위함입니다. "버그 수정 대화만", "특정 개념을 포함하는 대화만"과 같은 필터링이 가능해집니다.
사용자 설정은 표기법이 매우 불규칙한 부분입니다. "언어(Language)", "lang", "preferred_language"는 모두 동일한 의미를 가리킵니다. 따라서 src/core/memory.ts에 정규 키(Canonical key)와 에일리어스(Alias) 대응표를 갖게 하여, 감지된 설정을 정규화한 후 저장합니다.
CANONICAL_PREFERENCE_KEYS = [
"language", "response_style", "detail_level",
"code_style", "framework", "tone", "tools"
...
이를 통해 여러 세션에서 미세하게 다른 표현으로 설정이 감지되더라도 동일한 키로 집약됩니다.
claw-memory의 데이터는 ~/.claw-memory/memory.db라는 단일 SQLite 파일로 집약됩니다. 스키마는 src/core/db.ts에서 정의하며, 주요 테이블은 다음과 같습니다.
projects -- 프로젝트 마스터
session_summaries -- 세션 요약 (구조화된 Markdown)
user_preferences -- 사용자 설정 (정규화됨)
...
벡터를 별도의 서비스에 두지 않고 SQLite 내부에 넣을 수 있는 것은 sqlite-vec이라는 확장 기능 덕분입니다. vec_chunks는 vec0 가상 테이블로 정의되어, 코사인 거리(Cosine distance)를 이용한 KNN(최근접 이웃 탐색)을 SQL만으로 실행할 수 있습니다.
벡터(Vector), 메타데이터(Metadata), 전문 색인(Full-text index)을 분리하면서 ID로 연동함으로써, "의미로 찾기", "키워드로 찾기", "타입이나 날짜로 필터링하기"를 조합할 수 있는 구조로 설계했습니다.
동시 액세스(Concurrent access)에 대한 고려도 포함되어 있습니다. SQLite를 WAL(Write-Ahead Logging) 모드로 열고 busy_timeout을 설정했기 때문에, MCP 서버와 Web 뷰어가 동시에 읽고 써도 시스템이 무너지지 않습니다.
시맨틱 검색(Semantic search)에는 임베딩(Embedding, 벡터화)이 필요하지만, 이 부분을 외부 API에 의존하면 "데이터가 외부로 나가지 않는다"라는 설계 원칙이 깨집니다. 그래서 @xenova/transformers를 사용하여 Xenova/multilingual-e5-small (384차원, 다국어 지원)을 로컬에서 구동하고 있습니다. 구현부는 src/core/embeddings.ts입니다.
e5 계열 모델의 규칙에 따라 용도별로 접두사(Prefix)를 나누었습니다.
- 저장할 문장:
passage:를 앞에 붙임 (embedPassage()) - 검색 쿼리:
query:를 앞에 붙임 (embedQuery())
모델은 첫 번째 증류(Distillation) 시 약 100MB가 다운로드되어 ~/.cache 경로 아래에 캐시됩니다. 한 번 다운로드하면 오프라인에서도 작동합니다. 로딩은 지연 초기화(Lazy initialization) 방식을 사용하여, 프로세스 시작 시점이 아니라 처음으로 임베딩을 계산하는 타이밍에 단 한 번 실행됩니다. 이후에는 동일한 프로세스 내에서 재사용되므로 두 번째 실행부터는 매우 빠릅니다.
다국어 모델을 선택한 이유는 일본어와 영어가 혼재된 저의 대화 로그에서도 검색 정확도를 유지하고 싶었기 때문입니다. 영어 전용 모델이 더 가벼운 경우도 있으므로, 용도에 따라 선택지는 달라질 수 있습니다.
검색은 src/core/search.ts에서 구현하며, 시맨틱 검색과 키워드 검색을 조합합니다.
- 시맨틱(Semantic):
vec_chunks위에서 코사인 KNN(Cosine KNN)을 실행합니다 (프로젝트 ID나 메타데이터로 WHERE 필터링). - 키워드(Keyword): FTS5의 trigram 매칭을 병용합니다. 의미가 가깝지 않더라도 고유 명사나 에러 메시지 같은 "단어 그 자체"로 찾고 싶은 케이스를 포착합니다.
- 통합(Integration): 양쪽의 결과를 ID 단위로 중복 제거하고, 거리 순으로 정렬합니다.
벡터 검색만 사용하면 타입 이름이나 커맨드 이름 같은 짧은 고유 표현을 놓칠 수 있습니다. 반대로 키워드 검색만 사용하면 유의어(Paraphrasing)에 취약합니다. 이 둘을 합침으로써 실제 운용 시의 누락을 줄이고 있습니다.
시맨틱 히트(Semantic hit)의 엄격함은 MEMORY_SIMILARITY_MAX_DISTANCE (코사인 거리의 상한선)로 조정할 수 있습니다. 값을 작게 할수록 더 엄격해집니다.
기억은 사용될 때 비로소 의미를 갖습니다. claw-memory는 SessionStart와 UserPromptSubmit 훅(Hook)을 통해 src/core/recall.ts의 buildMemoryBlock()을 호출하고, 그 결과를 표준 출력(Standard output)에 기록합니다. 이 출력이 에이전트의 컨텍스트(Context)에 삽입되는 구조입니다.
주입되는 블록은 XML 형식이며, 성격이 다른 두 종류를 명확히 구분하고 있습니다.
<user-preferences instruction="always-apply">
다음 사용자 설정을 항상 준수하십시오.
- language: Japanese
...
핵심은 instruction 속성입니다. 설정은 always-apply (항상 적용), 과거 대화나 요약은 reference-only (참고만 하고 그대로 복창하지 말 것)로 지시를 나누었습니다. 모든 것을 "따라야 할 지시"로 전달하면, 에이전트가 과거 대화를 부자연스럽게 되풀이하는 경우가 있기 때문입니다. 배경 지식과 행동 지시를 구분함으로써 자연스러운 동작을 목표로 합니다.
주입하는 유사 대화는 무제한이 아니라, topK (기본 5개)와 거리 상한선으로 제한합니다. 프로젝트 단위로 기억을 분리했기 때문에 다른 프로젝트의 대화가 섞이는 일도 없습니다. 이를 통해 컨텍스트를 압박하지 않으면서 "효과적일 것 같은 기억만" 전달할 수 있습니다.
에이전트가 명시적으로 호출할 수 있는 도구(Tool)는 8개입니다. 구현은 src/mcp/server.ts에 있습니다.
| 도구 | 용도 |
|---|---|
memory_recall(query, cwd?, topK?) | 즉시 읽을 수 있는 컨텍스트 블록을 반환. 설정 + 최근 요약 + 유사 대화. 태스크 시작 시 호출하도록 설계됨 |
memory_search(query, cwd?, limit?, type?, concept?, file?, dateFrom?, dateTo?) | 토큰 효율이 높은 인덱스 (id + 제목 + 날짜 + type). 메타데이터로 필터링 가능 |
memory_get(ids) | 지정된 ID의 본문과 메타데이터를 가져옴 |
memory_remember(text, cwd?, sessionId?) | 자유 형식의 메모를 영구 저장 |
| `memory_distill(cwd?, sessionId? | transcriptPath?)` |
memory_get_preferences(cwd?) | 프로젝트 설정을 목록으로 확인 |
memory_search_logs(query, sources?, projectPath?, startDate?, endDate?, limit?, offset?) | 원시 로그 (Raw logs)의 전체 텍스트 검색 |
memory_forget(ids) | 청크를 논리적 삭제 (검색, 회상, 뷰어에서 제거) |
memory_search와 memory_get을 분리한 것은 토큰 소비를 억제하기 위한 장치입니다. 먼저 가벼운 인덱스(ID와 제목만 포함)로 후보를 살펴본 뒤, 정말로 필요한 것만 memory_get으로 본문을 가져옵니다. 처음부터 전체 텍스트를 반환하지 않음으로써 컨텍스트(Context)의 낭비를 방지합니다.
증류(Distillation)에는 LLM이 필요하지만, 이 부분을 특정 업체에 고정하고 싶지 않았습니다. src/core/llm.ts와 providers.ts에서 추상화하였으며, 환경 변수 CLAW_MEMORY_LLM_BACKEND를 통해 4가지 중 하나를 선택할 수 있습니다.
| 백엔드 | 인증 | 비고 |
|---|---|---|
agent-sdk (기본값) | Claude CLI 로그인 | API 키 불필요. Pro/Max/Team/Enterprise 구독으로 작동 |
codex-sdk | Codex CLI 로그인 | ChatGPT/Codex 플랜. 읽기 전용(read-only)으로 동작 |
anthropic | ANTHROPIC_API_KEY | Messages API를 직접 호출 |
openai-compatible | CLAW_MEMORY_OPENAI_API_KEY | Gemini / OpenRouter / LM Studio 등 |
API 키를 별도로 준비하지 않아도, 이미 로그인된 Claude나 Codex의 구독을 그대로 증류에 사용할 수 있다는 점이 도입 장벽을 낮춰준다고 생각합니다.
증류는 빈도가 높은 처리이므로, 티어 라우팅(Tier routing, CLAW_MEMORY_TIER_SUMMARY 등)을 통해 저렴한 모델에 할당하는 등의 조정도 가능합니다.
agent-sdk 백엔드에는 구현 과정에서 어려움을 겪었던 부분이 있습니다. 증류를 위해 에이전트를 호출하면, 그 자체가 새로운 세션을 생성하고, Stop 후크(Hook)가 다시 증류를 호출하는... 식의 무한 재귀나 후크의 폭주가 발생할 수 있습니다.
이를 방지하기 위해, 증류 시의 서브 세션(Sub-session)은 다음 설정으로 격리합니다.
settingSources: [] // ~/.claude 및 프로젝트 설정을 읽어오지 않음 (후크 재진입 및 비프음 연발을 방지하는 핵심 조치)
permissionMode: "bypassPermissions" // 권한 프롬프트를 띄우지 않음
maxTurns: 1, allowedTools: [] // 도구 없는 단발성 호출
재진입을 억제하는 핵심은 settingSources: []입니다. 이를 빈 값으로 설정함으로써, 사용자 자신의 후크(예: Stop 후크의 효과음)나 claw-memory 자체의 후크가 증류 시 발생하는 서브 세션에서 발화하는 것을 방지합니다. 최근 커밋에서도 이 격리가 불충분하여 발생했던 후크의 비프음 연발 문제를 수정했습니다. 자기 자신을 호출하는 구조를 가진 도구에서는 이러한 재진입 제어가 은근히 큰 역할을 합니다.
증류(Distillation) DB와는 별개의 경로로, 원본 로그(raw log)의 전체 텍스트 검색 기능이 있습니다. 구현은 src/core/logsearch/ 하위에서 이루어지며, 이는 제가 이전부터 사용해 왔던 로그 횡단 검색 도구의 개념을 이식한 것입니다.
검색 대상은 다음 두 곳입니다.
~/.claude/projects/*/*.jsonl # Claude Code의 트랜스크립트(transcript)
~/.codex/sessions/**/*.jsonl # Codex의 롤아웃(rollout) 로그
Claude Code와 Codex는 로그의 JSON 구조가 다르기 때문에, parse.ts에서 각각의 파서(parser)를 가지며, type이 user / assistant인 발화만을 추출합니다. 검색된 위치에는 앞뒤 100자의 문맥(context)을 덧붙여 반환하므로, 검색 결과만으로도 "무슨 내용이었는지" 대략적으로 파악할 수 있습니다.
이 경로의 가치는 claw-memory를 설치하기 전의 대화까지도 검색할 수 있다는 점에 있습니다. 증류(distillation)가 실행되지 않았더라도, 원본만 남아 있다면 나중에 찾아낼 수 있습니다.
기억의 내용을 눈으로 확인하고 싶을 때를 위해, 읽기 전용 웹 뷰어(Web viewer)를 준비했습니다. src/ui/server.ts에서 구현되었으며, hono 기반의 경량 서버입니다.
claw-memory ui --open # http://localhost:4319
프로젝트 목록, 세션 요약, 메타데이터가 포함된 대화 청크(chunk), 설정을 열람할 수 있으며, 원본 로그의 전체 텍스트 검색도 여기서 실행할 수 있습니다.
은근히 편리한 점은 서버 전송 이벤트(Server-Sent Events, SSE)를 통한 라이브 업데이트입니다. SQLite의 PRAGMA data_version을 1.5초 간격으로 폴링(polling)하여, 별도 프로세스의 MCP 서버가 DB를 변경한 것을 감지하면 뷰어로 업데이트 이벤트를 보냅니다. 에이전트가 백그라운드에서 기억을 쓰는 즉시 뷰어의 표시가 바뀌는 모습을 지켜볼 수 있습니다.
참고로, 이 뷰어는 실행했을 때만 작동합니다. 상주 프로세스는 가지 않으므로, 보고 싶을 때 실행하는 방식으로 사용하게 됩니다.
여기서부터는 실제로 시도해 보는 절차입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기