Refine vanilla AI prompts and sudo reliability

This commit is contained in:
Claude Code
2026-03-17 19:53:25 -04:00
parent 29c43e2d30
commit 4771d6c03e
4 changed files with 128 additions and 4 deletions
+4
View File
@@ -158,6 +158,10 @@ Main file: `/etc/mc_aigod.json`
| `bug_log_path` | string | File path used for `bug_log` entries | | `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_event_lines` | int | Number of in-memory interesting events to attach |
| `bug_log_raw_lines` | int | Number of raw tail log lines to attach | | `bug_log_raw_lines` | int | Number of raw tail log lines to attach |
| `bug_log_service_lines` | int | Number of recent AI action lines from service log |
| `tp_safety_enabled` | bool | Auto-protect risky vertical teleports with safety effects |
| `tp_safety_vertical_delta` | number | Relative `~ ~N ~` threshold that triggers safety wrapper |
| `tp_safety_absolute_y` | number | Absolute Y threshold that triggers safety wrapper |
### Current shrink-world example ### Current shrink-world example
+5
View File
@@ -112,6 +112,10 @@ This section captures decisions and context accumulated across conversations wit
- **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. - **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 <optional description>` 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. - **Bug intake trigger (2026-03-17):** Added `bug_log <optional description>` 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. - **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.
- **Paper bug report triage (2026-03-17):** `bug_log` captured a case where a prayer reward path issued vertical teleport (`execute as slingshooter08 run tp slingshooter08 ~ ~10 ~`) and felt unsafe/unintended for a build-oriented prayer; indicates need for pray-path movement guardrails and safer teleport policy.
- **Teleport safety guard (2026-03-17):** Added execution-time TP safety wrapper in vanilla `execute_response`: risky upward teleports (relative delta >= configured threshold or high absolute Y) are prefixed with `slow_falling` + `resistance` effects to prevent accidental lethal falls while keeping punishment/spectacle behavior.
- **Bug-log signal upgrade (2026-03-17):** `bug_log` entries now include filtered raw log lines (RCON connect/disconnect noise removed) plus a new "RECENT AI ACTIONS" section from service logs (`Prayer from`, `Executing RCON`, `SUDO execute`, results) for faster triage.
- **Weather normalization fix (2026-03-17):** added explicit prompt/context rule and command normalizer in vanilla script mapping `weather storm|rainstorm|thunderstorm` -> `weather thunder` before validation/execution.
### Infrastructure decisions ### Infrastructure decisions
@@ -119,6 +123,7 @@ This section captures decisions and context accumulated across conversations wit
- **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). - **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. - **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. - **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.
- **Context/prompt refinement (2026-03-17):** updated vanilla sudo/god prompts to emphasize strict 1.21 syntax (including `effect give ...`, no old enchant NBT, weather clear/rain/thunder only), concise JSON outputs, and avoidance of accidental high vertical teleports in benevolent responses; increased vanilla sudo translator token budget from 180 to 260 to reduce truncated JSON on longer kit requests.
- **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. - **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 ### Open threads / next ideas
+115 -4
View File
@@ -254,14 +254,38 @@ def _bug_log_path(config) -> str:
def _read_recent_raw_log_lines(log_path: str, max_lines: int) -> list: def _read_recent_raw_log_lines(log_path: str, max_lines: int) -> list:
lines = deque(maxlen=max_lines) lines = deque(maxlen=max_lines * 4)
noise = re.compile(r'RCON (?:Listener|Client)|Thread RCON Client')
try: try:
with open(log_path, 'r', encoding='utf-8', errors='replace') as f: with open(log_path, 'r', encoding='utf-8', errors='replace') as f:
for line in f: for line in f:
lines.append(line.rstrip('\n')) clean = line.rstrip('\n')
if noise.search(clean):
continue
lines.append(clean)
except Exception as e: except Exception as e:
return [f"<unable to read raw server log: {e}>"] return [f"<unable to read raw server log: {e}>"]
return list(lines) return list(lines)[-max_lines:]
def _service_log_path(config) -> str:
return config.get("service_log_path", "/var/log/mc_aigod.log")
def _read_recent_service_action_lines(path: str, max_lines: int) -> list:
lines = deque(maxlen=max_lines * 8)
keep = re.compile(
r'Prayer from|SUDO from|Executing RCON:|SUDO execute:|RCON result:|SUDO result:|BUG_LOG from'
)
try:
with open(path, 'r', encoding='utf-8', errors='replace') as f:
for line in f:
clean = line.rstrip('\n')
if keep.search(clean):
lines.append(clean)
except Exception as e:
return [f"<unable to read service log: {e}>"]
return list(lines)[-max_lines:]
def _format_recent_event_lines(max_events: int) -> list: def _format_recent_event_lines(max_events: int) -> list:
@@ -281,9 +305,11 @@ def process_bug_log(player: str, description: str, config):
event_count = int(config.get("bug_log_event_lines", 40)) event_count = int(config.get("bug_log_event_lines", 40))
raw_count = int(config.get("bug_log_raw_lines", 120)) raw_count = int(config.get("bug_log_raw_lines", 120))
service_count = int(config.get("bug_log_service_lines", 30))
recent_events = _format_recent_event_lines(event_count) recent_events = _format_recent_event_lines(event_count)
raw_lines = _read_recent_raw_log_lines(config.get("log_path", ""), raw_count) raw_lines = _read_recent_raw_log_lines(config.get("log_path", ""), raw_count)
service_lines = _read_recent_service_action_lines(_service_log_path(config), service_count)
bug_path = _bug_log_path(config) bug_path = _bug_log_path(config)
timestamp = datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S UTC') timestamp = datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S UTC')
@@ -302,6 +328,11 @@ def process_bug_log(player: str, description: str, config):
bf.write("\n".join(recent_events) + "\n") bf.write("\n".join(recent_events) + "\n")
else: else:
bf.write("(no recent in-memory events captured)\n") bf.write("(no recent in-memory events captured)\n")
bf.write("\n-- RECENT AI ACTIONS (SERVICE LOG) --\n")
if service_lines:
bf.write("\n".join(service_lines) + "\n")
else:
bf.write("(no AI service lines available)\n")
bf.write("\n-- RECENT RAW SERVER LOG LINES --\n") bf.write("\n-- RECENT RAW SERVER LOG LINES --\n")
if raw_lines: if raw_lines:
bf.write("\n".join(raw_lines) + "\n") bf.write("\n".join(raw_lines) + "\n")
@@ -644,6 +675,8 @@ MOVEMENT:
NEVER use: tp <player> minecraft:the_nether (this is wrong syntax) NEVER use: tp <player> minecraft:the_nether (this is wrong syntax)
WORLD/ENVIRONMENT (affects all players): WORLD/ENVIRONMENT (affects all players):
SYNTAX: weather <clear|rain|thunder> [duration]
NOTE: 'storm' is invalid; if intent says storm/rainstorm/thunderstorm use thunder.
time set day time set day
time set night time set night
weather clear 6000 weather clear 6000
@@ -705,9 +738,12 @@ def build_system_prompt(config):
"- Do not ask a player to gather materials they clearly don't have access to or that are rare relative to their current situation.\n" "- Do not ask a player to gather materials they clearly don't have access to or that are rare relative to their current situation.\n"
"- For give commands: use any valid Minecraft 1.21 item ID following the Item Naming Rules. Do not guess item IDs — consult the naming rules and common IDs list.\n" "- For give commands: use any valid Minecraft 1.21 item ID following the Item Naming Rules. Do not guess item IDs — consult the naming rules and common IDs list.\n"
"- For all other commands: only use forms shown in the Command Palette. Do not invent new command types.\n" "- For all other commands: only use forms shown in the Command Palette. Do not invent new command types.\n"
"- Weather values are only clear, rain, or thunder. If you mean storm/rainstorm/thunderstorm, output thunder.\n"
"- Reward humble, genuine prayers. Punish hubris, blasphemy, or naked greed.\n" "- Reward humble, genuine prayers. Punish hubris, blasphemy, or naked greed.\n"
"- Repeated greedy demands after recent gifts should usually receive correction (rebuke, debuff, or symbolic punishment), not more rewards.\n"
"- Powerful rewards (netherite, enchanted_golden_apple, totem) must be rare.\n" "- Powerful rewards (netherite, enchanted_golden_apple, totem) must be rare.\n"
"- kill {target} is reserved for extreme blasphemy only.\n" "- kill {target} is reserved for extreme blasphemy only.\n"
"- Avoid lethal accidents from vertical teleports. If using a high upward teleport as punishment or spectacle, pair it with slow_falling or resistance unless explicit execution is intended.\n"
"- When angered, chain commands: thunder + lightning + debuffs = divine wrath.\n\n" "- When angered, chain commands: thunder + lightning + debuffs = divine wrath.\n\n"
"=== COMMAND PALETTE ===\n" "=== COMMAND PALETTE ===\n"
f"{COMMAND_PALETTE}\n" f"{COMMAND_PALETTE}\n"
@@ -828,6 +864,9 @@ COMMANDS_SYSTEM_PROMPT = (
"- kill is reserved for extreme blasphemy only.\n" "- kill is reserved for extreme blasphemy only.\n"
"- For give: syntax is always give <player> minecraft:<item_id> <count>\n" "- For give: syntax is always give <player> minecraft:<item_id> <count>\n"
"- Count comes LAST. Namespace prefix minecraft: is REQUIRED.\n" "- Count comes LAST. Namespace prefix minecraft: is REQUIRED.\n"
"- For effects: use 'effect give <player> minecraft:<effect> <seconds> <amplifier>'.\n"
"- For weather use only clear/rain/thunder (NOT storm).\n"
"- Avoid accidental lethal movement in benevolent responses; do not launch players high unless explicitly asked.\n"
"- Beds: white_bed not bed. Logs: oak_log not log. Wool: white_wool not wool.\n" "- Beds: white_bed not bed. Logs: oak_log not log. Wool: white_wool not wool.\n"
"- Chain commands for dramatic effect: thunder + lightning + blindness = wrath.\n\n" "- Chain commands for dramatic effect: thunder + lightning + blindness = wrath.\n\n"
+ "=== COMMAND PALETTE ===\n" + "=== COMMAND PALETTE ===\n"
@@ -850,7 +889,10 @@ def build_sudo_commands_system_prompt(config=None) -> str:
"- If the request cannot be mapped safely, return commands: [].\n" "- If the request cannot be mapped safely, return commands: [].\n"
"- If player says 'me' or 'my', target the requesting player.\n" "- If player says 'me' or 'my', target the requesting player.\n"
"- You will receive LAST 10 SUDO ACTIONS. Use them for continuity and corrections when the player says previous output was wrong.\n" "- You will receive LAST 10 SUDO ACTIONS. Use them for continuity and corrections when the player says previous output was wrong.\n"
"- Keep output concise (usually <= 6 commands) and valid JSON only.\n"
"- For give syntax: give <player> minecraft:<item_id> <count> (count LAST, namespace required)\n" "- For give syntax: give <player> minecraft:<item_id> <count> (count LAST, namespace required)\n"
"- For effect syntax: effect give <player> minecraft:<effect> <seconds> <amplifier> [hideParticles]\n"
"- Never use old NBT enchant syntax like {Enchantments:[...]}; use item[enchantments={...}] only.\n"
"- Return commands only. No commentary.\n" "- Return commands only. No commentary.\n"
"\n" "\n"
"=== TELEPORT SYNTAX ===\n" "=== TELEPORT SYNTAX ===\n"
@@ -940,6 +982,7 @@ def build_message_system_prompt(config) -> str:
"Write a single spoken message to all players reacting to this prayer and action.\n" "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. " "Respond with ONLY the message text — no JSON, no quotes, no formatting. "
"Be vivid and dramatic, with occasional godlike sarcasm and irony. Any length is fine.\n" "Be vivid and dramatic, with occasional godlike sarcasm and irony. Any length is fine.\n"
"For punishments, prefer fear and humiliation over accidental instant death unless destruction is explicitly intended.\n"
) )
lore = config.get("god_lore", "") lore = config.get("god_lore", "")
if lore: if lore:
@@ -1210,6 +1253,53 @@ def _tp_inside_worldborder(cmd: str, config) -> bool:
return abs(x - cx) <= limit and abs(z - cz) <= limit return abs(x - cx) <= limit and abs(z - cz) <= limit
def _tp_safety_enabled(config) -> bool:
return bool(config.get("tp_safety_enabled", True))
def _extract_tp_target_y(cmd: str):
m = re.search(r'\btp\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)', cmd)
if not m:
return None, None
return m.group(1), m.group(3)
def _needs_vertical_tp_safety(resolved_cmd: str, config) -> tuple:
target, ytok = _extract_tp_target_y(resolved_cmd)
if not target or not ytok:
return False, ""
risky_delta = float(config.get("tp_safety_vertical_delta", 8.0))
risky_abs_y = float(config.get("tp_safety_absolute_y", 120.0))
if ytok.startswith("~"):
offs = ytok[1:].strip()
dy = float(offs) if offs else 0.0
if dy >= risky_delta:
return True, target
return False, target
try:
abs_y = float(ytok)
if abs_y >= risky_abs_y:
return True, target
except Exception:
pass
return False, target
def _tp_safety_prefix_commands(resolved_cmd: str, config) -> list:
if not _tp_safety_enabled(config):
return []
needs, target = _needs_vertical_tp_safety(resolved_cmd, config)
if not needs or not target:
return []
return [
f"effect give {target} minecraft:slow_falling 20 0",
f"effect give {target} minecraft:resistance 8 1",
]
def fix_give_command(cmd: str) -> str: def fix_give_command(cmd: str) -> str:
""" """
Correct common LLM give command mistakes: Correct common LLM give command mistakes:
@@ -1274,11 +1364,21 @@ def fix_effect_command(cmd: str) -> str:
return fixed return fixed
return cmd return cmd
def fix_weather_command(cmd: str) -> str:
"""Normalize weather synonyms to valid Minecraft literals."""
raw = (cmd or "").strip()
fixed = re.sub(r'\bweather\s+(storm|rainstorm|thunderstorm)\b', 'weather thunder', raw, flags=re.IGNORECASE)
if fixed != raw:
log.warning(f"Fixed weather syntax: '{cmd}' -> '{fixed}'")
return fixed
def validate_command(cmd, online_players, fallback_player, config=None): def validate_command(cmd, online_players, fallback_player, config=None):
"""Replace placeholders, auto-fix common give syntax errors, check safe prefix.""" """Replace placeholders, auto-fix common give syntax errors, check safe prefix."""
resolved = cmd.replace("{player}", fallback_player).replace("{target}", fallback_player) resolved = cmd.replace("{player}", fallback_player).replace("{target}", fallback_player)
resolved = fix_give_command(resolved) resolved = fix_give_command(resolved)
resolved = fix_effect_command(resolved) resolved = fix_effect_command(resolved)
resolved = fix_weather_command(resolved)
caps = get_server_capabilities(config) if config else SERVER_CAPABILITIES[DEFAULT_SERVER_TYPE] caps = get_server_capabilities(config) if config else SERVER_CAPABILITIES[DEFAULT_SERVER_TYPE]
prefixes = caps["safe_prefixes"] prefixes = caps["safe_prefixes"]
if not any(resolved.startswith(p) for p in prefixes): if not any(resolved.startswith(p) for p in prefixes):
@@ -1347,6 +1447,17 @@ def execute_response(response, context, config, praying_player=None):
resolved, is_safe = validate_command(cmd, context["online_players"], fallback, config) resolved, is_safe = validate_command(cmd, context["online_players"], fallback, config)
if not is_safe: if not is_safe:
continue continue
safety_prefix = _tp_safety_prefix_commands(resolved, config)
for scmd in safety_prefix:
sresolved, safe_ok = validate_command(scmd, context["online_players"], fallback, config)
if not safe_ok:
continue
log.info(f"Executing RCON: {sresolved}")
sresult = rcon(sresolved, config["rcon_host"], config["rcon_port"], config["rcon_password"])
log.info(f"RCON result: {sresult!r}")
time.sleep(0.15)
log.info(f"Executing RCON: {resolved}") log.info(f"Executing RCON: {resolved}")
result = rcon(resolved, config["rcon_host"], config["rcon_port"], config["rcon_password"]) result = rcon(resolved, config["rcon_host"], config["rcon_port"], config["rcon_password"])
log.info(f"RCON result: {result!r}") log.info(f"RCON result: {result!r}")
@@ -1526,7 +1637,7 @@ def process_sudo(player, prompt, config):
config=config, config=config,
fmt="json", fmt="json",
temperature=0.1, temperature=0.1,
max_tokens=180, max_tokens=260,
) )
parsed = _parse_llm_json(content) parsed = _parse_llm_json(content)
commands = parsed.get("commands") or [] commands = parsed.get("commands") or []
+4
View File
@@ -21,6 +21,10 @@
"bug_log_path": "/var/log/mc_aigod_bug.log", "bug_log_path": "/var/log/mc_aigod_bug.log",
"bug_log_event_lines": 40, "bug_log_event_lines": 40,
"bug_log_raw_lines": 120, "bug_log_raw_lines": 120,
"bug_log_service_lines": 30,
"tp_safety_enabled": true,
"tp_safety_vertical_delta": 8,
"tp_safety_absolute_y": 120,
"tp_border_guard_enabled": true, "tp_border_guard_enabled": true,
"worldborder_center_x": 0, "worldborder_center_x": 0,
"worldborder_center_z": 0, "worldborder_center_z": 0,