Minecraft-Friends-AI-ZeroGPU / voyager_runner.py
Erinaldordso's picture
Restore Naldo Muse connection and start a fresh normal world
a625b1a verified
Raw History Blame Contribute Delete
16 kB
from __future__ import annotations
import os
import json
import logging
import shutil
import sys
import threading
import time
from pathlib import Path
VOYAGER_HOME = Path(os.environ.get("VOYAGER_HOME", "/tmp/voyager-runtime/Voyager"))
if str(VOYAGER_HOME) not in sys.path:
sys.path.insert(0, str(VOYAGER_HOME))
from voyager import Voyager # noqa: E402
from voyager_verification import (
quarantine_invalid_skills, quarantine_template_skills, task_spec,
)
from voyager_creative_critic import VerifiedCreativeCritic
from voyager_runtime_contract import attest_original_runtime, write_attestation
from voyager_metrics import attach_role_metrics
def env(name: str, default: str) -> str:
return os.environ.get(name, default)
def main() -> None:
from voyager_learning import refresh_learning_state, start_learning_watcher
# Surface Mineflayer's subprocess logs as well as Python tracebacks.
logging.basicConfig(level=logging.INFO, format="[%(name)s] %(levelname)s %(message)s")
model = env("VOYAGER_MODEL", "muse-spark-1.3-contributor-free")
aux_model = env("VOYAGER_AUX_MODEL", "muse-spark-1.3-contributor-free")
base_url = env("VOYAGER_OPENAI_BASE", "http://127.0.0.1:7864/v1")
api_key = env("VOYAGER_API_KEY", "local-voyager")
ckpt_dir = env("VOYAGER_CKPT_DIR", "/persist/minecraft-friends/voyager/ckpt-20261003-normal-v1")
bot_username = env("MC_USERNAME", "Naldo_Bot")
brain_profile = env("VOYAGER_BRAIN_PROFILE", "")
if bot_username == "Naldo_Bot":
try:
from naldo_opencode import (
ALLOWED_MODELS as NALDO_ALLOWED_MODELS,
BRAIN_PROFILE as NALDO_BRAIN_PROFILE,
BRIDGE_BASE as NALDO_BRIDGE_BASE,
is_allowed_model as _naldo_model_allowed,
)
except Exception as exc:
raise RuntimeError(f"Naldo OpenCode brain module unavailable: {exc}") from exc
if (
brain_profile != NALDO_BRAIN_PROFILE
or base_url != NALDO_BRIDGE_BASE
or not _naldo_model_allowed(model)
or not _naldo_model_allowed(aux_model)
):
raise RuntimeError(
f"Brain lock rejected {bot_username}: profile={brain_profile!r}, base={base_url!r}, "
f"model={model!r}, aux={aux_model!r}; Naldo requires profile={NALDO_BRAIN_PROFILE!r}, "
f"base={NALDO_BRIDGE_BASE!r}, models in {sorted(NALDO_ALLOWED_MODELS)} (opencode provider only)"
)
expected = None
elif bot_username == "Gemini_Bot":
expected = ("gemini-cli", "http://127.0.0.1:7863/v1", "gemini-3.5-flash-lite", "gemini-3.5-flash-lite")
else:
expected = None
if expected and (brain_profile, base_url, model, aux_model) != expected:
raise RuntimeError(
f"Brain lock rejected {bot_username}: profile={brain_profile!r}, base={base_url!r}, "
f"model={model!r}, aux={aux_model!r}"
)
ckpt = Path(ckpt_dir)
required_resume_defaults = {
ckpt / "action" / "chest_memory.json": {},
ckpt / "curriculum" / "completed_tasks.json": [],
ckpt / "curriculum" / "failed_tasks.json": [],
ckpt / "curriculum" / "qa_cache.json": {},
ckpt / "skill" / "skills.json": {},
}
core_state = [
ckpt / "curriculum" / "completed_tasks.json",
ckpt / "curriculum" / "failed_tasks.json",
ckpt / "skill" / "skills.json",
]
has_persisted_state = any(path.exists() for path in core_state)
if has_persisted_state:
skills_json = ckpt / "skill" / "skills.json"
vector_dir = ckpt / "skill" / "vectordb"
if not skills_json.exists() and vector_dir.exists():
shutil.rmtree(vector_dir, ignore_errors=True)
print("[voyager] removed orphaned derived skill vectordb", flush=True)
for path, default in required_resume_defaults.items():
if not path.exists():
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(default, indent=2) + "\n", encoding="utf-8")
print(f"[voyager] repaired missing checkpoint file: {path.name}", flush=True)
required_resume_files = list(required_resume_defaults)
resume = all(path.exists() for path in required_resume_files)
# Keep long-lived curriculum memory compact and consistent. Old retries can
# leave the same task in failed_tasks many times, including tasks that were
# later completed under a spelling variant such as copper ore/copper_ore.
if resume:
def _norm_task(value: str):
return task_spec(value) or " ".join(str(value).lower().replace("_", " ").split())
completed_path = ckpt / "curriculum" / "completed_tasks.json"
failed_path = ckpt / "curriculum" / "failed_tasks.json"
try:
original_completed = json.loads(completed_path.read_text(encoding="utf-8"))
original_failed = json.loads(failed_path.read_text(encoding="utf-8"))
archive = ckpt / "quarantine" / "curriculum_history_before_semantic_compaction_v1.json"
if not archive.exists():
archive.parent.mkdir(parents=True, exist_ok=True)
archive.write_text(json.dumps({"completed": original_completed, "failed": original_failed}, indent=2) + "\n", encoding="utf-8")
def unique(values):
seen, result = set(), []
for value in values:
key = _norm_task(value)
if key not in seen:
seen.add(key)
result.append(str(value))
return result
completed = unique(original_completed)
completed_norm = {_norm_task(item) for item in completed}
failed = [item for item in unique(original_failed) if _norm_task(item) not in completed_norm]
# One-time repair for failures recorded while the Creative helper
# context file was missing. Those attempts never reached the model
# or Minecraft action, so they are not genuine task failures.
repair_marker = ckpt / ".creative_context_repair_v1"
if not repair_marker.exists():
failed = [item for item in failed if _norm_task(item) != "mine 1 wood log"]
repair_marker.write_text("ok\n", encoding="utf-8")
print("[voyager] removed false starter-task failure from missing Creative context", flush=True)
# Granite called placeBlocks("brick") / ("brick_block") for the
# legitimate "Place 1 bricks" task. Both failed before changing
# the world. Archive the failure before allowing one fresh attempt
# with the exact task ID bound by the Action parser.
bricks_marker = ckpt / ".bricks_block_id_repair_v1"
if bot_username == "Naldo_Bot" and not bricks_marker.exists():
if "Place 1 bricks" in failed and "Place 1 bricks" not in completed:
bricks_archive = ckpt / "quarantine" / "bricks_block_id_failure_v1.json"
bricks_archive.parent.mkdir(parents=True, exist_ok=True)
bricks_archive.write_text(json.dumps({"failed": original_failed}, indent=2) + "\n", encoding="utf-8")
failed = [item for item in failed if item != "Place 1 bricks"]
print("[voyager] archived invalid brick-ID failure for one retry", flush=True)
bricks_marker.write_text("ok\n", encoding="utf-8")
# The Babel bridge briefly rejected a Python len() call on its
# JavaScript Proxy, so these two logged tasks never executed.
# Keep the original failure records before allowing one retry.
proxy_marker = ckpt / ".action_proxy_length_repair_v1"
if not proxy_marker.exists():
retryable = {
"Naldo_Bot": {"Place 2 red_concrete"},
"Gemini_Bot": {"Place 1 emerald_block"},
}.get(bot_username, set())
removed = [item for item in failed if item in retryable and item not in completed]
if removed:
proxy_archive = ckpt / "quarantine" / "action_proxy_length_failures_v1.json"
proxy_archive.parent.mkdir(parents=True, exist_ok=True)
proxy_archive.write_text(json.dumps({"failed": original_failed, "retryable": removed}, indent=2) + "\n", encoding="utf-8")
failed = [item for item in failed if item not in removed]
print(f"[voyager] archived Action Proxy parser failures: {', '.join(removed)}", flush=True)
proxy_marker.write_text("ok\n", encoding="utf-8")
for path, values, original in ((completed_path, completed, original_completed), (failed_path, failed, original_failed)):
if values != original:
temporary = path.with_suffix(".json.tmp")
temporary.write_text(json.dumps(values, indent=2) + "\n", encoding="utf-8")
temporary.replace(path)
except Exception as exc:
print(f"[voyager] checkpoint curriculum cleanup skipped: {exc}", flush=True)
refresh_learning_state(ckpt, bot_username)
start_learning_watcher(ckpt, bot_username)
os.environ["OPENAI_API_BASE"] = base_url
os.environ["OPENAI_API_KEY"] = api_key
print(f"[voyager] upstream={VOYAGER_HOME}", flush=True)
print(f"[voyager] openai_base={base_url}", flush=True)
print(f"[voyager] minecraft=127.0.0.1:{env('MC_PORT', '25565')}", flush=True)
print(f"[voyager] model={model}", flush=True)
print(f"[voyager] auxiliary_roles_model={aux_model}", flush=True)
print(f"[voyager] bot_username={bot_username}", flush=True)
print(f"[voyager] brain_profile={brain_profile}", flush=True)
print(f"[voyager] checkpoint={ckpt_dir} resume={resume}", flush=True)
voyager = Voyager(
mc_port=int(env("MC_PORT", "25565")),
openai_api_key=api_key,
server_port=int(env("MINEFLAYER_API_PORT", "3000")),
env_wait_ticks=int(env("ENV_WAIT_TICKS", "20")),
env_request_timeout=int(env("ENV_REQUEST_TIMEOUT", "600")),
max_iterations=int(env("MAX_ITERATIONS", "160")),
action_agent_model_name=model,
action_agent_temperature=0,
action_agent_task_max_retries=4,
action_agent_show_chat_log=True,
curriculum_agent_model_name=model,
curriculum_agent_temperature=0,
curriculum_agent_qa_model_name=aux_model,
curriculum_agent_qa_temperature=0,
critic_agent_model_name=model,
critic_agent_temperature=0,
skill_manager_model_name=aux_model,
skill_manager_temperature=0,
skill_manager_retrieval_top_k=5,
openai_api_request_timeout=int(env("OPENAI_API_REQUEST_TIMEOUT", "240")),
ckpt_dir=ckpt_dir,
resume=resume,
)
attestation = attest_original_runtime(voyager, VOYAGER_HOME)
attach_role_metrics(voyager, bot_username)
voyager.critic_agent = VerifiedCreativeCritic(voyager.critic_agent)
write_attestation(env("VOYAGER_ATTESTATION_FILE", ""), attestation, bot_username)
print(
f"[voyager] verified original engine {attestation['upstream_commit']} for "
f"{bot_username}; decision methods attested: "
f"{', '.join(attestation['decision_methods_verified'])}; official Critic with "
f"independent world evidence",
flush=True,
)
# Remove the known stale Creative-mode mining skill produced before the
# mineBlock postcondition was fixed. In Creative, breaking copper ore does
# not imply a copper_ingot drop, so that cached skill teaches a false check.
stale = voyager.skill_manager.skills.get("mineCopperOre")
stale_code = str(stale.get("code", "")) if isinstance(stale, dict) else ""
if stale and ("copper_ingot" in stale_code or "raw_copper" in stale_code):
try:
voyager.skill_manager.vectordb._collection.delete(ids=["mineCopperOre"])
except Exception:
pass
voyager.skill_manager.skills.pop("mineCopperOre", None)
skills_path = ckpt / "skill" / "skills.json"
skills_path.write_text(
json.dumps(voyager.skill_manager.skills, indent=2) + "\n",
encoding="utf-8",
)
for folder, suffix in (("code", "*.js"), ("description", "*.txt")):
for path in (ckpt / "skill" / folder).glob("mineCopperOre" + suffix[1:]):
try:
path.unlink()
except OSError:
pass
try:
voyager.skill_manager.vectordb.persist()
except Exception:
pass
print("[voyager] removed stale Creative mineCopperOre skill", flush=True)
def _progress_fingerprint(directory: Path) -> str:
# Process-alive is not progress: readiness stayed true while position
# froze and no model calls advanced. Fingerprint the checkpoint that
# only moves when tasks complete/fail plus the newest curriculum file
# mtime, so a wedged bridge/Mineflayer is distinguishable from slow
# inference.
try:
completed = (directory / "curriculum" / "completed_tasks.json").read_text(encoding="utf-8")
except OSError:
completed = ""
try:
failed = (directory / "curriculum" / "failed_tasks.json").read_text(encoding="utf-8")
except OSError:
failed = ""
mtimes: list[str] = []
for relative in ("curriculum/completed_tasks.json", "curriculum/failed_tasks.json", "skill/skills.json"):
try:
mtimes.append(str((directory / relative).stat().st_mtime_ns))
except OSError:
mtimes.append("-")
return f"{len(completed)}:{len(failed)}:{'|'.join(mtimes)}"
stall_seconds = int(float(env("VOYAGER_STALL_SECONDS", "1800")))
_stall_state: dict[str, object] = {"fingerprint": _progress_fingerprint(ckpt), "at": time.monotonic()}
def _stall_watch() -> None:
while True:
time.sleep(60)
try:
current = _progress_fingerprint(ckpt)
except Exception:
continue
now = time.monotonic()
if current != _stall_state["fingerprint"]:
_stall_state["fingerprint"] = current
_stall_state["at"] = now
continue
if now - float(_stall_state["at"]) >= stall_seconds: # type: ignore[arg-type]
print(
f"[voyager] STALL WATCHDOG: no checkpoint progress for {stall_seconds}s; "
"exiting so the stack watchdog can hard-reset Mineflayer and resume",
flush=True,
)
os._exit(75)
threading.Thread(target=_stall_watch, daemon=True, name="voyager-stall-watch").start()
# Supported upstream option: retain the live connection and observe every step.
# The existing disconnect recovery and process watchdog still reconnect on failure.
try:
quarantine_invalid_skills(voyager, ckpt)
quarantine_template_skills(voyager, ckpt)
voyager.learn(reset_env=False)
finally:
# Upstream's non-daemon log-monitor thread keeps Python alive after a
# fatal curriculum error. Stop only this runner's Mineflayer child so
# the existing watchdog can observe exit and resume the checkpoint.
process = voyager.env.mineflayer.process
if process is not None and process.poll() is None:
try:
process.terminate()
process.wait(timeout=10)
except Exception:
process.kill()
process.wait(timeout=5)
if __name__ == "__main__":
main()