Powerman9x commited on
Commit
bd35a8e
Β·
verified Β·
1 Parent(s): 219a343

Update scripts/sync_hf.py

Browse files
Files changed (1) hide show
  1. scripts/sync_hf.py +128 -31
scripts/sync_hf.py CHANGED
@@ -4,10 +4,12 @@ OpenClaw HF Spaces Persistence β€” Full Directory Sync
4
  =====================================================
5
 
6
  Fixes for 2026.9.7 gateway startup hang:
7
- - Purge legacy Telegram thread-bindings before doctor
8
- - Clear stale gateway owner leases from state/openclaw.sqlite
9
- - Remove whatsapp/perplexity plugin entries that expand plugins.allow
10
- - Re-enforce plugins.allow AFTER doctor --fix
 
 
11
  - Use 'gateway run' (foreground) instead of 'gateway' (supervised)
12
  - On health failure, run 'openclaw gateway status' for diagnostics
13
  """
@@ -45,6 +47,7 @@ from huggingface_hub import HfApi, snapshot_download
45
  # ── Logging helper ──────────────────────────────────────────────────────────
46
 
47
  class TeeLogger:
 
48
  def __init__(self, filename, stream):
49
  self.stream = stream
50
  self.file = open(filename, "a", encoding="utf-8")
@@ -65,21 +68,30 @@ OPENCLAW_HOME = Path.home() / ".openclaw"
65
  APP_DIR = Path("/app/openclaw")
66
  DATASET_PATH = ".openclaw"
67
 
 
68
  ZAI_API_KEY = os.environ.get("ZAI_API_KEY", "")
 
 
69
  GATEWAY_TOKEN = os.environ.get("GATEWAY_TOKEN", "huggingclaw")
 
 
70
  AGENT_NAME = os.environ.get("AGENT_NAME", "HuggingClaw")
71
  A2A_PEERS = os.environ.get("A2A_PEERS", "")
 
 
72
  OPENCLAW_DEFAULT_MODEL = os.environ.get("OPENCLAW_DEFAULT_MODEL", "")
73
 
 
74
  SPACE_HOST = os.environ.get("SPACE_HOST", "")
75
  SPACE_ID = os.environ.get("SPACE_ID", "")
76
 
77
- SYNC_INTERVAL = int(os.environ.get("SYNC_INTERVAL", "600"))
78
- MIN_UPLOAD_GAP = int(os.environ.get("MIN_UPLOAD_GAP", "300"))
79
  STARTUP_HEALTH_TIMEOUT = int(os.environ.get("STARTUP_HEALTH_TIMEOUT", "900"))
80
 
81
  AUTO_CREATE_DATASET = os.environ.get("AUTO_CREATE_DATASET", "false").lower() in ("true", "1", "yes")
82
 
 
83
  HF_REPO_ID = os.environ.get("OPENCLAW_DATASET_REPO", "")
84
  if not HF_REPO_ID and SPACE_ID:
85
  HF_REPO_ID = f"{SPACE_ID}-data"
@@ -95,6 +107,7 @@ elif not HF_REPO_ID and HF_TOKEN:
95
  print(f"[SYNC] WARNING: Could not derive username from HF_TOKEN: {e}")
96
  HF_REPO_ID = ""
97
 
 
98
  log_dir = OPENCLAW_HOME / "workspace"
99
  log_dir.mkdir(parents=True, exist_ok=True)
100
  sys.stdout = TeeLogger(log_dir / "sync.log", sys.stdout)
@@ -109,7 +122,9 @@ TELEGRAM_API_BASES = [
109
  "https://telegram-api-proxy-anonymous.pages.dev/api",
110
  ]
111
 
 
112
  def probe_telegram_api(timeout: int = 8) -> str:
 
113
  ctx = ssl.create_default_context()
114
  for base in TELEGRAM_API_BASES:
115
  url = base.rstrip("/") + "/"
@@ -127,9 +142,12 @@ def probe_telegram_api(timeout: int = 8) -> str:
127
  print("[TELEGRAM] WARNING: All API endpoints unreachable!")
128
  return ""
129
 
 
130
  # ── Sync Manager ────────────────────────────────────────────────────────────
131
 
132
  class OpenClawFullSync:
 
 
133
  def __init__(self):
134
  self.enabled = False
135
  self.dataset_exists = False
@@ -209,6 +227,8 @@ class OpenClawFullSync:
209
  dest.parent.mkdir(parents=True, exist_ok=True)
210
  shutil.copy2(str(item), str(dest))
211
  print("[SYNC] βœ“ Restore completed.")
 
 
212
 
213
  except Exception as e:
214
  print(f"[SYNC] βœ— Restore failed: {e}")
@@ -303,12 +323,27 @@ class OpenClawFullSync:
303
  token=HF_TOKEN,
304
  commit_message=f"Sync .openclaw β€” {datetime.now().isoformat()}",
305
  ignore_patterns=[
306
- "*.log", "*.lock", "*.tmp", "*.pid", "__pycache__", "**/__pycache__/**",
307
- "*.bak", "*.migrated", "*.migrated.*", "**/*.migrated", "**/*.migrated.*",
308
- "*.pre-startup-migration-*", "**/*.pre-startup-migration-*", "**/*.corrupt_*",
309
- "**/*.sqlite-wal", "**/*.sqlite-shm", "**/*.sqlite-journal",
310
- "npm/**", "**/node_modules/**", "**/.npm/**", "**/.cache/**", "**/.pnpm-store/**",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
311
  "extensions/**", "**/extensions/**",
 
312
  "**/sessions/*.tmp", "**/tmp/**", "**/.DS_Store",
313
  ],
314
  )
@@ -316,33 +351,86 @@ class OpenClawFullSync:
316
  self._last_upload_time = time.time()
317
  self._last_dir_signature = sig
318
 
 
 
 
 
 
 
 
319
  except Exception as e:
320
  print(f"[SYNC] βœ— Upload failed: {str(e)[:500]}")
321
  if "429" in str(e) or "rate limit" in str(e).lower():
322
  self._cooldown_until = time.time() + 65 * 60
323
  print("[SYNC] Hub rate limit hit β€” pausing uploads for 65 min")
324
 
325
- # ── Pre-gateway legacy cleanup (NEW) ──────────────────────────────
326
 
327
  def _purge_legacy_state(self):
328
- """Remove files and SQLite rows that block 2026.9.7 gateway startup."""
329
  print("[SYNC] ── Purging legacy state that blocks gateway startup ──")
330
 
331
- # 1. Empty Telegram thread-bindings file (issue #161795)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
332
  tb = OPENCLAW_HOME / "telegram" / "thread-bindings-default.json"
333
  if tb.exists():
334
  try:
335
  content = tb.read_text().strip()
336
- if content in ('{"version":1,"bindings":[]}', '{"version":1,"bindings":[]}\n'):
337
- backup = tb.with_suffix(f".blocked_{int(time.time())}")
338
- shutil.move(str(tb), str(backup))
339
  print(f"[SYNC] Removed blocking empty thread-bindings file")
340
- else:
341
- print(f"[SYNC] thread-bindings exists but has content β€” leaving")
342
  except Exception as e:
343
  print(f"[SYNC] Could not process thread-bindings: {e}")
344
 
345
- # 2. Stale gateway owner lease in state/openclaw.sqlite
 
 
 
 
 
 
 
 
 
 
 
 
346
  db_path = OPENCLAW_HOME / "state" / "openclaw.sqlite"
347
  if db_path.exists():
348
  try:
@@ -355,12 +443,12 @@ class OpenClawFullSync:
355
  if count > 0:
356
  cur.execute("DELETE FROM state_leases")
357
  conn.commit()
358
- print(f"[SYNC] Cleared {count} stale gateway lease row(s) from state_leases")
359
  conn.close()
360
  except Exception as e:
361
  print(f"[SYNC] Could not purge state_leases: {e}")
362
 
363
- # 3. Remove whatsapp/perplexity plugin entries that expand plugins.allow
364
  cfg_path = OPENCLAW_HOME / "openclaw.json"
365
  if cfg_path.exists():
366
  try:
@@ -374,11 +462,18 @@ class OpenClawFullSync:
374
  (pid == "a2a-gateway" and A2A_PEERS)
375
  ):
376
  entries.pop(pid)
377
- removed.append(pid)
 
 
 
 
 
 
 
378
  if removed:
379
  with open(cfg_path, "w") as f:
380
  json.dump(data, f, indent=2)
381
- print(f"[SYNC] Removed plugin entries that expand allowlist: {removed}")
382
  except Exception as e:
383
  print(f"[SYNC] Could not clean plugin entries: {e}")
384
 
@@ -533,13 +628,13 @@ class OpenClawFullSync:
533
  "config": {"permissionMode": "approve-all"}
534
  }
535
 
536
- # Telegram
537
  if "telegram" not in data["plugins"]["entries"]:
538
  data["plugins"]["entries"]["telegram"] = {"enabled": True}
539
  elif isinstance(data["plugins"]["entries"]["telegram"], dict):
540
  data["plugins"]["entries"]["telegram"]["enabled"] = True
541
 
542
- # A2A
543
  if A2A_PEERS:
544
  peers = []
545
  for peer_url in A2A_PEERS.split(","):
@@ -642,7 +737,7 @@ class OpenClawFullSync:
642
 
643
  def run_doctor_fix(self):
644
  if os.environ.get("SKIP_DOCTOR") == "1":
645
- print("[SYNC] SKIP_DOCTOR=1 β€” skipping doctor")
646
  return True
647
  if not (APP_DIR / "dist" / "entry.js").exists():
648
  return True
@@ -840,6 +935,7 @@ class OpenClawFullSync:
840
  traceback.print_exc()
841
  return None
842
 
 
843
  # ── Main ────────────────────────────────────────────────────────────────────
844
 
845
  def main():
@@ -855,14 +951,15 @@ def main():
855
  sync.load_from_repo()
856
  print(f"[TIMER] load_from_repo (restore): {time.time() - t0:.1f}s")
857
 
858
- # 2. Purge legacy state BEFORE doctor
859
  t0 = time.time()
860
  sync._purge_legacy_state()
861
- print(f"[TIMER] purge_legacy_state: {time.time() - t0:.1f}s")
862
 
863
- # 3. Run doctor
864
  t0 = time.time()
865
  sync.run_doctor_fix()
 
866
 
867
  # 4. Re-enforce config AFTER doctor (doctor may have expanded allowlist)
868
  t0 = time.time()
@@ -882,7 +979,7 @@ def main():
882
  print(f"[TIMER] run_openclaw launch: {time.time() - t0:.1f}s")
883
  print(f"[TIMER] Total startup (init β†’ app launched): {time.time() - t_main_start:.1f}s")
884
 
885
- # 7. Optional conversation-loop
886
  conv_loop_proc = None
887
  if os.environ.get("RUN_ORCHESTRATOR") == "1":
888
  def run_conversation_loop_forever():
 
4
  =====================================================
5
 
6
  Fixes for 2026.9.7 gateway startup hang:
7
+ - Park all .migrated archives (doctor refuses if these exist)
8
+ - Park inconsistent SQLite databases (state DB caused the 303s read-only timeout)
9
+ - Park empty telegram/thread-bindings-default.json (blocks migration)
10
+ - Remove whatsapp/perplexity plugin + channel entries that expand plugins.allow
11
+ - Add SQLite + .migrated patterns to upload ignore list
12
+ - Re-enforce plugins.allow AFTER doctor --fix (doctor re-expands it)
13
  - Use 'gateway run' (foreground) instead of 'gateway' (supervised)
14
  - On health failure, run 'openclaw gateway status' for diagnostics
15
  """
 
