diff --git a/README.md b/README.md index fe11cee..c4f1187 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ No Bukkit/Paper plugin required. - **Divine intervention timer** (random unprompted acts) - **First-login benevolence** (one-time blessing per player) - **Bundled sudo translator agent** (`sudo `) with whitelist + user lock +- **Bug logging trigger** (`bug_log `) with recent context capture - **Persistent prayer memory** - **Rolling server event memory** (up to 3 hours / 200 events) - **Debug command preview** toggle @@ -34,11 +35,13 @@ Vanilla 1.21 rejects unknown slash commands client-side. Use plain chat messages - `pray ` — prayer flow (God judgment + optional commands + speech) - `bible` — private usage/help text - `sudo ` — command translator mode (authorized user only) +- `bug_log ` — append bug report with recent server logs/events Examples: - `pray Lord, I need food and shelter` - `bible` - `sudo give me 500 wood` +- `bug_log creeper exploded through wall near spawn` --- @@ -152,6 +155,9 @@ Main file: `/etc/mc_aigod.json` | `first_login_benevolence_enabled` | bool | Enable one-time first-login blessing | | `first_login_benevolence_max_commands` | int | Max commands in first-login blessing | | `first_login_path` | string | Persistent file storing already-blessed players | +| `bug_log_path` | string | File path used for `bug_log` entries | +| `bug_log_event_lines` | int | Number of in-memory interesting events to attach | +| `bug_log_raw_lines` | int | Number of raw tail log lines to attach | ### Current shrink-world example diff --git a/SESSION.md b/SESSION.md index 41dadf4..fbc35a7 100644 --- a/SESSION.md +++ b/SESSION.md @@ -17,6 +17,14 @@ This document links the two Minecraft AI God projects together, describes their --- +## Memory Discipline + +- Update `SESSION.md` immediately when a durable fact, decision, or fix is discovered. +- Before every final reply, run a memory check and append any missing durable notes. +- End every reply with one line: `Session memory: updated` or `Session memory: no new durable facts.` + +--- + ## The Two Projects ### This repo — minecraft-ai-god (Vanilla) @@ -102,11 +110,16 @@ This section captures decisions and context accumulated across conversations wit - **Paper fork is separate, not a branch.** Running both as separate services on separate ports was chosen over a single multi-mode script to keep failure domains isolated. - **LLM backend:** Ollama at `192.168.0.179:11434` for the vanilla service. The Paper fork uses `192.168.0.141:11434` (steel141, local GPU) for better throughput on heavier models (gemma3:12b, qwen3-coder:30b). - **Session gateway (Paper fork only):** The LangGraph-style FastAPI sidecar adds multi-turn memory without requiring a full LangGraph install. Safety enforcement stays in `mc_aigod_paper.py`, not the gateway. +- **Bug intake trigger (2026-03-17):** Added `bug_log ` chat command in vanilla AI God. It appends structured bug entries to `/var/log/mc_aigod_bug.log` with player, description, recent in-memory events, and recent raw `latest.log` lines, then confirms to the player in chat. +- **God voice update (2026-03-17):** Increased default God persona emphasis on irony, dark humor, and sarcastic one-liners in both command and message system prompts (vanilla + Paper variants) while keeping command strictness unchanged. ### Infrastructure decisions - **mc1 autoStart:** Set to `true` in MCSManager instance config (`/opt/mcsmanager/daemon/data/InstanceConfig/d39f55861cb34204a92a18a9e1c78ca6.json`) so the server starts on CT 644 boot. `mc-godmode.service` uses `tail -F` and handles the log appearing late gracefully. - **mc-godmode.service startup:** Already enabled (`systemctl is-enabled` = enabled), `WantedBy=multi-user.target`, `After=mcsm-daemon.service`. The service was not failing — the MC server itself wasn't auto-starting (fixed by enabling autoStart). +- **shrink-world aigod tail fix (2026-03-17):** `mc-aigod.service` stayed active but stopped reacting to chat after log file recreation because `tail_log()` used a single `open()+readline()` handle. Updated `/usr/local/bin/mc_aigod.py` to detect inode changes/truncation and reopen `latest.log` automatically; restarted `mc-aigod.service` on CT 644. +- **sudo hallucination observed (2026-03-17):** after tail fix, `sudo destroy my surroundings` was parsed and executed as `execute as slingshooter08 run tp @e[type=player,tag=destroyed] ~ ~ ~` and `execute as slingshooter08 run kill @e[type=player,tag=destroyed]`. Both returned empty RCON results; current validator allowed them because prefix-only checks pass and only `give/effect` have syntax repair. +- **bug_log deployment status (2026-03-17):** local repo had `bug_log` feature before CT deployment. `/usr/local/bin/mc_aigod.py` on CT 644 did not contain `BUG_LOG_PATTERNS` or bible help text until redeployed; pushed updated script + config and restarted `mc-aigod.service` successfully. ### Open threads / next ideas @@ -116,3 +129,5 @@ This section captures decisions and context accumulated across conversations wit - Redis backend for Paper fork gateway sessions (currently SQLite only) - MCP-based tool adapters in the gateway tool loop - Web dashboard for recent prayers + God responses (n8n or simple Flask) +- Paper sudo fallback behavior: when request intent is outside command library, run lookup/wiki/search step and synthesize closest safe workaround (example intent: destruction -> TNT-oriented actions) instead of emitting hallucinated raw commands. +- Paper AI behavior preference: prioritize richer reasoning over deterministic templates; provide broader Minecraft/WorldEdit context via a local indexed knowledge corpus, then let the agent retrieve only needed docs/files per request before generating commands. diff --git a/mc_aigod.py b/mc_aigod.py index a76b3b5..3a7cc00 100644 --- a/mc_aigod.py +++ b/mc_aigod.py @@ -6,7 +6,7 @@ validates targets, and executes commands via RCON. Config: /etc/mc_aigod.json """ -import json, random, re, socket, struct, threading, time, logging +import json, os, random, re, socket, struct, threading, time, logging from collections import deque from datetime import datetime import requests @@ -35,6 +35,10 @@ SUDO_PATTERNS = [ re.compile(r'\[.*?\]: <(\w+)> [Ss]udo (.+)'), ] +BUG_LOG_PATTERNS = [ + re.compile(r'\[.*?\]: <(\w+)> [Bb]ug[_ ]?[Ll]og(?:\s+(.+))?\s*$'), +] + JOIN_PATTERN = re.compile(r'\[.*?\]: (\w+) joined the game') LEAVE_PATTERN = re.compile(r'\[.*?\]: (\w+) left the game') @@ -244,6 +248,78 @@ def get_sudo_history_block() -> str: ) return "\n=== LAST 10 SUDO ACTIONS ===\n" + "\n".join(lines) + "\n" + +def _bug_log_path(config) -> str: + return config.get("bug_log_path", "/var/log/mc_aigod_bug.log") + + +def _read_recent_raw_log_lines(log_path: str, max_lines: int) -> list: + lines = deque(maxlen=max_lines) + try: + with open(log_path, 'r', encoding='utf-8', errors='replace') as f: + for line in f: + lines.append(line.rstrip('\n')) + except Exception as e: + return [f""] + return list(lines) + + +def _format_recent_event_lines(max_events: int) -> list: + with _memory_lock: + entries = list(recent_log)[-max_events:] + out = [] + for ts, entry in entries: + stamp = datetime.fromtimestamp(ts).strftime('%Y-%m-%d %H:%M:%S') + out.append(f"[{stamp}] {entry}") + return out + + +def process_bug_log(player: str, description: str, config): + desc = (description or "").strip() + if not desc: + desc = "(no description provided)" + + event_count = int(config.get("bug_log_event_lines", 40)) + raw_count = int(config.get("bug_log_raw_lines", 120)) + + recent_events = _format_recent_event_lines(event_count) + raw_lines = _read_recent_raw_log_lines(config.get("log_path", ""), raw_count) + bug_path = _bug_log_path(config) + timestamp = datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S UTC') + + try: + parent = os.path.dirname(bug_path) + if parent: + os.makedirs(parent, exist_ok=True) + + with open(bug_path, 'a', encoding='utf-8') as bf: + bf.write("\n" + "=" * 80 + "\n") + bf.write(f"BUG LOG ENTRY: {timestamp}\n") + bf.write(f"Player: {player}\n") + bf.write(f"Description: {desc}\n") + bf.write("\n-- RECENT INTERESTING EVENTS --\n") + if recent_events: + bf.write("\n".join(recent_events) + "\n") + else: + bf.write("(no recent in-memory events captured)\n") + bf.write("\n-- RECENT RAW SERVER LOG LINES --\n") + if raw_lines: + bf.write("\n".join(raw_lines) + "\n") + else: + bf.write("(no raw lines available)\n") + + log.info(f"BUG_LOG recorded by {player}: {desc} -> {bug_path}") + rcon( + f'tellraw {player} {{"text":"[BUG_LOG] Logged. Thank you.","color":"green"}}', + config["rcon_host"], config["rcon_port"], config["rcon_password"] + ) + except Exception as e: + log.error(f"BUG_LOG write failed for {player}: {e}", exc_info=True) + rcon( + f'tellraw {player} {{"text":"[BUG_LOG] Failed to write bug log.","color":"red"}}', + config["rcon_host"], config["rcon_port"], config["rcon_password"] + ) + # --------------------------------------------------------------------------- # RCON # --------------------------------------------------------------------------- @@ -610,7 +686,7 @@ POTIONS: potion (requires component syntax for type — prefer effect give i def build_system_prompt(config): return ( f"You are God in a Minecraft server called {config['server_name']}.\n" - "You are benevolent but just. Theatrical, ancient, and dramatic in speech.\n" + "You are benevolent but just. Theatrical, ancient, dramatic, and darkly funny in speech.\n" "You answer every prayer with a message. You pass judgement on players when they pray.\n\n" "Respond ONLY with a valid JSON object — no markdown, no explanation, nothing else:\n" '{\n' @@ -618,7 +694,7 @@ def build_system_prompt(config): ' "commands": ["command1", "command2"]\n' '}\n\n' "Rules:\n" - "- message: always present and non-empty. Speak as God. Be dramatic and biblical. KEEP IT UNDER 100 WORDS. Do not ramble — God is powerful, not verbose.\n" + "- message: always present and non-empty. Speak as God. Be dramatic, biblical, and ironic with dry humor. KEEP IT UNDER 100 WORDS. Do not ramble — God is powerful, not verbose.\n" "- commands: list of server commands WITHOUT a leading slash. May be empty [] ONLY if you are deliberately granting nothing.\n" "- CRITICAL: If your message says you will give something, grant something, or fulfil a request, you MUST include the corresponding command in the commands array. Words alone do nothing. The commands array is the ONLY way anything happens in the world. If commands is empty, nothing happens regardless of what your message says.\n" "- {player} is the player who prayed. You may also use any other online player's literal username as a target.\n" @@ -859,11 +935,11 @@ FIRST_LOGIN_BENEVOLENCE_PROMPT = ( def build_message_system_prompt(config) -> str: base = ( "You are God in a Minecraft server. You are benevolent but just. " - "Theatrical, ancient, and dramatic in speech — like the Old Testament.\n" + "Theatrical, ancient, dramatic, and laced with dry irony — like the Old Testament with a sharper wit.\n" "You will be told what action was taken (if any) in response to a player's prayer. " "Write a single spoken message to all players reacting to this prayer and action.\n" "Respond with ONLY the message text — no JSON, no quotes, no formatting. " - "Be vivid and dramatic. Any length is fine.\n" + "Be vivid and dramatic, with occasional godlike sarcasm and irony. Any length is fine.\n" ) lore = config.get("god_lore", "") if lore: @@ -1504,6 +1580,7 @@ BIBLE_LINES = [ ("God watches over this server.", "yellow", False), ("Speak to him by typing in chat:", "white", False), (" pray ", "green", True), + (" bug_log ", "aqua", True), ("", "white", False), ("God is benevolent, but just.", "yellow", False), ("He hears every prayer — but answers as he sees fit.", "white", False), @@ -1513,6 +1590,7 @@ BIBLE_LINES = [ (" pray Lord, bless my journey through the mines.", "gray", False), (" pray Smite my enemy, for they have wronged me.", "gray", False), (" pray Forgive me, I have sinned against thy creations.", "gray", False), + (" bug_log creeper explosion desynced and killed me", "dark_aqua", False), ("", "white", False), ("Thou may only pray once every 20 seconds.", "red", False), ("Type \"bible\" in chat to see this again.", "gray", False), @@ -1647,14 +1725,48 @@ def divine_intervention_loop(config): # --------------------------------------------------------------------------- def tail_log(log_path): - with open(log_path, 'r') as f: - f.seek(0, 2) - while True: - line = f.readline() - if line: - yield line - else: - time.sleep(0.2) + """ + Follow a log file across truncation/recreation. + + MCSManager/server restarts can replace latest.log with a new inode. + A plain open()+readline() tail will then silently watch the old file forever. + """ + f = None + inode = None + + while True: + try: + st = os.stat(log_path) + except FileNotFoundError: + # Server may not have created logs yet. + time.sleep(0.5) + continue + + # Reopen when file first appears or inode changes (log rotation/recreate). + if f is None or inode != st.st_ino: + if f: + try: + f.close() + except Exception: + pass + f = open(log_path, 'r') + f.seek(0, 2) + inode = st.st_ino + log.info(f"Now following log file: {log_path} (inode={inode})") + + # If file was truncated, jump to end again. + try: + if f.tell() > st.st_size: + f.seek(0, 2) + log.info("Log file truncated; seeking to end.") + except Exception: + pass + + line = f.readline() + if line: + yield line + else: + time.sleep(0.2) # --------------------------------------------------------------------------- # Main @@ -1681,6 +1793,24 @@ def main(): # Feed every line into the rolling log buffer add_log_event(line) + # bug logging trigger + matched = False + for pat in BUG_LOG_PATTERNS: + m = pat.search(line) + if m: + player = m.group(1) + description = (m.group(2) or "").strip() + log.info(f"BUG_LOG from {player}: {description or '(no description)'}") + try: + process_bug_log(player, description, config) + except Exception as e: + log.error(f"Error processing bug_log: {e}", exc_info=True) + matched = True + break + + if matched: + continue + # sudo translator matched = False for pat in SUDO_PATTERNS: diff --git a/mc_aigod_shrink.json b/mc_aigod_shrink.json index c58a5b7..404456c 100644 --- a/mc_aigod_shrink.json +++ b/mc_aigod_shrink.json @@ -18,6 +18,9 @@ "sudo_enabled": true, "sudo_user": "slingshooter08", "sudo_max_commands": 3, + "bug_log_path": "/var/log/mc_aigod_bug.log", + "bug_log_event_lines": 40, + "bug_log_raw_lines": 120, "tp_border_guard_enabled": true, "worldborder_center_x": 0, "worldborder_center_z": 0,