Add paper bug_log flow and more ironic God persona

This commit is contained in:
Claude Code
2026-03-17 14:53:54 -04:00
parent d6e84e9fbf
commit f4ce19db6d
4 changed files with 507 additions and 38 deletions
+17
View File
@@ -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
### Sister repo — minecraft-ai-god (Vanilla)
@@ -103,6 +111,15 @@ This section captures decisions and context accumulated across conversations wit
- **LLM backend:** Ollama at `192.168.0.179:11434` for the vanilla service. This fork uses `192.168.0.141:11434` (steel141, local GPU) for better throughput on heavier models (gemma3:12b message model, qwen3-coder:30b command model).
- **Session gateway (this 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.
- **Build templates:** Deterministic templates are tried first for `sudo build/make/create` requests. LLM translation is the fallback. This reduces surprises for structure generation tasks.
- **Localized retrieval for sudo reasoning (2026-03-17):** `langgraph_gateway.py` now supports a local knowledge corpus (`/var/lib/mc-langgraph-gateway/knowledge`) with index/search/read tools (`local.search`, `local.read`), optional startup bootstrap from Minecraft/Paper docs URLs, and automatic search->read retrieval hop in sudo tool mode before command generation.
- **Post-deploy behavior check (2026-03-17):** retrieval is active (`tool_trace` shows `local.search` + `local.read`) for `sudo destroy my surroundings`, but generated command remained `execute as <player> run fill ~-5 ~-5 ~-5 ~5 ~5 ~5 air` and produced no useful visible outcome in gameplay (empty/weak RCON feedback). Retrieval alone did not guarantee effective action selection.
- **Sudo execution hardening follow-up (2026-03-17):** added command-shape normalization for sudo (`execute as <player> run ...` -> `execute at <player> run ...`), blocked execute-wrapper bypass in validator (unsafe inner tails like `gameMode` inside `execute ... run`), and added adaptive destructive fallback for failed `destroy/nuke` intents (absolute `fill ... air` around player + TNT summons / TNT kit fallback when position unavailable).
- **Sudo effectiveness feedback loop (2026-03-17):** sudo now scores execution effectiveness from RCON results for all intents, reports translated commands + outcomes + ineffective flag back to gateway session memory (`mode=sudo_feedback`, `feedback_only=true`), and gateway stores this feedback without running extra LLM/tool calls.
- **Lookup contextual memory fix (2026-03-17):** `sudo lookup/search/wiki` now resolves pronoun references (e.g., "that command", "do it again") against the player's most recent executed sudo command, includes that command in lookup context payloads, and passes sudo history into gateway lookup context to improve follow-up reasoning.
- **Gamemode/effect sudo fix (2026-03-17):** Added `gamemode` to safe prefixes/whitelist and command palette, added syntax normalizer for malformed variants (`gameMode s`, short aliases, missing target) and execute-wrapped gamemode forms; confirmed valid RCON forms are `gamemode <mode> <player>` and `effect give <player> <effect> <duration> <amplifier> [hideParticles]`.
- **AI-driven build/template flow (2026-03-17):** `process_sudo` now defaults to non-deterministic build planning (legacy deterministic builder templates disabled unless `sudo_deterministic_build_templates=true`), supports AI-emitted `template ...` meta-commands in sudo execution loop, and includes an AI template planner override for build/make/create prompts when initial translation does not emit template workflow steps.
- **Failed-execution retry pipeline upgrade (2026-03-17):** sudo now runs a generic retry-repair pass on ineffective results before intent fallback. Added TNT-specific repair for malformed `summon tnt ... <count>` outputs (expands into bounded multiple summon commands) and invulnerability-effect repair to valid protection effects. Added `tnt` to destructive-intent keywords so TNT requests can trigger destructive fallback when needed.
- **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
+4 -4
View File
@@ -570,7 +570,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'
@@ -578,7 +578,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"
@@ -723,11 +723,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:
+5
View File
@@ -38,6 +38,11 @@
"sudo_enabled": true,
"sudo_user": "slingshooter08",
"sudo_max_commands": 12,
"bug_log_path": "/var/log/mc_aigod_paper_bug.log",
"bug_log_event_lines": 40,
"bug_log_raw_lines": 120,
"sudo_build_max_commands": 6,
"sudo_deterministic_build_templates": false,
"tp_border_guard_enabled": true,
"worldborder_center_x": 0,
"worldborder_center_z": 0,
+481 -34
View File
@@ -37,6 +37,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')
@@ -963,6 +967,90 @@ def get_sudo_history_block() -> str:
)
return "\n=== LAST 10 SUDO ACTIONS ===\n" + "\n".join(lines) + "\n"
def get_last_sudo_executed_command(player: str) -> str:
"""Return the most recent executed sudo command for a player, if any."""
with _memory_lock:
entries = list(sudo_history)
for _, p, _, _, executed in reversed(entries):
if p != player:
continue
if executed:
return str(executed[-1])
return ""
def _bug_log_path(config) -> str:
return config.get("bug_log_path", "/var/log/mc_aigod_paper_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"<unable to read raw server log: {e}>"]
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
# ---------------------------------------------------------------------------
@@ -1225,18 +1313,18 @@ SERVER_CAPABILITIES = {
"vanilla": {
"safe_prefixes": [
'give ', 'effect ', 'xp ', 'tp ', 'time ', 'weather ',
'execute ', 'kill ', 'summon ', 'tellraw ', 'worldborder ',
'execute ', 'kill ', 'summon ', 'tellraw ', 'worldborder ', 'gamemode ',
],
"sudo_whitelist_note": "give, effect, xp, tp, time, weather, execute, kill, summon, tellraw, worldborder",
"sudo_whitelist_note": "give, effect, xp, tp, time, weather, execute, kill, summon, tellraw, worldborder, gamemode",
"template_build": False,
},
"paper": {
"safe_prefixes": [
'give ', 'effect ', 'xp ', 'tp ', 'time ', 'weather ',
'execute ', 'kill ', 'summon ', 'tellraw ', 'worldborder ',
'fill ', 'setblock ', 'clone ',
'fill ', 'setblock ', 'clone ', 'gamemode ',
],
"sudo_whitelist_note": "give, effect, xp, tp, time, weather, execute, kill, summon, tellraw, worldborder, fill, setblock, clone",
"sudo_whitelist_note": "give, effect, xp, tp, time, weather, execute, kill, summon, tellraw, worldborder, fill, setblock, clone, gamemode",
"template_build": True,
},
}
@@ -1260,6 +1348,7 @@ GIVE (any item, based on player need — see Item Naming Rules below):
xp add {target} <amount> levels
EFFECTS (replace {target} with any online player's username):
SYNTAX: effect give <player> minecraft:<effect> <seconds> <amplifier>
effect give {target} minecraft:regeneration 120 2
effect give {target} minecraft:strength 300 1
effect give {target} minecraft:speed 300 2
@@ -1275,6 +1364,12 @@ EFFECTS (replace {target} with any online player's username):
effect give {target} minecraft:levitation 5 3
effect clear {target}
GAMEMODE:
gamemode survival {target}
gamemode creative {target}
gamemode adventure {target}
gamemode spectator {target}
MOVEMENT:
tp {target} 0 64 0
tp {target} <x> <y> <z>
@@ -1329,7 +1424,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'
@@ -1337,7 +1432,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"
@@ -1490,11 +1585,17 @@ def build_sudo_commands_system_prompt(config=None) -> str:
"{\"commands\": [\"cmd1\", \"cmd2\"]}\n\n"
"Rules:\n"
f"- Use commands from this whitelist only: {whitelist}.\n"
"- You may also output meta-commands starting with 'template ' for schematic workflows (not RCON).\n"
"- If the request cannot be mapped safely, return commands: [].\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"
"- For give syntax: give <player> minecraft:<item_id> <count> (count LAST, namespace required)\n"
"- Return commands only. No commentary.\n"
"- For build requests, prefer template workflow in one response when possible:\n"
" template search <query>\n"
" template pick <n> [name]\n"
" template build <name or filename>\n"
"- Keep template workflow concise (2-4 commands max).\n"
"\n"
"=== TELEPORT SYNTAX ===\n"
"Same dimension: tp <player> <x> <y> <z>\n"
@@ -1577,11 +1678,11 @@ SUDO_COMMANDS_SYSTEM_PROMPT = build_sudo_commands_system_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:
@@ -2033,6 +2134,39 @@ def fix_effect_command(cmd: str) -> str:
return fixed
return cmd
def fix_gamemode_command(cmd: str, fallback_player: str) -> str:
"""
Normalize common gamemode variants:
- gameMode -> gamemode
- short aliases (s/c/a/sp) -> survival/creative/adventure/spectator
- add target player when omitted
"""
raw = (cmd or "").strip()
m = re.match(r'^(?:gameMode|gamemode)\s+(\S+)(?:\s+(\w+))?$', raw, flags=re.IGNORECASE)
if not m:
return cmd
mode, target = m.group(1).lower(), (m.group(2) or "").strip()
alias = {
"s": "survival",
"0": "survival",
"c": "creative",
"1": "creative",
"a": "adventure",
"2": "adventure",
"sp": "spectator",
"3": "spectator",
}
mode = alias.get(mode, mode)
if mode not in ("survival", "creative", "adventure", "spectator"):
return cmd
if not target:
target = fallback_player
fixed = f"gamemode {mode} {target}"
if fixed != raw:
log.warning(f"Fixed gamemode syntax: '{cmd}' -> '{fixed}'")
return fixed
def validate_command(cmd, online_players, fallback_player, config=None):
"""Replace placeholders, auto-fix common give syntax errors, check safe prefix."""
resolved = cmd.replace("{player}", fallback_player).replace("{target}", fallback_player)
@@ -2041,16 +2175,194 @@ def validate_command(cmd, online_players, fallback_player, config=None):
resolved = resolved[1:]
resolved = fix_give_command(resolved)
resolved = fix_effect_command(resolved)
resolved = fix_gamemode_command(resolved, fallback_player)
caps = get_server_capabilities(config) if config else SERVER_CAPABILITIES[DEFAULT_SERVER_TYPE]
prefixes = caps["safe_prefixes"]
if not any(resolved.startswith(p) for p in prefixes):
log.warning(f"Command blocked (unknown prefix for server_type={config.get('server_type', DEFAULT_SERVER_TYPE) if config else DEFAULT_SERVER_TYPE}): {resolved}")
return resolved, False
# Prevent execute-wrapper bypass (e.g. execute ... run gameMode s)
if resolved.startswith("execute "):
tail = resolved
for _ in range(4):
if not tail.startswith("execute "):
break
marker = " run "
idx = tail.find(marker)
if idx < 0:
break
tail = tail[idx + len(marker):].strip()
if tail and not tail.startswith("execute "):
inner_prefixes = [p for p in prefixes if p != "execute "]
if not any(tail.startswith(p) for p in inner_prefixes):
log.warning(f"Command blocked (unsafe execute tail): {resolved}")
return resolved, False
if config and _extract_tp_abs_xyz(resolved) and not _tp_inside_worldborder(resolved, config):
log.warning(f"Command blocked (tp outside worldborder): {resolved}")
return resolved, False
return resolved, True
def _is_destructive_intent(prompt: str) -> bool:
p = (prompt or "").lower()
keys = (
"destroy", "destruction", "nuke", "blow up", "blowup", "explode",
"erase", "delete", "annihilate", "wreck", "flatten", "ruin", "tnt",
)
return any(k in p for k in keys)
def _normalize_sudo_command_shape(cmd: str, player: str) -> str:
c = (cmd or "").strip()
if not c:
return c
# Collapse execute-wrapped gamemode into direct targeted form.
gm = re.match(rf'^execute\s+as\s+{re.escape(player)}\s+run\s+(?:gameMode|gamemode)\s+(\S+)(?:\s+(\w+))?$', c, flags=re.IGNORECASE)
if gm:
mode = gm.group(1)
target = gm.group(2) or player
fixed = f"gamemode {mode} {target}"
log.warning(f"Normalized execute-wrapped gamemode: '{c}' -> '{fixed}'")
return fixed
# Prefer position-context execution; this yields more reliable behavior than nested execute chains.
m = re.match(rf'^execute\s+as\s+{re.escape(player)}\s+run\s+execute\s+positioned\s+~\s+~\s+~\s+run\s+(.+)$', c)
if m:
fixed = f"execute at {player} run {m.group(1).strip()}"
log.warning(f"Normalized nested execute: '{c}' -> '{fixed}'")
return fixed
m = re.match(rf'^execute\s+as\s+{re.escape(player)}\s+run\s+(.+)$', c)
if m:
fixed = f"execute at {player} run {m.group(1).strip()}"
log.warning(f"Normalized execute anchor: '{c}' -> '{fixed}'")
return fixed
# Drop known no-op clone shape (source to same destination).
if re.match(r'^clone\s+~\S*\s+~\S*\s+~\S*\s+~\S*\s+~\S*\s+~\S*\s+~\s+~\s+~$', c):
log.warning(f"Dropped likely no-op clone: '{c}'")
return ""
return c
def _build_destructive_fallback(player: str, config) -> list:
pos = get_player_xyz(player, config)
if not pos:
return [f"give {player} minecraft:tnt 64", f"give {player} minecraft:flint_and_steel 1"]
x, y, z = pos
radius = int(config.get("sudo_destroy_radius", 12))
depth = int(config.get("sudo_destroy_depth", 18))
height = int(config.get("sudo_destroy_height", 6))
y1 = max(-64, y - depth)
y2 = min(319, y + height)
return [
f"fill {x-radius} {y1} {z-radius} {x+radius} {y2} {z+radius} minecraft:air",
f"summon minecraft:tnt {x} {min(319, y+2)} {z}",
f"summon minecraft:tnt {x+3} {min(319, y+2)} {z}",
f"summon minecraft:tnt {x-3} {min(319, y+2)} {z}",
]
def _sudo_result_is_effective(result: str) -> bool:
r = (result or "").strip().lower()
if not r:
return False
bad_markers = (
"unknown", "incorrect argument", "expected", "syntax error",
"no entity was found", "cannot", "failed", "error",
)
if any(m in r for m in bad_markers):
return False
good_markers = (
"successfully", "summoned", "set the", "set block", "filled",
"teleported", "gave", "applied effect", "killed",
)
return any(m in r for m in good_markers)
def _report_sudo_feedback(player: str, prompt: str, translated: list, results_seen: list, ineffective: bool, config):
if not _gateway_enabled(config):
return
try:
summary_rows = []
for cmd, res in results_seen[:8]:
summary_rows.append({"command": cmd, "result": (res or "")[:220]})
_gateway_send(
player=player,
mode="sudo",
text=f"execution feedback for sudo request: {prompt}",
context_payload={
"mode": "sudo_feedback",
"feedback_only": True,
"request": prompt,
"translated_commands": translated[:8],
"execution_results": summary_rows,
"ineffective": bool(ineffective),
},
config=config,
allow_tools=False,
max_tool_steps=0,
)
except Exception as e:
log.warning(f"Could not report sudo feedback to gateway: {e}")
def _repair_failed_sudo_commands(player: str, results_seen: list, config) -> list:
"""Build bounded retry commands from observed command/result failures."""
out = []
max_retry = int(config.get("sudo_retry_max_commands", 10))
for cmd, result in results_seen:
c = (cmd or "").strip()
r = (result or "").lower()
if not c or not r:
continue
# Fix malformed TNT count usage:
# execute at <p> run summon tnt ~ ~1 ~ 80 -> multiple summon commands.
if "expected compound tag" in r and "summon" in c and "tnt" in c:
prefix = ""
body = c
m_pref = re.match(rf'^(execute\s+at\s+{re.escape(player)}\s+run\s+)(.+)$', c)
if m_pref:
prefix = m_pref.group(1)
body = m_pref.group(2)
m = re.match(r'^summon\s+(?:minecraft:)?tnt\s+(\S+)\s+(\S+)\s+(\S+)(?:\s+(\d+))?$', body)
if m:
x, y, z, cnt = m.groups()
count = int(cnt) if cnt and cnt.isdigit() else 1
count = max(1, min(count, int(config.get("sudo_tnt_retry_cap", 24))))
for i in range(count):
dx = (i % 6) - 3
dz = (i // 6) - 2
xx = x if x.startswith("~") else str(int(float(x)) + dx)
zz = z if z.startswith("~") else str(int(float(z)) + dz)
if x.startswith("~") and dx != 0:
xx = f"~{dx:+d}"
if z.startswith("~") and dz != 0:
zz = f"~{dz:+d}"
out.append(f"{prefix}summon minecraft:tnt {xx} {y} {zz}")
if len(out) >= max_retry:
return out
# Fix nonexistent invulnerability effect to a real durable protection set.
if "mob_effect" in r and "invulnerability" in r:
out.extend([
f"effect give {player} minecraft:resistance 1200 4 true",
f"effect give {player} minecraft:regeneration 1200 2 true",
f"effect give {player} minecraft:absorption 1200 4 true",
])
if len(out) >= max_retry:
return out[:max_retry]
return out[:max_retry]
def execute_response(response, context, config, praying_player=None):
message = response.get("message") or ""
commands = response.get("commands") or []
@@ -2289,11 +2601,19 @@ def process_sudo(player, prompt, config):
if not query:
_send_private(player, "[SUDO-LOOKUP] Usage: sudo lookup <question>", config, "yellow")
return
# Resolve contextual references like "that command" using sudo memory.
last_cmd = get_last_sudo_executed_command(player)
lookup_query = query
if last_cmd and re.search(r'\b(that|it|same|again|last command)\b', query.lower()):
lookup_query = f"{query} (reference command: {last_cmd})"
try:
_send_private(player, "[SUDO-LOOKUP] Result:", config, "aqua")
if last_cmd:
_send_private(player, f"context command: {last_cmd}", config, "dark_gray")
wiki_rows = _info_lookup_wiki(query)
web_rows = _info_lookup_web(query)
wiki_rows = _info_lookup_wiki(lookup_query)
web_rows = _info_lookup_web(lookup_query)
if wiki_rows:
_send_private(player, "minecraft.wiki:", config, "dark_aqua")
@@ -2314,7 +2634,9 @@ def process_sudo(player, prompt, config):
context_payload={
"mode": "sudo_lookup",
"lookup_only": True,
"query": query,
"query": lookup_query,
"reference_command": last_cmd,
"sudo_history": get_sudo_history_block(),
"wiki_hits": wiki_rows,
"web_hits": web_rows,
},
@@ -2345,27 +2667,28 @@ def process_sudo(player, prompt, config):
online = players_online(config)
# Deterministic builder templates (Paper-optimized) for sudo build requests.
templated = build_template_commands(player, prompt, config)
if templated is not None:
commands = templated[: int(config.get("sudo_max_commands", 12))]
safe_preview = " | ".join(commands).replace("\\", "\\\\").replace('"', '\\"')
rcon(
f'tellraw {player} {{"text":"[SUDO-BUILD] {safe_preview}","color":"dark_aqua","italic":true}}',
config["rcon_host"], config["rcon_port"], config["rcon_password"]
)
executed = []
for cmd in commands:
resolved, is_safe = validate_command(cmd, online, player, config)
if not is_safe:
continue
log.info(f"SUDO-BUILD execute: {resolved}")
result = rcon(resolved, config["rcon_host"], config["rcon_port"], config["rcon_password"])
log.info(f"SUDO-BUILD result: {result!r}")
executed.append(resolved)
time.sleep(0.15)
add_sudo_history(player, prompt, commands, executed)
return
# Legacy deterministic builder templates are optional; default is AI-driven build planning.
if bool(config.get("sudo_deterministic_build_templates", False)):
templated = build_template_commands(player, prompt, config)
if templated is not None:
commands = templated[: int(config.get("sudo_max_commands", 12))]
safe_preview = " | ".join(commands).replace("\\", "\\\\").replace('"', '\\"')
rcon(
f'tellraw {player} {{"text":"[SUDO-BUILD] {safe_preview}","color":"dark_aqua","italic":true}}',
config["rcon_host"], config["rcon_port"], config["rcon_password"]
)
executed = []
for cmd in commands:
resolved, is_safe = validate_command(cmd, online, player, config)
if not is_safe:
continue
log.info(f"SUDO-BUILD execute: {resolved}")
result = rcon(resolved, config["rcon_host"], config["rcon_port"], config["rcon_password"])
log.info(f"SUDO-BUILD result: {result!r}")
executed.append(resolved)
time.sleep(0.15)
add_sudo_history(player, prompt, commands, executed)
return
# Collect positions for all online players.
position_lines = []
@@ -2390,6 +2713,7 @@ def process_sudo(player, prompt, config):
context_hint = (
f"Requesting player: {player}\n"
f"Online players: {', '.join(online) or 'none'}\n"
+ "Template workflow commands available: template search <query> ; template pick <n> [name] ; template build <name|filename>\n"
+ (positions_block + "\n" if positions_block else "")
+ f"Natural language request: {prompt}\n"
+ get_sudo_history_block()
@@ -2410,6 +2734,32 @@ def process_sudo(player, prompt, config):
parsed = _parse_llm_json(content)
return parsed.get("commands") or []
def _ai_template_plan() -> list:
planner_system = (
"You are a Minecraft template workflow planner. Return ONLY JSON: {\"commands\": [\"...\"]}.\n"
"Output only template workflow meta-commands (not RCON):\n"
"- template search <query>\n"
"- template pick <n> [name]\n"
"- template build <name|filename>\n"
"Keep to 2-4 commands and preserve user intent."
)
content = _llm_call(
model=command_model,
system=planner_system,
user=(
f"Player: {player}\n"
f"Request: {prompt}\n"
f"Recent sudo history:\n{get_sudo_history_block()}"
),
config=config,
fmt="json",
temperature=0.2,
max_tokens=140,
)
parsed = _parse_llm_json(content)
cmds = parsed.get("commands") or []
return [c for c in cmds if isinstance(c, str) and c.lower().startswith("template ")]
try:
if _gateway_enabled(config):
try:
@@ -2418,6 +2768,7 @@ def process_sudo(player, prompt, config):
mode="sudo",
text=prompt,
context_payload={
"request": prompt,
"online_players": online,
"sudo_history": get_sudo_history_block(),
"mode": "sudo",
@@ -2441,8 +2792,26 @@ def process_sudo(player, prompt, config):
)
return
max_cmds = config.get("sudo_max_commands", 3)
commands = commands[:max_cmds]
build_intent = any(prompt.lower().strip().startswith(x) for x in ("build ", "make ", "create "))
has_template_cmd = any(isinstance(c, str) and c.lower().startswith("template ") for c in commands)
if build_intent and not has_template_cmd:
try:
planned = _ai_template_plan()
if planned:
log.info(f"SUDO template planner override: {planned}")
commands = planned
except Exception as e:
log.warning(f"SUDO template planner failed: {e}")
max_cmds = int(config.get("sudo_max_commands", 3))
low_prompt = prompt.lower().strip()
if any(low_prompt.startswith(x) for x in ("build ", "make ", "create ")):
max_cmds = max(max_cmds, int(config.get("sudo_build_max_commands", 6)))
commands = [
_normalize_sudo_command_shape(c, player)
for c in commands[:max_cmds]
]
commands = [c for c in commands if c]
if not commands:
add_sudo_history(player, prompt, [], [])
@@ -2460,7 +2829,20 @@ def process_sudo(player, prompt, config):
)
executed = []
results_seen = []
for cmd in commands:
if cmd.lower().startswith("template "):
log.info(f"SUDO template action: {cmd}")
try:
process_sudo_template_command(player, cmd, config)
executed.append(cmd)
results_seen.append((cmd, "template action executed"))
except Exception as e:
log.warning(f"SUDO template action failed: {e}")
results_seen.append((cmd, f"template action failed: {e}"))
time.sleep(0.15)
continue
resolved, is_safe = validate_command(cmd, online, player, config)
if not is_safe:
continue
@@ -2468,8 +2850,53 @@ def process_sudo(player, prompt, config):
result = rcon(resolved, config["rcon_host"], config["rcon_port"], config["rcon_password"])
log.info(f"SUDO result: {result!r}")
executed.append(resolved)
results_seen.append((resolved, str(result or "")))
time.sleep(0.2)
effective_hits = sum(1 for _, res in results_seen if _sudo_result_is_effective(res))
ineffective = (len(executed) == 0) or (effective_hits == 0)
# Generic failed-execution repair pass.
if ineffective:
retry_cmds = _repair_failed_sudo_commands(player, results_seen, config)
if retry_cmds:
log.warning(f"SUDO retry pipeline engaged: {retry_cmds}")
_send_private(player, "[SUDO] Initial command failed; retrying with repaired syntax.", config, "yellow")
for cmd in retry_cmds:
resolved, is_safe = validate_command(cmd, online, player, config)
if not is_safe:
continue
log.info(f"SUDO retry execute: {resolved}")
result = rcon(resolved, config["rcon_host"], config["rcon_port"], config["rcon_password"])
log.info(f"SUDO retry result: {result!r}")
executed.append(resolved)
results_seen.append((resolved, str(result or "")))
time.sleep(0.12)
effective_hits = sum(1 for _, res in results_seen if _sudo_result_is_effective(res))
ineffective = (len(executed) == 0) or (effective_hits == 0)
# Adaptive fallback for destructive intent when output appears ineffective.
if _is_destructive_intent(prompt) and ineffective:
fallback_cmds = _build_destructive_fallback(player, config)
log.warning(f"SUDO destructive fallback engaged for prompt={prompt!r}: {fallback_cmds}")
_send_private(player, "[SUDO] Initial plan was weak; applying destructive fallback.", config, "yellow")
for cmd in fallback_cmds[:max_cmds]:
resolved, is_safe = validate_command(cmd, online, player, config)
if not is_safe:
continue
log.info(f"SUDO fallback execute: {resolved}")
result = rcon(resolved, config["rcon_host"], config["rcon_port"], config["rcon_password"])
log.info(f"SUDO fallback result: {result!r}")
executed.append(resolved)
results_seen.append((resolved, str(result or "")))
time.sleep(0.15)
effective_hits = sum(1 for _, res in results_seen if _sudo_result_is_effective(res))
ineffective = (len(executed) == 0) or (effective_hits == 0)
_report_sudo_feedback(player, prompt, commands, results_seen, ineffective, config)
add_sudo_history(player, prompt, commands, executed)
@@ -2586,6 +3013,7 @@ BIBLE_LINES = [
("God watches over this server.", "yellow", False),
("Speak to him by typing in chat:", "white", False),
(" pray <your message>", "green", True),
(" bug_log <optional description>", "aqua", True),
("", "white", False),
("God is benevolent, but just.", "yellow", False),
("He hears every prayer — but answers as he sees fit.", "white", False),
@@ -2595,6 +3023,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),
@@ -2783,6 +3212,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: