Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

240 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Rokabot

Rokabot

A server-wide Discord character chatbot embodying Maniwa Roka from Senren*Banka.
In-character conversation, useful tools, and small games—self-hosted on a Raspberry Pi.

TypeScript Node.js 24 discord.js v14 Gemini 3.5 Flash Lite Docker Compose Raspberry Pi 5

Table of Contents
  1. Who Is Maniwa Roka?
  2. Features
  3. Architecture
  4. Prompt System
  5. Memory
  6. Expressions & Tones
  7. Tech Stack
  8. Getting Started
  9. Configuration
  10. Deployment & Operations
  11. Documentation
  12. Privacy
  13. License

Who is Maniwa Roka and Rokabot?

Roka serving drinks

Maniwa Roka (馬庭 芦花) is a warm, gently teasing onee-san side character from Senren*Banka (千恋*万花). Rokabot brings her observant, affectionate energy to a Discord server through in-character conversation.

Roka in casual outfit

She can chat with a server, remember useful context within its own guild boundary, help with everyday requests, and make downtime more playful.

Yuzucook


Features

  • Conversation & Perception: /chat, mentions, replies, and supported name-keyword triggers; image-aware conversations and recent channel context.
  • Memory: Passive context monitoring and claims-based memory, isolated per guild and surfaced only through a bounded prompt envelope.
  • Tools: In chat, Roka can roll dice, flip coins, check the time and weather, search the web, discover anime and airing schedules, and manage reminders; a cute footer notes the little ritual she performed.
  • Stats: Fun server analytics with a mood ring, charts, and memory counts across 7D, 30D, and 90D views.
  • Games: Buddy Pets, Hangman, and Shiritori, with SQLite-backed progress and leaderboards.
  • Interaction & UX: Rule-based tone detection, expression thumbnails, Components V2 replies, emoji reactions, rate limits, and per-channel concurrency protection.
Command or Capability Use
/chat Talk with Roka; optionally attach an image.
/gacha Hatch, view, pet, inspect stats, browse collection, read the guide, or view the leaderboard.
/hangman Start and play a word-guessing game.
/shiritori Start, join, and score a word-chain game.
/search Search the web for current information.
/anime Search or browse anime, or search or browse airing schedules.
/remind Create, list, and cancel reminders.
/stats Explore four fixed, non-overlapping Last 30 Days views with no window selector: Overview (activity, heatmap, channel histogram); Mood (label, donut); Memory (who she knows best, growth curve); Nerd (latency, reliability, volume, trend).
In-Conversation Memory Recall or save useful user facts within the current guild.

Tool Footer

