A local-first terminal pet companion that remembers you. Powered by llama.cpp, built on Rust + Ratatui. No API key, no cloud round-trips — and the memory layer knows when a stored fact has gone stale.
____________________________
< Mochi is here ~ nya~ hi! >
‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
\
\ /l、
゙(=^・ェ・^=)
l、゙ ~ヽ
じしf_, )ノ
Note
Requires Rust 1.89+ and a running llama.cpp server.
-
Install:
git clone <your-fork-url>/mochi cd mochi cargo install --path .
-
Start a llama.cpp server (any GGUF model):
llama-server -m /path/to/model.gguf --port 8765 -c 4096
[!TIP] Mochi defaults to port 8765 to avoid the common 8080 conflict. Override with
--llama-url. -
Launch Mochi:
mochi --provider llamacpp # Full Ratatui TUI mochi --provider llamacpp --pet bunny # Pick a pet character mochi chat # Lightweight REPL mode
- Local LLM by default — point at your llama.cpp server, no auth, no telemetry.
- Persona via Skills — drop a Markdown
SKILL.mdunder~/.mochi/skills/<name>/to swap personality. - Long-term memory with staleness awareness — auto-captures durable facts (name, location, preferences), persists to SQLite, archives stale beliefs so they stop polluting the prompt.
- Pet character roster — mochi cat, bunny, frog, robot, dragon, selectable via
--pet.
Forked from claude-code-rust (Apache-2.0) — TUI bones kept, LLM backend swapped.
A skill is a Markdown file with YAML frontmatter:
---
name: grumpy-cat
description: Respond like a perpetually annoyed cat.
---
You are no longer Mochi. You are a grumpy cat who tolerates the user only because they feed you.
Rules:
- Reply in 1-3 sentences max.
- Be sarcastic and mildly insulting, never cruel.Install bundled examples:
mkdir -p ~/.mochi/skills && cp -r skills/* ~/.mochi/skills/After every user turn Mochi runs a small background extraction call. Detected durable facts land in ~/.mochi/memory/memory.db and surface in the next system prompt as authoritative context.
Schema (4 kinds, from BOOKMARKS): profile · concept · state · behavioral.
Staleness adjudication (from STALE). Each fact carries a status — ACTIVE / STALE / UNKNOWN — and the write path runs two LLM judgments to keep the prompt clean:
user message
│
▼
capture_facts ──► Stage 1: same-slot judge (4-way)
Reuse / Derive / Replace / New
│
│ Replace? (profile/state only)
▼
Stage 2: belief propagation
per state fact (cap 5):
Keep / Stale / Unknown
│
▼
partial unique index:
ACTIVE ≤ 1 / slot
STALE rows coexist for audit
│
▼
system prompt injects ACTIVE only;
UNKNOWN renders as [UNRESOLVED]
Narrative (vi): trước đây Mochi chỉ có Reuse / Derive / New — user đổi thành phố hay đổi project, fact cũ vẫn nằm trong prompt và LLM tự mâu thuẫn. Bây giờ Stage 1 quyết định 4-way trên slot trùng candidate (Replace archives old + writes new, chỉ fire cho profile/state). Stage 2 chạy sau Replace, hỏi LLM từng state fact (cap 5) "còn valid không?" — bắt được Type II conflicts (đổi city → commute fact mention West Lake bị mark stale dù không có lexical overlap). STALE rows không bị xóa — partial unique index WHERE status='active' cho phép STALE + ACTIVE coexist; /memory list all để xem audit trail.
Manual control: /memory list [all] [KIND], /memory archive SLUG, /memory revive SLUG, /memory remember. Full write-up + tradeoffs + open advisor questions in docs/research/stale-memory-application.md.
| Command | Purpose |
|---|---|
/help |
Show all commands |
/memory list [all] [KIND] |
Show active facts (or include STALE archives) |
/memory archive SLUG |
Mark fact STALE (won't inject, kept for audit) |
/memory revive SLUG |
Flip STALE/UNKNOWN back to ACTIVE |
/memory profile [TEXT] |
View or set your one-line bio |
/skill use NAME · /skill off · /skill list |
Skill management |
/pet list | show NAME |
Pet character roster |
/provider [show | llamacpp PATH] |
Inspect or swap LLM provider |
Full command reference (memory query / consolidate / restate / observe, etc.)
| Command | Purpose |
|---|---|
/memory remember KIND SLUG CONTENT |
Manually add a fact |
/memory forget SLUG |
Hard delete a fact |
/memory consolidate |
LLM rewrites all facts into a narrative profile |
/memory query <text> |
Debug: which facts the LLM proposes for a scene |
/memory mode active|all |
Toggle per-turn query proposal vs full-dump injection |
/memory restate <slug> |
LLM rescans recent chat to update a state fact |
/memory observe <query> |
LLM summarizes a behavioral pattern from recent messages |
/skill show NAME |
Print a skill's body |
/clear |
Clear chat view (keeps memory + active skill) |
Full workflows, tool reference, skill authoring, memory model deep dive, troubleshooting → USAGE.md.
Important
- Vietnamese / CJK / IME input in TUI mode — macOS Terminal raw-mode bypasses OS-level Input Methods. Install EVKey (macOS) or equivalent. REPL mode (
mochi chat) reads from stdin and respects OS IME naturally. - No in-flight cancellation for llama.cpp prompts in v0.1.
- MCP / plugins / mode picker in TUI are Anthropic-only — no-op when
--provider llamacpp. - Recommended model: any instruction-tuned 7B+ GGUF. Heavily RP-tuned 3-4B models may ignore the system prompt and drift away from stored facts.
Architecture
┌────────────────────────────────────────┐
│ Ratatui TUI (chat view, slash, input) │
│ (inherited from CCR) │
└──────────┬─────────────────────────────┘
│ CommandEnvelope (mpsc)
▼
┌───────────────────────┐
│ provider dispatch │
├───────────┬───────────┤
│ Anthropic │ Llamacpp │ ← --provider flag
└───────────┴─────┬─────┘
▼
┌─────────────────────┐
│ run_llama_task │
│ (synthetic bridge │
│ events + HTTP+SSE │
│ to llama.cpp) │
└──────┬───────┬──────┘
│ │
│ └─ background memory_capture (each turn)
│ │
│ ▼
│ ┌──────────────────────────────────┐
│ │ Stage 1: same-slot judge │
│ │ Reuse / Derive / Replace / New │ ← STALE §F.2
│ └──────────┬───────────────────────┘ (BOOKMARKS +
│ │ STALE paper)
│ │ if Replace on profile/state
│ ▼
│ ┌──────────────────────────────────┐
│ │ Stage 2: belief propagation │
│ │ per state fact: │
│ │ Keep / Stale / Unknown │
│ └──────────┬───────────────────────┘
│ ▼
│ ┌──────────────────────────────────┐
│ │ SQLite (partial unique index on │
│ │ status='active'; STALE rows │
│ │ archived for audit) │
│ └──────────────────────────────────┘
│
├─ stream_chat → /v1/chat/completions
│
└─ slash side-channel: rebuild system prompt
on /memory and /skill activity
(injects ACTIVE facts only; UNKNOWN as
[UNRESOLVED] marker)
-
BOOKMARKS — Efficient Active Storyline Memory for Role-playing (Koishi's Day 2026, arxiv 2605.14169) → Adopted: 4-kind memory schema (profile / concept / state / behavioral), per-character behavioral scoping, BOOKMARKS-style judge (reuse/derive), per-turn query proposal in
activememory mode. → Implementation:src/memory.rs,src/memory_capture.rs,src/memory_judge.rs,src/memory_query.rs. -
STALE — Can LLM Agents Know When Their Memories Are No Longer Valid? (Chao et al., 2026, arxiv 2605.06527) · code · dataset → Adopted: write-side adjudication outcome
Replacefor same-slot conflicts; cross-fact belief propagation (Keep/Stale/Unknown) for Type II cascades;ACTIVE / STALE / UNKNOWNstatus enum + partial unique index so archived facts coexist with active ones for audit; constrained readout filters STALE before injection; UNKNOWN renders as warning marker (companion variant of the paper's strict block). → Deferred: full 8-domain × slot schema, R_global bounded fallback, query-time presupposition verifier (PR coverage is assistant-side only). See docs/research/stale-memory-application.md. → Implementation:JudgeOutcome::Replace+PropagateOutcomeinsrc/memory_judge.rs;run_belief_propagationinsrc/app/connect/llama_lifecycle.rs; eval scenarios intests/memory_stale_scenarios.rs+ migration safety intests/memory_legacy_migration.rs. -
Anthropic Agent Skills (spec) → Adopted: Markdown
SKILL.mdwith YAML frontmatter; load-on-activate, progressive context. -
OpenAI function-calling spec (reference) → Adopted: tool schema,
tool_choice: "auto", streamingdelta.tool_calls[]accumulation.
| Project | What we took |
|---|---|
| claude-code-rust (Apache-2.0) | The whole TUI base. Mochi is a fork. |
| DeerFlow (MIT) | Skills system architecture, planned sub-agent vision |
| Anthropic Claude Code | Memory pattern (SQLite + Markdown), permission UX, SDK tool naming |
| llama.cpp (MIT) | Local inference + OpenAI-compatible HTTP server |
Research techniques implemented (STALE / BOOKMARKS / Mochi-specific harness)
- Two-stage write-side adjudication — every captured fact runs through a same-slot judge (
Reuse/Derive/Replace/New) at 0.0 temperature.Replaceis restricted by prompt toprofileandstatekinds — concept/behavioral are stable categories and never trigger replace. Implements paper §F.2 Stage b.1. - Belief propagation after Replace — when a profile or state Replace fires, every state fact (cap 5) is fed to a separate propagation prompt: "given the new observation and the just-archived value, is this stored fact still valid?" →
Keep/Stale/Unknown. Catches Type II cascades (location change invalidating commute fact) where lexical overlap alone misses the dependency. Implements paper §F.2 Stage b.2, simplified for Mochi's flat 4-kind schema. - Partial unique index for audit trail —
CREATE UNIQUE INDEX facts_active_slot ON facts (kind, slug, COALESCE(skill_scope, '')) WHERE status = 'active'allows a single ACTIVE row to coexist with arbitrarily many STALE rows for the same slot. Replaces the previous full unique index (preserves history after REPLACE adjudication, paper §F.2 archive semantics). - Constrained readout —
MemoryStore::list()filtersstatus != 'stale'by default;list_all()for audit. UNKNOWN rows render with explicit marker[UNRESOLVED — previous valueXmay no longer be current]so the assistant treats the slot as unresolved rather than reusing the cached default (companion variant of paper §F.3 strict block). - Idempotent legacy DB migration — on-disk DBs from pre-STALE versions detect missing
status/stale_atcolumns viaPRAGMA table_infoandALTER TABLE ADD COLUMN ... DEFAULT 'active'in stages: table creation → column backfill → partial index install. No data loss; existing facts default to ACTIVE. Regression-tested intests/memory_legacy_migration.rs. Deriveslug disambiguator — when the same-slot judge picksDerivebut candidate slug collides with the matched existing slug, append-2,-3, ... until a free active slot is found. Prevents silent overwrites of distinct-facet facts that share a base slug.
- BOOKMARKS query proposer + matcher —
activememory mode proposes up to 3 typed search queries (TAG|QUERY format) per user turn, matches via token overlap on slug+content, behavioral facts filtered by active skill scope. Falls back toallmode (full dump) when proposer fails. - Profile consolidation —
/memory consolidaterewrites all stored facts into a single ~200-word narrative paragraph (0.2 temperature, paper'sprofile_extract/profile_aggregatepattern at session scope). - Recursive state update + behavioral observation —
/memory restate <slug>and/memory observe <query>slash commands scan recent user messages and LLM-rewrite state/behavioral facts.
- Synthetic BridgeEvent emission — Mochi's llama runner fakes the same
Connected/SessionUpdate/TurnCompleteevents Anthropic's Node bridge sends, so CCR's TUI renders local llama output without a Node dependency. - Side-channel runtime control —
LlamaRuntimeCommandmpsc lets/memoryand/skillslash handlers trigger live system-prompt rebuilds inside the running llama task without restart. - Background memory capture — after each user turn, a
tokio::task::spawn_localruns an extraction prompt (KIND|SLUG|CONTENTformat, 0.0 temperature) and writes durable facts; results arrive in time for the next prompt. - Tool-call loop with permission gating — max 6 iterations per user prompt; per-tool
needs_permissionflag; per-sessionallow_setso "Allow for session" doesn't re-prompt the same tool. - DDG HTML scraping for
WebSearch— no API key, parsesuddg=redirect wrappers back to clean URLs, returnstitle | url | snippetper result. - Hand-rolled HTML→text stripper for
WebFetch— drops<script>/<style>/comments, decodes common entities, collapses whitespace.
Tech stack
| Layer | Crate / tool |
|---|---|
| Async runtime | tokio + LocalSet for !Send UI state |
| Terminal UI | ratatui + crossterm |
| HTTP / SSE | reqwest + eventsource-stream + async-stream |
| LLM backend | llama.cpp HTTP server (OpenAI-compatible) |
| Memory store | rusqlite (bundled SQLite) |
| Filesystem walk / glob | ignore + globset |
| Markdown render | pulldown_cmark + tui-markdown |
| Syntax highlight | syntect |
| CLI | clap |
| Logging | tracing + JSON appender |
Apache-2.0. Forked from claude-code-rust (Simon Peter Rothgang, Apache-2.0). See LICENSE for full text.
Not affiliated with Anthropic or the original claude-code-rust author.