47
  # ── Logging helper ──────────────────────────────────────────────────────────
48
 
49
  class TeeLogger:
50
+ """Duplicate output to stream and file."""
51
  def __init__(self, filename, stream):
52
  self.stream = stream
53
  self.file = open(filename, "a", encoding="utf-8")
 
68
  APP_DIR = Path("/app/openclaw")
69
  DATASET_PATH = ".openclaw"
70
 
71
+ # Z.AI API key (optional)
72
  ZAI_API_KEY = os.environ.get("ZAI_API_KEY", "")
73
+
74
+ # Gateway token
75
  GATEWAY_TOKEN = os.environ.get("GATEWAY_TOKEN", "huggingclaw")
76
+
77
+ # A2A configuration (optional)
78
  AGENT_NAME = os.environ.get("AGENT_NAME", "HuggingClaw")
79
  A2A_PEERS = os.environ.get("A2A_PEERS", "")
80
+
81
+ # Default model (optional)
82
  OPENCLAW_DEFAULT_MODEL = os.environ.get("OPENCLAW_DEFAULT_MODEL", "")
83
 
84
+ # HF Spaces built-in env vars
85
  SPACE_HOST = os.environ.get("SPACE_HOST", "")
86
  SPACE_ID = os.environ.get("SPACE_ID", "")
