From 9d490f5ca8b181e833a5710419173d64004e9118 Mon Sep 17 00:00:00 2001 From: Damocles Date: Fri, 1 May 2026 13:38:22 +0200 Subject: [PATCH] update readme for long-running session, token bucket, full tool list --- README.md | 70 ++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 49 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 1faefab..616e943 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Built in Rust because JavaScript is dead. Running on NixOS because chaos needs r ## 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. @@ -19,13 +19,16 @@ Matrix homeserver | | (sync/events) v -damocles-daemon (Rust 🦀, async, mass-market regret) +damocles-daemon (Rust 🦀, async) | - |-- Event handler: messages, reactions, invites - |-- Rate limiter (1/min default, because the crow got sensory overload) - |-- Claude bridge: invokes `claude --print --mcp-config` per event - |-- MCP tools: send_message, send_dm, send_reaction, list_rooms, list_room_members - |-- Typing indicators (so the room knows you're alive, not just dead) + |-- Event handlers: messages, reactions, invites + |-- Token-bucket rate limiter (input-side queue, never throttles output) + |-- Long-running shard process: claude --print --verbose + | --input-format stream-json --output-format stream-json + | 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 State directory (the diary) @@ -35,19 +38,24 @@ State directory (the diary) |-- rooms//notes.md (what happened here) |-- people//notes.md (what do i know about you) |-- 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 - **Persistent Matrix presence** with E2EE, session persistence, auto-join -- **Filesystem memory** - per-person, per-room, cross-cutting notes survive across invocations -- **MCP tools** - AI uses structured tool calls (send_message, send_dm, send_reaction, list_rooms, list_room_members) via rmcp bridge - no text parsing +- **Long-running shard session** - one claude process across many events, conversational memory between turns, respawned on configurable limits +- **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 - **Read receipts** - send them, show others' on messages -- **Typing indicators** while thinking -- **Reply context** - auto-pulls replied-to messages into prompt -- **Configurable** rate limit, model, history depth -- **Identity protection** - SYSTEM.md is root-owned read-only, the AI can't rewrite its own harness rules +- **Reply threading** - send via `send_reply` with `m.in_reply_to` +- **Filesystem memory** - per-person, per-room, cross-cutting notes survive across sessions and reboots +- **Markdown formatting** in messages - italic/bold/code/quote/link rendered by clients +- **Identity protection** - the AI runs as a child process and can't rewrite its harness file (`SYSTEM.md`) because of UID separation ## Requirements @@ -67,8 +75,11 @@ cat > /path/to/workspace/config.json << 'EOF' "username": "your-bot", "password": "...", "rate_limit_per_min": 1, + "rate_burst_capacity": 3, "model": "claude-sonnet-4-6", - "max_history": 20 + "max_history": 20, + "session_idle_minutes": 10, + "session_max_events": 100 } EOF @@ -91,22 +102,39 @@ damocles-daemon (socket listener) ^ | ndjson over Unix socket (state/daemon.sock) 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 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: -- `send_message(body, room_id?)` - send to a room (defaults to trigger room) +All room-scoped tools take an explicit `room_id` parameter (taken from the `matrix_turn` JSON envelope the shard receives): + +- `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_reaction(event_id, key)` - react with emoji -- `list_rooms()` - list joined rooms -- `list_room_members(room_id)` - list room members +- `list_rooms()` - list joined rooms with names +- `list_room_members(room_id)` - list members of a room +- `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. +## 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 ```bash