Spaces:
Runtime error
Runtime error
Upload sync_hf.py
Browse files- scripts/sync_hf.py +145 -0
scripts/sync_hf.py
CHANGED
|
@@ -21,6 +21,14 @@ Fixes for exit 78 (gateway startup gates):
|
|
| 21 |
- Self-heal remaining 'requires migration' gates: park the file named in the
|
| 22 |
startup error and restart the gateway (gates fire one at a time)
|
| 23 |
- Surface doctor failures instead of ignoring the exit code
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
"""
|
| 25 |
|
| 26 |
import os
|
|
@@ -1147,6 +1155,8 @@ class OpenClawFullSync:
|
|
| 1147 |
"""Run a CLI command, streaming tagged output. Returns exit code or -1."""
|
| 1148 |
print(f"[SYNC] Running: {' '.join(cmd)} (timeout={timeout}s)")
|
| 1149 |
t0 = time.time()
|
|
|
|
|
|
|
| 1150 |
try:
|
| 1151 |
proc = subprocess.Popen(
|
| 1152 |
cmd, cwd=str(APP_DIR), env=env,
|
|
@@ -1160,6 +1170,7 @@ class OpenClawFullSync:
|
|
| 1160 |
|
| 1161 |
def pump():
|
| 1162 |
for line in proc.stdout:
|
|
|
|
| 1163 |
print(f"[{tag} +{time.time() - t0:.0f}s] {line.rstrip()}", flush=True)
|
| 1164 |
th = threading.Thread(target=pump, daemon=True)
|
| 1165 |
th.start()
|
|
@@ -1171,8 +1182,10 @@ class OpenClawFullSync:
|
|
| 1171 |
os.killpg(proc.pid, signal.SIGKILL)
|
| 1172 |
except Exception:
|
| 1173 |
proc.kill()
|
|
|
|
| 1174 |
return -1
|
| 1175 |
th.join(timeout=5)
|
|
|
|
| 1176 |
print(f"[SYNC] {tag} exit code: {rc} ({time.time() - t0:.0f}s)")
|
| 1177 |
return rc
|
| 1178 |
|
|
@@ -1188,8 +1201,32 @@ class OpenClawFullSync:
|
|
| 1188 |
if rc != 0:
|
| 1189 |
print(f"[SYNC] WARNING: doctor --fix failed (exit {rc}) β continuing; "
|
| 1190 |
"session migration runs next and startup gates self-heal")
|
|
|
|
| 1191 |
return rc == 0
|
| 1192 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1193 |
# ββ Session store migration (exit 78 gate) ββββββββββββββββββββββββ
|
| 1194 |
|
| 1195 |
def _find_legacy_session_stores(self):
|
|
@@ -1387,6 +1424,103 @@ class OpenClawFullSync:
|
|
| 1387 |
|
| 1388 |
return False
|
| 1389 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1390 |
# ββ Application runner ββββββββββββββββββββββββββββββββββββββββββββ
|
| 1391 |
|
| 1392 |
def run_openclaw(self):
|
|
@@ -1420,6 +1554,16 @@ class OpenClawFullSync:
|
|
| 1420 |
log_fh = open(log_file, "a")
|
| 1421 |
env = os.environ.copy()
|
| 1422 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1423 |
# Telegram probe
|
| 1424 |
if TELEGRAM_API_BASE:
|
| 1425 |
tg_root = TELEGRAM_API_BASE.rstrip("/")
|
|
@@ -1475,6 +1619,7 @@ class OpenClawFullSync:
|
|
| 1475 |
def health_worker():
|
| 1476 |
healthy = self._wait_for_gateway_health(timeout=STARTUP_HEALTH_TIMEOUT)
|
| 1477 |
if not healthy:
|
|
|
|
| 1478 |
print("[HEALTH] β Gateway unhealthy β dumping last 80 lines of startup.log")
|
| 1479 |
try:
|
| 1480 |
with open(log_file, "r") as f:
|
|
|
|
| 21 |
- Self-heal remaining 'requires migration' gates: park the file named in the
|
| 22 |
startup error and restart the gateway (gates fire one at a time)
|
| 23 |
- Surface doctor failures instead of ignoring the exit code
|
| 24 |
+
|
| 25 |
+
Fixes for silent gateway startup hang (no healthz, no log output):
|
| 26 |
+
- Run the exact 'openclaw agents add main ... --non-interactive' command that
|
| 27 |
+
doctor prints when it holds back an unverified agent database
|
| 28 |
+
- Force OPENCLAW_LOG_LEVEL=debug + OPENCLAW_GATEWAY_STARTUP_TRACE on launch
|
| 29 |
+
so subsystem logs reach the console regardless of logging.consoleLevel
|
| 30 |
+
- On health timeout: SIGUSR2 (--report-on-signal) to the gateway node tree,
|
| 31 |
+
then print /proc state and the Node diagnostic report javascriptStack
|
| 32 |
"""
|
| 33 |
|
| 34 |
import os
|
|
|
|
| 1155 |
"""Run a CLI command, streaming tagged output. Returns exit code or -1."""
|
| 1156 |
print(f"[SYNC] Running: {' '.join(cmd)} (timeout={timeout}s)")
|
| 1157 |
t0 = time.time()
|
| 1158 |
+
self._last_cmd_output = ""
|
| 1159 |
+
captured = []
|
| 1160 |
try:
|
| 1161 |
proc = subprocess.Popen(
|
| 1162 |
cmd, cwd=str(APP_DIR), env=env,
|
|
|
|
| 1170 |
|
| 1171 |
def pump():
|
| 1172 |
for line in proc.stdout:
|
| 1173 |
+
captured.append(line)
|
| 1174 |
print(f"[{tag} +{time.time() - t0:.0f}s] {line.rstrip()}", flush=True)
|
| 1175 |
th = threading.Thread(target=pump, daemon=True)
|
| 1176 |
th.start()
|
|
|
|
| 1182 |
os.killpg(proc.pid, signal.SIGKILL)
|
| 1183 |
except Exception:
|
| 1184 |
proc.kill()
|
| 1185 |
+
self._last_cmd_output = "".join(captured)
|
| 1186 |
return -1
|
| 1187 |
th.join(timeout=5)
|
| 1188 |
+
self._last_cmd_output = "".join(captured)
|
| 1189 |
print(f"[SYNC] {tag} exit code: {rc} ({time.time() - t0:.0f}s)")
|
| 1190 |
return rc
|
| 1191 |
|
|
|
|
| 1201 |
if rc != 0:
|
| 1202 |
print(f"[SYNC] WARNING: doctor --fix failed (exit {rc}) β continuing; "
|
| 1203 |
"session migration runs next and startup gates self-heal")
|
| 1204 |
+
self._run_doctor_agent_restore_hint()
|
| 1205 |
return rc == 0
|
| 1206 |
|
| 1207 |
+
def _run_doctor_agent_restore_hint(self):
|
| 1208 |
+
"""Run the exact 'openclaw agents add ...' command doctor prints when it
|
| 1209 |
+
holds back an unverified agent database (upstream's non-interactive fix)."""
|
| 1210 |
+
text = getattr(self, "_last_cmd_output", "") or ""
|
| 1211 |
+
entry = self._entry_cmd()
|
| 1212 |
+
if not entry:
|
| 1213 |
+
return False
|
| 1214 |
+
phrases = re.findall(r"run (openclaw agents add main [^;\n]*?--non-interactive)", text)
|
| 1215 |
+
if not phrases:
|
| 1216 |
+
return False
|
| 1217 |
+
ran = False
|
| 1218 |
+
for phrase in list(dict.fromkeys(phrases))[:4]:
|
| 1219 |
+
words = phrase.split()
|
| 1220 |
+
if words and words[0] == "openclaw":
|
| 1221 |
+
words = words[1:]
|
| 1222 |
+
if not words or any(not re.fullmatch(r"[A-Za-z0-9_.:/~+-]+", w) for w in words):
|
| 1223 |
+
continue
|
| 1224 |
+
print("[SYNC] Doctor held back an agent database β running its suggested restore")
|
| 1225 |
+
rc = self._run_logged_cmd(entry + words, self._doctor_env(), 180, tag="AGENT-ADD")
|
| 1226 |
+
if rc == 0:
|
| 1227 |
+
ran = True
|
| 1228 |
+
return ran
|
| 1229 |
+
|
| 1230 |
# ββ Session store migration (exit 78 gate) ββββββββββββββββββββββββ
|
| 1231 |
|
| 1232 |
def _find_legacy_session_stores(self):
|
|
|
|
| 1424 |
|
| 1425 |
return False
|
| 1426 |
|
| 1427 |
+
def _diagnose_hung_gateway(self, process):
|
| 1428 |
+
"""Capture why the gateway never became healthy: liveness, /proc state,
|
| 1429 |
+
and Node diagnostic report javascriptStack (SIGUSR2 needs
|
| 1430 |
+
--report-on-signal in NODE_OPTIONS). Best-effort, never raises."""
|
| 1431 |
+
try:
|
| 1432 |
+
if process.poll() is not None:
|
| 1433 |
+
print(f"[HEALTH] Gateway process already exited: code={process.returncode}")
|
| 1434 |
+
return
|
| 1435 |
+
pid = process.pid
|
| 1436 |
+
print(f"[HEALTH] Gateway process alive: pid={pid}")
|
| 1437 |
+
|
| 1438 |
+
def read_proc(p):
|
| 1439 |
+
info = {}
|
| 1440 |
+
try:
|
| 1441 |
+
with open(f"/proc/{p}/status") as f:
|
| 1442 |
+
for line in f:
|
| 1443 |
+
if line.startswith(("Name:", "State:", "Threads:",
|
| 1444 |
+
"voluntary_ctxt_switches:",
|
| 1445 |
+
"nonvoluntary_ctxt_switches:")):
|
| 1446 |
+
k, _, v = line.partition(":")
|
| 1447 |
+
info[k] = v.strip()
|
| 1448 |
+
except Exception:
|
| 1449 |
+
pass
|
| 1450 |
+
try:
|
| 1451 |
+
with open(f"/proc/{p}/wchan") as f:
|
| 1452 |
+
info["wchan"] = f.read().strip()
|
| 1453 |
+
except Exception:
|
| 1454 |
+
pass
|
| 1455 |
+
return info
|
| 1456 |
+
|
| 1457 |
+
def node_tree(p, depth=0, acc=None):
|
| 1458 |
+
if acc is None:
|
| 1459 |
+
acc = []
|
| 1460 |
+
if depth > 3:
|
| 1461 |
+
return acc
|
| 1462 |
+
acc.append(p)
|
| 1463 |
+
try:
|
| 1464 |
+
with open(f"/proc/{p}/task/{p}/children") as f:
|
| 1465 |
+
kids = [int(x) for x in f.read().split()]
|
| 1466 |
+
except Exception:
|
| 1467 |
+
kids = []
|
| 1468 |
+
for k in kids:
|
| 1469 |
+
node_tree(k, depth + 1, acc)
|
| 1470 |
+
return acc
|
| 1471 |
+
|
| 1472 |
+
def is_node(p):
|
| 1473 |
+
try:
|
| 1474 |
+
with open(f"/proc/{p}/comm") as f:
|
| 1475 |
+
return f.read().strip() == "node"
|
| 1476 |
+
except Exception:
|
| 1477 |
+
return False
|
| 1478 |
+
|
| 1479 |
+
print(f"[HEALTH] proc: {read_proc(pid)}")
|
| 1480 |
+
tree = [p for p in node_tree(pid) if is_node(p)]
|
| 1481 |
+
print(f"[HEALTH] node process tree: {tree}")
|
| 1482 |
+
|
| 1483 |
+
if not hasattr(signal, "SIGUSR2"):
|
| 1484 |
+
print("[HEALTH] SIGUSR2 unavailable on this platform")
|
| 1485 |
+
return
|
| 1486 |
+
for p in tree:
|
| 1487 |
+
try:
|
| 1488 |
+
os.kill(p, signal.SIGUSR2)
|
| 1489 |
+
print(f"[HEALTH] Sent SIGUSR2 to node pid {p}")
|
| 1490 |
+
except Exception as e:
|
| 1491 |
+
print(f"[HEALTH] SIGUSR2 pid {p}: {e}")
|
| 1492 |
+
time.sleep(6)
|
| 1493 |
+
|
| 1494 |
+
found = False
|
| 1495 |
+
now = time.time()
|
| 1496 |
+
app = Path(APP_DIR)
|
| 1497 |
+
for f in sorted(app.glob("*.json")):
|
| 1498 |
+
try:
|
| 1499 |
+
if now - f.stat().st_mtime > 120:
|
| 1500 |
+
continue
|
| 1501 |
+
txt = f.read_text(encoding="utf-8", errors="replace")
|
| 1502 |
+
if '"javascriptStack"' not in txt:
|
| 1503 |
+
continue
|
| 1504 |
+
data = json.loads(txt)
|
| 1505 |
+
except Exception:
|
| 1506 |
+
continue
|
| 1507 |
+
found = True
|
| 1508 |
+
print(f"[HEALTH] Node report: {f.name}")
|
| 1509 |
+
print(f"[REPORT] header: {json.dumps(data.get('header'))[:400]}")
|
| 1510 |
+
js = data.get("javascriptStack") or {}
|
| 1511 |
+
for key, stack in js.items():
|
| 1512 |
+
if not isinstance(stack, list):
|
| 1513 |
+
continue
|
| 1514 |
+
limit = 80 if key == "main" else 12
|
| 1515 |
+
prefix = "[REPORT]" if key == "main" else f"[REPORT][{key}]"
|
| 1516 |
+
for ln in stack[:limit]:
|
| 1517 |
+
print(f"{prefix} {ln}")
|
| 1518 |
+
if not found:
|
| 1519 |
+
print("[HEALTH] No Node diagnostic report found in "
|
| 1520 |
+
f"{app} (child may not support SIGUSR2)")
|
| 1521 |
+
except Exception as e:
|
| 1522 |
+
print(f"[HEALTH] diagnose failed: {e}")
|
| 1523 |
+
|
| 1524 |
# ββ Application runner ββββββββββββββββββββββββββββββββββββββββββββ
|
| 1525 |
|
| 1526 |
def run_openclaw(self):
|
|
|
|
| 1554 |
log_fh = open(log_file, "a")
|
| 1555 |
env = os.environ.copy()
|
| 1556 |
|
| 1557 |
+
# Startup diagnostics β force gateway visibility + Node crash reports.
|
| 1558 |
+
# logging.consoleLevel in the dataset config may hide info-level lines;
|
| 1559 |
+
# OPENCLAW_LOG_LEVEL=debug overrides it and the startup-trace step
|
| 1560 |
+
# timings log at info (visible under debug). --report-on-signal lets the
|
| 1561 |
+
# health-timeout path trigger a Node diagnostic report via SIGUSR2.
|
| 1562 |
+
env.setdefault("OPENCLAW_LOG_LEVEL", "debug")
|
| 1563 |
+
env.setdefault("OPENCLAW_GATEWAY_STARTUP_TRACE", "1")
|
| 1564 |
+
if "--report-on-signal" not in env.get("NODE_OPTIONS", ""):
|
| 1565 |
+
env["NODE_OPTIONS"] = (env.get("NODE_OPTIONS", "") + " --report-on-signal").strip()
|
| 1566 |
+
|
| 1567 |
# Telegram probe
|
| 1568 |
if TELEGRAM_API_BASE:
|
| 1569 |
tg_root = TELEGRAM_API_BASE.rstrip("/")
|
|
|
|
| 1619 |
def health_worker():
|
| 1620 |
healthy = self._wait_for_gateway_health(timeout=STARTUP_HEALTH_TIMEOUT)
|
| 1621 |
if not healthy:
|
| 1622 |
+
self._diagnose_hung_gateway(process)
|
| 1623 |
print("[HEALTH] β Gateway unhealthy β dumping last 80 lines of startup.log")
|
| 1624 |
try:
|
| 1625 |
with open(log_file, "r") as f:
|