Powerman9x commited on
Commit
2d89c31
Β·
verified Β·
1 Parent(s): 68c40b6

Upload sync_hf.py

Browse files
Files changed (1) hide show
  1. 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: