CLI·MCP 서버
engram은 코딩 에이전트가 해결한 버그, 선택한 설계, 프로젝트 관례를 구조화된 메모리로 저장하는 프로그램입니다.
에이전트는 MCP 도구로 기록하고, 사람은 CLI나 TUI로 열람할 수 있습니다.
기본 저장 위치는 ~/.engram/engram.db이며, 핵심 저장 계층은 internal/store입니다.
이 저장소에서 배울 핵심은 AI의 기억도 결국 소유권·중복·검색·재시도를 설계해야 하는 데이터라는 점입니다. 모델의 가중치를 바꾸는 학습이 아니라, 필요한 기록을 다음 대화의 입력에 다시 넣는 방식입니다. 저장된 내용이 있어도 에이전트가 검색해서 읽지 않으면 그 대화에 활용되지 않습니다.
대화창은 오늘의 회의실이고, SQLite는 퇴근해도 남는 서랍입니다. “로그인 오류를 고쳤다”는 기록에 원인과 수정 파일을 함께 적어 두면, 다음 담당자가 같은 구덩이에 빠질 확률을 줄일 수 있습니다.
수첩이 있다고 내용이 저절로 정확해지지는 않습니다. 무엇을 적을지 고르는 역할은 주로 에이전트에 있고, engram은 그 기록을 보존하고 되찾는 장치를 제공합니다.
Observation에는 제목·본문·종류·프로젝트·세션뿐 아니라 수정 횟수, 중복 횟수, 검토 예정 시점 같은 관리 정보도 있습니다.
대화 전체와 기록 하나를 구분하는 것이 첫 번째 읽기 포인트입니다.mem_save가 프로젝트를 확인하고 저장 계층을 호출합니다.observations와 검색 색인이 함께 갱신됩니다.mem_context·mem_search로 후보를 찾습니다.mem_get_observation으로 필요한 기록만 자세히 읽습니다.# 설치된 실행 파일로 사용하는 CLI 예시
engram save '로그인 재시도 원인' \
'What: 토큰 갱신 경합 수정. Why: 동시 요청이 갱신을 중복 실행. Where: auth/refresh.go. Learned: 갱신을 직렬화한다.' \
--type bugfix --project study-app --topic auth/refresh-race
engram search '토큰 갱신' --project study-app --match all
engram context --project study-app
cmd/engram/main.go의 parseSaveArgs()에서 제목과 본문은 위치 인자입니다.
토픽 옵션 이름은 --topic입니다.
내부의 topic_key 필드와 CLI 옵션의 이름이 다르므로 문서와 실제 파서를 함께 읽어야 합니다.
처음부터 클라우드를 이해할 필요는 없습니다.
cmdSave() → AddObservationWithResult() → SearchContext()를 먼저 읽고,
같은 저장 규칙을 MCP와 HTTP가 어떻게 공유하는지 넓혀 가면 됩니다.
긴 개발 작업에서는 에이전트를 바꾸거나 대화를 압축할 때 이전 결정의 근거가 빠지기 쉽습니다. 같은 프로젝트의 테스트 관례나 실패한 접근을 계속 설명해야 한다면, 기록을 대화 밖으로 분리할 이유가 생깁니다. engram은 이 문제를 특정 모델의 내부 기능 대신 별도 저장소와 공통 도구 인터페이스로 다룹니다.
2026-10-23 GitHub Trending Go Weekly #7, 스타 7,554개라는 수치는 관심의 크기를 보여줍니다. 다만 순위만으로 유입 원인이나 성능 우위를 알 수는 없습니다. 다음 세 가지는 구현에서 읽을 수 있는 매력에 대한 해석입니다.
에이전트와 기억 저장소를 분리하면, 사용하는 에이전트가 바뀌어도 기록의 수명을 유지할 수 있습니다. 단, 도구를 연결하는 것과 에이전트가 적절한 시점에 도구를 쓰는 것은 별개입니다. 저장 규칙을 안내하는 프로토콜과 플러그인도 함께 봐야 합니다.
| 접근 | 잘 맞는 상황 | engram의 장점과 교환 조건 |
|---|---|---|
| AGENTS.md | 팀이 합의한 규칙을 코드와 함께 관리 | 경험을 세션별로 쌓고 검색하기 쉽습니다. 대신 정제된 규칙 문서를 대체하지는 않습니다. |
| 대화 요약 파일 | 다음 대화에 적은 분량을 넘기기 | 프로젝트 필터·토픽별 갱신·수정 이력을 제공합니다. 저장 구조와 운영을 배워야 합니다. |
| 임베딩 기반 RAG | 표현이 달라도 의미가 비슷한 문서 찾기 | 기본 검색에 모델·벡터 서버가 필요 없습니다. 어휘가 다른 동의어를 알아서 연결하는 능력은 제한됩니다. |
| claude-mem | 에이전트의 작업 흔적을 기억으로 만드는 인접 제품 | engram 저자는 에이전트가 고른 요약과 단일 Go 실행 파일을 차별점으로 설명합니다. 경쟁 제품의 현재 기능은 별도 확인이 필요합니다. |
마지막 행은 docs/COMPARISON.md에 실린 engram 저자의 비교 관점입니다.
그 문서의 “웹 UI” 행은 engram을 TUI 중심으로만 설명하지만, 현재 소스에는 Cloud 대시보드도 있습니다.
따라서 비교 문서 전체를 최신 경쟁 제품 평가표로 받아들이기보다, 설계 철학을 설명하는 자료로 읽는 편이 정확합니다.
README의 “single binary, zero dependencies”는 기본 로컬 실행에 별도 Node.js·Python·Docker가 필요 없다는 설명입니다.
go.mod에는 SQLite 드라이버, MCP SDK, TUI 라이브러리 등 다수의 빌드 의존성이 있습니다.
Cloud 서버는 PostgreSQL을 쓰고, 선택적 의미 비교는 외부 에이전트 CLI를 호출합니다.
완성된 자동차를 운전할 때 부품 공장이 필요 없다는 말과 비슷합니다. 자동차에 부품이 없다는 뜻은 아닙니다. 빌드 의존성, 실행 의존성, 선택 기능의 의존성을 나누면 홍보 문장을 기술적으로 읽을 수 있습니다.
| 기술 | 실제 선언 | 역할과 읽을 위치 |
|---|---|---|
| Go | go 1.25.10 | go.mod의 언어·도구 체인 요구. 모듈 경로는 github.com/Gentleman-Programming/engram/v3. |
| SQLite 드라이버 | modernc.org/sqlite v1.58.0 | C 컴파일러에 의존하지 않는 로컬 DB 접근. internal/store/store.go. |
| MCP SDK | mcp-go v0.44.0 | 도구 스키마·호출 결과·stdio 서버. internal/mcp/mcp.go. |
| PostgreSQL 드라이버 | pgx/v5 v5.7.6 | 선택형 Cloud 저장 계층. internal/cloud/cloudstore. |
| 네트워크·문자 처리 | x/net v0.57.0x/text v0.40.0 | Go 확장 모듈. 함께 선언된 x/sys는 v0.47.0. |
| 설정 파일 | yaml.v3 v3.0.1 | YAML 처리. 실제 제품의 JSON·환경변수 설정과 구분해서 봅니다. |
| HTTP·SQL 기초 | Go 표준 라이브러리 | net/http의 ServeMux, database/sql의 연결·트랜잭션을 직접 사용합니다. |
v1.58.0은 Go 드라이버 모듈의 버전입니다.
SQLite 엔진 버전과 같은 번호라고 읽으면 안 됩니다.
FTS5는 이 저장소에서 사용하는 SQLite의 전문 검색 기능이며, 검색을 위해 외부 벡터 데이터베이스를 띄우지 않습니다.
| 화면·통합 | 버전·근거 | 구현 방식 |
|---|---|---|
| 터미널 UI | bubbletea v1.3.10bubbles v1.0.0lipgloss v1.1.0 | internal/tui에서 상태·입력 메시지·출력 화면을 분리합니다. |
| Cloud HTML | templ v0.3.1001 | *.templ을 Go 렌더링 코드로 생성합니다. 브라우저 화면도 서버가 만듭니다. |
| 부분 화면 갱신 | HTMX 2.0.4 | internal/cloud/dashboard/static/htmx.min.js의 버전 문자열. 저장소에 포함된 정적 파일입니다. |
| 기본 웹 스타일 | Pico CSS 2.1.0 | internal/cloud/dashboard/static/pico.min.css 헤더 기준. 별도 internal/cloud/dashboard/static/styles.css와 함께 사용합니다. |
| Pi 통합 | gentle-engram 0.5.0 | plugin/pi/package.json. 개발 의존성 typebox 1.3.34, Pi TUI 피어 범위 >=0.74.0. |
| Obsidian 통합 | engram-brain 1.0.0 | plugin/obsidian/package.json. typescript ^5.0.0, esbuild ^0.21.0, obsidian ^1.4.0. |
캐럿(^)이 붙은 값은 허용 범위이며, 정확히 그 버전이 설치된다는 뜻은 아닙니다.
Pi의 * 피어 의존성도 고정 버전이 아닙니다.
웹 대시보드에는 React SPA를 빌드하는 루트 package.json이 없고, 플러그인별 Node.js 생태계가 따로 있습니다.
dashboard.Mount()의 페이지 경로와 부분 응답 경로를 비교하면, JSON API와 다른 화면 구성법을 배울 수 있습니다.| 항목 | 설정에서 확인한 값 | 배울 점 |
|---|---|---|
| 로컬 DB | WAL, busy_timeout(5000), synchronous(NORMAL), foreign_keys(1) | 저장소의 동시성과 내구성 정책을 DSN에 명시합니다. |
| Cloud 개발 구성 | postgres:16-alpine, 포트 18080 | docker-compose.cloud.yml에서 DB 준비 상태를 기다립니다. |
| 컨테이너 | golang:1.25.10-alpine → alpine:3.21 | docker/cloud/Dockerfile의 다단계 빌드. 실행 사용자는 UID 10001. |
| 배포 바이너리 | CGO_ENABLED=0, linux/darwin/windows × amd64/arm64 | .goreleaser.yaml의 6개 OS·아키텍처 조합과 체크섬 생성. |
| 정적 분석 | golangci-lint 2.13.2 | Makefile과 CI에서 버전을 맞춥니다. |
| CI | Go 1.25.10, Node.js 24 | 단위 테스트, 서버 E2E, Windows 전용 확인, 플러그인 빌드를 나눕니다. |
스택을 읽을 때는 사용자가 실행하는 부품과, 개발·배포할 때만 필요한 도구를 분리해야 합니다. CI의 Node.js 24를 보고 모든 engram 사용자가 Node.js를 설치해야 한다고 해석하면 구조를 놓칩니다.
.github/workflows/ci.yml은 make templ 뒤 생성된 *_templ.go의 차이를 검사합니다.
이어 go test ./...와 실행 파일의 빠른 자체 검증을 구성합니다.
이 설정은 검증 의도를 보여주며, 특정 환경의 실제 통과 결과나 성능 수치를 대신하지는 않습니다.
실행 진입점은 cmd/engram/main.go입니다.
engram mcp는 표준 입력·출력으로 통신하는 서버이고,
engram serve는 기본 127.0.0.1:7437의 로컬 JSON API입니다.
engram cloud serve는 PostgreSQL과 웹 대시보드를 가진 별도 실행 모드입니다.
파일 첫머리의 오래된 사용법 주석에는 serve가 “HTTP + MCP”라고 적혀 있지만,
실제 분기는 cmdServe()와 cmdMCP()로 나뉩니다.
실행 경로를 확인할 때는 요약 주석보다 명령 분기와 핸들러 등록을 우선해야 합니다.
internal/mcp/mcp.go의 NewServerWithConfig()와 registerToolsWithQueue()는
MCP 도구와 핸들러를 묶습니다. handleSave()는 인자를 해석하고 프로젝트·세션을 확인한 뒤
Store.AddObservation()을 호출합니다. CLI의 cmdSave()도 같은 저장 계층으로 이어집니다.
입구마다 JSON·명령행 인자·키보드 메시지의 모양은 다르지만,
제목 검증, 비공개 구간 제거, 토픽 갱신, 중복 판정, 동기화 변경 기록은 internal/store가 맡습니다.
이런 구조를 어댑터와 도메인 분리라는 관점으로 읽을 수 있습니다.
// internal/store/runner.go의 실제 인터페이스
type SemanticRunner interface {
Compare(ctx context.Context, prompt string) (SemanticVerdict, error)
}
internal/server/server.go에는 SemanticRunnerFactory와 SemanticPromptBuilder가 있습니다.
서버가 LLM 구현 패키지를 직접 소유하지 않고, cmd/engram에서 필요한 함수를 주입합니다.
기능을 공유하되 패키지 순환 참조를 피하는 구체적인 사례입니다.
전화 주문과 앱 주문이 같은 주방으로 들어갑니다. 주문을 받는 형식은 달라도 조리 규칙은 주방에 있어야 합니다. MCP와 HTTP마다 중복 판정을 따로 구현하면 같은 기억을 저장해도 결과가 달라질 수 있습니다.
AddObservationWithResult()는 저장 전에 프로젝트를 정규화하고,
stripPrivateTags()와 prepareStoredContent()로 본문을 정리합니다.
제목·본문을 검증한 다음 withWriteTimings()의 트랜잭션 안에서 다음 분기를 처리합니다.
recordObservationSaveOperationTx()는 선택적으로
observation_save_operations에 작업 ID·fingerprint·기록 ID를 쓰고,
enqueueSyncMutationTx()로 전송 대상 변경을 기록하는 경로를 호출합니다.
다만 enqueueSyncMutationWithSourceTx()는 isProjectEnrolledTx()로 복제 등록 여부를 검사합니다.
미등록 프로젝트는 로컬 저장만 유지하고 Cloud 변경 행을 쌓지 않으며, 등록할 때 현재 상태를 백필합니다.
등록된 프로젝트에서 저장과 변경 예약의 틈을 줄이는 트랜잭션 아웃박스로 해석할 수 있습니다.
복제에 등록된 프로젝트에서는 기억 본문과 전송할 변경이 같은 트랜잭션에서 함께 확정됩니다. 클라우드 전송 자체가 이 트랜잭션 안에서 끝난다는 뜻은 아닙니다. 네트워크 실패는 나중에 재시도하고, 로컬 기록은 먼저 확정합니다.
| 장치 | 막으려는 문제 | 실제 판정 |
|---|---|---|
| 토픽 갱신 | 같은 주제의 최신 결론이 여러 행으로 흩어짐 | 같은 프로젝트·범위·topic_key의 살아 있는 행을 갱신하고 이전 버전을 보관. |
| 내용 중복 제거 | 짧은 시간에 같은 기록을 반복 저장 | 기본 15분(기록 생성 시각 기준), 정규화 본문 해시 + 프로젝트·범위·종류·제목을 비교. |
| 작업 ID 장부 | 응답 유실 뒤 재시도가 부수 효과를 반복 | 같은 OperationID와 fingerprint면 원래 결과를 반환. 다른 내용이면 충돌. |
세 장치는 같은 문제가 아닙니다.
토픽 갱신은 내용을 바꾸고, 중복 제거는 중복 횟수를 올리지만,
동일 작업의 재시도는 원래 결과를 돌려주며 같은 변경을 반복하지 않도록 합니다.
작업 ID를 주지 않는 호출에는 장부 기반 보장이 적용되지 않습니다.
현재 MCP의 handleSave()는 저장 매개변수에 OperationID를 넘기지 않으므로,
저장 계층의 기능을 모든 MCP 저장의 재실행 보장으로 확대하면 안 됩니다.
internal/store/observation_save_operation_test.go는 같은 작업 ID의 반복과 내용 충돌을 별도로 다룹니다.원본은 observations, 검색용 구조는 observations_fts입니다.
FTS 테이블은 content='observations'와 content_rowid='id'로 원본에 연결되며,
삽입·수정·삭제 트리거가 색인을 갱신합니다. migrateFTS()는 스키마와 트리거를 점검합니다.
-- internal/store/store.go의 검색 테이블 선언
CREATE VIRTUAL TABLE IF NOT EXISTS observations_fts USING fts5(
title, content, tool_name, type, project, topic_key,
tokenize='trigram',
content='observations',
content_rowid='id'
);
SearchContext()와 MCP용 SearchPreviewsContext()는
세 글자 미만 검색어가 있으면 hasShortFTSTerm()을 통해 LIKE 경로로 보냅니다.
나머지는 FTS5로 검색합니다. 세 글자는 바이트 길이가 아니라 utf8.RuneCountInString()으로 셉니다.
| 질의 예시 | 주요 경로 | 읽어야 할 차이 |
|---|---|---|
토큰갱신 | trigram FTS5 | 세 글자씩 겹치는 조각을 이용합니다. 한국어 형태소 분석기는 아닙니다. |
토큰 | LIKE 대체 경로 | 두 글자도 찾을 수 있지만 FTS 순위 대신 수정 시점 순으로 반환합니다. |
토큰 갱신 | 짧은 단어가 있으므로 LIKE | 기본 all은 단어 조건을 AND로, any는 OR로 결합합니다. |
auth/refresh-race | 정확한 토픽 조회도 시도 | 슬래시가 있으면 topic_key 직접 일치 경로를 추가로 사용합니다. |
buildSearchFTSQueryWithColumns()의 BM25 가중치는 제목 5.0, 본문 1.0,
토픽 3.0입니다. 고정(pinned) 여부·최근성·수정 및 중복 횟수로 정렬값도 보정합니다.
FTS5의 BM25는 더 작은 음수 값이 좋은 결과이며, 코드도 보정한 값의 오름차순으로 정렬합니다.
정렬에 쓰는 복합 값과 결과의 rank 필드는 구분해야 합니다.
SQL은 rank에 원래 BM25 값을 실어 주고, ORDER BY에는 보정한 식을 씁니다.
숫자 하나만 보고 최종 순위를 설명하면 틀릴 수 있습니다.
MCP의 handleSearch()는 기본적으로 미리보기를 조회합니다.
SQL의 substr(o.content, 1, 300)으로 본문 일부만 가져오고, 필요할 때 상세 도구를 호출하게 합니다.
이 300은 SQLite 문자열 기준의 문자 수이며, 저장 본문 제한의 바이트 수와 다릅니다.
internal/sync/transport.go의 Transport는 저장 매체를 네 메서드로 추상화합니다.
파일 구현은 FileTransport, 원격 청크 구현은 internal/cloud/remote/transport.go의 RemoteTransport입니다.
동기화 알고리즘이 파일 경로와 HTTP 요청을 직접 모두 알 필요를 줄입니다.
백그라운드 복제는 internal/cloud/autosync의 별도 CloudTransport 계약으로 변경을 주고받으며,
cmd/engram/main.go에서 MutationTransport를 어댑터로 연결합니다.
// internal/sync/transport.go의 실제 계약
type Transport interface {
ReadManifest() (*Manifest, error)
WriteManifest(m *Manifest) error
WriteChunk(chunkID string, data []byte, entry ChunkEntry) error
ReadChunk(chunkID string) ([]byte, error)
}
FileTransport.WriteChunk()는 청크를 gzip으로 압축해 저장합니다.
chunkcodec.ChunkID()는 SHA-256 결과의 앞 8개 16진수 문자를 ID로 사용합니다.
콘텐츠에서 ID를 만들지만 전체 SHA-256을 ID로 보관하는 설계는 아니며, 이 짧은 ID를 보안 인증값으로 해석하면 안 됩니다.
주석과 확장자는 .jsonl.gz라고 표현합니다.
그러나 internal/sync/sync.go의 내보내기는 jsonMarshalChunk(chunk)를 호출하고, 기본 함수는 json.Marshal입니다.
실제 청크는 세션·기록·변경을 묶은 JSON 객체이므로, “한 줄마다 메모리 하나”라고 단정해서 파서를 만들면 안 됩니다.
storeDSN()은 _txlock=immediate를 지정합니다.
읽은 뒤 쓰기로 전환하다가 다른 쓰기와 충돌하는 일을 줄이기 위해 트랜잭션 시작에 쓰기 예약을 잡습니다.
WAL과 5초 잠금 대기, 저장 계층의 잠금 오류 재시도도 함께 사용합니다.
db.SetMaxOpenConns(1)은 각 Store의 연결 풀을 하나로 제한합니다.
MCP 프로세스가 여러 개면 데이터베이스 전체 연결이 하나인 것은 아닙니다.
internal/mcp/write_queue.go는 기본 크기 32의 채널로 쓰기를 직렬화하고, 가득 차면 재시도를 안내합니다.
최근 커밋의 diagnostic.BuildRepairPlan()은
vetoProjectAliasMoves()로 현재 별칭과 저장된 디렉토리 증거를 다시 읽습니다.
별칭이 이동 대상과 충돌하면 project_alias_contradicts_reclassification 사유로 제외합니다.
별칭은 잘못된 이동을 막는 증거이며, 그것만으로 과거 기록의 소유자를 바꾸도록 허가하지는 않습니다.
“저장 직후 응답이 끊기면?”, “검색 원본만 수정되면?”, “진단 뒤 별칭이 바뀌면?”을 물어보세요. 장부, 트리거, 증거 재확인은 각각 이 질문에 대한 코드 수준의 답입니다.
| 순서 | 읽을 파일·함수 | 확인할 질문 |
|---|---|---|
| ① 입구 | cmd/engram/main.gocmdSave(), cmdSearch() | 인자는 어떤 구조체로 바뀌고, 언제 저장소가 열리는가? |
| ② 저장 | internal/store/store.goAddObservationWithResult() | 새 행·토픽 갱신·중복·재실행의 분기는 어디에 있는가? |
| ③ 검색 | 같은 파일의 SearchContext() | 프로젝트 필터와 검색어 길이가 SQL을 어떻게 바꾸는가? |
| ④ 에이전트 | internal/mcp/mcp.gohandleSearch() | SQL 결과가 에이전트가 읽을 응답으로 어떻게 바뀌는가? |
| ⑤ 확장 | internal/sync/transport.go | 같은 알고리즘에서 저장 매체를 어떻게 교체하는가? |
파일을 처음부터 끝까지 읽기보다, 사용자 행동 하나를 골라 입구에서 저장소까지 따라가면 구조가 보입니다.
특히 internal/store/store.go는 약 14,444줄입니다.
첫 정독 대상으로 삼기보다 함수명과 호출 경로를 기준으로 나눠 읽는 편이 낫습니다.
커밋 ab461b9의 Git 추적 파일을 확장자별로 구분한 물리적 줄 수입니다. 빈 줄과 주석을 포함하며, 문서·이미지·JSON은 아래 합계에 넣지 않았습니다. 생성 코드는 따로 표시했습니다. 압축된 JS·CSS는 한 줄에 많은 코드가 들어갈 수 있습니다.
| 구분 | 파일 수 | 줄 수 |
|---|---|---|
| Go 전체 | 596 | 254,967 |
| Go 구현 | 182 | 71,475 — 테스트·templ 생성 코드 제외 |
| Go 테스트 | 411 | 178,241 — *_test.go |
| Go 생성 코드 | 3 | 5,251 — *_templ.go |
| TypeScript | 6 | 6,535 |
| JavaScript | 101 | 22,441 — .js 453줄 + .mjs 21,988줄 |
| templ | 3 | 1,584 |
| CSS | 2 | 1,640 |
| 셸·PowerShell | 23 | 4,127 — .sh 3,627줄 + .ps1 500줄 |
테스트가 많다는 사실은 읽을 수 있는 동작 사례가 많다는 뜻입니다. 테스트 줄 수만으로 결함 수, 커버리지, 운영 안정성을 추정할 수는 없습니다. 코드 줄 수는 개발 이력의 길이나 장기간 기여 비율을 알려주지 않습니다.
Transport는 파일 시스템과 HTTP라는 구현보다 “청크를 읽고 쓴다”는 요구를 먼저 드러냅니다.
SemanticRunner는 외부 CLI 이름 대신 비교 결과의 형태를 계약으로 고정합니다.
Go에서는 구현체에 별도의 implements 선언이 없어도 메서드가 맞으면 인터페이스를 만족합니다.
context.Context의 취소 전달.Transport 모형을 별도 연습 코드로 설계합니다.소스의 if err != nil은 장식이 아닙니다.
“파일이 없다”와 “읽을 권한이 없다”를 모두 빈 데이터로 취급하면 복구 동작이 위험해집니다.
FileTransport.ReadManifest()가 이 둘을 구분하는 방식을 확인해 보세요.
withWriteTimings()는 시작·본문 실행·커밋을 공통으로 감싸고 실패하면 롤백합니다.
핵심은 문법보다 묶음의 범위입니다. 기록과 선택적 작업 장부, 복제 등록된 프로젝트의 동기화 변경을 같은 묶음으로 다룹니다.
observations.deleted_at은 기록을 숨기는 소프트 삭제에 쓰입니다.
검색 SQL에 o.deleted_at IS NULL이 들어가는 이유를 원본 보존과 사용자 노출의 분리로 읽어 보세요.
기본 검색은 임베딩 유사도가 아니라 FTS5의 문자열 기반 검색입니다. 제목과 토픽의 가중치가 높으므로 “버그 수정”보다 “토큰 갱신 경합으로 중복 요청 발생”처럼 원인을 담은 제목이 유용합니다. 검색 품질은 랭킹 식뿐 아니라 기록 품질에도 달려 있습니다.
토큰·토큰갱신·갱신토큰의 검색 경로와 결과 차이를 예측합니다.escapeLIKE()는 %와 _를 이스케이프합니다.
SQL 매개변수 바인딩은 SQL 구조 삽입을 막는 문제이고, 와일드카드 이스케이프는 검색 의미를 보존하는 문제입니다.
둘 중 하나만 해서는 같은 효과를 얻지 못합니다.
MCP는 모델이 호출할 도구의 이름·인자·결과를 제공합니다.
ResolveTools()는 프로필 이름과 도구 이름의 쉼표 목록을 해석합니다.
모든 도구가 항상 필요하지 않은 환경에서는 도구 집합도 구성 요소가 됩니다.
# 필요한 도구만 노출하는 stdio 서버 구성 예시
engram mcp --project study-app --tools=mem_search,mem_get_observation
HTTP에서는 경로·메서드·상태 코드·인증이 별도의 계약이 됩니다.
internal/server/server.go의 routes()와
internal/cloud/cloudserver의 경계를 섞지 말고 읽어야 합니다.
같은 실행 파일에 들어 있어도 로컬 API와 Cloud API가 같은 권한 모델인 것은 아닙니다.
도구를 연결하는 문제와 도구가 다룰 데이터의 소유권을 확인하는 문제는 각각 설계해야 합니다.
저장 계층의 UpdateObservationForProject()·DeleteObservationForProject()는
호출자가 주장한 프로젝트와 실제 소유 프로젝트를 트랜잭션 안에서 비교합니다.
이 일관성 확인만으로 사용자를 인증하는 것은 아닙니다.
internal/tui/model.go의 Init()는 초기 명령을 반환하고,
internal/tui/update.go의 Update()는 입력이나 검색 결과 메시지로 상태를 바꿉니다.
internal/tui/view.go의 View()는 현재 상태를 문자열 화면으로 표현합니다.
웹 쪽에서는 dashboard.Mount()가 전체 페이지와 부분 응답의 경로를 등록합니다.
internal/cloud/dashboard/embed.go의 go:embed static은 CSS·HTMX를 실행 파일에 포함합니다.
TUI와 웹 모두 “현재 상태를 어디서 관리하고, 어떤 입력이 상태를 바꾸는가”라는 질문으로 읽을 수 있습니다.
| 테스트 파일 | 읽을 동작 | 학습 질문 |
|---|---|---|
internal/store/store_fts_test.go | 짧은 질의, CJK 검색, 색인 수명주기, 기존 FTS 업그레이드 | 언어·데이터 변경·과거 스키마가 검색을 어떻게 깨뜨리는가? |
internal/store/observation_save_operation_test.go | 재실행·충돌·삭제 후 만료·재시작·롤백 | 첫 성공 이후 같은 요청이 와도 부수 효과가 반복되지 않는가? |
internal/store/concurrency_repro_test.go | DEFERRED와 IMMEDIATE, 프로세스 간 쓰기 경합 | 같은 프로세스의 큐만으로 여러 프로세스의 잠금까지 해결되는가? |
internal/mcp/write_queue_test.go | 큐 포화·취소·panic 이후 작업자 유지 | 실패가 뒤의 요청을 영구적으로 막지 않는가? |
internal/diagnostic/project_alias_test.go | 진단 이후 추가된 별칭도 복구 계획에서 재확인 | 이전 보고서의 증거를 현재 사실로 믿어도 되는가? |
저장·검색·동시성 테스트는 internal/store/에, MCP·진단 테스트는 internal/의 각 패키지에 있습니다.
CI의 검증 구성과 배포 대상 6개 조합을 함께 보면,
“내 컴퓨터에서 동작”을 여러 운영체제의 제품으로 넓히는 비용이 보입니다.
| 사용 방식 | 필요한 환경 | 구분할 점 |
|---|---|---|
| 로컬 바이너리 | macOS·Linux·Windows, amd64 또는 arm64용 실행 파일, 쓰기 가능한 로컬 디스크 | Go·Node.js·Python·Docker는 기본 로컬 메모리 실행의 필수 조건이 아닙니다. |
| 소스 빌드 | go.mod의 Go 1.25.10 요구를 만족하는 도구 체인 | 설치 문서의 “Go 1.24+”보다 현재 모듈 선언을 우선해서 확인합니다. |
| MCP 연동 | stdio MCP를 지원하는 에이전트와 실행 파일 경로 설정 | 일반 stdio 구성은 별도 HTTP 서버 실행 없이 사용할 수 있습니다. |
| HTTP API | 사용 가능한 로컬 포트, 기본 7437 | ENGRAM_PORT로 조정. Cloud 기본 포트는 8080이며, 개발 Compose에서는 18080로 바꿉니다. |
| Cloud 서버 | PostgreSQL 연결, 인증 설정, 서버 운영 환경 | 개발 Compose는 PostgreSQL 16과 Cloud 포트 18080을 사용합니다. |
| 플러그인 개발 | 해당 에이전트·앱, Node.js 계열 도구 | 플러그인 빌드 요구를 Go 코어 전체의 실행 요구로 확대하지 않습니다. |
| 선택적 의미 비교 | claude 또는 opencode CLI와 사용 환경 | llm.NewRunner() 기준. 모델 사용 비용·네트워크는 해당 CLI 설정에 따릅니다. |
공식 문서에는 CPU 코어 수, 최소 RAM, 처리량에 대한 정량 보장이 없습니다. 기본 저장·FTS 검색에 GPU를 요구하는 모델 실행 경로는 없습니다. 에이전트 모델 자체의 실행 환경은 engram 로컬 저장소의 요구사항과 별도로 판단해야 합니다.
실행 파일은 가벼운 배포 형태이지만, 데이터 용량과 동시 쓰기 비용까지 사라지는 것은 아닙니다.
DefaultConfig()의 본문 잘림 기준은 50000바이트입니다.
이를 넘으면 UTF-8 문자 경계에서 자른 뒤 “... [truncated]”를 덧붙이므로 저장 문자열은 기준보다 길어질 수 있습니다.
한국어 5만 글자를 뜻하지 않으며, 검색 색인·버전 이력·WAL·백업은 추가 디스크를 사용합니다.
기록 10,000개에 본문이 평균 2,000바이트라면 본문 합계만 약 20MB입니다. 모두 50,000바이트를 채우면 본문만 약 500MB입니다. 이는 곱셈으로 구한 본문 크기 예시이며, 실제 DB 파일 크기나 메모리 사용량의 측정값이 아닙니다.
ENGRAM_DATA_DIR의 비어 있지 않은 값은 절대 경로여야 합니다.
저장소의 파일 시스템 검사와 설치 문서는 알려진 NFS·SMB/CIFS 위치를 거부하도록 합니다.
여러 기기의 공유는 DB 파일을 네트워크 드라이브에 놓는 방식보다 청크·Cloud 복제 경로를 이해하고 사용해야 합니다.
docker-compose.cloud.yml은 개발용 비밀번호와 ENGRAM_CLOUD_INSECURE_NO_AUTH=1을 포함합니다.
이 설정을 공개 서버의 인증 구성으로 옮기면 안 됩니다.
WAL 모드에서는 engram.db-wal에도 아직 반영되지 않은 변경이 있을 수 있습니다.
파일 이동은 관련 프로세스를 모두 멈추고 DB·WAL·SHM의 일관성을 유지하는 절차로 다뤄야 합니다.
실행 과제는 설치된 engram과 실제 기록이 없는 전용 실습 데이터 디렉토리를 전제로 합니다. 변경하는 과제는 별도 연습용 사본에서 진행하고, 우선 소스와 테스트만 읽어 예상 결과를 적어도 좋습니다.
같은 토픽에 다른 내용을 저장하는 경우와, 토픽 없이 같은 제목·본문을 반복하는 경우를 비교합니다.
기록 수만 보지 말고 revision_count와 duplicate_count를 구분해 예상합니다.
# POSIX 셸 예시: 실습 디렉토리 안에서 시작
mkdir -p engram-lab
cd engram-lab
export ENGRAM_DATA_DIR="$PWD/data"
engram save '인증 정책' '만료된 토큰은 한 번 갱신한다.' \
--project study-app --topic auth/policy
engram save '인증 정책' '동시 갱신은 하나로 합친다.' \
--project study-app --topic auth/policy
engram search '인증 정책' --project study-app
engram save '검증 기록' '테스트용 동일 본문' --project study-app
engram save '검증 기록' '테스트용 동일 본문' --project study-app
완료 기준: 첫 두 저장은 같은 토픽의 기록 갱신이며 이전 버전이 보관된다는 점, 마지막 두 저장은 기본 중복 판정 시간 안에서 중복 횟수 증가로 이어진다는 점을 설명합니다.
읽을 곳: AddObservationWithResult()의 토픽 분기가 내용 중복 분기보다 먼저 실행되는 순서.
본문에 “토큰갱신 실패”, “접속 토큰 만료”, “인증 정책 변경”을 넣은 기록을 준비합니다.
토큰·토큰갱신·토큰 정책을 검색하고 AND/OR 차이를 비교합니다.
engram search '토큰' --project study-app
engram search '토큰갱신' --project study-app
engram search '토큰 정책' --project study-app --match all
engram search '토큰 정책' --project study-app --match any
완료 기준: 각 질의의 FTS/LIKE 분기를 맞히고, 두 경로의 정렬 방식이 다른 이유를 설명합니다. 검색 지원을 곧 한국어 형태소 이해로 해석하지 않아야 합니다.
읽을 곳: hasShortFTSTerm(), buildSearchLIKEQueryWithColumns(),
TestShortTermFallbackEscapesAndPreservesFiltersAndMatchModes.
TestAddObservationWithOperationIDIsIdempotent를 바탕으로 별도 실습 사본에 표 형태의 사례를 추가합니다.
같은 ID·같은 내용, 같은 ID·다른 내용, 다른 ID·같은 내용, 기존 기록 삭제 뒤 재시도를 구분합니다.
Replayed=true를 기대합니다.ErrObservationOperationConflict를 기대합니다.완료 기준: 기록 수뿐 아니라 작업 장부와 변경 기록의 수가 왜 그렇게 되는지 설명합니다. 복제 미등록 프로젝트에서는 Cloud 변경 기록이 0행일 수 있다는 조건도 반영합니다. 삭제된 대상의 작업 ID가 새 기록 생성 명령으로 바뀌지 않는 이유도 적습니다.
핵심 제한: 저장 계층의 OperationID 지원이 모든 MCP 호출에 자동 적용되는 것은 아닙니다.
handleSave()가 만드는 매개변수에 실제로 어떤 필드를 넘기는지 확인합니다.
Transport의 네 메서드를 메모리 안의 맵으로 구현하는 연습 코드를 만듭니다.
두 번째 청크 쓰기에서 의도적으로 오류를 반환하고, 다시 시도했을 때 어떤 청크와 변경이 남아야 할지 먼저 표로 정의합니다.
exportCloudMutationChunks()에서 전송 확인과 변경 승인 순서를 읽습니다.완료 기준: 재시도 후 누락 없이 복구되는 조건과, 모형이 검증하지 못하는 실제 파일·HTTP 실패를 구분합니다. 외부 Cloud 서버 없이도 핵심 상태 전이를 공부할 수 있어야 합니다.
internal/diagnostic/project_alias_test.go의 TestProjectAliasGuard를 읽고,
진단 직후 별칭이 추가되는 시간 순서를 그림으로 옮깁니다.
먼저 “별칭 없음 / 대상과 일치 / 다른 대상 / 충돌”에 대한 기대 결과 표를 만듭니다.
BuildRepairPlan()이 어떤 검사에서 별칭 거부 장치를 호출하는지 찾습니다.완료 기준: “금지할 근거”와 “허용할 근거”가 다르다는 점을 테스트 사례로 설명합니다. 복구 도구는 정상 기능보다 더 강한 증거가 필요하다는 것을 배웁니다.
주마다 “읽기 → 작은 모형 → 실패 사례 → 설명”을 한 번씩 반복합니다. 처음부터 모든 플러그인이나 Cloud 인증을 구현할 필요는 없습니다. 한 경로의 입력·저장·응답을 끝까지 설명하는 것이 우선입니다.
| 주차 | 주제·읽을 코드 | 작은 산출물과 완료 기준 |
|---|---|---|
| 1주 | Go 기초·CLI 인자cmdSave(), cmdSearch() |
제목·본문·프로젝트를 받는 작은 메모 프로그램을 설계합니다. 구조체와 오류 반환을 설명하고 인자 누락 사례를 정리합니다. |
| 2주 | SQL·트랜잭션·소유권AddObservationWithResult() |
세션과 기록의 관계도를 만듭니다. 토픽 갱신·중복·소프트 삭제의 전후 상태를 각각 그립니다. |
| 3주 | FTS5·trigram·BM25SearchContext() |
한국어 질의 10개의 예상 검색 경로를 만듭니다. 짧은 단어 대체 경로와 제목 가중치의 효과를 설명합니다. |
| 4주 | MCP·HTTP·응답 크기handleSearch(), routes() |
같은 검색을 CLI·MCP·HTTP 입력으로 표현합니다. 미리보기에서 상세 조회로 넘어가는 조건과 프로젝트 선택 규칙을 적습니다. |
| 5주 | 멱등성·아웃박스·복제Transport, 작업 장부 테스트 |
응답 유실·부분 전송·중복 재시도의 실패 표를 만듭니다. 로컬 커밋과 원격 확인이 서로 다른 시점임을 보여줍니다. |
| 6주 | 동시성·진단·배포writeQueue, BuildRepairPlan(), CI |
큐 포화와 오래된 복구 증거의 처리 방식을 설명합니다. 운영체제별 검증 항목과 남은 한계를 한 페이지로 정리합니다. |
진도를 나가는 기준은 읽은 파일 수보다, 실패했을 때 남는 상태를 스스로 예측할 수 있는지입니다. 예측이 어긋난 지점이 다음에 읽어야 할 함수입니다. 결과만 맞히기보다 “어느 트랜잭션과 검증이 그 결과를 보장하는가”까지 연결해 보세요.
① 같은 저장 요청이 두 번 오면 어떤 분기가 실행되는가? ② 두 글자 검색은 어떤 SQL과 정렬을 사용하는가? ③ 저장은 성공하고 전송은 실패했을 때, 다음 시도에 필요한 증거는 어디에 남는가?
| 용어 | 쉬운 설명 |
|---|---|
| 영속 메모리 | 대화나 프로세스가 끝나도 디스크에 남는 기록. 모델의 가중치 학습과는 다른 방식입니다. |
| 로컬 우선 | 내 기기의 저장과 조회를 기본으로 설계하고, 원격 복제를 선택적으로 연결하는 방향입니다. |
| 세션 | 한 번의 작업 흐름을 묶는 단위. 여러 관찰 기록이 같은 세션에 속할 수 있습니다. |
| Observation | 제목·본문·종류·프로젝트를 갖춘 기억 한 건. 수정·중복·검토 정보도 함께 관리합니다. |
| topic_key | 계속 발전하는 주제의 안정적인 열쇠. 같은 프로젝트·범위에서 기존 기록을 갱신하는 데 씁니다. |
| scope | 기억을 어떤 범위에서 사용할지 나타내는 값. project·personal·global이 있습니다. |
| MCP | AI 애플리케이션과 도구 서버가 기능·인자·결과를 주고받는 공통 프로토콜입니다. |
| stdio 전송 | 프로세스의 표준 입력·출력으로 메시지를 교환하는 방식. MCP 서버가 꼭 웹 서버인 것은 아닙니다. |
| FTS5 | SQLite 안에서 긴 텍스트를 찾고 순위를 매기는 전문 검색 기능입니다. |
| 역색인 | 문자열 조각이 어느 기록에 있는지 미리 정리한 찾아보기. 매번 본문 전체를 읽는 비용을 줄입니다. |
| trigram | 겹치는 세 글자 조각으로 검색 대상을 구성하는 방식. 형태소나 의미를 이해하는 모델은 아닙니다. |
| BM25 | 단어의 희소성·출현 횟수·문서 길이 등을 이용한 관련도 점수. FTS5에서는 작은 값이 더 좋은 일치입니다. |
| 검색 투영 | 원본에서 검색·표시에 필요한 형태를 따로 만드는 것. FTS 색인과 짧은 미리보기가 서로 다른 예입니다. |
| 트랜잭션 | 여러 데이터 변경을 한 묶음으로 성공시키거나 취소하는 장치. 본문과 장부의 어긋남을 막는 데 씁니다. |
| WAL | 변경을 먼저 로그에 남기는 SQLite 저널 방식. DB 본체 외의 파일도 일관성에 영향을 줍니다. |
| BEGIN IMMEDIATE | 트랜잭션 시작에 쓰기 예약을 확보하는 방식. 읽은 뒤 쓰기로 바꿀 때의 경합을 줄이려는 선택입니다. |
| 멱등성 | 같은 요청을 반복해도 의도한 변경이 중복 적용되지 않게 하는 성질입니다. |
| Fingerprint | 요청 내용을 구분하기 위한 지문. 같은 작업 ID로 다른 내용을 보내는 충돌을 판별합니다. |
| 트랜잭션 아웃박스 | 전송 대상 데이터의 변경과 나중에 전송할 변경 기록을 같은 트랜잭션에 쓰는 패턴입니다. |
| 청크·manifest | 공유할 기록을 묶은 덩어리와 그 목록. manifest의 이름만으로 실제 청크 존재를 보장할 수는 없습니다. |
| 소프트 삭제 | 즉시 물리 삭제하지 않고 삭제 시점을 표시해 일반 조회에서 숨기는 방식입니다. |
| 인증·소유권 확인 | 누구인지 확인하는 일과, 요청한 프로젝트가 데이터의 실제 소유자와 맞는지 확인하는 일입니다. |
| 백프레셔 | 처리 용량이 찼을 때 더 받지 않고 호출자에게 기다리거나 재시도하도록 알리는 제어입니다. |
| templ·HTMX | 서버에서 HTML을 생성하는 도구와 브라우저에서 HTML 영역을 부분 갱신하는 도구입니다. |
| CGO | Go에서 C 코드와 연결하는 기능. 배포 설정의 CGO_ENABLED=0은 이를 사용하지 않는 빌드를 선택합니다. |
scope=personal이나 프로젝트 소유권 검사가 디스크 암호화를 의미하지는 않습니다.
기능의 이름이 무엇을 약속하는지, 실제 저장·조회 조건이 무엇인지 나누어 이해해야 합니다.
cmdSave()·cmdSearch()·로컬 런타임 조립.AddObservationWithResult()·SearchContext()·storeDSN().handleSave()·handleSearch()·도구 프로필.routes()와 기본 루프백 바인딩.| 항목 | 상태 |
|---|---|
| 저장소 | Gentleman-Programming/engram |
| 선정 기준 | GitHub Trending Go Weekly #7 |
| 스타 / 포크 | 7,554 / 789 — 2026-10-23 GitHub 기준 |
| 주 언어 | Go |
| 생성일 | 2026-02-16 |
| 최신 릴리스 | v3.5.2 — 2026-10-21 |
| 최근 push | 2026-10-23 |
| 최근 커밋 | ab461b9 — 2026-10-23fix(doctor): veto project repairs that contradict aliases (#2056) |
| Go 코드 규모 | 596파일, 약 25.5만 줄. 구현 71,475줄 / 테스트 178,241줄 / 생성 코드 5,251줄. |
| 그 밖의 코드 | TypeScript 약 6.5천 줄, JS·MJS 약 2.2만 줄, templ 1,584줄, CSS 1,640줄. |
| 실행·저장 형태 | Go 바이너리 + 로컬 SQLite·FTS5. Cloud는 선택형 PostgreSQL 복제·공유 계층. |
| 라이선스 | MIT — 저장소의 LICENSE 기준 |
기록 품질: 저장된 결론은 틀리거나 오래될 수 있습니다.
What / Why / Where / Learned 형식은 에이전트 지침이며, 저장 계층이 모든 본문에서 이를 강제하지는 않습니다.
검색한 기억은 현재 코드와 대조해야 합니다.
검색·판단: 기본 검색은 어휘 중심입니다.
의미 비교는 internal/llm의 외부 CLI 선택 기능이며, 기본 검색을 자동으로 벡터 검색으로 바꾸지 않습니다.
관련 후보가 있다는 것과 실제 모순이라는 판단도 구분해야 합니다.
비공개 정보: <private>...</private> 제거는 명시된 구간을 가리는 장치입니다.
비밀 자동 탐지, 디스크 암호화, 완전한 유출 방지를 뜻하지 않습니다.
프롬프트·메모리·공유 청크에 넣을 내용을 직접 검토해야 합니다.
버전·증거 범위: 소스 설명은 커밋 ab461b9 기준이며 최신 릴리스 v3.5.2와 완전히 같은 시점이 아닙니다.
테스트 코드의 존재만으로 전체 테스트 통과나 운영 성능을 보증할 수는 없습니다.
코드 규모는 메모리 사용량·처리량의 측정값이 아닙니다.
경쟁 제품 비교와 오래된 요구 버전 문구도 현재 구현과 별도로 확인해야 합니다.