- Save servers_config.json atomically via temp file + fsync + rename
- Keep .bak backup and auto-restore when main config is empty/corrupt
- Add /servers/recover endpoint for manual recovery
- Auto-recover basic server configs on startup when config is empty but bot is in guilds
Previously, when a user replied to Miku's message via Discord's reply
feature, Miku's quoted words were embedded directly into the user's
message text using the format:
[Replying to your message: "Miku's words"] User's response
This caused two problems:
1. The LLM had to parse "your message" to determine the quoted text
was MIKU's words — fragile and frequently misattributed
2. When stored in episodic memory as [User]: ..., Miku's quoted words
were permanently mislabeled under the user's speaker prefix
Now reply context flows through as structured metadata:
- bot/bot.py captures the replied-to text WITHOUT embedding it in prompt
- cat_client.py passes it as discord_reply_context in the WebSocket payload
- discord_bridge.py injects it as agent_input['reply_context'] — a
CLEARLY LABELED note: [The user is replying to what you (Miku) said — ...]
- miku_personality.py + evil_miku_personality.py render it via
{reply_context} placeholder in the prompt suffix, between memory
context and conversation history
This keeps Miku's words as a separate context note, never mixed into
the user's HumanMessage. Episodic memory only stores the user's actual
words. The fallback path (when Cat is unavailable) also uses a cleaner
format with explicit speaker labels.
- bot/Dockerfile: Add ffmpeg to reinstall line after apt-get autoremove
(autoremove was sweeping up ffmpeg as 'no longer needed' after playwright install)
- bot/utils/image_handling.py: Increase video analysis timeout 120s→300s, 6→3 for Tenor GIFs (GTX 1660 VRAM constraint)
- bot/utils/activities.py: Add _activity_changed_at timestamp tracking,
get_current_activity_label() and get_current_activity_fresh() with 30-min decay
- bot/utils/cat_client.py: Pass current Discord activity to Cheshire Cat pipeline
- bot/utils/llm.py: Inject current Discord activity into system prompt
- cat-plugins/*: Forward Discord activity through working_memory to personality plugins
- bot/persona/*/preamble.txt: Add Discord status usage guidelines for character prompts
- llama-swap-rocm-config.yaml: Add qwen3.5 model entry for ComfyUI prompt generation
- AGENTS.md: New project documentation file
- Three dropdowns for Regular Miku, Evil Miku, Japanese Mode models
- GPU availability badges (Both GPUs / NVIDIA Only / AMD Only)
- Refresh Models + Refresh Status buttons
- Load models on tab switch with defensive checks
- Bump cache-busting version for all JS files
- Remove redundant Current Status section
- Add models.text, models.evil, models.japanese to config/set globals sync
- Fix language toggle log to show actual model name instead of hardcoded string
- Add models.text, models.evil, models.japanese to restore_runtime_settings
- Add model keys to reset_to_defaults with CONFIG defaults
- Include model info in runtime_state for API visibility
- switch_to_evil_personality now reads EVIL_TEXT_MODEL from globals
- switch_to_normal_personality now reads TEXT_MODEL from globals
- Removes desync risk when user changes models via Web UI
- GET /models/available: query both llama-swap instances for model lists
- POST /models/select: set per-persona model (regular/evil/japanese) with persistence
- GET /models/status: return current per-persona model assignments
- Fall back to known model list when containers are unreachable
- Disable per-server mood controls when Evil Miku is active
- Show explanatory notice for disabled server mood dropdowns
- Populate global mood dropdown with evil moods when Evil Mode is on
- Fix initialization race condition by awaiting evil mode status first
- Add CSS styles for disabled mood controls
Preamble:
- Sentence limit 1-3 → 2-4 (revert to original 'sting, then land' range)
- Remove 'if you can say it in one, say it in one' (encouraged lazy dismissals)
- Add engagement rule: 'Always engage with what was said — acknowledge the
question or statement, then twist the knife. Ignoring isn\'t sharp, it\'s lazy.'
Suffix:
- Remove '[Keep responses short and cutting — 1-3 sentences. No monologues.]'
The suffix was the LAST thing the model processed, so its brevity hammer
overpowered the preamble's engagement instruction. Preamble alone is enough.
Five targeted fixes:
1. discord_bridge (priority 100): Skip 'cheerful virtual idol' wrapper and
'CRITICAL INSTRUCTION' about facts when evil_mode is active. Evil Miku
gets her own prompt from evil_miku_personality plugin.
2. memory_consolidation (priority 10): Soften fact-usage pressure:
'Use THESE facts when answering' → 'You may reference these facts if
relevant to the conversation'. Also soften username command tone.
3. evil_miku_personality (priority 100→101): Bump above discord_bridge
so Evil Miku's prefix replacement deterministically discards any
Miku-mode wrappers regardless of plugin load order.
4. evil preamble: Restructure for brevity — add 'Be SHORT and SHARP'
declaration, move RESPONSE RULES before mood, tighten sentence limit
from 2-4 to 1-3 with 'if you can say it in one, say it in one.'
5. evil suffix: Add final brevity reminder '[Keep responses short and
cutting — 1-3 sentences. No monologues.]' right before conversation
for maximum recency influence.
Cat's WebSocket handler returns HTTP 500 for user IDs without the
'discord_' prefix. The consolidation WS was using 'system_consolidation'
which failed immediately with WSServerHandshakeError (500). Changed to
'discord_consolidation' which connects successfully.
Three fixes for consolidation reliability:
1. Fire-and-forget API: POST /memory/consolidate now launches consolidation
as an asyncio background task and returns immediately. The old approach
blocked until Cat's WS response, which could take 5+ minutes (LLM
extraction calls), exceeding both the WS timeout and browser fetch
timeout. Web UI now polls /memory/status to track completion.
2. Increased timeout: cat_client.trigger_consolidation() timeout raised
from 300s to 600s (configurable via parameter). Logs unexpected WS
message types for debugging.
3. Better logging: Consolidation log messages prefixed with 🌙 for
grep-friendliness. cat_client errors include exc_info=True for
traceback visibility. Web UI shows elapsed time while polling.
Step 4 of memory system overhaul: single source of truth for prompts.
Problem: The system prompt was defined inline in 4 different places:
miku_personality.py, evil_miku_personality.py, llm.py, discord_bridge.py.
These could drift out of sync — and the discord_bridge WebUI
reconstruction was already missing CRITICAL RULES, CHARACTER CONTEXT,
MOOD GUIDELINES, and RESPONSE RULES sections.
Fix:
- Create persona/miku/preamble.txt — canonical normal Miku preamble
- Create persona/evil/preamble.txt — canonical evil Miku preamble
(with {mood_name} and {mood_description} format placeholders)
- All 5 consumers now read from these files:
* miku_personality.py (Cat plugin, primary path)
* evil_miku_personality.py (Cat plugin, primary path)
* discord_bridge.py (WebUI 'Last Prompt' reconstruction)
* llm.py (fallback path, normal Miku)
* evil_mode.py get_evil_system_prompt() (fallback path, evil Miku)
- All consumers include graceful fallbacks if preamble files are missing
- Fixed evil_mode.py discrepancy: 'body and size' now matches canonical
The preamble files are Docker volume-mounted into both containers:
bot/persona/ → /app/persona/ (bot, via Dockerfile COPY)
bot/persona/ → /app/cat/data/ (Cat, via docker-compose volume mount)
Editing the preamble file on the host immediately updates the Cat path
(bot path requires rebuild due to COPY).
The new consolidation_scheduler.py uses get_logger('consolidation')
but the component wasn't registered in COMPONENTS dict, causing
on_ready to crash with a ValueError.
Step 2 of memory system overhaul: automated scheduling.
- New consolidation_scheduler.py: run_nightly_consolidation() function that
checks Cat health, triggers consolidation via WebSocket, and tracks
run history with success/failure stats
- bot.py on_ready: register APScheduler cron job (hour=4, minute=0)
alongside the existing daily DM analysis job
- routes/memory.py: expose consolidation status (last_run, last_result,
last_error, is_running, total_runs, successful_runs) in the
/memory/status API response
- Web UI: show consolidation schedule info (last run time, success/fail,
run counts) below the manual consolidate button, with 'running now'
indicator when active
The 'sleep consolidation' metaphor is now actually automated instead of
being manual-only.
Step 1 of memory system overhaul: persona tagging.
- discord_bridge: tag user messages with 'persona' metadata at storage time
- memory_consolidation: tag Miku's own responses with 'persona' metadata
- memory_consolidation: tag declarative facts with source persona during extraction
- memory_consolidation: pass persona context to LLM extraction prompt
- memory_consolidation: annotate cross-persona facts in prompt injection
(e.g., '(learned as Evil Miku)' when Evil facts appear for Normal Miku)
- Web UI: show persona badge (🎤 Miku / 😈 Evil Miku) on facts and episodic
memories in the Memory Management tab
This lets both personas know which version of Miku each memory came from,
enabling Evil Miku to distinguish her own memories from Normal Miku's.
- Use stable section IDs (without Date.now()) so collapse state can be
tracked across re-renders
- Snapshot collapsed state before innerHTML replacement, restore after
- Prevents the 10s polling from expanding all subsections every time
- Added ?v=20260502 query param to all <script src=...> and <link> tags
- Added Cache-Control: no-cache, no-store, must-revalidate to index route
- Added <meta> cache-control tags in HTML head for extra coverage
- This ensures the browser always fetches fresh HTML/JS/CSS after deploy,
preventing the old loadLastPrompt() from running against new HTML
(which would crash since #prompt-cat-info no longer exists)
The empty #prompt-display div collapsed to 0 height, making it appear
'gone'. Added min-height: 3rem and a 'No prompt selected.' placeholder
that clearPromptDisplay() now sets via innerHTML.
The renderPromptEntry() function sets innerHTML on #prompt-display, which
was wiping out the child <pre id="last-prompt"> element. This caused
copyPromptToClipboard() to fail silently and the display to appear empty.
Fix: keep <pre> as a hidden sibling outside #prompt-display, used only as
a text buffer for the copy function.
- globals.py: add collections.deque(maxlen=10) PROMPT_HISTORY with _prompt_id_counter
- globals.py: add legacy accessor functions _get_last_fallback_prompt() and _get_last_cat_interaction()
- bot.py: append to PROMPT_HISTORY instead of setting LAST_CAT_INTERACTION, remove 500-char truncation, add guild/channel/model fields
- image_handling.py: same pattern for Cat media responses
- llm.py: append fallback prompts to PROMPT_HISTORY with response filled after LLM reply
- routes/core.py: new GET /prompts and GET /prompts/{id} endpoints, legacy /prompt and /prompt/cat use accessor functions
- Question override now decays after 6 turns: after turn 6, the LLM's own
[CONTINUE] signal is respected even when questions are asked. This prevents
infinite question-ping-pong where both personas keep asking questions.
- _parse_response now accepts turn_count parameter; generate_response_with_continuation
and handle_dialogue_turn pass it through.
- Rewrote Evil Miku's conversation-mode overlay with explicit CRITICAL RULES:
ANSWER questions, engage with what she says, ask questions too, don't just
repeat dismissive one-liners. The old overlay said 'be playful-cruel' but
didn't actually tell her to participate in the conversation.
The snippet loader used wrong file paths (/app/cat/data/ instead of persona/)
causing 'Loaded 0 personality snippets' for both personas. Since the previous
commit now injects full system prompts (get_miku_system_prompt_compact and
get_evil_system_prompt) into every argument exchange, the snippet system is
redundant — all lore/lyrics/personality are already provided by the system prompts.
Created get_miku_system_prompt() and get_miku_system_prompt_compact() in
context_manager.py — mirrors get_evil_system_prompt() so both personas have
equally rich prompts with lore, lyrics, mood integration, and personality.
Previously only Evil Miku had a proper system prompt function. Regular Miku's
arguments and dialogues used a bare-bones hardcoded prompt with no lore/lyrics
— making arguments feel flat compared to normal conversation.
Changes:
- context_manager.py: added get_miku_system_prompt() (full) and
get_miku_system_prompt_compact() (lore+personality, no lyrics for tokens)
- bipolar_mode.py: both argument prompt functions now accept system_prompt
param; run_argument() builds miku_system and evil_system once and passes
them to every exchange
- persona_dialogue.py: dialogue prompts now use get_miku_system_prompt_compact()
instead of hardcoded stub, matching Evil Miku's full prompt approach
- Removed redundant hardcoded personality text from argument prompts since
the system prompts now provide it
The topic was only being injected into the initial breakthrough message via
get_argument_start_prompt(). After that, every subsequent exchange called
get_miku_argument_prompt() / get_evil_argument_prompt() which had no concept
of the topic — so both personas forgot what they were arguing about after the
first exchange and reverted to generic identity-crisis arguments.
Fix: added argument_topic parameter to both persona prompt functions and inject
it as a bold ARGUMENT THEME reminder in every single exchange. The topic block
explicitly tells the LLM to stay on-topic and not drift into generic territory.
- Removed separate 'topic' field from BipolarTriggerRequest model
- Removed topic parameter from force_trigger_argument, force_trigger_argument_from_message_id, and run_argument
- trigger_context now doubles as the argument theme: if provided by user, it becomes the topic;
if blank, a random topic is selected from the rotation pool
- Web UI: replaced two confusing fields (Context + Topic) with one clear field labeled
'What should they argue about? (optional)' with a plain-English description
- JS: removed topic field reference, context.trim() ensures empty strings aren't sent
- Added optional 'topic' field to BipolarTriggerRequest model
- Added topic parameter to force_trigger_argument and force_trigger_argument_from_message_id
- Updated run_argument to accept optional custom topic (None=random, ''=no topic, str=custom)
- Added topic input field to Web UI trigger-argument section
- Updated JS to send topic in API request body
- Custom topics bypass the random rotation system, allowing manual theme control
- Added mood-specific argument behavioral guidance: 9 moods for Evil Miku, 9 for Miku
Each mood changes argument style (e.g. cunning=chess moves, manic=chaotic, bubbly=playful deflections)
- Added personality snippet injection from Cat plugin lore/lyrics data files
40% chance per prompt to include a random lore/lyric snippet for unique material
- Added parting shot feature: 20% chance the LOSER gets a bitter final line before the winner's victory
Adds dramatic tension and prevents clean-win monotony
- Mood guidance and personality flavor injected into both argument prompts
- Changed cooldown from global (ALL channels blocked) to per-channel dict keyed by channel_id
- Added conversation streak tracker: 3 near-miss interjection scores in a row force a dialogue trigger
- Expanded topic relevance keywords: added enthusiasm/vulnerability for Evil Miku, provocation/dismissal for Miku
- Lowered keyword divisor from /3.0 to /2.0 for higher base trigger scores
- Tension rebalance: added natural decay (-0.03/turn), reduced escalation weight (0.08->0.05), increased de-escalation weight (0.06->0.08)
- Reduced momentum multiplier (1.2->1.1) and intensity multiplier (1.3->1.2)
- Added spike cooldown: if last turn tension delta >0.15, next delta halved (prevents runaway spirals)
- Added user-message interjection check in bot.py on_message() (was only checking bot's own messages)
- Added random 15% argument trigger roll on user messages in normal message flow (was only from autonomous.py)
Twitter changed the JS bundle structure from the old single-map format
(e=>e+"."+{...}[e]+"a.js") to a new two-map format
(u.u=e=>""+(({name})[e]||e)+"."+({hash})[e]+"a.js"), breaking
x-client-transaction-id generation.
This caused IndexError: list index out of range, which twscrape
interpreted as an account timeout (15-min lockout), preventing Miku
from fetching/sharing tweets.
The fix adds:
- A robust multi-pattern parser that tries known formats in order
- The _js_obj_to_dict helper from PR #303 for handling unquoted numeric
keys and scientific notation in JS object literals
- Debug logging to capture the JS snippet when ALL patterns fail,
making future breakage easier to diagnose
References:
- https://github.com/vladkens/twscrape/issues/302
- https://github.com/vladkens/twscrape/pull/303
This commit completes a major refactoring of the Miku control panel from a single 7,191-line monolithic HTML file to a modern modular architecture:
CHANGES:
- Extracted 872 lines of CSS into css/style.css
- Created 10 specialized JavaScript modules (4,964 lines total):
* core.js: Global state, utilities, initialization, polling system
* servers.js: Server management and mood handling
* modes.js: Evil mode, GPU selection, bipolar mode, scoreboard
* actions.js: Autonomous/manual actions, custom prompts, reactions
* image-gen.js: Image generation system
* status.js: Status display and statistics
* dm.js: DM user management and conversation analysis
* chat.js: LLM chat interface with streaming and voice calls
* memories.js: Cheshire Cat memory integration (episodic/declarative/procedural)
* profile.js: Profile picture, album gallery, activities editor
- Cleaned index.html to 1,351 lines (structure only, zero inline JS/CSS)
- Removed 12 duplicate variable declarations
- Maintained strict script load order for dependency resolution
- Added backup comment to index.html.bak for historical reference
VERIFICATION COMPLETED:
✓ All 191 functions/variables from original accounted for
✓ Cross-referenced with backup to ensure nothing lost
✓ All onclick handlers and modal systems validated
✓ No circular dependencies or broken references
✓ HTML structure integrity verified (11 tabs, all buttons/modals intact)
✓ CropperJS CDN links preserved
The refactored code is production-ready with improved maintainability and clear separation of concerns.
Critical fixes:
- Add threading.Lock for all shared mutable state (override, cache, current activity)
- Atomic YAML writes (temp file + os.replace) to prevent corruption on crash
- Deep-copy cache on reads to prevent callers from mutating shared state
High-severity fixes:
- Validate entries in pick_activity_for_mood() — skip/log malformed instead of KeyError
- Log warning on unrecognized activity type fallback
- Normalize empty-string state to None (avoid 'None' display)
- release_manual_override() now uses force=True so bot always shows activity
- Add try/except in release_manual_override() to handle failures gracefully
Medium fixes:
- Remove dead 'test' mood from activities.yaml
- Validate name length (128 char Discord limit) in CRUD and manual set
- Validate streaming entries have URL in CRUD path
- Add JSON parse error handling in API routes
- on_ready preserves active manual override instead of overwriting
- Log override expiry timestamp (HH:MM:SS) for easier debugging
- exc_info=True on presence update errors for full stack traces
Low fixes:
- JS activitySetFromEntry() shows notification on parse error
Each activity in the mood lists now has a 🎯 Set button that immediately
sets it as the bot's current Discord activity (30-min manual override),
so users can pick from existing entries instead of typing manually.
- Rewrite utils/activities.py with mood energy-driven activity probability
(high-energy moods like excited/bubbly show activity ~80-85% of the time,
low-energy moods like sleepy/melancholy only ~15-25%)
- Add manual override system with 30-min auto-expiry for Web UI control
- Support all 5 Discord activity types: listening, playing, watching,
competing, streaming (with purple LIVE badge via discord.Streaming)
- Add current activity tracking (get_current_activity)
- Add force=True param to update_bot_presence for on_ready (bot.py)
- Add 4 new API routes for manual override:
GET/POST/DELETE /activities/current, POST /activities/current/auto
- Expand activities.yaml from 139 to 157 entries, adding watching,
competing, and streaming entries across 11 moods
- Update Web UI: activity type dropdown with all 5 types, conditional
URL field for streaming, 'Current Activity' override panel with
set/clear/auto controls, type-aware icons and labels
- Add 'state' field to all 139 activity entries in activities.yaml
- Songs: state shows artist (e.g. 'by kz (livetune)')
- Games: state shows genre (e.g. 'Rhythm Game', 'Sandbox', 'FPS')
- Update pick_activity_for_mood() to return 3-tuple (type, name, state)
- Update update_bot_presence() to pass state to discord.Activity()
- Add state validation in set_activities_for_mood() (optional string)
- Update Web UI editor: view shows state, edit form has state input
- State is fully optional — backward compatible, no breaking changes
The 'state' field appears as a secondary text line in Discord profile
popup, the richest display possible for bot accounts (full Rich Presence
with cover art/buttons is server-side restricted to OAuth applications).
Collapsible section in the Status tab with:
- Normal and Evil mood sections, each collapsible
- Per-mood expandable rows showing songs (🎵) and games (🎮)
- Inline editing: change type, name, weight
- Add/remove entries per mood
- Save via API with client-side validation
- Reload from disk button
- Lazy-loads data only when section is expanded