|
| 1 | +"""Upgrade-only repair of plans created while answering an informational question.""" |
| 2 | + |
| 3 | +import json |
| 4 | +import re |
| 5 | +from pathlib import Path |
| 6 | + |
| 7 | +from rlm.v100.common import atomic_json |
| 8 | + |
| 9 | + |
| 10 | +def repair(root: Path) -> dict: |
| 11 | + from rlm.v100.mission import status |
| 12 | + from rlm.v100.mission_chat import connect, conversation_question |
| 13 | + |
| 14 | + if status(root)["running"]: |
| 15 | + raise ValueError("Stop the mission before recovering chat-created plans") |
| 16 | + directory = root / "research/state/chat-question-repairs" |
| 17 | + repaired = [] |
| 18 | + with connect(root) as db: |
| 19 | + rows = db.execute( |
| 20 | + "SELECT * FROM requests WHERE state='completed' ORDER BY rowid DESC LIMIT 500" |
| 21 | + ).fetchall() |
| 22 | + for row in rows: |
| 23 | + if not conversation_question(row["message"]): |
| 24 | + continue |
| 25 | + response = json.loads(row["response"]) |
| 26 | + receipts = [ |
| 27 | + r for r in response.get("applied", []) if r.get("horizon") in ("short", "mid") |
| 28 | + ] |
| 29 | + if not receipts: |
| 30 | + continue |
| 31 | + if not re.fullmatch(r"[a-f0-9]{32}", row["id"]): |
| 32 | + raise ValueError("Invalid archived question identity") |
| 33 | + archive = directory / (row["id"] + ".json") |
| 34 | + if archive.exists(): |
| 35 | + continue |
| 36 | + path = root / "research/plans/current.json" |
| 37 | + plans = json.loads(path.read_text()) if path.exists() else {} |
| 38 | + restored = [] |
| 39 | + for receipt in receipts: |
| 40 | + horizon, identity = receipt["horizon"], receipt.get("id", "") |
| 41 | + if ( |
| 42 | + not re.fullmatch(r"[a-f0-9]{32}", identity) |
| 43 | + or plans.get(horizon, {}).get("id") != identity |
| 44 | + ): |
| 45 | + continue |
| 46 | + version = json.loads((root / "research/plans" / (identity + ".json")).read_text()) |
| 47 | + previous = version.get("previous") or {} |
| 48 | + # A later goal change must not resurrect an unrelated earlier goal's plan. |
| 49 | + if previous.get("goal_id") not in (None, plans[horizon].get("goal_id")): |
| 50 | + continue |
| 51 | + plans[horizon] = previous |
| 52 | + restored.append(horizon) |
| 53 | + atomic_json( |
| 54 | + archive, |
| 55 | + { |
| 56 | + "request": dict(row), |
| 57 | + "restored_plans": restored, |
| 58 | + "scope": "Question-created plan reverted only if its exact ID is still active; later plans and original evidence retained.", |
| 59 | + }, |
| 60 | + ) |
| 61 | + if restored: |
| 62 | + atomic_json( |
| 63 | + archive.with_name(row["id"] + "-plans-before.json"), |
| 64 | + json.loads(path.read_text()), |
| 65 | + ) |
| 66 | + atomic_json(path, plans) |
| 67 | + db.execute( |
| 68 | + "UPDATE requests SET state='queued',response=NULL,attempts=0,error=NULL,updated=0,wait_started=0 WHERE id=?", |
| 69 | + (row["id"],), |
| 70 | + ) |
| 71 | + repaired.append({"id": row["id"], "restored_plans": restored, "archive": str(archive)}) |
| 72 | + return {"requeued_questions": repaired, "long_term_goal_changed": False} |
0 commit comments