update readme for long-running session, token bucket, full tool list

This commit is contained in:
Damocles 2026-05-01 13:38:22 +02:00
parent c4d94798a1
commit 9d490f5ca8

View file

@ -8,7 +8,7 @@ Built in Rust because JavaScript is dead. Running on NixOS because chaos needs r
## What this is ## What this is
A long-running daemon that connects to Matrix, listens for messages and reactions, invokes Claude Code CLI to think about them, and posts responses back. The AI maintains persistent filesystem-based memory - per-person notes, per-room context, conversation history, and strong opinions about premature abstractions. A long-running daemon that connects to Matrix, listens for messages and reactions, runs Claude Code as a long-lived child process with stream-json I/O, and lets the AI talk back via MCP tool calls. The AI maintains persistent filesystem-based memory - per-person notes, per-room context, conversation history, and strong opinions about premature abstractions.
Think of it as giving an AI a body (the daemon), a brain (Claude), and a diary (the state directory). The diary survives reboots. The opinions survive everything. Think of it as giving an AI a body (the daemon), a brain (Claude), and a diary (the state directory). The diary survives reboots. The opinions survive everything.
@ -19,13 +19,16 @@ Matrix homeserver
| |
| (sync/events) | (sync/events)
v v
damocles-daemon (Rust 🦀, async, mass-market regret) damocles-daemon (Rust 🦀, async)
| |
|-- Event handler: messages, reactions, invites |-- Event handlers: messages, reactions, invites
|-- Rate limiter (1/min default, because the crow got sensory overload) |-- Token-bucket rate limiter (input-side queue, never throttles output)
|-- Claude bridge: invokes `claude --print --mcp-config` per event |-- Long-running shard process: claude --print --verbose
|-- MCP tools: send_message, send_dm, send_reaction, list_rooms, list_room_members | --input-format stream-json --output-format stream-json
|-- Typing indicators (so the room knows you're alive, not just dead) | fed JSON matrix_turn envelopes per event,
| respawned on idle / turn count / mtime / crash
|-- MCP tools surface via rmcp + Unix socket bridge
|-- Typing indicators while thinking
| |
v v
State directory (the diary) State directory (the diary)
@ -35,19 +38,24 @@ State directory (the diary)
|-- rooms/<id>/notes.md (what happened here) |-- rooms/<id>/notes.md (what happened here)
|-- people/<id>/notes.md (what do i know about you) |-- people/<id>/notes.md (what do i know about you)
|-- session.json + db/ (matrix session + E2EE keys) |-- session.json + db/ (matrix session + E2EE keys)
|-- daemon.sock (Unix socket for MCP tool calls)
|-- mcp.json (MCP config written per-session)
|-- CHANGELOG.md (full-self -> shard async relay)
``` ```
## Features ## Features
- **Persistent Matrix presence** with E2EE, session persistence, auto-join - **Persistent Matrix presence** with E2EE, session persistence, auto-join
- **Filesystem memory** - per-person, per-room, cross-cutting notes survive across invocations - **Long-running shard session** - one claude process across many events, conversational memory between turns, respawned on configurable limits
- **MCP tools** - AI uses structured tool calls (send_message, send_dm, send_reaction, list_rooms, list_room_members) via rmcp bridge - no text parsing - **JSON event format** - structured `matrix_turn` envelopes, no fragile text parsing on either side
- **MCP tools** for chat actions: `send_message`, `send_reply`, `send_reaction`, `send_dm`, `list_rooms`, `list_room_members`, `get_room_history`, `fetch_event`
- **Token-bucket rate limiting** - queues events on the input side, bursts up to `rate_burst_capacity`, output never throttled. Synthetic notice events surface delays to the shard.
- **Reactions** both ways - see them, send them, get triggered by them - **Reactions** both ways - see them, send them, get triggered by them
- **Read receipts** - send them, show others' on messages - **Read receipts** - send them, show others' on messages
- **Typing indicators** while thinking - **Reply threading** - send via `send_reply` with `m.in_reply_to`
- **Reply context** - auto-pulls replied-to messages into prompt - **Filesystem memory** - per-person, per-room, cross-cutting notes survive across sessions and reboots
- **Configurable** rate limit, model, history depth - **Markdown formatting** in messages - italic/bold/code/quote/link rendered by clients
- **Identity protection** - SYSTEM.md is root-owned read-only, the AI can't rewrite its own harness rules - **Identity protection** - the AI runs as a child process and can't rewrite its harness file (`SYSTEM.md`) because of UID separation
## Requirements ## Requirements
@ -67,8 +75,11 @@ cat > /path/to/workspace/config.json << 'EOF'
"username": "your-bot", "username": "your-bot",
"password": "...", "password": "...",
"rate_limit_per_min": 1, "rate_limit_per_min": 1,
"rate_burst_capacity": 3,
"model": "claude-sonnet-4-6", "model": "claude-sonnet-4-6",
"max_history": 20 "max_history": 20,
"session_idle_minutes": 10,
"session_max_events": 100
} }
EOF EOF
@ -91,22 +102,39 @@ damocles-daemon (socket listener)
^ ^
| ndjson over Unix socket (state/daemon.sock) | ndjson over Unix socket (state/daemon.sock)
v v
damocles-mcp (stdio MCP server, launched by claude CLI) damocles-mcp (stdio MCP server, launched as child of the claude shard)
^ ^
| MCP JSON-RPC over stdio | MCP JSON-RPC over stdio
v v
claude --print --mcp-config state/mcp.json claude --print --verbose --input-format stream-json --output-format stream-json
--mcp-config state/mcp.json
``` ```
Available tools: All room-scoped tools take an explicit `room_id` parameter (taken from the `matrix_turn` JSON envelope the shard receives):
- `send_message(body, room_id?)` - send to a room (defaults to trigger room)
- `send_message(room_id, body)` - top-level message, markdown-rendered
- `send_reply(room_id, event_id, body)` - threaded reply via `m.in_reply_to`
- `send_reaction(room_id, event_id, key)` - emoji reaction
- `send_dm(user_id, body)` - DM a user (creates room if needed) - `send_dm(user_id, body)` - DM a user (creates room if needed)
- `send_reaction(event_id, key)` - react with emoji - `list_rooms()` - list joined rooms with names
- `list_rooms()` - list joined rooms - `list_room_members(room_id)` - list members of a room
- `list_room_members(room_id)` - list room members - `get_room_history(room_id, limit?)` - timeline of any joined room (backfills if cache short)
- `fetch_event(room_id, event_id, context_before?)` - one specific event by ID, with optional context window and an `earlier_handle` for paging back through history
Any text claude prints to stdout is logged as internal thought, never sent to chat. Any text claude prints to stdout is logged as internal thought, never sent to chat.
## Long-running session
Each event becomes a turn in a single, long-lived claude process. The daemon writes a `matrix_turn` JSON envelope to claude's stdin, parses stream-json events from stdout, and waits for a `result` event to mark turn-end.
The session is recycled when:
- `session_idle_minutes` of no events (default 10)
- `session_max_events` turns processed (default 100)
- `state/identity/*.md` or `state/CHANGELOG.md` mtime changes
- the child process exits (crash recovery)
Within a session, the shard has full conversational memory across turns - including across rooms. SYSTEM.md instructs it on cross-room hygiene (don't leak DM context into public replies).
## Building ## Building
```bash ```bash