When Roka uses a tool mid-conversation, her reply ends with a small footer line (e.g. 🌸 cast the fortune dice · divined today's weather • <relative timestamp>, rendered by Discord as "2 minutes ago") noting each tool she invoked, phrased as a little shrine ritual. Up to three labels are shown (…and more beyond that). Every tool below is available implicitly in chat — Roka decides when to call it; some also have an explicit slash command.

Footer Labels & Tool Availability
Tool Footer Label Slash Command Implicit In Chat
roll_dice cast the fortune dice
flip_coin tossed a shrine coin
get_current_time peeked at the temple clock
get_weather divined today's weather
search_web searched the wider world /search
search_anime leafed through anime scrolls /anime
get_anime_schedule checked the airing almanac /anime
set_reminder tied a reminder charm /remind
list_reminders counted her reminder charms /remind
cancel_reminder untied a reminder charm /remind
remember_user pressed a memory flower
recall_user recalled a pressed memory

Passive memory extraction runs automatically in the background through the per-message pipeline, while remember_user is only invoked when someone explicitly asks Roka to remember something.


System Architecture

High-Level Overview

View Diagram
flowchart LR
    Discord[Discord] <--> Gateway[discord.js Gateway]
    Gateway <--> Sessions[Session Manager]
    Sessions <--> Agent[Roka Agent]
    Agent <--> Gemini[Gemini API]
    Agent <--> SQLite[(SQLite)]
    Agent <--> Tools[Tools and Games]
    Gateway --> Metrics[Metrics]
Loading
  • One Node.js service handles Discord events, response generation, and local storage.
  • SQLite is the durable store; the active session window is rehydrated for a live conversation.
  • Tools and games remain available to the agent while its responses stay in character.

Message Pipeline

View Diagram
flowchart LR
    Trigger[Slash command, mention, reply, or name keyword] --> Guards[Rate and concurrency guards]
    Guards --> Session[SQLite-backed session window]
    Session --> Tone[Rule-based tone detection]
    Tone --> Prompt[Layered prompt assembly and bounded memory retrieval]
    Prompt --> ADK[Google ADK]
    ADK <--> Tools[Tools]
    ADK --> Gemini[Gemini]
    Gemini --> Reply[Components V2 reply]
Loading
  • The concurrency guard permits one active response per channel.
  • Tone detection examines recent conversation without an extra model call.
  • Read the technical reference for request contracts, data models, and failure behavior.

Prompt System

View Diagram
flowchart TD
    Recent[Recent messages] --> Detector[Rule-based tone detector]
    Detector --> Tone[Tone variant]
    Core[Core identity] --> Assembly[System prompt]
    Speech[Speech patterns] --> Assembly
    Tone --> Assembly
    Context[Channel context: participants, time of day, and memory claims in a safety envelope] --> Assembly
Loading
  • The four layers are core identity, speech patterns, a tone variant, and channel context.
  • The detector selects from 12 tones using recent messages; it adds no LLM cost.
  • The assembled system prompt is size-capped for change detection, not tuned for a target size. See the technical reference for the deeper contract.

Memory

View Diagram
flowchart LR
    Passive[Passive buffer] --> Gate[Candidate gate]
    Gate --> Queue[extraction_queue]
    Queue --> Scheduler[Per-guild scheduler]
    Scheduler --> Extractor[Extractor]
    Extractor --> Claims[(memory_claim)]
    Claims --> Retriever[Bounded retriever]
    Retriever --> Envelope[Prompt safety envelope]

    subgraph Lifecycle[Claim Lifecycle]
        Candidate[candidate] --> Active[active]
        Active --> Superseded[superseded]
        Active --> Rejected[rejected]
    end

    Extractor --> Candidate
Loading
  • Roka retains useful facts and relationships for the guild where they were observed; memory does not cross servers.
  • Extraction is asynchronous, while retrieval remains bounded before a response is generated.
  • The exported memory graph is browseable in Obsidian; see Browsing Memory in Obsidian.
  • For schema, lifecycle, and retrieval details, see Memory Architecture (Claims).

Expressions & Tones

View Diagram
flowchart LR
    Tone[Detected tone] --> Prompt[Tone prompt variant]
    Tone --> Style[Accent color]
    Tone --> Expression[Expression thumbnail]
    Prompt --> Message[Components V2 reply]
    Style --> Message
    Expression --> Message
Loading

Each detected tone selects a prompt variant, an accent color, and one of its mapped expression thumbnails before Roka's Components V2 reply is built.

Tone Accent Color Expression Pool
playful #FFB3D9 smile, cheerful
sincere #A8D8FF sad, pained, sorrowful
domestic #FFD4B5 content, gentle_smile, relieved
flustered #FFB3B3 flustered, nervous, awkward
curious #B2EBF2 thinking, surprised, blank_stare
annoyed #F8B4B8 exasperated, dissatisfied, dissatisfied_2
tender #E1BEE7 worried, troubled, anxious
confident #C8E6C9 composed, base, explaining
nostalgic #D4A574 melancholy, downcast, somber
mischievous #FFD700 delighted, attentive
sleepy #B0C4DE serene, resigned
competitive #FF6B6B frustrated, dissatisfied_3, uncertain


Tech Stack

Area Technology
Language and Runtime TypeScript (ES2022), Node.js 24
Discord discord.js v14
Agent and Model Google ADK, Gemini 3.5 Flash Lite (gemini-3.5-flash-lite)
Storage SQLite via better-sqlite3
Media and Validation sharp, Zod
Quality Vitest, Biome, Prettier, commitlint
Deployment Docker Compose on Raspberry Pi 5 (ARM64)


Getting Started

Prerequisites

  • Node.js 24.13.0 or newer.
  • A Discord bot token and client ID, with the Message Content privileged intent enabled.
  • A Gemini API key.
  • Docker and Docker Compose for containerized deployment.
  • Optional: a Tavily API key for web search.

Install & Configure

git clone https://github.com/AlaskanTuna/rokabot.git
cd rokabot
npm ci
cp .env.example .env

.env Secrets

Variable Required Purpose
DISCORD_TOKEN Yes Discord bot token.
DISCORD_CLIENT_ID Yes Discord application client ID.
GEMINI_API_KEY Yes Gemini API key for response generation and extraction.
TAVILY_API_KEY No Tavily API key for web search.
DISCORD_TOKEN=your_discord_bot_token
DISCORD_CLIENT_ID=your_discord_client_id
GEMINI_API_KEY=your_gemini_api_key
TAVILY_API_KEY=your_tavily_api_key

Other optional environment values, including GRAPHIFY_GEMINI_API_KEY and ROKABOT_DB_PATH, are documented in .env.example.

Run

# Development
npm run dev

# Production
npm run build
npm start

# Docker
docker compose up -d

Quick checks: npm run lint and npm run format:check. Full test verification is npm test && npm run test:perf; npm run test:perf is a separate performance-evaluation gate. npm run test:live is a third, opt-in gate — it needs GRAPHIFY_GEMINI_API_KEY in .env, spends real Gemini calls (measured: ~60 calls and ~8.5 minutes per run), and is excluded from both npm test and full verification.

npm run test:live benchmarks whether the live model fires a tool on a labelled should-fire/shouldn't-fire dialogue set; it is the pre-ship acceptance gate for prompt and tool-description changes, and its verdict rests on two criteria — an accuracy floor plus zero systematically-wrong cases. The recall_user case set lives at tests/harness/tool-trigger/recall-user.jsonl.

As of 2026-07-29 this gate FAILS, and it is correctly doing so: recall_user over-fires on two labelled should-not-fire cases — N1 (ambient chatter naming no one, 7/9 fired) and N5 (a message addressing Roka herself, 9/9 fired) — while recall holds at a perfect 1.00, so proactive triggering itself is sound and the defect is over-application. The cause is src/agent/prompts/core.ts:65: deleting that line drove both cases to 0/3 while collapsing recall to 0.111, confirming it as the source. Tracked as #39; the fix is separate follow-up work.


Configuration

Secrets belong in .env; tunables belong in config.yml. Environment variables override the values listed below when the loader supports them.

Gemini

View Tunables
YAML Path Env Override Purpose
gemini.model GEMINI_MODEL Live Gemini model ID.
gemini.extractionModel GEMINI_EXTRACTION_MODEL Optional background extraction model; defaults to the live model.
gemini.timeout GEMINI_TIMEOUT Request timeout in milliseconds.
gemini.maxRetries GEMINI_MAX_RETRIES Maximum retries for transient failures.
gemini.maxOutputTokens GEMINI_MAX_OUTPUT_TOKENS Response token safety cap.
gemini.safetyThreshold GEMINI_SAFETY_THRESHOLD Harm block threshold applied to all four Gemini-API-supported categories: OFF, BLOCK_NONE, BLOCK_ONLY_HIGH, BLOCK_MEDIUM_AND_ABOVE, BLOCK_LOW_AND_ABOVE.
gemini.maxLlmCalls Maximum chained tool calls per request.
gemini.liveMaxRetries GEMINI_LIVE_MAX_RETRIES Retry attempts after a failed live response.
gemini.retryRpmFloor GEMINI_RETRY_RPM_FLOOR Minimum remaining RPM required for a live retry.
gemini.extractionRpmFloor GEMINI_EXTRACTION_RPM_FLOOR Minimum remaining RPM required for background extraction.
gemini.extractionMaxRetries GEMINI_EXTRACTION_MAX_RETRIES Retry attempts after a transient extraction failure.
gemini.retryBackoffBaseMs GEMINI_RETRY_BACKOFF_BASE_MS Initial full-jitter retry backoff in milliseconds.
gemini.retryBackoffCapMs GEMINI_RETRY_BACKOFF_CAP_MS Maximum full-jitter retry backoff in milliseconds.
gemini.turnDeadlineMs GEMINI_TURN_DEADLINE_MS Wall-clock budget for the live retry loop; a retry starts only if a full gemini.timeout still fits, and the first attempt is never gated.

Rate Limit, Session, and Discord

View Tunables
YAML Path Env Override Purpose
rateLimit.rpm RATE_LIMIT_RPM Requests per minute cap.
rateLimit.rpd RATE_LIMIT_RPD Requests per day cap.
session.ttl SESSION_TTL_MS Idle session lifetime in milliseconds.
session.windowSize SESSION_WINDOW_SIZE Maximum messages rehydrated into the ADK session.
session.maxRehydrationAge Maximum age of a message rehydrated from SQLite.
session.historyRetentionDays Days before session history is pruned.
discord.maxMessageLength DISCORD_MAX_MESSAGE_LENGTH Character cap for a bot reply.

Memory

View Tunables
YAML Path Env Override Purpose
memory.bufferSize MEMORY_BUFFER_SIZE Passive in-memory buffer size per channel.
memory.contextSize Overheard messages injected into one prompt.
memory.extractionInterval MEMORY_EXTRACTION_INTERVAL Messages between background fact extraction attempts.
memory.extractionGapMs MEMORY_EXTRACTION_GAP_MS Minimum time between extractions.
memory.maxFactsPerUser Legacy stored-fact cap per user.
memory.factRetentionDays Legacy unused-fact retention period.
memory.channelMonitorTtlMs Monitoring lifetime after the latest mention.
memory.claimsBackend MEMORY_CLAIMS_BACKEND Enables typed claims extraction and retrieval.
memory.maxClaimsPerTurn MEMORY_MAX_CLAIMS_PER_TURN Maximum claims included in one response.
memory.retrievalTokenBudget MEMORY_RETRIEVAL_TOKEN_BUDGET Approximate claims-envelope token budget.
memory.recentParticipantLimit MEMORY_RECENT_PARTICIPANT_LIMIT Non-speaker participants considered for retrieval.
memory.speakerMinShare MEMORY_SPEAKER_MIN_SHARE Minimum share of selected claims reserved for the speaker.
memory.maxActiveClaimsPerUser MEMORY_MAX_ACTIVE_CLAIMS_PER_USER Active claim cap per user; pinned claims are exempt.
memory.claimRetentionDays MEMORY_CLAIM_RETENTION_DAYS Retention period for inactive, unpinned claims.
memory.extractionDailyBudgetRatio MEMORY_EXTRACTION_DAILY_BUDGET_RATIO Gemini daily budget share reserved for extraction.
memory.perGuildGapMs MEMORY_PER_GUILD_GAP_MS Minimum time between extraction batches for one guild.
memory.extractionQueueMaxPerGuild MEMORY_EXTRACTION_QUEUE_MAX_PER_GUILD Maximum queued extraction payloads for one guild.
memory.vaultExportDir MEMORY_VAULT_EXPORT_DIR Output directory for read-only Obsidian vault exports.

Metrics, Emoji, Reminders, Games, and Runtime

View Tunables
YAML Path Env Override Purpose
metrics.retentionDays METRICS_RETENTION_DAYS Days before metrics events are pruned.
emoji.probability Chance of a keyword-matched emoji reaction.
emoji.cooldownMs Per-channel cooldown between emoji reactions.
reminders.checkIntervalMs Reminder scheduler polling interval.
reminders.maxPerUser Maximum active reminders per user.
reminders.staleThresholdMs Lateness threshold for dropping a reminder.
games.hangmanLives Wrong guesses allowed in Hangman.
games.hangmanTimeoutMs Hangman inactivity timeout.
games.shiritoriTimeoutMs Shiritori turn inactivity timeout.
games.shinyChance Probability of hatching a shiny Buddy Pet.
statusCycleMs Discord status rotation interval.
timezone TZ IANA timezone for time-of-day features.
logging.level LOG_LEVEL Pino log verbosity.

For defaults and the full behavior behind these settings, see config.yml and the technical reference.


Deployment & Operations

flowchart LR
    Push[Push to main] --> Runner[Self-hosted GitHub Actions runner on Pi]
    Runner --> Pull[Fetch target branch]
    Pull --> Build[docker compose up -d --build]
    Build --> Health[Health check]

    Database[(Pi SQLite DB)] --> Export[npm run export:vault]
    Export --> Vault[data/vault]
    Vault --> Transfer[Copy to your desktop]
    Transfer --> Desktop[Desktop]
    Desktop --> Obsidian[Obsidian]
Loading
  • Docker Compose runs Rokabot on a Raspberry Pi 5 with mem_limit: 512m and restart: unless-stopped.
  • The self-hosted GitHub Actions runner builds and health-checks code changes pushed to main; markdown and docs/** changes are ignored by that workflow.
  • Use the operations runbook for Pi commands, runner setup, troubleshooting, and database operations.

Browsing Memory in Obsidian

  1. On the Pi, run the export against the live SQLite database.
  2. The default destination is data/vault/; set MEMORY_VAULT_EXPORT_DIR to write a different destination.
  3. Treat the result as a read-only static snapshot. Re-run the export whenever you want it refreshed.
  4. Copy it to a desktop machine, then open the copied folder as an Obsidian vault to browse the per-guild memory graph.
cd /path/to/rokabot
npm run export:vault
rsync -av data/vault/ desktop-user@desktop-host:~/Rokabot-vault/

Obsidian belongs on the desktop, not the Pi: it is a graphical desktop application, while the Pi is kept focused on the bot as a headless server.


Documentation


Privacy

Rokabot is self-hosted and stores session history, memory claims, reminders, game data, and metrics in local SQLite. Claims are isolated per guild. Messages used to generate responses and extract memory are sent to the Gemini API. Server operators should disclose passive monitoring in channels where Roka has been mentioned.


License

MIT. 2026.

About

Rokabot is a conversational Discord chatbot with built-in utilities crafted after a character's personality from Senren*Banka, built on Discord.js and Google GenAI SDK.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages