From d0ab2165cf609bd70e910154ace43517173904b7 Mon Sep 17 00:00:00 2001 From: cjus Date: Mon, 11 May 2026 10:22:55 -0600 Subject: [PATCH] rewrite readme to focus on motivations; move features into docs/ --- README.md | 241 +++++++---------------------------------------- docs/FEATURES.md | 41 ++++++++ 2 files changed, 75 insertions(+), 207 deletions(-) create mode 100644 docs/FEATURES.md diff --git a/README.md b/README.md index 0de6c1e..aca4a57 100644 --- a/README.md +++ b/README.md @@ -1,233 +1,60 @@ # Solrac -> A self-hosted, transparent, hackable Claude-Code-style agent that lives in a Bun process, listens to Telegram, and uses Anthropic's Claude Agent SDK for thinking and tool use. Think of it as your own open Claude — on your machine, with your filesystem, your MCP servers, your tools, your audit log. +> A self-hosted, hackable personal Agent: free local Ollama by default, Claude Sonnet/Opus on demand via Anthropic's Claude Agent SDK. Reach it from Telegram or a browser; own every audit row, permission rule, and budget cap. -## Is this for you? - -Solrac fits if **all** of the following are true: - -- You want a chat-driven agent that can read your code, run shell, and edit files — but you want to own every piece of the stack. -- You're willing to trade convenience (no plug-and-play hosting, no UI) for transparency (a small, focused TypeScript codebase, four moving parts, full audit trail). -- You're operating at one-user-or-few scale. If you need multi-tenancy or a UI, look elsewhere. - -If you'd rather use Claude Code's official Telegram plugin, that's a perfectly good choice — it's actively maintained and zero-setup. Solrac exists because we wanted custom permission rules, per-chat budget caps, an audit log we control, and a foundation extensible to email/Slack/scheduled jobs. - -**Operational dependencies:** - -- Bun ≥1.3.0 (runtime). -- A Telegram bot token + your `from.id`. -- An Anthropic API key (`@`/`!` paths). -- A local **Ollama daemon + tools-capable model** for the recommended Ollama-default config (no-prefix routes to local). For Claude-only deploys, set `SOLRAC_DEFAULT_ENGINE=primary` instead. - -## Features - -- **Customizable persona via `SOUL.md` + `SOLRAC.md`** — two operator-editable markdown files at the launch directory. `SOUL.md` (voice, stance, safety) ships with the package and is read once at boot. `SOLRAC.md` (operator overlay: who runs it, channel posture, project context) is re-read every turn so live edits land on the next message without a restart. See [docs/USAGE.md#customizing-solrac-soulmd-and-solracmd](./docs/USAGE.md#customizing-solrac-soulmd-and-solracmd). -- **Slash commands** — `/help`, `/status`, `/context`, `/clear`, `/compact` give the operator visibility and control over conversation context, spend, and session state without leaving Telegram. Both `/cmd` and `:cmd` invoke the same handler (`:` avoids Telegram's auto-link on bold text). -- **Operator-defined skills** — drop a `SKILL.md` into `$SOLRAC_SKILLS_DIR//` and that filename becomes a slash command on the next boot. Tool-less single-turn prompts with `{{args}}` templating; tier defaults to `SOLRAC_DEFAULT_ENGINE` (free on Ollama deploys); cost-capped under the existing per-chat hourly budget. Optional `tool: true` frontmatter exposes the skill as a callable MCP tool to the local Ollama agent (Phase 1: `tier: ollama` only) so natural-language requests can route through your prompts. Off by default; enable with `SOLRAC_SKILLS_ENABLED=true`. -- **Scheduled tasks** — drop a `TASK.md` into `$SOLRAC_TASKS_DIR//` and the prompt fires on its configured schedule (`every 1h`, `daily_at 09:00`, `at 2026-05-15T13:00:00Z`) into a configured chat. Engine inheritance (defaults to `config.defaultEngine`), per-task `max_cost_usd`, boot catch-up jitter; fires synthesize updates through the same turn queue so all existing safety machinery applies. `/tasks` lists loaded tasks with last + next fire; `/tasks run ` triggers on demand. Off by default; enable with `SOLRAC_TASKS_ENABLED=true`. See [docs/USAGE.md#scheduled-tasks](./docs/USAGE.md#scheduled-tasks). -- **Multi-user, multi-chat** — gated by per-`from.id` allowlist. -- **Three-tier permission policy** — auto-allow / auto-deny / Telegram-inline-keyboard-confirm. Configurable rule tables. -- **Per-chat hourly cost cap** — sliding 60-minute window over the audit log. Default $1.00/chat/hour. -- **Loop detector** — denies the third call to the same `(toolName, input)` within a turn. Order-insensitive over JSON keys. -- **Persistent audit trail** — every turn (allowed, denied, queue-full) writes a SQLite row with prompt, response, tool calls, cost, tokens, session id, status, **and engine** (`claude:primary:` / `claude:secondary:` / `ollama:`). -- **Local-first engine routing** — *Claude only when explicitly requested.* No-prefix messages route to local Ollama (free) by default; `@` escalates to Sonnet, `!` escalates to Opus. Pinable via `SOLRAC_DEFAULT_ENGINE` (`ollama` | `primary` | `secondary`) for Claude-only deploys. Boot validation rejects unreachable combinations. -- **Local Ollama with tool support** — when `OLLAMA_TOOLS_ENABLED=true`, the local model (e.g. `gemma4:e4b`) calls the same `mcp__solrac__*` integrations the Claude tiers see. Multi-round tool loop with shared loop detector, broker UX, and iteration cap (`OLLAMA_MAX_TOOL_ITERATIONS=8`). Cross-engine context bridge means switching between local and Claude preserves the conversation thread. -- **Dual-Claude tier routing** — `@` → primary tier (Sonnet by default), `!` → secondary tier (Opus by default). Each tier keeps its own SDK session id so prompt caching survives same-tier turns. Per-tier thinking-stub emoji (🙂 primary / 🤔 secondary) makes the routing visible in chat. -- **Optional browser web UI** — a second `Bun.serve` instance on a configurable port serves a minimal vanilla-JS chat interface with the same agent loop, slash commands, engine routing, and tool-confirm UX as Telegram. Full markdown rendering (headers, lists, tables, fenced code) on both transports — Claude/Ollama responses get a server-side markdown→HTML pass for Telegram and the raw markdown to the browser. Off by default; enable with `SOLRAC_WEB_ENABLED=true` plus a token. See [docs/USAGE.md#web-ui-browser-interface](./docs/USAGE.md#web-ui-browser-interface). -- **Session resume across restarts** — SDK session ids persisted per chat **and per tier**; conversations survive process death. -- **Inline-keyboard confirm UX** — 60-second timeout, fail-closed on send failure, verdict stamped into chat history after tap. -- **Bearer-gated `/stats` endpoint** — RSS, in-flight turns, 24h spend; `node:crypto.timingSafeEqual` constant-time auth. -- **Daily cost report** — DM'd to allowlist's first entry; idempotent via meta-key check; UTC midnight window. -- **Graceful shutdown** — SIGINT/SIGTERM aborts polling, drains in-flight turns (60s cap), checkpoints WAL, removes PID file, exits cleanly. -- **Weekly auto-bounce** — systemd timer mitigates Bun long-uptime memory drift. -- **DB-pollution defenses** — denial throttle (1 row per `from.id` per minute under flood), per-chat queue depth cap, prompt truncation with surrogate-pair safety. -- **Sub-agent default-deny** — `Agent`/`Task` tools disabled at SDK + policy layers. -- **Concurrency primitives** — per-chat `KeyedMutex`, global `Semaphore`, drain-aware `TurnTracker`. -- **No HTTP framework, no Telegram framework runtime, no queue server, no Docker** — focused TypeScript, no hidden middleware. +## Why Solrac -## Quick start +Solrac is a single-process Bun agent that bridges Telegram (and an optional browser UI) to a local Ollama model, escalating to Claude Sonnet or Opus only when you explicitly ask. It was built as part of [PNXStudios.com](https://pnxstudios.com) to manage a complex monorepo from anywhere — and, in doing so, to explore the mechanics of building a personal agent from first principles while enforcing hard cost controls and behavior auditing on every turn. -### Install — packaged binary (macOS, Linux) +It's deliberately smaller and narrower than other personal-assistant projects: -```sh -curl -fsSL https://cjus.dev/solrac/install.sh | sh -``` +- **[OpenClaw](https://github.com/openclaw/openclaw)** — Node/TypeScript "Gateway" daemon with macOS/iOS/Android companion apps, Voice Wake, Live Canvas, and ~25 inbound channels. +- **[Hermes Agent](https://github.com/NousResearch/hermes-agent)** (Nous Research) — Python, multi-provider, self-improving agent with seven execution backends and broad transport (Telegram/Discord/Slack/WhatsApp/Signal/Email/CLI). + +Both are broader and better-resourced. **Solrac's distinct value:** -Drops a self-contained binary at `~/.solrac/bin/solrac` and (with sudo if needed) symlinks it onto `/usr/local/bin/solrac`. Add `ANTHROPIC_API_KEY`, `TELEGRAM_BOT_TOKEN`, and `ALLOWLIST_BOOTSTRAP` to `~/.solrac/.env`, then run `solrac`. Persona files (`SOUL.md`, `SOLRAC.md`) and the SQLite data dir land in `~/.solrac/` on first boot. Reinstalling never touches your customizations. +- **Local-LLM-first economics.** No-prefix messages route to free Ollama; `@` and `!` are paid Claude escalations only on operator intent. +- **Cost enforcement, not just visibility.** Sliding hourly USD caps that *deny* turns when hit, plus a daily cost-report DM. +- **Audit-before-acting.** Every update (allowed, denied, queue-full) writes a row to one append-only SQLite table. +- **Single-process minimalism.** No HTTP framework, no Telegram framework runtime, no queue server, no Docker, no sub-agents. A few thousand lines of TypeScript you can read in an afternoon and fork. -Full reference: [docs/INSTALL.md](./docs/INSTALL.md). +If you need multi-tenancy, voice wake, mobile companions, or 25 chat platforms, use OpenClaw or Hermes. If you want a small, cost-capped, fully audited foundation you can bend to your shape, Solrac fits. -### Install — from source (developers) +## Quick start -If you have Bun and a Telegram bot: +**Packaged binary** (macOS/Linux): ```sh -git clone https://github.com/cjus/solrac.git -cd solrac -npm install -cp .env.example .env # then fill in 3 required values -npm run dev # starts on PORT (default 8443) +curl -fsSL https://cjus.dev/solrac/install.sh | sh ``` -Then DM your bot. You should see a 🤔 stub within a second. - -If you don't have Bun, a Telegram bot, or an Anthropic API key — see [docs/SETUP.md](./docs/SETUP.md). Total walkthrough: ~20 minutes. - -**Engine routing — at a glance** (with the recommended `SOLRAC_DEFAULT_ENGINE=ollama`): +**From source** (Bun required): -| Prefix | Engine | Model env | Default | -|--------|--------|-----------|---------| -| (none) | Local Ollama (default) | `OLLAMA_MODEL` | `gemma4:e4b` (recommended) | -| `@` | Primary Claude — escalate | `SOLRAC_PRIMARY_MODEL` | `claude-sonnet-4-6` | -| `!` | Secondary Claude — heaviest | `SOLRAC_SECONDARY_MODEL` | `claude-opus-4-7` | - -There is no `>`-style escape prefix; a leading `>` is literal user text routed via the default engine. For Claude-only deploys, set `SOLRAC_DEFAULT_ENGINE=primary` (no-prefix → Sonnet) and `OLLAMA_ENABLED=false`. See [docs/USAGE.md#engine-routing-prefix-table](./docs/USAGE.md#engine-routing-prefix-table) and [docs/ARCHITECTURE.md#engine-routing](./docs/ARCHITECTURE.md#engine-routing). +```sh +git clone https://github.com/cjus/solrac.git +cd solrac && npm install && cp .env.example .env +npm run dev +``` -**Optional — browser web UI.** Set `SOLRAC_WEB_ENABLED=true` and `SOLRAC_WEB_TOKEN=$(openssl rand -hex 32)` in `.env`; browse to `http://127.0.0.1:8080` for a chat interface with full markdown rendering. Bind `SOLRAC_WEB_HOST=0.0.0.0` to expose it on a LAN/Tailnet (token gates access). See [docs/USAGE.md#web-ui-browser-interface](./docs/USAGE.md#web-ui-browser-interface) and [docs/ARCHITECTURE.md#web-ui-transport-optional](./docs/ARCHITECTURE.md#web-ui-transport-optional). +Need help with Bun, a Telegram bot, or an Anthropic API key? See [docs/SETUP.md](./docs/SETUP.md) (~20 min walkthrough). Full install reference at [docs/INSTALL.md](./docs/INSTALL.md). ## Documentation | Doc | Audience | What it covers | |-----|----------|---------------| -| [docs/INSTALL.md](./docs/INSTALL.md) | Operators | curl-pipe install, `~/.solrac/` layout, upgrade & uninstall, building local binaries | -| [docs/SETUP.md](./docs/SETUP.md) | First-time users | Bun install, Telegram bot creation, `from.id` lookup, Anthropic key, `.env`, first boot | -| [docs/USAGE.md](./docs/USAGE.md) | Daily users | Concepts (turn / session / `from.id` vs `chat.id`), interaction patterns, permission UX, cost cap, loop detector | -| [docs/CONFIG.md](./docs/CONFIG.md) | Operators | Full env-var reference: defaults, ranges, validation, secret-scrub rules | -| [docs/ARCHITECTURE.md](./docs/ARCHITECTURE.md) | Developers | Module map, data flow, SDK integration, concurrency, SQLite schema, three-tier policy, threat model, tricky seams | -| [docs/OPERATIONS.md](./docs/OPERATIONS.md) | Operators | systemd deploy, `/health` and `/stats`, daily report, log events, audit queries, backups | -| [docs/SCHEMA.md](./docs/SCHEMA.md) | Operators / debuggers | SQLite schema reference + query cookbook for debugging, forensics, performance, cross-engine analysis | -| [docs/RUNBOOK.md](./docs/RUNBOOK.md) | On-call | Incident recovery: 409 conflict, drain timeout, runaway cost, OOM, db corruption, zombie poller, network drops | -| [docs/GLOSSARY.md](./docs/GLOSSARY.md) | Everyone | Alphabetical reference for Solrac-specific terms | -| [docs/ROADMAP.md](./docs/ROADMAP.md) | Maintainers | Step 9 webhook spec, 13 open questions, deferred enhancements | - -Auxiliary references in the source tree: - -- `docs/SDK_NOTES.md` — verified Claude Agent SDK surface, pinned to `0.2.119` -- `deploy/systemd/README.md` — install commands for the three systemd units - -Web UI deep-dives are split across the existing docs: - -- [SETUP.md §11](./docs/SETUP.md#11-optional-enable-the-browser-web-ui) — turning it on, security notes -- [USAGE.md "Web UI"](./docs/USAGE.md#web-ui-browser-interface) — feature parity with Telegram, markdown rendering -- [CONFIG.md](./docs/CONFIG.md#variables) — the five `SOLRAC_WEB_*` env vars -- [OPERATIONS.md "Web UI (optional)"](./docs/OPERATIONS.md#web-ui-optional) — boot verification, route reference, audit queries -- [ARCHITECTURE.md "Web UI transport"](./docs/ARCHITECTURE.md#web-ui-transport-optional) — module map, markdown sidecar, anti-goal posture -- [RUNBOOK.md "Web UI not reachable"](./docs/RUNBOOK.md#web-ui-issues) and ["streaming silent"](./docs/RUNBOOK.md#web-ui-stream-silent) — troubleshooting - -## Repository layout - -``` -solrac/ -├── README.md — you are here -├── SOUL.md — voice + safety; canonical default copied to launch cwd -├── SOLRAC.md — operator overlay template; copied to launch cwd -├── package.json -├── tsconfig.json -├── .env.example — copy to .env -│ -├── src/ -│ ├── log.ts JSON-to-stdout logger -│ ├── config.ts env validation -│ ├── db.ts bun:sqlite + prepared statements -│ ├── allowlist.ts isAllowed / bootstrap -│ ├── session.ts per-chat, per-tier session state -│ ├── mutex.ts KeyedMutex with depth() -│ ├── semaphore.ts counting global concurrency cap -│ ├── turn-tracker.ts drain-aware in-flight set -│ ├── queue.ts compose mutex + semaphore + tracker -│ ├── telegram.ts raw fetch + tgCall + 429 retry -│ ├── poll.ts long-poll + PID file + dedupe -│ ├── policy.ts classifier, cost cap, broker, loop, hooks, engine-prefix parser -│ ├── instance.ts SOUL.md / SOLRAC.md bootstrap + load -│ ├── agent.ts Claude Agent SDK wiring (per-tier) + cross-engine bridge -│ ├── ollama.ts local Ollama runner (default engine) -│ ├── commands.ts slash command parser, dispatcher, handlers -│ ├── skills.ts operator-defined SKILL.md discovery -│ ├── skill-tools.ts expose tool:true skills to Ollama (ALS-propagated context) -│ ├── scheduler.ts TASK.md discovery + tick loop -│ ├── server.ts /health + /stats -│ ├── lifecycle.ts graceful shutdown -│ ├── daily-report.ts cost report cron -│ ├── main.ts transport wiring + engine dispatch -│ └── *.test.ts bun:test units -│ -├── test/ -│ └── smokes/ -│ ├── harness.ts openTestDb / mkUpdate helpers -│ ├── flood.ts db-pollution defense smoke -│ └── ollama.ts live Ollama smoke -│ -├── deploy/systemd/ -│ ├── solrac.service main long-running unit -│ ├── solrac-bounce.service oneshot restart helper -│ ├── solrac-bounce.timer weekly bounce schedule -│ └── README.md install + verify -│ -├── docs/ (this README's siblings) -│ ├── SETUP.md -│ ├── USAGE.md -│ ├── CONFIG.md -│ ├── ARCHITECTURE.md -│ ├── OPERATIONS.md -│ ├── RUNBOOK.md -│ ├── GLOSSARY.md -│ ├── ROADMAP.md -│ └── SDK_NOTES.md -│ -└── data/ gitignored - ├── solrac.sqlite + WAL + SHM - ├── solrac.pid PID file - └── workspaces// per-chat agent cwd -``` - -## Stack - -- **Runtime:** [Bun](https://bun.sh) (≥1.3.0). Required for `bun:sqlite`, `bun:test`, and `Bun.serve`. -- **Package manager:** npm. -- **Agent SDK:** [`@anthropic-ai/claude-agent-sdk`](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk) `0.2.119` (pinned exact, no caret). -- **Database:** `bun:sqlite` with `journal_mode = WAL`. -- **HTTP:** `Bun.serve` `routes` (no framework). -- **Telegram client:** raw `fetch` + `tgCall` (no framework runtime; `@grammyjs/types` for types only). -- **Process supervision:** systemd (`Type=simple`, `Restart=on-failure`, `TimeoutStopSec=90`, hardening via `NoNewPrivileges` / `ProtectSystem=strict` / `ProtectHome` / `PrivateTmp`). - -## Design philosophy - -Three commitments that shape every decision: - -1. **Own the host process.** No HTTP framework, no Telegram framework runtime, no queue server. Everything that touches a chat, a tool, or the database lives in this Bun process. We trade libraries for clarity at production-scale-of-one. -2. **Audit before acting.** Every update — allowed, denied, queue-full — writes an `audit` row. The audit log is the source of truth for "what did the bot do today?" -3. **Defense in depth.** Allowlist + three-tier classifier + cost cap + loop detector + db-pollution defenses + sub-agent default-deny. Each defense is independent. - -See [docs/ARCHITECTURE.md#philosophy](./docs/ARCHITECTURE.md#philosophy) for the full discussion. - -## What's intentionally not here - -See [docs/ARCHITECTURE.md#anti-goals](./docs/ARCHITECTURE.md#anti-goals) for the full list. Highlights: - -- No HTTP framework. No Telegram framework runtime. No queue server. No Docker. -- No MarkdownV2 outbound (HTML's three escape characters beat MarkdownV2's twenty). -- No Bedrock/Vertex auth (direct Anthropic only). -- No sub-agents ([OQ#8](./docs/ROADMAP.md#oq8-sub-agent-enablement)). -- No webhook transport ([Webhook transport](./docs/ROADMAP.md#webhook-transport)). - -If you want to revisit any of these, write the case in a PR description and treat it as an explicit reversal. - -## Testing - -```sh -npm test # bun test -npm run typecheck # tsc --noEmit -npm run smoke:flood # synthetic db-pollution defense smoke -npm run smoke:ollama # live Ollama smoke (requires Ollama on $OLLAMA_URL) -``` - -For live smokes against a dev bot, see [docs/RUNBOOK.md](./docs/RUNBOOK.md). - -## Origin - -Solrac was built as part of the [PNXStudios.com](https://pnxstudios.com) project to manage work on a complex monorepo from anywhere — Telegram in, code edits and shell out, with full audit and per-chat budget control. It's open-sourced as a complete, hackable foundation: the same TypeScript codebase that drives a real production workflow, with the building blocks — auditable agent loop, local-Ollama default with optional tool-calling, dual-Claude tier escalation, per-chat cost caps, three-tier permission policy, operator-defined skills — laid bare for anyone to read, run, fork, or extend to a different transport (email, Slack, scheduled jobs, in-house dashboards). +| [docs/FEATURES.md](./docs/FEATURES.md) | Everyone | Complete feature list, grouped by theme | +| [docs/INSTALL.md](./docs/INSTALL.md) | Operators | curl-pipe install, `~/.solrac/` layout, upgrade & uninstall | +| [docs/SETUP.md](./docs/SETUP.md) | First-time users | Bun, Telegram bot, `from.id`, Anthropic key, first boot | +| [docs/USAGE.md](./docs/USAGE.md) | Daily users | Concepts, interaction patterns, permission UX, cost cap, loop detector | +| [docs/CONFIG.md](./docs/CONFIG.md) | Operators | Full env-var reference | +| [docs/ARCHITECTURE.md](./docs/ARCHITECTURE.md) | Developers | Module map, data flow, concurrency, schema, policy, threat model, philosophy, anti-goals | +| [docs/OPERATIONS.md](./docs/OPERATIONS.md) | Operators | systemd deploy, `/health` & `/stats`, daily report, audit queries | +| [docs/SCHEMA.md](./docs/SCHEMA.md) | Operators / debuggers | SQLite schema + query cookbook | +| [docs/RUNBOOK.md](./docs/RUNBOOK.md) | On-call | Incident recovery: cost runaway, drain timeout, db corruption, … | +| [docs/GLOSSARY.md](./docs/GLOSSARY.md) | Everyone | Solrac-specific terms | +| [docs/ROADMAP.md](./docs/ROADMAP.md) | Maintainers | Open questions, deferred enhancements | ## Contact diff --git a/docs/FEATURES.md b/docs/FEATURES.md new file mode 100644 index 0000000..9c6a99c --- /dev/null +++ b/docs/FEATURES.md @@ -0,0 +1,41 @@ +# Features + +The complete feature list, grouped by theme. See [../README.md](../README.md) for the project overview, motivations, and quick-start instructions. + +## Engines & routing + +- **Local-first engine routing** — *Claude only when explicitly requested.* No-prefix messages route to local Ollama (free) by default; `@` escalates to Sonnet, `!` escalates to Opus. Pinable via `SOLRAC_DEFAULT_ENGINE` (`ollama` | `primary` | `secondary`) for Claude-only deploys. Boot validation rejects unreachable combinations. +- **Local Ollama with tool support** — when `OLLAMA_TOOLS_ENABLED=true`, the local model (e.g. `gpt-oss:20b`) calls the same `mcp__solrac__*` integrations the Claude tiers see. Multi-round tool loop with shared loop detector, broker UX, and iteration cap (`OLLAMA_MAX_TOOL_ITERATIONS=8`). Cross-engine context bridge means switching between local and Claude preserves the conversation thread. +- **Dual-Claude tier routing** — `@` → primary tier (Sonnet by default), `!` → secondary tier (Opus by default). Each tier keeps its own SDK session id so prompt caching survives same-tier turns. Per-tier thinking-stub emoji (🦙 Ollama / 🙂 primary / 🤔 secondary) makes the routing visible in chat. + +## Persona, commands & extensions + +- **Customizable persona via `SOUL.md` + `SOLRAC.md`** — two operator-editable markdown files at the launch directory. `SOUL.md` (voice, stance, safety) ships with the package and is read once at boot. `SOLRAC.md` (operator overlay: who runs it, channel posture, project context) is re-read every turn so live edits land on the next message without a restart. See [USAGE.md#customizing-solrac-soulmd-and-solracmd](./USAGE.md#customizing-solrac-soulmd-and-solracmd). +- **Slash commands** — `/help`, `/status`, `/context`, `/clear`, `/compact` give the operator visibility and control over conversation context, spend, and session state without leaving Telegram. Both `/cmd` and `:cmd` invoke the same handler (`:` avoids Telegram's auto-link on bold text). +- **Operator-defined skills** — drop a `SKILL.md` into `$SOLRAC_SKILLS_DIR//` and that filename becomes a slash command on the next boot. Tool-less single-turn prompts with `{{args}}` templating; tier defaults to `SOLRAC_DEFAULT_ENGINE` (free on Ollama deploys); cost-capped under the existing per-chat hourly budget. Optional `tool: true` frontmatter exposes the skill as a callable MCP tool to the local Ollama agent (Phase 1: `tier: ollama` only) so natural-language requests can route through your prompts. Off by default; enable with `SOLRAC_SKILLS_ENABLED=true`. +- **Scheduled tasks** — drop a `TASK.md` into `$SOLRAC_TASKS_DIR//` and the prompt fires on its configured schedule (`every 1h`, `daily_at 09:00`, `at 2026-05-15T13:00:00Z`) into a configured chat. Engine inheritance (defaults to `config.defaultEngine`), per-task `max_cost_usd`, boot catch-up jitter; fires synthesize updates through the same turn queue so all existing safety machinery applies. `/tasks` lists loaded tasks with last + next fire; `/tasks run ` triggers on demand. Off by default; enable with `SOLRAC_TASKS_ENABLED=true`. See [USAGE.md#scheduled-tasks](./USAGE.md#scheduled-tasks). + +## Transport + +- **Optional browser web UI** — a second `Bun.serve` instance on a configurable port serves a minimal vanilla-JS chat interface with the same agent loop, slash commands, engine routing, and tool-confirm UX as Telegram. Full markdown rendering (headers, lists, tables, fenced code) on both transports — Claude/Ollama responses get a server-side markdown→HTML pass for Telegram and the raw markdown to the browser. Off by default; enable with `SOLRAC_WEB_ENABLED=true` plus a token. See [USAGE.md#web-ui-browser-interface](./USAGE.md#web-ui-browser-interface). +- **Multi-user, multi-chat** — gated by per-`from.id` allowlist. + +## Safety & audit + +- **Three-tier permission policy** — auto-allow / auto-deny / Telegram-inline-keyboard-confirm. Configurable rule tables. +- **Per-chat hourly cost cap** — sliding 60-minute window over the audit log. Default $1.00/chat/hour. +- **Loop detector** — denies the third call to the same `(toolName, input)` within a turn. Order-insensitive over JSON keys. +- **Persistent audit trail** — every turn (allowed, denied, queue-full) writes a SQLite row with prompt, response, tool calls, cost, tokens, session id, status, **and engine** (`claude:primary:` / `claude:secondary:` / `ollama:`). +- **Session resume across restarts** — SDK session ids persisted per chat **and per tier**; conversations survive process death. +- **Inline-keyboard confirm UX** — 60-second timeout, fail-closed on send failure, verdict stamped into chat history after tap. +- **Sub-agent default-deny** — `Agent`/`Task` tools disabled at SDK + policy layers. +- **DB-pollution defenses** — denial throttle (1 row per `from.id` per minute under flood), per-chat queue depth cap, prompt truncation with surrogate-pair safety. + +## Operations + +- **Bearer-gated `/stats` endpoint** — RSS, in-flight turns, 24h spend; `node:crypto.timingSafeEqual` constant-time auth. +- **Daily cost report** — DM'd to allowlist's first entry; idempotent via meta-key check; UTC midnight window. +- **Graceful shutdown** — SIGINT/SIGTERM aborts polling, drains in-flight turns (60s cap), checkpoints WAL, removes PID file, exits cleanly. +- **Weekly auto-bounce** — systemd timer mitigates Bun long-uptime memory drift. +- **Concurrency primitives** — per-chat `KeyedMutex`, global `Semaphore`, drain-aware `TurnTracker`. +- **No HTTP framework, no Telegram framework runtime, no queue server, no Docker** — focused TypeScript, no hidden middleware.