계층형 보존과 하이브리드 검색을 출시한 지 3일 뒤, 우리는 애초에 영구 메모리 서버를 구축하려 했던 바로 그 실패 상황과 마주했다. 한 머신의 SQLite 데이터베이스는 비어 있었고, Qdrant Cloud에는 모든 벡터가 여전히 남아 있었다. 모든 콘텐츠 — 모든 메모리의 실제 텍스트 — 는 SQLite에만 존재했다. 벡터는 온전했지만, 그 벡터가 인코딩한 텍스트 없이는 쓸모가 없었다.
이 사건이 BHGBrain 1.3을 탄생시켰다. 이 릴리스는 메모리 서버를 스토리지 장애에 견고하게 만들고, 둘 이상의 머신에서 BHGBrain을 운영하는 팀을 위한 다중 기기 지원을 추가하며, 공유 벡터 스토어에서 전체 로컬 데이터베이스를 재구성할 수 있는 재해 복구 도구를 제공한다.
문제: 듀얼 스토어, 단일 장애점
BHGBrain의 아키텍처는 두 개의 스토어를 사용한다. SQLite는 콘텐츠, 메타데이터, 전문 검색용이고, Qdrant는 벡터 임베딩과 의미 유사도용이다. 모든 쓰기는 양쪽에 모두 들어간다. 모든 검색은 양쪽을 조인한다.
이 설계는 SQLite가 내구성이 있다고 가정했다. 맞다 — sql.js는 쓰기-임시파일-후-이름변경 방식으로 디스크에 원자적으로 플러시한다. 하지만 데이터베이스 파일이 손실되거나 재생성되거나 다른 머신에서 새로 시작되면 모든 콘텐츠가 사라진다. Qdrant에는 임베딩과 일부 메타데이터(태그, 타입, 중요도)가 있지만, 실제 메모리 텍스트는 없다. SQLite 조인에 의존하는 검색 결과는 조용히 아무것도 반환하지 않는다.
이것이 바로 일어난 일이었다. 두 개의 BHGBrain 인스턴스 — 하나는 주 워크스테이션, 하나는 Windows 365 Cloud PC — 가 같은 Qdrant Cloud 클러스터를 바라보고 있었다. Cloud PC의 SQLite는 비어 있었다. 모든 회상이 0개의 결과를 반환했고, Qdrant는 높은 신뢰도의 일치를 가진 수십 개의 벡터를 보유하고 있었음에도 불구하고 말이다.
수정 1: Qdrant 페이로드에 콘텐츠 저장
첫 번째 변경은 간단하다. 벡터와 함께 Qdrant 페이로드에 전체 메모리 콘텐츠를 저장하는 것이다.
1.3 이전에는 Qdrant 업서트 페이로드에 메타데이터만 포함되었다 — namespace, type, tags, importance, retention_tier, decay_eligible, expires_at. 콘텐츠 텍스트는 SQLite 전용이었다.
이제 writeMemory와 updateMemory는 모든 Qdrant 업서트에 content, summary, category, source, created_at, device_id를 포함한다. Qdrant가 콘텐츠의 중복 백업 역할을 한다. SQLite는 여전히 쿼리, 전문 검색, 수명 주기 관리를 위한 기본 스토어다 — 하지만 그것을 잃는다고 데이터를 잃는 일은 더 이상 없다.
수정 2: 자동 검색 폴백
검색 서비스의 buildSearchResults 메서드는 과거에 모든 Qdrant 결과를 SQLite에서 조회했다. 메모리 ID가 로컬에 없으면 제외되었다. 이것이 버그를 혼란스럽게 만든 조용한 실패였다 — Qdrant는 일치 항목을 반환했지만, 호출자는 빈 결과를 보았다.
이제 Qdrant 결과에 일치하는 SQLite 행이 없으면, 검색 서비스는 Qdrant 페이로드에서 직접 결과를 구성한다:
Qdrant returns match → SQLite lookup → found? → return full record
→ not found? → payload has content?
→ yes → return from payload
→ no → skip (pre-1.3 memory)
이것은 기존 Qdrant 클러스터를 바라보는 새로운 BHGBrain 인스턴스가 즉시 검색 결과를 반환한다는 뜻이다 — SQLite 데이터가 필요 없다. 결과에는 페이로드에 저장된 모든 것이 포함된다: 콘텐츠, 요약, 타입, 태그, 보존 계층, 기기 ID, 생성 타임스탬프.
수정 3: 복구 도구
완전한 복구 — 전문 검색, 수명 주기 관리, 접근 추적이 제대로 작동하도록 SQLite를 재구성 — 를 위해, 이제 repair MCP 도구가 있다:
{
"tool": "bhgbrain.repair",
"params": {
"dry_run": true
}
}
복구 도구의 동작:
- 컬렉션 API를 통해 Qdrant에서 모든
bhgbrain_*컬렉션을 발견한다 - 모든 컬렉션의 모든 포인트를 스크롤한다 (배치 페이징)
- 각 포인트에 대해: ID가 로컬 SQLite에 존재하는지 확인한다
- 누락되었고 Qdrant 페이로드에
content가 있으면: SQLite에 전체MemoryRecord를 삽입한다 - 누락되었고 페이로드에 콘텐츠가 없으면: 건너뛴다 (1.3 이전 메모리, 콘텐츠 복구 불가)
- 통계를 보고한다: 스캔한 컬렉션, 스캔한 포인트, 복구됨, 건너뜀, 오류
아무것도 쓰지 않고 복구될 내용을 미리 보려면 dry_run: true를 사용하라. 특정 기기의 메모리로 복구를 제한하려면 device_id를 사용하라.
이것이 재해 복구 경로다. 또한 새 기기의 온보딩 경로이기도 하다 — 새 BHGBrain 인스턴스를 기존 Qdrant 클러스터로 향하게 하고, repair를 실행하면, 로컬 SQLite가 공유 스토어의 모든 것으로 채워진다.
다중 기기 메모리
1.3에서 더 큰 아키텍처 변화는 단일 Qdrant 백엔드를 공유하는 여러 BHGBrain 인스턴스에 대한 일급 지원이다.
아키텍처
Device A (Workstation) Device B (Cloud PC)
┌──────────────────┐ ┌──────────────────┐
│ SQLite (local) │ │ SQLite (local) │
│ device_id: ws-1 │ │ device_id: w365 │
└────────┬─────────┘ └────────┬─────────┘
│ │
└──────────┬───────────────────┘
│
┌──────────▼──────────┐
│ Qdrant Cloud │
│ (shared backend) │
│ content + vectors │
│ device_id index │
└─────────────────────┘
각 기기는 자체 SQLite 데이터베이스를 유지한다. Qdrant가 공유 계층이다. 모든 쓰기는 양쪽 스토어에 콘텐츠를 저장하고 메모리에 출처 device_id를 태그로 붙인다.
기기 식별
각 인스턴스는 시작 시 안정적인 device_id를 결정한다:
- 명시적 설정:
config.json의device.id - 환경 변수:
BHGBRAIN_DEVICE_ID - 자동 생성:
os.hostname()에서 파생, 소문자화 및 정제
결정된 ID는 첫 실행 시 config.json에 저장된다. 이 ID는 모든 Qdrant 페이로드, 모든 SQLite 레코드, 모든 검색 결과에 나타난다 — 그래서 항상 어느 기기가 메모리를 생성했는지 알 수 있다.
교차 기기 가시성
두 기기 모두 모든 메모리를 본다. 기기 B가 기기 A가 저장한 무언가를 검색할 때, Qdrant 검색이 일치 항목을 반환한다. 기기 B의 SQLite에 레코드가 없으면, 검색 폴백이 Qdrant 페이로드에서 결과를 구성한다. 어떤 데이터도 보이지 않는 일은 없다.
| 출처 | 기기 A가 보는 것 | 기기 B가 보는 것 |
|---|---|---|
| 기기 A의 메모리 (SQLite) | 전체 레코드 | Qdrant 폴백 |
| 기기 B의 메모리 (SQLite) | Qdrant 폴백 | 전체 레코드 |
완전한 로컬 기능(전문 검색, 수명 주기 추적, 접근 횟수)을 원하면, 기기에서 repair를 실행하여 Qdrant에서 SQLite를 채우라.
설정
두 기기를 서로 다른 기기 ID로 동일한 Qdrant 클러스터로 향하게 하라:
// Device A
{
"device": { "id": "workstation" },
"qdrant": {
"mode": "external",
"external_url": "https://your-cluster.cloud.qdrant.io",
"api_key_env": "QDRANT_API_KEY"
}
}
// Device B
{
"device": { "id": "cloud-pc" },
"qdrant": {
"mode": "external",
"external_url": "https://your-cluster.cloud.qdrant.io",
"api_key_env": "QDRANT_API_KEY"
}
}
끝이다. 두 인스턴스가 같은 메모리 풀을 공유한다. 둘 다 자신의 쓰기에 출처를 태그로 붙인다. 둘 다 모든 것을 볼 수 있다.
문서화: 8개의 Mermaid 다이어그램
README는 이제 모든 주요 하위 시스템을 다루는 상세한 Mermaid 다이어그램을 포함한다:
- 아키텍처 — 전체 서버 스택을 보여주는 컴포넌트 다이어그램
- 다중 기기 토폴로지 — 기기별 로컬 SQLite와 공유 Qdrant
- 쓰기 파이프라인 — 완전한 중복 제거 결정 플로우차트
- 계층 할당 — 우선순위 정렬 분류 로직
- 계층 수명 주기 — 승격, TTL, 만료 흐름이 있는 상태 다이어그램
- 하이브리드 검색 — RRF 퓨전을 통한 병렬 의미/전문 경로
- 백업 및 복원 — 생성 및 복원 흐름에 대한 시퀀스 다이어그램
- 복구 흐름 — 재해 복구 플로우차트
네 개 번역(독일어, 스페인어, 프랑스어, 중국어 간체) 모두 영어 README와 완전한 동일성을 유지하도록 업데이트되었으며, 모든 다이어그램과 새로운 다중 기기 섹션을 포함한다.
1.3으로 업그레이드
수동 마이그레이션은 필요 없다. 업그레이드 후 첫 시작 시:
- SQLite에 null 가능한
device_id컬럼이 추가된다 (기존 메모리는null로 유지) - Qdrant 컬렉션에
device_id키워드 인덱스가 생성된다 - 설정에 호스트 이름에서 자동 결정된
device.id필드가 추가된다 - 모든 새 쓰기는 Qdrant 페이로드에 콘텐츠를 저장한다
Qdrant에 콘텐츠가 없는 1.3 이전 메모리는 SQLite를 통해 정상적으로 계속 작동한다. 다만 SQLite가 손실되면 repair로 복구할 수 없다 — 그 메모리가 쓰였을 때 콘텐츠가 Qdrant에 없었기 때문이다.
핵심 교훈
SQLite 사건에서 얻은 가장 큰 교훈: 시스템에 두 개의 스토어가 있고 그중 하나가 조용히 저하되면, 운영자 개입 없이 다른 하나가 부담을 이어받아야 한다는 것이다. BHGBrain 1.3은 Qdrant를 SQLite의 안전망으로 만들고 반대의 경우도 마찬가지다. 검색은 자동으로 폴백한다. 복구는 단일 도구 호출이다. 다중 기기 공유는 두 스토어 모두가 전체 그림을 담고 있기에 작동한다.
둘 이상의 머신에서 BHGBrain을 실행 중이라면, 1.3으로 업그레이드하고 각 기기에서 repair를 실행하라. 한 머신에서 실행 중이라도 업그레이드하라 — Qdrant 내 콘텐츠 저장 변경은 다음 SQLite 사고가 데이터 손실이 아니라 아무 일도 아닌 일이 되게 한다.
BHGBrain은 오픈 소스이며, MIT 라이선스이고, github.com/Big-Hat-Group-Inc/BHGBrain에서 이용할 수 있다.
Kevin Kaminski는 Big Hat Group의 주역으로, 엔터프라이즈 AI 인프라, Microsoft 365, Windows 365에 집중하고 있다. 그는 업무에서 AI 에이전트를 운영하는 팀을 위한 오픈 소스 도구를 구축한다.