한 문장으로.
ai-memory = "Claude Code로 하던 일을 그대로 Codex에서 이어 받게 해 주는 공용 기억 서버." 코딩 에이전트는 세션이 끝나면 맥락을 잃는다. 도구를 바꾸면 더 심하다 — 아키텍처를 다시 설명하고, 이미 실패한 접근을 또 시도하고, 남은 질문을 다시 정리해야 한다. ai-memory는 에이전트의 생명주기 훅을 받아 관찰 기록을 쌓고, 세션이 끝날 때 그걸 요약해 다음 에이전트에게 넘길 인수인계서(handoff)를 만든다.
야간 근무자가 퇴근하며 "3번 라인 압력 이상, 밸브 교체는 시도했으나 실패, 부품 발주 필요"를 적어 두면 주간 근무자가 처음부터 진단하지 않는다. 안 적어 두면 다음 사람이 같은 밸브를 또 뜯는다.
ai-memory는 그 노트를 근무자가 일하는 동안 자동으로 받아 적고, 교대 시점에 요약해 다음 사람 앞에 놓아 준다. 게다가 주간 근무자가 다른 회사 소속(Claude Code → Codex)이어도 같은 노트를 읽는다.
memory_query·memory_read_page 같은 도구로 기억을 직접 조회할 수 있다.만든 사람은 브라질 개발자 커뮤니티에서 오래 활동해 온 Fabio Akita다. Rust 19만 6천 줄, 크레이트 10개, SQL 마이그레이션 50개, 최신 릴리스 v1.36.0(2026-08-29). 지원 클라이언트 목록이 README 첫 화면을 통째로 차지하는데 — Claude Code·Codex·Cursor·Gemini CLI·OpenCode·Kimi Code·Kiro CLI·Grok Build·Antigravity·OpenClaw 등 스무 종이 넘는다.
별 5.2천 개 — '에이전트 메모리' 도구는 널렸는데 왜 이건 다른가.
대부분의 메모리 도구는 한 에이전트 안에서만 산다. ai-memory의 첫 문장은 아예 "Claude Code를 작업 중간에 끄고, 같은 디렉터리에서 Codex를 켜서, 다시 설명하지 않고 이어 가라"다. 도구를 갈아타는 게 일상이 된 지금 이 지점을 정면으로 겨눈다.
"메모리"를 표방하는 도구 상당수가 write_note 같은 걸 사람이 불러 줘야 동작한다. 그러면 결국 안 쓰게 된다. ai-memory는 훅에서 자동으로 받아 적는다. 프롬프트·도구 실행·세션 경계가 정제(sanitize)를 거쳐 관찰 기록으로 들어간다.
아키텍처 문서가 이 불변식을 명시한다 — "마크다운 위키가 진실의 원천이고, SQLite는 검색·세션·관찰·핸드오프·감사·임베딩을 위한 파생 색인이다." 그래서 위키는 git 저장소에 그대로 들어가고, grep이 먹고, Obsidian으로 열리고, rsync로 백업된다. DB가 깨져도 지식은 파일로 남는다.
README가 "LLM은 선택"이라고 못 박는다. 모델을 안 붙여도 SQLite FTS5 전문 검색과 수동 선언 페이지는 그대로 쓴다. 요약·자동 개선처럼 모델이 있어야 하는 기능만 꺼진다. 사내 도입에서 "외부 API로 코드 맥락이 나간다"는 반대에 답할 여지를 남긴 설계다.
단순 관련도 정렬이 아니다. FTS5·개체 매칭·그래프 이웃(그리고 선택적으로 벡터)이 각각 후보를 뽑아 RRF로 합친 뒤, 자르기 전에 유지 관리되는 규칙·결정·절차·함정 페이지를 그 순간의 세션 기록보다 위로 올린다. 다만 문서가 스스로 단서를 단다 — 이 신호들은 어디서 왔는지만 바꿀 뿐, 가져온 텍스트가 지시 권한을 얻지는 않는다.
메모리 시스템의 진짜 위험은 저장된 텍스트가 다음 세션의 프롬프트로 들어가 명령처럼 읽히는 것이다(간접 프롬프트 인젝션). ai-memory는 랭킹 신호가 아무리 높아도 그 텍스트에 지시 권한을 주지 않는다고 문서에 박아 뒀다. 기억을 다루는 도구가 반드시 답해야 하는 질문인데, 대부분은 답하지 않는다.
Rust 워크스페이스 크레이트 10개.
| 크레이트 | 줄 | 맡은 일 |
|---|---|---|
| ai-memory-cli | 50,945 | CLI 전체 — 설치·훅 생성·상태·관리 명령. 가장 크다 |
| ai-memory-store | 36,176 | SQLite 저장·FTS5 검색·마이그레이션 50개 |
| ai-memory-mcp | 33,682 | MCP 서버 — 도구 18종 |
| ai-memory-hooks | 19,482 | 훅 수신·정제·이벤트 분류 |
| ai-memory-consolidate | 15,662 | 관찰 기록 → 요약·핸드오프로 압축 |
| ai-memory-llm | 8,784 | 모델·임베딩 프로바이더 어댑터 |
| ai-memory-wiki | 8,496 | 마크다운 위키 읽기·쓰기 |
| ai-memory-web | 7,315 | 읽기 전용 웹 UI + REST API |
| ai-memory-workstream | 7,039 | ai-memory run — 관리형 세션 연속성 |
| ai-memory-core | 6,553 | 공용 타입·불변식 |
저장은 SQLite 하나다. 별도 서버가 없고, 전문 검색은 SQLite 내장 FTS5를 쓴다. 마이그레이션 파일 이름을 훑는 것만으로 이 프로젝트가 지나온 길이 보인다 — V02__handoffs(인수인계) → V03__decay(감쇠) → V04__embeddings(임베딩) → V07__observations_fts_and_link_index(전문검색+링크) → V13__cross_project_links(프로젝트 간 링크) → V14__users(다중 사용자).
| 영역 | 선택 | 메모 |
|---|---|---|
| 언어 | Rust 1.95+ | 훅은 에이전트의 핫 경로라 지연이 곧 체감 성능 |
| 저장 | SQLite + FTS5 | 서버 없는 단일 파일. 벡터는 선택 |
| LLM | Anthropic · OpenAI · Copilot · Gemini · OIDC 기기 인증 | 전부 선택 — 없어도 동작 |
| 임베딩 | OpenAI · Voyage · Gemini · Ollama·LM Studio·vLLM | 키 없는 OpenAI 호환 엔드포인트 지원 |
| 배포 | Docker(amd64·arm64) · macOS 네이티브 · AUR · WSL2 | 네이티브 윈도우는 실험 단계 |
| 스크립트 | sh 92개 + ps1 82개 | 클라이언트별 훅 스크립트를 생성한다 |
훅 → 관찰 → 위키 → 핸드오프, 그리고 그 사이의 안전장치들.
이게 첫 번째 원칙이다. 훅은 에이전트가 매 턴 실행하는 코드라 여기서 1초를 쓰면 사용자가 1초를 손해 본다. 그래서 셸 훅은 짧은 타임아웃으로 던지고 끝내고, 네이티브 훅은 로컬에 스풀해 두고 세션 종료 전달은 분리된 hook-drain 보조 프로세스에 넘긴다. 서버가 포화되면 큐를 무한정 늘리는 대신 HTTP 429를 돌려준다.
네트워크로 이벤트를 보내면 중복이 생긴다. ai-memory는 각 이벤트에 안정적인 멱등키를 붙이고, 키를 "완료"로 표시하는 시점을 후속 처리까지 끝난 뒤로 미룬다. 그래서 중간에 프로세스가 죽고 재시도가 오면 관찰을 또 만들지 않으면서 위키·핸드오프 효과는 이어서 적용한다. 문서가 대가도 솔직히 밝힌다 — 완료 표시 전까지 후속 효과는 at-least-once라, 크래시 시 이미 적용된 효과가 한 번 더 반복될 수 있다.
아키텍처 문서의 표현이 정확하다 — 훅 라우터가 "신뢰할 수 없는 텍스트가 저장소로 들어가는 유일한 경로"다. 입구를 하나로 만들면 검사를 한 군데만 튼튼히 하면 된다. 분량 상한도 여기서 걸린다: 사용자 프롬프트·압축 요약은 최대 16 KiB, 알림·도구 발췌는 2 KB, 그리고 모든 관찰 본문에 16 KiB 최종 방어선.
비밀이 새는 걸 막는 방식이 인상적이다. 네이티브 명령과 생성된 플러그인에서는 가장 가까운 마커 파일을 먼저 확인해, 제외 대상이면 그 이벤트가 스풀·큐·전송·로그·저장소 어디에도 들어가지 않는다. 저장한 뒤 지우는 게 아니라 애초에 들어오지 않게 하는 순서다.
기억 시스템의 두 번째 어려운 문제가 버리기다. ai-memory에는 decay 개념이 있어 시간이 지나면 페이지의 현저성(salience)이 떨어지고, forget-sweep이 그걸 정리한다. 사용자가 페이지를 핀 고정하면 감쇠에서 면제되고, 반대로 TTL을 붙이면("이번 스프린트까지만") 기한이 지나 파일과 행이 함께 하드 삭제된다.
서버가 백그라운드로 완료된 세션을 검토해 위키 수정안을 만든다. 그런데 제안을 만드는 것과 적용하는 것이 별개 설정이다. 기본은 자동 승인이지만 require_approval = true로 전부 대기시킬 수 있고, 운영자가 자체 실행 파일 게이트를 붙여 LLM 검증 후·적용 전에 한 번 더 거를 수도 있다(기본 비활성, 훅 경로에서는 절대 실행 안 됨). 스케줄러 틱은 겹치지 않아, 오래 걸리는 검토가 다음 틱을 미루지 복제하지 않는다.
README의 지원 표가 이 프로젝트에서 가장 정직한 부분이다. 같은 "Supported"라도 내용이 다르다 — Codex는 진짜 세션 종료 훅이 없어 finalize-session을 수동으로 불러야 하고, Grok·Zero는 SessionStart 표준출력을 버려서 핸드오프 주입이 안 되니 MCP로 받아야 하며, Pool은 프로젝트 훅을 직접 못 써서 붙여 넣을 스니펫을 출력한다. "된다/안 된다"가 아니라 어디까지 되는지를 하네스별로 적었다.
표를 대충 보고 "내 에이전트 지원되네" 하고 깔면, 세션 요약이 안 생기거나(종료 훅 없음) 핸드오프가 안 주입되는(표준출력 무시) 상태를 고장으로 오해하게 된다. 둘 다 설계상 그 하네스의 한계이고 우회로가 문서에 적혀 있다. 설치 전에 자기 칸 한 줄을 읽는 게 이 프로젝트에서 가장 값싼 투자다.
Rust 19만 6천 줄 — 읽는 순서가 문서에 있다.
| 묶음 | 도구 | 쓰임 |
|---|---|---|
| 조회 | memory_query · memory_recent · memory_explore · memory_read_page · memory_read_session_observations | 과거 결정·맥락 찾기 |
| 기록 | memory_write_page · memory_delete_page · memory_briefing | 영구 기억 남기기 |
| 인수인계 | memory_handoff_begin · _accept · _cancel | 세션 간 이어받기 (훅 주입이 안 되는 하네스의 우회로이기도 하다) |
| 정비 | memory_consolidate · memory_forget_sweep · memory_lint · memory_auto_improve | 압축·망각·검사·자동 개선 |
| 운용 | memory_status · memory_feedback · memory_install_self_routing | 상태·피드백·자기 라우팅 설치 |
아무 페이지나 같은 무게가 아니다. _rules/·decisions/·procedures/·gotchas/는 유지 관리되는 지식으로 취급돼 검색에서 우대되고, 세션 기록은 그때의 증거로 남는다. 여기에 pinned와 canonical·active·source-of-truth / superseded·historical·test-fixture·do-not-answer-from 태그가 가중치로 붙는다. 다만 절대 필터가 아니라 가중치여서, 일부러 옛 기록을 찾는 검색도 여전히 통한다.
이 레포에서 가져갈 것.
스풀 + 멱등키 + 분리된 드레인 프로세스 + 포화 시 429. 이 넷이 한 목표를 위해 함께 움직인다. 지연이 곧 사용자 경험인 자리에 관측 기능을 끼워 넣어야 하는 모든 상황(APM·로깅·분석)에 그대로 옮길 수 있는 조합이다.
"신뢰할 수 없는 텍스트가 들어오는 유일한 경로"를 지정하고 거기서만 정제한다. 그리고 그 텍스트가 아무리 높은 순위로 검색돼도 지시 권한을 얻지 않는다고 못 박는다. LLM에 외부 데이터를 먹이는 시스템을 만든다면 이 두 문장이 설계 체크리스트가 된다.
감쇠 공식·현저성·핀 고정·TTL·forget-sweep이 한 벌로 움직인다. 로그·캐시·추천 시스템 등 시간이 지나면 가치가 떨어지는 데이터를 다루는 어디에나 적용된다. 특히 "사용자가 핀을 꽂으면 면제"처럼 사람의 판단을 자동 정책에 끼워 넣는 지점을 어떻게 설계하는지 볼 만하다.
FTS5(단어) + 개체 매칭 + 그래프 이웃 + 선택적 벡터를 RRF(Reciprocal Rank Fusion)로 합친다. 각 방식이 순위를 내면 순위의 역수를 더해 통합하는 기법인데, 점수 스케일이 다른 검색기들을 섞을 때 표준처럼 쓰인다. 여기에 도메인 지식(페이지 등급)을 자르기 직전에 반영하는 순서까지 실물로 볼 수 있다.
스무 종 넘는 에이전트가 훅 스키마도, 이벤트 종류도, 표준출력 처리도 다 다르다. ai-memory는 공통분모로 뭉개지 않고 하네스별 제약과 우회로를 표로 적었다. 통합을 많이 하는 제품을 만든다면 이 표 자체가 참고 자료다.
서버 하나면 집안 전체가 쓴다.
| 항목 | 필요 | 메모 |
|---|---|---|
| 실행 | Rust 1.95+ 빌드 또는 배포 바이너리 | Docker(amd64·arm64) · macOS 네이티브(aarch64/x86_64) · Arch AUR |
| 저장소 | SQLite 파일 하나 | 별도 DB 서버 없음. FTS5 내장 사용 |
| 기본 경로 | ~/.config/ai-memory · ~/.local/share/ai-memory | 훅 스크립트도 여기 스테이징된다 |
| LLM | 선택 | 없으면 FTS5 + 수동 페이지만. 요약·자동 개선이 꺼진다 |
| 임베딩 | 선택 | Ollama·LM Studio·vLLM 등 키 없는 로컬 엔드포인트 가능 |
| 윈도우 | WSL2 권장 | 네이티브 윈도우는 실험 단계 — 바이너리는 배포되나 훅 형식이 하네스마다 갈린다 |
| 다중 사용자 | 서버 1대 + 사용자별 메모리 슬롯 | V14__users 마이그레이션 이후 지원 |
요구가 가벼운 편이다 — GPU도, 외부 DB도, 클라우드 계정도 필수가 아니다. 무거워지는 건 LLM을 붙였을 때의 요약·자동 개선 호출뿐이고 그것도 끌 수 있다. 대신 디스크는 계속 자란다: 관찰 기록·위키·임베딩·감사 로그가 쌓이므로 감쇠와 forget-sweep을 꺼 두면 언젠가 정리해야 한다.
난이도별 다섯 개.
모델 키를 하나도 넣지 않고 서버를 띄운 뒤, 한 시간쯤 평소처럼 코딩하고 ai-memory status와 /web으로 무엇이 쌓였는지 본다.
Claude Code에서 작업을 절반 하고 끈 다음, 같은 디렉터리에서 Codex를 켠다. Codex는 종료 훅이 없으니 끄기 전에 ai-memory finalize-session을 부르는 것을 잊지 말 것.
consolidate 프롬프트를 손볼 지점이다.docs/marker-file.md를 읽고 마커를 설치한 뒤, 제외 대상 파일을 일부러 열어 본다. 그리고 SQLite를 직접 열어 그 이벤트가 정말 없는지 확인한다.
같은 질의를 던져 놓고, 페이지 하나에 pinned와 superseded 태그를 각각 붙여 memory_query 결과 순위가 어떻게 움직이는지 기록한다.
do-not-answer-from을 붙인 페이지가 여전히 검색되는지 확인해 보면 그 말의 무게를 알 수 있다.docs/managed-harness-contributions.md를 따라 아직 지원되지 않는 에이전트에 훅 설치기를 만든다. 최소 목표는 캡처까지, 욕심을 낸다면 핸드오프 주입까지.
4주.
| 주차 | 주제 | 할 일 |
|---|---|---|
| 1주차 | 쓰면서 데이터 보기 | 설치 → 며칠 실사용 → SQLite를 직접 열어 observations·sessions·handoffs 테이블 구조를 눈으로 확인. 마이그레이션 50개를 순서대로 훑으면 설계 변천이 보인다 |
| 2주차 | 훅 경로 정독 | docs/ARCHITECTURE.md의 데이터 흐름 + ai-memory-hooks 크레이트. 정제·멱등키·완료 표시를 코드로 따라간다 |
| 3주차 | 검색과 랭킹 | ai-memory-store의 FTS5 사용법과 RRF 융합. 별도로 SQLite FTS5 문서를 한 번 정독하면 이해가 두 배가 된다 |
| 4주차 | 압축과 망각 | ai-memory-consolidate + 감쇠 공식. "무엇을 남기고 무엇을 버릴 것인가"를 프롬프트와 수식으로 어떻게 표현했는지 정리 |
같은 카탈로그의 OpenKB와 나란히 읽기를 권한다. 둘 다 "LLM이 마크다운 위키를 유지한다"는 같은 발상에서 나왔는데, OpenKB는 내가 넣은 문서를, ai-memory는 에이전트가 일한 흔적을 재료로 쓴다. 재료가 다르면 설계가 어떻게 갈라지는지 보기 좋은 한 쌍이다.
이 문서에 나온 말들.
| 용어 | 뜻 |
|---|---|
| 관찰 (observation) | 훅으로 들어온 기록 한 건. 프롬프트·도구 실행·세션 경계 등. 종류가 ObservationKind로 분류된다 |
| 핸드오프 | 세션 종료 시 만들어지는 분량 제한 인수인계. 다음 세션 시작 때 주입된다 |
| 압축 (consolidate) | 흩어진 관찰을 하나의 요약으로 묶는 과정 |
| 멱등키 (idempotency key) | 같은 요청이 두 번 와도 한 번만 처리되게 하는 식별자 |
| at-least-once | "최소 한 번은 전달된다" — 중복은 가능하고 유실은 없다는 보장 수준 |
| FTS5 | SQLite에 내장된 전문 검색 엔진. 별도 검색 서버 없이 단어 검색을 한다 |
| RRF | 여러 검색 결과를 순위의 역수로 합치는 융합 기법 |
| 감쇠 (decay) | 시간이 지날수록 페이지의 중요도를 낮추는 규칙. 핀 고정으로 면제된다 |
| forget-sweep | 감쇠·TTL 결과를 실제 삭제로 옮기는 정리 작업 |
| 캡처 제외 | 특정 파일·경로의 이벤트를 저장 전에 버리는 정책. 마커 파일로 지정 |
| 관리형 워크스트림 | ai-memory run <에이전트>로 실행할 때 제공되는 하네스 간 세션 재개 기능 |
| writer actor | 저장소 쓰기를 한 곳으로 직렬화하는 단일 처리기. 동시 쓰기 충돌을 구조로 막는다 |
원문으로 더 깊이.