spaceagent
제 개인 비서 시스템이에요. 웹 UI와 텔레그램 뒤에 claude 에이전트들이 있고, 모두가 메모리 하나를 같이 써요
- 기간
- 2026년 8월 – 지금
- 대상
- 저 자신
- 역할
- 1인: 설계, 개발, 배포, 운영
- 기술
- TypeScript, Claude Agent SDK, Next.js, Postgres + pgvector, SQLite, Prisma, grammY, Swift
- 규모
- 저장소 2개에 커밋 68개, 1.6만 줄, 서비스 3개와 맥 앱 하나
- 개인 프로젝트
다른 언어: english
이게 뭔가요
spaceagent는 제가 비서랑 대화하는 창구예요. 제 서버에서 도는 next.js 앱이고, 스레드 하나하나가 호스트의 폴더 안에서 실제 셸을 가지고 돌아가는 claude 에이전트예요. 브라우저에서 보는 스레드와 텔레그램으로 메시지를 보내는 스레드가 같은 스레드예요. 제 노트 볼트, 일정, 메일, 데일리 리포트, 그리고 크론 작업 여러 개를 이걸로 돌려요.
나머지 절반은 agent-memory예요. 모든 머신의 모든 에이전트가 읽고 쓰는 메모리 서비스 하나인데, 일부는 제 화면을 지켜보는 작은 맥 앱이 채워 넣어요.
런타임
스레드마다 Claude Agent SDK의 query() 하나를 streaming-input 모드로 들고 있고, 이건 턴 하나가 끝나도 계속 살아 있어요. SDK 메시지는 전부 시퀀스 번호를 붙여 이벤트 로그에 쌓고, SSE로 뿌려요. 새로고침하거나 탭을 하나 더 열면 마지막으로 본 seq부터 다시 재생한 뒤 실시간으로 이어 받아요. 그래서 에이전트는 브라우저가 열려 있든 말든 상관없이 돌아가요. 살아 있는 에이전트는 모듈 스코프에 있어서, 일부러 프로세스 하나로만 돌려요.
뭐가 터졌나
긴 스레드가 엄청 느려졌어요. 한 스레드가 이벤트 12,284개, 33MB까지 커졌어요. 열 때마다 이걸 전부 재생했고, 토큰 하나가 스트리밍될 때마다 스크롤백 전체의 마크다운을 다시 파싱했어요. 지금은 그릴 수 있는 이벤트 중 최신 600개만 재생하고 아주 긴 문자열은 잘라내요. 33MB가 1.08MB로 줄었어요. 토큰 델타는 한 프레임에 많아야 카드 하나만 다시 그려요.
서버가 자꾸 멈췄어요. better-sqlite3는 동기식이에요. 토큰 델타마다 행을 하나씩 쓰다 보니 그게 테이블의 90% 정도였고, 커밋할 때마다 fsync 때문에 서버 전체가 막혔어요. 정리 작업은 VACUUM까지 돌렸는데, 하나뿐인 스레드에서 ~500MB 파일을 통째로 다시 쓰느라 모든 요청이 1분 동안 멈췄어요. 이제 델타는 스트리밍만 하고 저장하지 않고, vacuum은 없앴어요.
계정. 프롬프트 캐시가 계정별이라서 스레드 하나는 구독 하나에 고정되고, 턴 사이에서만 옮겨가요. 9월에 이게 라우터로 커졌고, 제 프리랜서 게이트웨이도 이걸 같이 써요. 호출하는 쪽은 계정이 아니라 레인(frontier, intelligent, budget)만 요청해요. 규칙 하나는 장애에서 나왔어요. 딱 0.70 예비선에 걸려 있던 구독이 크레딧 없는 종량제 API 키보다 뒤로 정렬됐고, 고객 턴 88개가 402 Insufficient Balance로 죽었어요. 지금은 유료 키가 무조건 맨 뒤예요. 라우터는 429 응답을 보고 계정마다 분당 요청 한도를 학습하고, 헤더가 제때 안 오면 다른 계정으로 넘겨요. 어떤 업스트림이 연결을 받아 놓고 한 시간 동안 아무것도 안 보낸 일이 있었거든요.
텔레그램. 에이전트가 쓰는 대로 답이 텔레그램 초안에 스트리밍돼요. 답하는 도중에 메시지를 보내면 바로 살아 있는 세션으로 들어가요. 이게 실제로 어떻게 동작하는지 재봤어요. 에이전트가 20까지 세는 동안 "reply BANANA instead"를 끼워 넣었더니 1부터 20까지 다 세고 나서 BANANA라고 했어요. CLI는 어느 쪽이든 지금 쓰던 메시지를 끝까지 쓰고, 진짜로 끊고 들어가는 건 interrupt뿐이에요.
남은 프로세스. 살아 있는 스레드 하나가 80~200MB짜리 claude 서브프로세스예요. 이제 놀고 있는 건 30분 뒤에 닫는데, 서브에이전트가 아직 일하는 중이면 닫지 않아요. 첫 버전은 작업 도중인 걸 죽였거든요. 예전에는 재시작하면 바쁜 스레드가 전부 exit 143으로 에러가 났어요. 지금은 종료 훅이 그 스레드들을 적어 두고, 다음에 부팅할 때 각각에 이어서 하라는 턴을 보내요.
메모리
agent-memory는 작은 API와 MCP 서버 뒤에 있는 Postgres + pgvector예요. 검색은 세 가지 순위를 reciprocal rank fusion으로 합쳐요. 정확한 키워드 태그(가중치를 더 줌), full-text와 trigram, 그리고 1,024차원 gemini 임베딩에 대한 코사인 유사도예요. 임베딩 덕분에 한국어와 영어가 한 공간에 들어가요. 임베딩 API가 rate limit에 걸리면 검색이 실패하지 않고 앞의 두 가지로만 돌아가요.
spaceagent 스레드는 전부 이걸 도구로 가지고 있어요. 스레드가 닫히면(30분 동안 아무 일 없거나 /new) 아직 저장 안 된 대화를 싼 모델 호출 한 번으로 메모리로 정리하고, 백그라운드 리뷰가 10턴마다 같은 일을 해요.
맥 앱은 swift로 600줄쯤이에요. 1분에 한 번 스크린샷을 찍고, 거의 안 바뀐 프레임은 건너뛰고, 화면이 잠겼거나 뚜껑이 닫혔거나 배터리로 돌고 있으면 멈춰요. 오디오는 기기에서 먼저 측정해요. 거의 조용한 구간은 speech-to-text로 아예 안 보내요. whisper가 무음에서 "thank you"를 지어내거든요. 매일 밤 파이프라인이 하루를 앱과 창 단위로 나누고, 싼 비전 모델이 ~30분 블록마다 카드를 하나씩 쓰고, 더 센 모델이 카드와 트랜스크립트를 하루 요약과 메모리 후보로 바꿔요. 그다음 후보마다 가장 가까운 이웃들과 비교해서 새것인지, 중복인지, 기존 걸 대체하는지 판단해요. 카드 번호, 긴 토큰, OTP처럼 생긴 코드는 저장하기 전에 다 지워요.
지금은
둘 다 서버 한 대에서 돌고, 저는 매일 써요. 주로 폰에서 텔레그램으로요. 메모리에는 가져온 노트, 데일리 리포트, 세션, 화면 캡처 등 출처 여덟 곳에서 온 항목이 330개쯤 들어 있어요.