Local-first roleplay client with a memory that actually works.
Chronicler is an open-source, self-hosted roleplay/character-chat app. You run it on your own machine via docker compose up. It imports the community v2/v3 character-card format (chub.ai-compatible), talks to any OpenAI-compatible LLM endpoint (OpenAI, Anthropic, Ollama, OpenRouter, llama.cpp, vLLM, nano-gpt, β¦), and remembers the things that matter about every character across every session β automatically, locally, without sending anything to a cloud.
The problem with existing RP clients isn't the UI β it's that memory falls apart after a few sessions. Chronicler is built around a persistent cognitive memory engine (YantrikDB) and a strict three-tier write contract that keeps canon clean and accumulates real continuity over hundreds of hours.
Fastest β one file, no clone:
curl -O https://raw.githubusercontent.com/yantrikos/chronicler/main/docker-compose.yml
docker compose up -d
open http://localhost:3001Docker pulls both published images (~500 MB total) from GitHub Container Registry and starts the stack. First boot waits ~60s for YantrikDB to finish loading its embedding model.
Or clone for development:
git clone https://github.com/yantrikos/chronicler && cd chronicler
docker compose up -ddocker compose up prefers the published image; if you modify the source and run docker compose build, it rebuilds locally and your image replaces the pulled one.
| Image | Purpose | Size |
|---|---|---|
ghcr.io/yantrikos/chronicler:latest |
Web + API proxy | ~270 MB |
ghcr.io/yantrikos/chronicler-yantrikdb:latest |
YantrikDB MCP server + CPU-only torch | ~1.9 GB |
Both are multi-platform (linux/amd64 + linux/arm64) and rebuilt on every push to main via .github/workflows/docker-images.yml. Semver tags (v0.1.0, etc.) publish stable versions as they're cut.
- Settings β Your persona β name + optional description
- Settings β Providers β add Ollama (local), OpenAI-compat, or Anthropic with a model name
- Settings β Extraction provider (optional) β small/fast model for background fact extraction (e.g.
qwen2.5:1.5b) - Settings β Proactive messages β off by default;
passivelets the character take initiative when urges accumulate and you've been idle - + card to import a v2/v3 character card, or demo: Ren to try the built-in character
- Type. Memories appear in the right sidebar as they land.
First-run flow:
- Settings β Your persona β set your user name and (optionally) a short description.
- Settings β Providers β add an Ollama (local), OpenAI-compat (nano-gpt / OpenRouter / local endpoints), or Anthropic provider with a model name. For Qwen3-family Ollama models, check "disable thinking" β massive latency cut.
- Settings β extraction provider (optional) β pick a smaller, faster model to run the fact extractor in parallel with generation.
- Settings β save.
- + card to import a v2/v3 character card (.png or .json), or demo: Ren to try a built-in character.
- Type and send. First reply takes a beat while memories seed; subsequent turns stream.
- Three-tier write contract. Every memory is tagged as reflex (ephemeral scene state), heuristic (inferred, reviewable), or canon (durable, user-confirmed). Chat noise doesn't pollute canon; drafts promote to canon only after repeated, uncorrected reinforcement across sessions. Full user-facing inspector with pin / demote / forget / retcon controls.
- Mechanically enforced privacy in group chats. Each memory carries a
visible_toACL. In a group scene, a character physically cannot recall a secret they weren't told β the retrieval layer filters before ranking. Not prompt-engineered, not relying on the model's discretion. - Semantic lorebook replacement. Community
character_bookentries are honored with full trigger semantics (keys + secondary keys + selective + constant + position + insertion_order + case sensitivity) AND supplemented by semantic recall. Retires the brittle keyword-only mechanic without breaking compatibility. - Session replay harness. Every tier transition logs a structured entry; the auto-promotion threshold can be retuned and replayed against prior sessions to see exactly which promotions would have fired. The "sink-risk" of the whole system is visible and tunable.
- Anti-confabulation clause. Prepended to every system prompt: "treat only the facts in
<canon>and<scene>as real, do not reference prior events not in those sections." Combined with the visibility ACL, the model cannot invent memory it wasn't given. - "Previously on..." recap at session start. Pulled from consolidated canon, not raw chat history. Strict anti-confab prompting on the recap itself after we caught (and fixed) a real-world confabulation where the recap misattributed facts.
- Verified character learning. Patterns the model shows repeatedly across sessions (deflection styles, conduct rules, decision rituals, lessons from past failures) get distilled into typed
skill_substrateentries β but only after an LLM verifier passes on each candidate, biased toward rejection. Skills surface back into future prompts when relevant, score+1/β1from user reactions (regenerate / edit / delete vs accept and move on), and transition throughcandidate β active β suppressed β archivedbased on accumulated outcomes. There's a "Character development" tab next to "Memory" with approve / disable / archive controls; the local override always wins over the derived state. See docs/LCDB-v0.md for the ablation harness that proves the contract holds. - Crystallizing character identity (Phase 11). Skills that hold up over weeks of sessions promote past
activeinto a 5th state,core_traitβ always-on identity facets like "Adira is fundamentally guarded with strangers" that inject into every system prompt unconditionally. Combined with a periodically-generated first-person self-model ("I am Adira. I'm a wandering musicianβ¦"), the substrate carries the character across LLMs. We measured it: same Adira throughqwen2.5:7bandqwen3.5:9b, Ο=0.087 cross-provider on mean overall scores (moderate model-independence). The Identity inspector shows the crystallized traits + self-model + benchmark verdict. See docs/CHARACTER-EMERGENCE.md for the thesis and docs/CHARACTER-EMERGENCE-RESULTS.md for the run.
All of the above is verified by automated tests: three-day-continuity, auto-promote, secret-stays-private, session-replay, lorebook, extract, skill-former, skill-outcomes, lcdb-v0, mcp-connectivity, core-trait-promoter, self-model-generator, identity-aggregator, cross-model-benchmark. Everything green.
For a head-to-head comparison against SillyTavern, RisuAI, and AgnAistic, see docs/COMPARISON.md. Headline:
| Chronicler | SillyTavern | RisuAI | AgnAistic | |
|---|---|---|---|---|
| Tiered cross-session memory (canon / heuristic / reflex) | β | π‘ plugin | π‘ lorebook | π‘ memory book |
| Anti-confabulation clause built into every prompt | β | β user adds | β | β |
| Memory conflict detection + auto-resolve | β | β | β | β |
| Skills + drift + preferences substrates (3 inspectors) | β | β | β | β |
| Model-independent character continuity (substrate-driven) | β Phase 11 (Ο=0.087 within qwen family) | β | β | β |
| Prompt inspector with token budget + retrieval reasoning | β | π‘ structure only | π‘ | β |
Group-chat memory ACL (visible_to, retrieval-time filter) |
β | β prompt-level | β | β |
| Scene Intensity dropdown (first-class, no jailbreak) | β | β | β | β |
| Extension ecosystem | β Grimoire (v0.3 hooks + slash + UI slots + MCP tools/resources + npx scaffold) | β huge | π‘ | π‘ |
Because the above is wasted if you can't actually RP:
- Edit / delete / regenerate / continue / swipes β hover any message for the toolbar; cycle swipes on the last reply with βΉ βΊ arrows.
- Impersonate user β click "impersonate" near the Send button and the LLM suggests your next line, which you can edit before sending.
- Character avatars β embedded card PNG image, or initials fallback with deterministic color per character.
- Markdown rendering β
**bold**,*italic*, code, block quotes, lists. - Streaming tokens β see the reply appear word by word.
- Author's note β persistent scene-level steering instruction, per-session.
- Alternate greetings β dropdown picker for multi-greeting cards.
- Sampling controls β temperature, top_p, top_k, min_p, repetition_penalty; per provider.
- Prompt inspector β see the exact system prompt + history sent to the LLM on every turn, including which lorebook entries activated.
- Session list β switch between past chats, rename, delete, export each as a Markdown transcript.
- Backup / restore β export full config + characters + all sessions as a single JSON for machine-to-machine transfer.
- User persona β set your name + a short self-description, injected into every system prompt.
- Group chats β add a second character; each turn composes context from that character's POV only (privacy ACLs enforced live).
docker compose pull
docker compose up -dPulls the latest published images and restarts. Your memory DB persists in the named volume (chronicler-memory) across restarts.
- Frontend: React 19 + TypeScript + Vite + Tailwind v4 + react-markdown
- Server: tiny Node HTTP proxy β serves the built SPA, routes
/api/mcp/*to YantrikDB, routesPOST /api/llmto configured providers (keeps API keys host-side, no browser CORS) - Memory: YantrikDB β local semantic memory with knowledge graph, conflict detection, consolidation, temporal triggers, personality inference, procedural memory
- LLM: Ollama native (
/api/chatwiththink: falsesupport) + OpenAI-compatible + Anthropic native; streaming on all three
See docs/ADR-001-stack.md for why web+Docker over native (yes, we pivoted from Tauri).
ββββ browser (React + TS) βββββββββββββββββββββββββββ
β ChatPane SessionList MemoryInspector β
β Settings PromptInspector β
βββββββββββββββββββββββββββββ¬ββββββββββββββββββββββββ
β fetch
βΌ
ββββ Node proxy (same origin) βββββββββββββββββββββββ
β /api/mcp/* β transparent reverse proxy β
β POST /api/llm β { target_url, method, headers, β
β body } β upstream provider β
β / /index.html β serves dist/ β
ββββ¬ββββββββββββββββββββββ¬βββββββββββββββββββββββββββ
β β
βΌ βΌ
βββββββββββββββ ββββββββββββββββ
β YantrikDB β β any LLM β
β (docker β β (Ollama / β
β service) β β Anthropic / β
β β β OpenAI API) β
βββββββββββββββ ββββββββββββββββ
chronicler/
server/index.mjs Node proxy + static
src/
lib/
yantrikdb/ typed MCP client + conventions
orchestrator/ per-turn pipeline, compose, write, extract, scene,
auto-promote, lorebook scanner, anti-confabulation
cards/ v2/v3 parser + decomposition
providers/ OpenAI-compat / Anthropic / Ollama / Mock
session/ lifecycle, store, markdown export
recap/ previously-on generator
instrumentation/ promotion + session logs (redacted by default)
components/
Chat/ ChatPane with recap + swipes + toolbar
Inspector/ MemoryInspector + PromptInspector
Sessions/ SessionList
Settings/ SettingsPanel
App.tsx
Dockerfile web build + runtime
yantrikdb.Dockerfile yantrikdb-mcp image with CPU-only torch
docker-compose.yml both services wired
docs/
ADR-001-stack.md why web+Docker
ADR-002-memory-conventions.md the three-tier write contract
DOGFOOD.md pre-launch testing protocol
PATTERN.md the reusable memory pattern (standalone read)
tests/ seven test suites, all required to ship
- All traffic binds to
127.0.0.1by default. Remote access requires you to remove that binding yourself and add an auth layer in front (Tailscale / Caddy). - LLM API keys live in your browser's localStorage and in the proxy request body; they never leave your machine except to reach the provider you configured.
- Promotion and session logs redact memory text by default. Opt into verbose local-only logging with
CHRONICLER_VERBOSE_LOGS=1. - Session content is never transmitted to anywhere except your configured LLM provider. The YantrikDB service runs alongside Chronicler in the same Docker network; your memories never leave your machine.
- Group-chat privacy is enforced mechanically via per-memory
visible_toACLs and pre-ranking retrieval filters β verified bytests/secret-stays-private.test.ts.
- Bugs and usage questions β SUPPORT.md. GitHub Issues is intentionally disabled because session content is often sensitive; structural bugs route to Discussions, content-bearing reports route to private email.
- Contributing code β CONTRIBUTING.md. Scope is narrow and deliberate; discussion-first for new feature areas.
- Security β SECURITY.md. Private email, coordinated disclosure.
- Code of Conduct β CODE_OF_CONDUCT.md. Contributor Covenant 2.1 + project-specific notes.
npm install
# frontend dev with HMR (expects API sidecar on :3001)
npm run dev
# second terminal: API sidecar
npm run dev:server
# full prod-mode run
npm run build && npm start
# seven-suite test run (pure TS, no services required)
npm test
# live MCP integration smoke (requires compose stack running)
npm run test:integration- Autonomous character behavior / personality evolution without user consent β see
docs/ADR-002for why this is a soft-suggestion-only feature - Mobile-responsive layout β desktop browser first
- Image generation / TTS / sprite expressions β leave to adjacent tools
- A hosted SaaS offering β this is self-hosted by design
- Full plugin ecosystem β intentionally closed surface until dogfood signal says otherwise
TBD before public release.
Built by @spranab. Powered by YantrikDB.
Companion read: docs/PATTERN.md β a standalone write-up of the memory architecture, useful if you're building anything that needs a trustworthy memory layer on top of a language model.