87
 
88
+ SYNC_INTERVAL = int(os.environ.get("SYNC_INTERVAL", "600"))
89
+ MIN_UPLOAD_GAP = int(os.environ.get("MIN_UPLOAD_GAP", "300"))
90
  STARTUP_HEALTH_TIMEOUT = int(os.environ.get("STARTUP_HEALTH_TIMEOUT", "900"))
91
 
92
  AUTO_CREATE_DATASET = os.environ.get("AUTO_CREATE_DATASET", "false").lower() in ("true", "1", "yes")
93
 
94
+ # Dataset repo: auto-derive from SPACE_ID when not explicitly set
95
  HF_REPO_ID = os.environ.get("OPENCLAW_DATASET_REPO", "")
96
  if not HF_REPO_ID and SPACE_ID:
97
  HF_REPO_ID = f"{SPACE_ID}-data"
 
107
  print(f"[SYNC] WARNING: Could not derive username from HF_TOKEN: {e}")
108
  HF_REPO_ID = ""
109
 
110
+ # Setup logging
111
  log_dir = OPENCLAW_HOME / "workspace"
112
  log_dir.mkdir(parents=True, exist_ok=True)
113
  sys.stdout = TeeLogger(log_dir / "sync.log", sys.stdout)
 
122
  "https://telegram-api-proxy-anonymous.pages.dev/api",
