Skip to content

About

Self-hosted AI roleplay client with memory that survives long campaigns — tiered canon/heuristic/reflex writes, retrieval-time privacy ACL for group chats, native MCP tool support.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

15 stars

Watchers

0 watching

Forks

Latest commit

 

History

48 Commits

Folders and files

Repository files navigation

Chronicler — self-hosted AI roleplay client with long-term memory

A local-first alternative to SillyTavern, RisuAI, and Faraday, built around memory that survives long campaigns.

Docker images License: MIT Tests

Chronicler is an open-source, self-hosted AI roleplay and character-chat app you run on your own machine with docker compose up. It imports the community v2/v3 character card format (chub.ai-compatible), talks to any OpenAI-compatible LLM — Ollama, OpenAI, Anthropic, OpenRouter, llama.cpp, vLLM, nano-gpt — and remembers what matters about every character across every session, automatically and 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 on a persistent cognitive memory engine (YantrikDB) with a strict three-tier write contract that keeps canon clean and accumulates real continuity over hundreds of hours.

What you get that other clients don't: verified long-term memory instead of a rolling summary · per-memory visible_to ACL so characters can't recall secrets they were never told · a prompt inspector showing exactly what was sent and why · native MCP support (tools, resources, prompts) · an extension SDK on npm.

Looking for a SillyTavern alternative with real memory · an AI companion that remembers across sessions · a local/offline character chat app · long-term memory for LLM roleplay · an MCP-native chat client? That's what this is.


Quick start

Fastest — one file, no clone:

curl -O https://raw.githubusercontent.com/yantrikos/chronicler/main/docker-compose.yml
docker compose up -d
open http://localhost:3001

Docker 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 -d

docker 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.

Published images

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.

First-run in the app

  1. Settings → Your persona — name + optional description
  2. Settings → Providers — add Ollama (local), OpenAI-compat, or Anthropic with a model name
  3. Settings → Extraction provider (optional) — small/fast model for background fact extraction. Use 7B or larger (e.g. qwen2.5:7b). Measured 2026-08-04: at 1.5B the extractor cannot reliably follow fact-attribution rules and will write the user's facts as facts about the character — e.g. storing "Ren's birthday is April 2nd" when April 2nd is yours and the character merely repeated it back. 7B and 9B attribute correctly. A structural filter catches the worst artifacts regardless of model, but it cannot repair a confidently mis-attributed fact.
  4. Settings → Proactive messages — off by default; passive lets the character take initiative when urges accumulate and you've been idle
  5. + card to import a v2/v3 character card, or demo: Ren to try the built-in character
  6. Type. Memories appear in the right sidebar as they land.

First-run flow:

  1. Settings → Your persona — set your user name and (optionally) a short description.
  2. 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.
  3. Settings → extraction provider (optional) — pick a smaller, faster model to run the fact extractor in parallel with generation.
  4. Settings → save.
  5. + card to import a v2/v3 character card (.png or .json), or demo: Ren to try a built-in character.
  6. Type and send. First reply takes a beat while memories seed; subsequent turns stream.

My character never replies

Almost always the model connection, not the app. When a reply fails Chronicler shows a red bar near the top of the chat saying why; the usual causes:

  • "doesn't have " — the model name in Settings isn't installed on that Ollama. Run ollama pull <model> in a terminal, or pick one you have (ollama list). The first-run wizard now checks this for you.

  • "Couldn't reach …" — Chronicler can't connect to the address in Settings. If Chronicler runs in Docker, localhost means the container itself: use http://host.docker.internal:11434 for an Ollama on the same machine. On Linux, also start Ollama with OLLAMA_HOST=0.0.0.0 so it listens beyond loopback (the bundled docker-compose.yml maps host.docker.internal for you; re-pull it if you copied an older one).

  • "rejected the credentials" — the API key in Settings is wrong or expired.

  • The dots spin for a long time, then you get "spent its whole reply budget thinking" (or, on an older version, nothing at all) — a reasoning model (Qwen3 / 3.5 / 3.8, DeepSeek-R1, gpt-oss…) used its whole token budget thinking and wrote no reply. Turn thinking off for that provider: Settings → Providers → "turn thinking off". The switch differs by server, so Chronicler sends the right one:

    Server / how it is added What Chronicler sends when thinking is off Checked
    Ollama (native provider) think: false (gpt-oss ignores false, so it gets "low") yes, against Ollama 0.33 with qwen3.5, qwen3.6, qwen3.8, gpt-oss, qwen2.5
    Ollama through an OpenAI-compatible URL (…:11434/v1) reasoning_effort: "none" — Ollama's /v1 endpoint silently ignores think: false yes
    vLLM, llama.cpp server (CPU / CUDA / Metal / Vulkan builds), LM Studio chat_template_kwargs: {"enable_thinking": false} — choose vLLM / llama.cpp / LM Studio under "how" not checked — no such server was available; it follows those projects' documentation
    Hosted OpenAI-style APIs nothing (they reject unknown fields) —

    Measured on one machine with qwen3.5:4b: thinking left on used all 1,024 tokens and returned no text after ~21 s; with it off the reply arrived in ~1.5 s.

  • A slow machine: replies cut short or "dropped" — Chronicler waits up to 10 minutes for a model to start answering and 3 minutes of silence mid-reply (set CHRONICLER_LLM_TIMEOUT_MS / CHRONICLER_LLM_IDLE_MS on the chronicler service to change them). A smaller model, or thinking off, is usually the real fix. Pressing Stop now also stops the model.

  • A big model feels slow, and the next reply lags — after every reply Chronicler also runs background calls (memory extraction, the scene board, the ledger, the story summary) on the same model unless you assign another. On a small model that is a couple of seconds; on a big one it is most of a minute (measured on an M4 Pro with qwen3.8 27B at ~5 tokens/s: ~35–60 s of background work per turn, versus ~8 s on qwen3.5:4b). Your next message now goes first — background calls wait for it, are cancelled the moment you send, and retried afterwards — but the work itself still takes that long, so Settings → "use a small model for background tasks" (shown when your chat model is large) assigns a small fast one. It needs enough memory to keep both models loaded; if Ollama has to swap them each swap costs a few seconds. Also, generation speed is the model's own: qwen3.8 27B ran ~5 tok/s here against ~37–58 tok/s for the 4B/9B.

  • Reply cut off mid-sentence, a background task that never finishes, or the model forgets older context — these are limits you can change. Settings → Providers → "sampling & limits (advanced)" sets a model's max reply tokens and, for Ollama, its context window; Settings → Advanced — limits lists everything else with its default and an explanation: the prompt budget (how much memory and scene text goes into each prompt), how many tokens each background task may write (fact extraction, scene board, ledger, story summary, recap, audits…), how long to wait for a model's first word and for a silent stream, and the background scheduler. Leave a field empty to use the default; out-of-range values are corrected for you. Keep prompt budget + reply length below the model's context window — Settings warns when they don't fit.

  • Not sure which models you have — in Settings → Providers press load models from this server (Ollama, vLLM, llama.cpp, LM Studio, OpenRouter, OpenAI, Anthropic, Gemini) and click one.

  • You get a strange placeholder like "(stays in character, does not invent anything not in canon)" — you chose "Skip" in the wizard, so the built-in mock provider is answering. Add a real provider in Settings → Providers.


