spaceagent
my personal assistant stack: claude agents behind a web ui and telegram, plus one memory they all share
- 時間
- 2026年8月 – 現在
- 對象
- myself
- 角色
- solo: design, build, deploy, operate
- 技術
- TypeScript, Claude Agent SDK, Next.js, Postgres + pgvector, SQLite, Prisma, grammY, Swift
- 規模
- 68 commits across 2 repos, 16k lines, 3 services + a mac app
- 個人專案
這篇只有以下語言: english
what it is
spaceagent is how i talk to my assistant. it is a next.js app on my server where every thread is a claude agent running in a folder on the host, with a real shell. the thread i see in the browser is the same one i message on telegram. it runs my notes vault, my schedule, mail, daily reports and a set of cron jobs.
agent-memory is the other half: one memory service that every agent on every machine reads and writes, fed partly by a small mac app that watches my screen.
the runtime
each thread holds one query() from the Claude Agent SDK in streaming-input mode, and it outlives individual turns. every SDK message is appended to an event log with a sequence number and fanned out over SSE. a reload or a second tab replays from the last seq it saw, then tails live, so the agent never depends on a browser being open. live agents sit in module scope, which is why it is deliberately one process.
what broke
long threads crawled. one thread reached 12,284 events and 33MB. all of it was replayed on open, and every streaming token re-parsed the markdown of the whole scrollback. the stream now replays only the newest 600 drawable events and clips huge strings: 33MB down to 1.08MB. token deltas repaint one card at most once a frame.
the server kept stalling. better-sqlite3 is synchronous. writing one row per token delta was about 90% of the table, and every commit blocked the whole server on fsync. the cleanup sweep also ran VACUUM, which rewrote a ~500MB file on the only thread and froze every request for a minute. deltas are now streamed but never stored, and the vacuum is gone.
accounts. a thread pins to one subscription because the prompt cache is per account, and it only moves between turns. in september this grew into a router that my freelance gateway uses too: callers ask for a lane (frontier, intelligent, budget), never an account. one rule came from an outage. a subscription sitting exactly on the 0.70 reserve line sorted behind a metered API key with no credit, and 88 customer turns died on 402 Insufficient Balance. paid keys are now strictly last. the router also learns each account's requests-per-minute cap from its 429s, and fails over if headers don't arrive in time, after one upstream accepted a connection and then sent nothing for an hour.
telegram. replies stream into a telegram draft as the agent writes. a message sent mid-reply goes straight into the live session. i measured what that actually does: injecting "reply BANANA instead" while the agent counted to 20 produced 1 to 20 and then BANANA. the CLI finishes its current message either way, and only an interrupt truly preempts.
leftover processes. each live thread is a claude subprocess of 80 to 200MB. idle ones now close after 30 minutes, but not while a sub-agent is still working; the first version killed one mid-task. a restart used to error every busy thread with exit 143. now the shutdown hook writes them down and the next boot sends each a continuation turn.
the memory
agent-memory is Postgres with pgvector behind a small API and an MCP server. search fuses three rankings with reciprocal rank fusion: exact keyword tags (weighted higher), full-text plus trigram, and cosine over gemini embeddings at 1,024 dims, which puts korean and english in one space. if the embedding API is rate limited, search falls back to the first two instead of failing.
every spaceagent thread has it as a tool. when a thread closes, after 30 idle minutes or a /new, its unsaved conversation is distilled into memories by one cheap model call, and a background review does the same every 10 turns.
the mac app is about 600 lines of swift. it takes a screenshot a minute, skips frames that barely changed, and stops while the screen is locked, the lid is closed, or the laptop is on battery. audio is metered on the device: near-silent segments never reach speech-to-text, because whisper hallucinates "thank you" on silence. every night a pipeline splits the day by app and window, has a cheap vision model write a card per ~30 minute block, has a stronger model turn the cards and transcripts into a day summary plus candidate memories, and checks each candidate against its nearest neighbours: new, duplicate, or supersede. card numbers, long tokens and OTP-looking codes are stripped before anything is stored.
where it is now
both run on one server and i use them every day, mostly from telegram on my phone. the memory holds about 330 entries from eight sources, including imported notes, daily reports, sessions and screen capture.