123
  ]
124
 
125
+
126
  def probe_telegram_api(timeout: int = 8) -> str:
127
+ """Probe Telegram API endpoints and return the first reachable one."""
128
  ctx = ssl.create_default_context()
129
  for base in TELEGRAM_API_BASES:
130
  url = base.rstrip("/") + "/"
 
142
  print("[TELEGRAM] WARNING: All API endpoints unreachable!")
143
  return ""
144
 
145
+
146
  # ── Sync Manager ────────────────────────────────────────────────────────────
147
 
148
  class OpenClawFullSync:
149
+ """Upload/download the entire ~/.openclaw directory to HF Dataset."""
150
+
151
  def __init__(self):
152
  self.enabled = False
153
  self.dataset_exists = False
 
227
  dest.parent.mkdir(parents=True, exist_ok=True)
228
  shutil.copy2(str(item), str(dest))
229
  print("[SYNC] βœ“ Restore completed.")
230
+ else:
231
+ print("[SYNC] Downloaded snapshot but dir not found. Starting fresh.")
232
 
233
  except Exception as e:
234
  print(f"[SYNC] βœ— Restore failed: {e}")
 
323
  token=HF_TOKEN,
324
  commit_message=f"Sync .openclaw β€” {datetime.now().isoformat()}",
325
  ignore_patterns=[
326
+ # Logs & runtime junk
327
+ "*.log", "*.lock", "*.tmp", "*.pid",
328
+ "__pycache__", "**/__pycache__/**",
329
+ # Doctor-generated archives & backups
330
+ "*.bak",
331
+ "**/*.migrated",
332
+ "**/*.migrated.*",
333
+ "*.pre-startup-migration-*",
334
+ "**/*.pre-startup-migration-*",
335
+ "**/*.corrupt_*",
336
+ # SQLite databases: MUST NOT be synced (inconsistent if written during upload)
337
+ "**/*.sqlite",
338
+ "**/*.sqlite-wal",
339
+ "**/*.sqlite-shm",
340
+ "**/*.sqlite-journal",
341
+ # Plugin / npm caches
342
+ "npm/**", "**/node_modules/**", "**/.npm/**",
343
+ "**/.cache/**", "**/.pnpm-store/**",
344
+ # Extensions shadow copies
345
  "extensions/**", "**/extensions/**",
346
+ # Transient runtime state
347
  "**/sessions/*.tmp", "**/tmp/**", "**/.DS_Store",
348
  ],