What makes it different

  • 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_to ACL. 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_book entries 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_substrate entries — but only after an LLM verifier passes on each candidate, biased toward rejection. Skills surface back into future prompts when relevant, score +1 / −1 from user reactions (regenerate / edit / delete vs accept and move on), and transition through candidate → active → suppressed → archived based 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, experimental). Skills that hold up over weeks of sessions promote past active into a 5th state, core_trait — always-on identity facets like "Adira is fundamentally guarded with strangers" that inject into every system prompt unconditionally, alongside a periodically-generated first-person self-model ("I am Adira. I'm a wandering musician…"). The Identity inspector makes all of it auditable. Status, stated plainly: the substrate and a cross-model benchmark harness both ship, but the benchmark does not yet demonstrate model-independent character — mean trait adherence was 0.275, and the run had no control arm. An earlier README claimed "σ=0.087 moderate model-independence"; that was computed on a blended score and is retracted. The gap where the core-trait verifier rejected almost every real trait was found and fixed on 2026-09-29 (it accepted 0–2 of 8 test patterns; it now accepts 5–6 of 8 with no false accepts among 26 negatives — see IDENTITY-LAYER.md), but promotion still needs days of accumulated evidence and the layer has not yet been shown end to end on real play. A separate consistency audit (Settings → More → Check character consistency) measures drift from the character card directly. Treat this as research in progress, not a working feature. See the thesis and the corrected results.

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 — substrate + benchmark harness ship; the effect is not yet demonstrated (results, with correction) ❌ ❌ ❌
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 🟡 🟡

Table-stakes RP features

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).

Updating

docker compose pull
docker compose up -d

Pulls the latest published images and restarts. Your memory DB persists in the named volume (chronicler-memory) across restarts.

Stack

  • Frontend: React 19 + TypeScript + Vite + Tailwind v4 + react-markdown
  • Server: tiny Node HTTP proxy — serves the built SPA, routes /api/mcp/* to YantrikDB, routes POST /api/llm to 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/chat with think: false support) + 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).

Architecture

┌─── 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) │
└─────────────┘    └──────────────┘

Repo layout

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

Privacy

  • All traffic binds to 127.0.0.1 by 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_to ACLs and pre-ranking retrieval filters — verified by tests/secret-stays-private.test.ts.

Reporting bugs / getting involved

  • 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.

Develop

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

Non-goals (deferred by design, not oversight)

  • Autonomous character behavior / personality evolution without user consent — see docs/ADR-002 for 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

Related projects

Chronicler is the roleplay client. The memory underneath it is a separate stack you can use on its own:

  • yantrikdb — the cognitive memory engine Chronicler stores canon in: temporal decay, consolidation, contradiction detection. Rust with Python bindings, Apache-2.0.
  • yantrikdb-mcp — the same memory as an MCP server for Claude Code, Cursor and Windsurf (pip install yantrikdb-mcp). This is what Chronicler's docker-compose runs.
  • yantrikdb-server — HTTP gateway and HA cluster, if you want one memory store behind several clients.
  • @chronicler/grimoire — the plugin SDK for extending this app (hooks, slash commands, UI slots, MCP integration).

License

MIT — see LICENSE.

Chronicler talks to YantrikDB over its MCP server (the default in docker-compose.yml). The engine is Apache-2.0 as of 2026-08-18, so both sides of that boundary are permissive and nothing here imposes obligations on your own code or a service you host.


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.

About

Self-hosted AI roleplay client with memory that survives long campaigns — tiered canon/heuristic/reflex writes, retrieval-time privacy ACL for group chats, native MCP tool support.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

15 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages