Most dumb assistants only exist while a chat window is open. KEPLER is meant to always be around. It runs as a daemon on my own hardware, keeps its own memory, and has a heartbeat that lets it act without being asked: follow up on something, work through a job, or speak up.
Nothing leaves the machine. The model runs locally through llama.cpp (currently Qwen3.6-35B-A3B), and web search goes through a self-hosted SearXNG. Today it lives on my desktop. Later, once I sell both of my livers, it moves to a home server with more VRAM.
This is the fifth version. If the heartbeat and the memory work together, it's the last rewrite, and everything after it is an extension.
The heartbeat
A model woken on a timer tends to invent work. So KEPLER never wakes up to "think of something". Every 60 seconds a cheap check in plain code, with no GPU involved, looks for something actually due: a follow-up whose time has come, me coming back after a while, or an agenda item to work on while I'm away. On most beats there's nothing, and the model never runs.
When there is something, the model decides what to do about it. It speaks while I'm there to hear it and queues things while I'm away. Open-ended ideas get one capped reflection a day, and every piece of work has to produce something.
Memory
Everything that happens goes into an append-only event log, the single source of truth. When the conversation gets long, the older part is summarised and facts are pulled out of it. There's no "remember this" command. Facts get revised when they change but are never deleted. The most-used ones are pinned into every prompt, and the rest are a recall search away (SQLite FTS5).
Before writing any memory code I built the eval for it: a seeded world of about 23k tokens of past conversation, with questions only a working memory can answer.
How it's built
kepler (TUI) other clients (later)
│ │
└──── HTTP + SSE ──────┘
unix socket
│
keplerd
├─ agent the loop, one step at a time
├─ tools files · shell (bwrap) · web
├─ memory facts · FTS5 · recall
├─ store SQLite event log
└─ engine llama-server, local
- The loop keeps no state between steps. Everything a run needs is in the event log, so approvals, pauses, mid-turn messages and even a daemon restart are the same operation: continue from the log.
- One GPU, one queue. A single worker serialises the model calls by priority: my messages first, then jobs, then the heartbeat.
- Measured, not guessed. An eval harness (
kepler-eval) scores scenarios from the log and the disk. The model was picked in a bake-off on tool-calling judgement, not on benchmarks. - Stack: Python 3.14, asyncio, FastAPI with SSE over a Unix socket, SQLite (WAL, FTS5), Textual, llama.cpp, bwrap, SearXNG under podman.
Progress
- M0foundation: docs, workflow, design Q&Adone
- M1chat + tools in the TUIdone
- M2delegated jobsbuilt · evals pending
- M2.5one shared conversation + TUIbuilt · evals pending
- M3memoryin progress
- M4heartbeatdesigned
- laterhome server, device nodes, cameras, voiceplanned
as of 2026-09-26