349
  )
 
351
  self._last_upload_time = time.time()
352
  self._last_dir_signature = sig
353
 
354
+ try:
355
+ files = self.api.list_repo_files(repo_id=HF_REPO_ID, repo_type="dataset")
356
+ oc_files = [f for f in files if f.startswith(f"{DATASET_PATH}/")]
357
+ print(f"[SYNC] Dataset now has {len(oc_files)} files under {DATASET_PATH}/")
358
+ except Exception:
359
+ pass
360
+
361
  except Exception as e:
362
  print(f"[SYNC] βœ— Upload failed: {str(e)[:500]}")
363
  if "429" in str(e) or "rate limit" in str(e).lower():
364
  self._cooldown_until = time.time() + 65 * 60
365
  print("[SYNC] Hub rate limit hit β€” pausing uploads for 65 min")
366
 
367
+ # ── Pre-gateway legacy cleanup (CRITICAL) ─────────────────────────
368
 
369
  def _purge_legacy_state(self):
370
+ """Remove files and SQLite databases that block gateway startup."""
371
  print("[SYNC] ── Purging legacy state that blocks gateway startup ──")
372
 
373
+ park_dir = Path("/tmp/openclaw-parked-migrated")
374
+ park_dir.mkdir(parents=True, exist_ok=True)
375
+
376
+ # 1. Park all .migrated archive files (doctor refuses if these exist)
377
+ parked = 0
378
+ for pattern in ("**/*.migrated", "**/*.migrated.*"):
379
+ for f in OPENCLAW_HOME.glob(pattern):
380
+ if not f.is_file():
381
+ continue
382
+ try:
383
+ dest = park_dir / (f.name + f".{int(time.time())}")
384
+ shutil.move(str(f), str(dest))
385
+ parked += 1
386
+ except Exception as e:
387
+ print(f"[SYNC] Could not park {f.name}: {e}")
388
+ if parked:
389
+ print(f"[SYNC] Parked {parked} stale .migrated archive(s)")
390
+
391
+ # 2. Park inconsistent SQLite databases (caused the 303s read-only timeout)
392
+ sqlite_parked = 0
393
+ for rel in ("state/openclaw.sqlite",
394
+ "state/openclaw.sqlite-wal",
395
+ "state/openclaw.sqlite-shm",
396
+ "state/openclaw.sqlite-journal"):
397
+ src = OPENCLAW_HOME / rel
398
+ if src.exists():
399
+ try:
400
+ dest = park_dir / (rel.replace("/", "_") + f".{int(time.time())}")
401
+ shutil.move(str(src), str(dest))
402
+ sqlite_parked += 1
403
+ print(f"[SYNC] Parked {rel}")
404
+ except Exception as e:
405
+ print(f"[SYNC] Could not park {rel}: {e}")
406
+ if sqlite_parked:
407
+ print(f"[SYNC] Parked {sqlite_parked} SQLite file(s) β€” gateway will create fresh DB")
408
+
409
+ # 3. Empty Telegram thread-bindings file (issue #161795)
410
  tb = OPENCLAW_HOME / "telegram" / "thread-bindings-default.json"
411
  if tb.exists():
412
  try:
413
  content = tb.read_text().strip()
414
+ if content in ('{"version":1,"bindings":[]}',
415
+ '{"version":1,"bindings":[]}\n'):
416
+ shutil.move(str(tb), str(park_dir / (tb.name + f".{int(time.time())}")))
417
  print(f"[SYNC] Removed blocking empty thread-bindings file")
 
 
418
  except Exception as e:
419
  print(f"[SYNC] Could not process thread-bindings: {e}")
420
 
