- Python 91.7%
- Dockerfile 6.9%
- Shell 1.4%
| agent | ||
| claude | ||
| docker | ||
| mcp/senia | ||
| private@d6e1b114f5 | ||
| scripts | ||
| .dockerignore | ||
| .gitignore | ||
| .gitmodules | ||
| README.md | ||
Sénia
a personal butler/coder AI assistant.
runs in a docker container with persistent state, an HTTP API for chat, autonomous heartbeat behavior, and a memory system designed to mimic human-style recall (granular recent, fuzzy distant).
current state
running
- docker container, debian trixie,
restart: unless-stopped - senia user (uid 750) with narrow
sudo(apt/apt-get only) - claude code as the current LLM backend (subscription-auth via mounted
~/.claude/), installed system-wide so updates happen via container rebuild rather than self-update - read-only
:romounts enforce sandbox boundaries even against in-container root - HTTP API on port 8000, OpenAI-compatible (
/v1/chat/completions,/v1/models)
the agent loop (senia/agent/)
- fastapi service running inside the container as the main process (uvicorn)
- backend-agnostic design via a
ModelBackendstrategy pattern: swapping models is a one-line change - current backend: shells out to
claude -pfor non-interactive turns - dynamic per-turn context assembled fresh: current time + today's diary entries
- persona handled by the backend natively (claude code autodetects
CLAUDE.mdfrom the workspace) - dependency management with
uv, a single static rust binary, dramatically faster than pip/poetry, uses standard PEP 621 metadata in pyproject.toml
memory
- diary log MCP tool: short timestamped entries in sqlite, agent-authored throughout the day
- task tracking MCP tool: categorized by owner
agent(to be done autonomously during heartbeats) oruser(to periodically remind the user of) - single shared db at
/etc/senia/data/senia.db, migration-tracked - persona files (SOUL.md / USER.md / AGENTS.md) for stable identity across model swaps
autonomous behavior
- variable heartbeat script runs every 30 min via host cron +
docker exec - wakes Sénia when there are pending tasks OR more than 2.5 hours since the last wake (5 hours since last wake during the night)
- captures her response and delivers it via ntfy push notification
- runs orthogonal to the HTTP API; the heartbeat directly invokes claude code, doesn't pass through the loop
tooling for the agent herself
uv(preferred) andpip --userfor python packagesnpm install -gfor node tools (configured to her user-local npm prefix)sudo apt installfor system packages, narrowly allowed
public / private repo split
the persona content (CLAUDE.md) and saved conversation contexts contain real personal data that shouldn't live in a public portfolio. they're tracked in a separate private companion repository (senia-private), referenced as a git submodule at private/.
the setup script handles both cases:
- with access to the private repo: the submodule pulls down, real persona and contexts are mounted into the container
- without access: placeholder files are generated locally, the container runs in a demonstrable state with a generic persona
both cases produce a fully working system; only the content differs. this preserves portfolio-readability while protecting personal data.
planned
chat interface
- Open WebUI behind authentik for the primary chat surface (browser + mobile)
- Conduit as the native mobile client (FOSS, flutter, authentik-aware)
- the loop's HTTP API registered with Open WebUI as a custom OpenAI provider
- ntfy kept for asynchronous Sénia-initiated push (heartbeats, alerts)
memory expansion
- daily markdown summaries: end-of-day compaction job consolidates raw logs into structured daily entries with frontmatter
- monthly markdown summaries: same pattern, dailies into a monthly digest
- embeddings layer: semantic retrieval via
sqlite-vec, multilingual embedding model running locally (e.g. multilingual-e5-base via ollama) - session-start context injection: already partially done via the loop; will expand as compaction lands
model strategy
- swap the backend to a local-first hybrid: Qwen3.6 quantized via ollama as the default for english butler tasks
- delegation pattern: catalan inputs + coding tasks routed to a strong API model (Claude Sonnet)
- OpenCode as a tool Sénia can shell out to for LSP-aware code work
- expected to be a single new
OllamaBackendclass inagent/orchestrator/models/+ routing logic in the loop
knowledge
- DokuWiki as the private wiki (deferred for later)
- Sénia reads via HTTP API, not direct filesystem; DokuWiki handles link resolution and rendering
- drafts to a staging area for documentation help; user commits to wiki manually, preserving curation quality
project structure
~/senia/
├── .gitmodules # references the private submodule
├── .gitignore
├── .dockerignore # at project root (the docker build context)
├── README.md
├── scripts/
│ └── setup.sh # bridges public + private repos with fallback
├── docker/
│ ├── compose.yml
│ └── Dockerfile
├── claude/
│ └── CLAUDE.example.md # placeholder persona, tracked publicly
├── workspace/ # mounted as /workspace, Sénia's per-turn cwd
├── data/ # runtime state, owned by senia user
│ ├── senia.db # single sqlite db (diary, tasks, future tables)
│ ├── memory/ # future: daily and monthly summaries
│ ├── heartbeat/ # heartbeat state + logs
│ └── scripts/
│ └── heartbeat.py # variable cron-driven wake-up
├── mcp/ # MCP server source, mounted :ro
│ └── senia/
│ ├── server.py
│ ├── db.py
│ ├── memory/
│ │ └── diary.py
│ ├── tasks.py
│ └── migrations/
├── private/ # submodule: real CLAUDE.md + contexts/
│ # (gitignored except for .gitmodules entry)
└── agent/ # the orchestration loop, baked into the image
├── pyproject.toml
├── uv.lock
├── README.md
└── orchestrator/
├── api.py # fastapi entrypoint
├── loop.py # per-turn orchestration
├── context.py # dynamic per-turn context builder
└── models/
├── base.py # ModelBackend ABC, ModelRequest/Response types
└── claude_code.py # current backend
working with the project
first-time setup
git clone git@github.com:vylion/senia.git
cd senia
./scripts/setup.sh # initializes private submodule or generates placeholders
cd docker
docker compose build
docker compose up -d
docker compose logs -f senia
updating agent code
the agent code (everything under agent/) is baked into the image at build time, not mounted at runtime. so changes to agent/ require rebuilding:
cd ~/senia/docker
docker compose build senia
docker compose up -d
thanks to docker's layer caching (dependencies are copied/installed in a separate step from application code), rebuilds where only python code changed complete in seconds. only changes to pyproject.toml or uv.lock trigger a full dependency reinstall.
other things that don't require rebuild:
- the persona files, mcp server code, data, and workspace: all mounted at runtime
- the heartbeat script (lives on the host, runs via
docker exec)
design principles
- own the loop where dynamic context injection matters; use mature tools where they earn their keep (e.g. OpenCode for LSP-aware coding when added, Open WebUI for chat UI polish)
- memory hierarchy mirrors human recall: granular recent, compressed distant, semantic retrieval on demand
- model is swappable; system stays the same: persona, tools, and memory all model-agnostic
- container is the security boundary: in-container non-root user +
:romounts give Sénia freedom in her sandbox without compromising host - diary as audit trail: installations, decisions, and observations all logged, so the user can review what's been changed
- orthogonal channels: chat goes through the loop's HTTP API; heartbeats invoke claude code directly. each channel uses the simplest path that works
- separation of public code and private data: architecture is open, content is private, both versioned