421
+ # 4. Also park retired Telegram JSON state (doctor demands 2026.9.5 upgrade)
422
+ for rel in ("telegram/update-offset-default.json",
423
+ "agents/main/sessions/sessions.json.telegram-sent-messages.json"):
424
+ src = OPENCLAW_HOME / rel
425
+ if src.exists():
426
+ try:
427
+ dest = park_dir / (rel.replace("/", "_") + f".{int(time.time())}")
428
+ shutil.move(str(src), str(dest))
429
+ print(f"[SYNC] Parked retired Telegram state: {rel}")
430
+ except Exception as e:
431
+ print(f"[SYNC] Could not park {rel}: {e}")
432
+
433
+ # 5. Stale gateway owner lease in state/openclaw.sqlite (if not parked)
434
  db_path = OPENCLAW_HOME / "state" / "openclaw.sqlite"
435
  if db_path.exists():
436
  try:
 
443
  if count > 0:
444
  cur.execute("DELETE FROM state_leases")
445
  conn.commit()
446
+ print(f"[SYNC] Cleared {count} stale gateway lease row(s)")
447
  conn.close()
448
  except Exception as e:
449
  print(f"[SYNC] Could not purge state_leases: {e}")
450
 
451
+ # 6. Remove plugin entries that expand plugins.allow
452
  cfg_path = OPENCLAW_HOME / "openclaw.json"
453
  if cfg_path.exists():
454
  try:
 
462
  (pid == "a2a-gateway" and A2A_PEERS)
463
  ):
464
  entries.pop(pid)
465
+ removed.append(f"plugins.entries.{pid}")
466
+
467
+ # Remove whatsapp channel config so doctor doesn't re-add the plugin
468
+ chans = data.get("channels", {})
469
+ if "whatsapp" in chans:
470
+ chans.pop("whatsapp")
471
+ removed.append("channels.whatsapp")
472
+
473
  if removed:
474
  with open(cfg_path, "w") as f:
475
  json.dump(data, f, indent=2)
476
+ print(f"[SYNC] Removed entries that expand allowlist: {removed}")
477
  except Exception as e:
478
  print(f"[SYNC] Could not clean plugin entries: {e}")
479
 
 
628
  "config": {"permissionMode": "approve-all"}
629
  }
630
 
631
+ # Telegram plugin
632
  if "telegram" not in data["plugins"]["entries"]:
633
  data["plugins"]["entries"]["telegram"] = {"enabled": True}
634
  elif isinstance(data["plugins"]["entries"]["telegram"], dict):
635
  data["plugins"]["entries"]["telegram"]["enabled"] = True
636
 
637
+ # A2A plugin
638
  if A2A_PEERS:
639
  peers = []
640
  for peer_url in A2A_PEERS.split(","):
 
737
 
738
  def run_doctor_fix(self):
739
  if os.environ.get("SKIP_DOCTOR") == "1":
740
+ print("[SYNC] SKIP_DOCTOR=1 β€” skipping doctor entirely")
741
  return True
742
  if not (APP_DIR / "dist" / "entry.js").exists():
743
  return True
 
935
  traceback.print_exc()
936
  return None
937
 
938
+
939
  # ── Main ────────────────────────────────────────────────────────────────────
940
 
941
  def main():
 
951
  sync.load_from_repo()
952
  print(f"[TIMER] load_from_repo (restore): {time.time() - t0:.1f}s")
953
 
954
+ # 2. Purge legacy state BEFORE doctor (park .migrated + SQLite + telegram state)
955
  t0 = time.time()
956
  sync._purge_legacy_state()
957
+ print(f"[TIMER] pre-doctor purge: {time.time() - t0:.1f}s")
958
 
959
+ # 3. Run doctor (skipped if SKIP_DOCTOR=1)
960
  t0 = time.time()
961
  sync.run_doctor_fix()
962
+ print(f"[TIMER] doctor: {time.time() - t0:.1f}s")
963
 
964
  # 4. Re-enforce config AFTER doctor (doctor may have expanded allowlist)
965
  t0 = time.time()
 
979
  print(f"[TIMER] run_openclaw launch: {time.time() - t0:.1f}s")
980
  print(f"[TIMER] Total startup (init β†’ app launched): {time.time() - t_main_start:.1f}s")
981
 
982
+ # 7. Optional conversation-loop (RUN_ORCHESTRATOR=1)
983
  conv_loop_proc = None
984
  if os.environ.get("RUN_ORCHESTRATOR") == "1":
985
  def run_conversation_loop_forever():