| |
| """ |
| OpenClaw HF Spaces Persistence β Full Directory Sync |
| ===================================================== |
| |
| Simplified persistence: upload/download the entire ~/.openclaw directory |
| as-is to/from a Hugging Face Dataset repo. |
| |
| - Startup: snapshot_download β ~/.openclaw |
| - Periodic: upload_folder β dataset openclaw_data/ |
| - Shutdown: final upload_folder β dataset openclaw_data/ |
| """ |
|
|
| import os |
| import sys |
| import time |
| import threading |
| import subprocess |
| import signal |
| import json |
| import shutil |
| import tempfile |
| import traceback |
| import re |
| import urllib.request |
| import ssl |
| from pathlib import Path |
| from datetime import datetime |
| |
| os.environ.setdefault("HF_HUB_DOWNLOAD_TIMEOUT", "300") |
| os.environ.setdefault("HF_HUB_UPLOAD_TIMEOUT", "600") |
| |
| os.environ.setdefault("HF_HUB_DISABLE_PROGRESS_BARS", "1") |
| os.environ.setdefault("HF_HUB_VERBOSITY", "warning") |
|
|
| import logging as _logging |
| _logging.getLogger("huggingface_hub").setLevel(_logging.WARNING) |
| _logging.getLogger("huggingface_hub.utils").setLevel(_logging.WARNING) |
| _logging.getLogger("filelock").setLevel(_logging.WARNING) |
|
|
| from huggingface_hub import HfApi, snapshot_download |
|
|
| |
|
|
| class TeeLogger: |
| """Duplicate output to stream and file.""" |
| def __init__(self, filename, stream): |
| self.stream = stream |
| self.file = open(filename, "a", encoding="utf-8") |
| def write(self, message): |
| self.stream.write(message) |
| self.file.write(message) |
| self.flush() |
| def flush(self): |
| self.stream.flush() |
| self.file.flush() |
| def fileno(self): |
| return self.stream.fileno() |
|
|
| |
|
|
| HF_TOKEN = os.environ.get("HF_TOKEN") |
| OPENCLAW_HOME = Path.home() / ".openclaw" |
| APP_DIR = Path("/app/openclaw") |
|
|
| |
| DATASET_PATH = ".openclaw" |
|
|
| |
| ZAI_API_KEY = os.environ.get("ZAI_API_KEY", "") |
|
|
| |
| GATEWAY_TOKEN = os.environ.get("GATEWAY_TOKEN", "huggingclaw") |
|
|
| |
| AGENT_NAME = os.environ.get("AGENT_NAME", "HuggingClaw") |
| A2A_PEERS = os.environ.get("A2A_PEERS", "") |
|
|
| |
| OPENCLAW_DEFAULT_MODEL = os.environ.get("OPENCLAW_DEFAULT_MODEL", "") |
|
|
| |
| SPACE_HOST = os.environ.get("SPACE_HOST", "") |
| SPACE_ID = os.environ.get("SPACE_ID", "") |
|
|
| SYNC_INTERVAL = int(os.environ.get("SYNC_INTERVAL", "60")) |
| AUTO_CREATE_DATASET = os.environ.get("AUTO_CREATE_DATASET", "false").lower() in ("true", "1", "yes") |
|
|
| |
| |
| |
| HF_REPO_ID = os.environ.get("OPENCLAW_DATASET_REPO", "") |
| if not HF_REPO_ID and SPACE_ID: |
| |
| HF_REPO_ID = f"{SPACE_ID}-data" |
| print(f"[SYNC] OPENCLAW_DATASET_REPO not set β auto-derived from SPACE_ID: {HF_REPO_ID}") |
| elif not HF_REPO_ID and HF_TOKEN: |
| |
| try: |
| _api = HfApi(token=HF_TOKEN) |
| _username = _api.whoami()["name"] |
| HF_REPO_ID = f"{_username}/HuggingClaw-data" |
| print(f"[SYNC] OPENCLAW_DATASET_REPO not set β auto-derived from HF_TOKEN: {HF_REPO_ID}") |
| del _api, _username |
| except Exception as e: |
| print(f"[SYNC] WARNING: Could not derive username from HF_TOKEN: {e}") |
| HF_REPO_ID = "" |
|
|
| |
| log_dir = OPENCLAW_HOME / "workspace" |
| log_dir.mkdir(parents=True, exist_ok=True) |
| sys.stdout = TeeLogger(log_dir / "sync.log", sys.stdout) |
| sys.stderr = sys.stdout |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| |
| TELEGRAM_API_BASE = os.environ.get("TELEGRAM_API_BASE", "") |
|
|
| TELEGRAM_API_BASES = [ |
| "https://api.telegram.org", |
| "https://telegram-api.mykdigi.com", |
| "https://telegram-api-proxy-anonymous.pages.dev/api", |
| ] |
|
|
|
|
| def probe_telegram_api(timeout: int = 8) -> str: |
| """Probe Telegram API endpoints and return the first reachable one. |
| |
| First checks if official api.telegram.org is reachable (HTTP level). |
| If not, tries mirrors. No bot token required β just tests connectivity. |
| Returns the working base URL (without trailing slash), or "" if all fail. |
| """ |
| ctx = ssl.create_default_context() |
| for base in TELEGRAM_API_BASES: |
| url = base.rstrip("/") + "/" |
| try: |
| req = urllib.request.Request(url, method="GET") |
| resp = urllib.request.urlopen(req, timeout=timeout, context=ctx) |
| print(f"[TELEGRAM] β Reachable: {base} (HTTP {resp.status})") |
| return base.rstrip("/") |
| except urllib.error.HTTPError as e: |
| |
| print(f"[TELEGRAM] β Reachable: {base} (HTTP {e.code})") |
| return base.rstrip("/") |
| except Exception as e: |
| reason = str(e)[:80] |
| print(f"[TELEGRAM] β Unreachable: {base} ({reason})") |
| continue |
|
|
| print("[TELEGRAM] WARNING: All API endpoints unreachable!") |
| return "" |
|
|
|
|
| |
|
|
| class OpenClawFullSync: |
| """Upload/download the entire ~/.openclaw directory to HF Dataset.""" |
|
|
| def __init__(self): |
| self.enabled = False |
| self.dataset_exists = False |
| self.api = None |
|
|
| if not HF_TOKEN: |
| print("[SYNC] WARNING: HF_TOKEN not set. Persistence disabled.") |
| return |
| if not HF_REPO_ID: |
| print("[SYNC] WARNING: Could not determine dataset repo (no SPACE_ID or OPENCLAW_DATASET_REPO).") |
| print("[SYNC] Persistence disabled.") |
| return |
|
|
| self.enabled = True |
| self.api = HfApi(token=HF_TOKEN) |
| self.dataset_exists = self._ensure_repo_exists() |
|
|
| |
|
|
| def _ensure_repo_exists(self): |
| """Check if dataset repo exists; auto-create only when AUTO_CREATE_DATASET=true AND HF_TOKEN is set.""" |
| try: |
| self.api.repo_info(repo_id=HF_REPO_ID, repo_type="dataset") |
| print(f"[SYNC] Dataset repo found: {HF_REPO_ID}") |
| return True |
| except Exception: |
| if not AUTO_CREATE_DATASET: |
| print(f"[SYNC] Dataset repo NOT found: {HF_REPO_ID}") |
| print(f"[SYNC] Set AUTO_CREATE_DATASET=true to auto-create.") |
| print(f"[SYNC] Persistence disabled (app will still run normally).") |
| return False |
| print(f"[SYNC] Dataset repo NOT found: {HF_REPO_ID} β creating...") |
| try: |
| self.api.create_repo( |
| repo_id=HF_REPO_ID, |
| repo_type="dataset", |
| private=True, |
| ) |
| print(f"[SYNC] β Dataset repo created: {HF_REPO_ID}") |
| return True |
| except Exception as e: |
| print(f"[SYNC] β Failed to create dataset repo: {e}") |
| return False |
|
|
| |
|
|
| def load_from_repo(self): |
| """Download from dataset β ~/.openclaw""" |
| if not self.enabled: |
| print("[SYNC] Persistence disabled - skipping restore") |
| self._ensure_default_config() |
| self._patch_config() |
| return |
|
|
| if not self.dataset_exists: |
| print(f"[SYNC] Dataset {HF_REPO_ID} does not exist - starting fresh") |
| self._ensure_default_config() |
| self._patch_config() |
| return |
|
|
| print(f"[SYNC] βΆ Restoring ~/.openclaw from dataset {HF_REPO_ID} ...") |
| OPENCLAW_HOME.mkdir(parents=True, exist_ok=True) |
|
|
| try: |
| files = self.api.list_repo_files(repo_id=HF_REPO_ID, repo_type="dataset") |
| openclaw_files = [f for f in files if f.startswith(f"{DATASET_PATH}/")] |
| if not openclaw_files: |
| print(f"[SYNC] No {DATASET_PATH}/ folder in dataset. Starting fresh.") |
| self._ensure_default_config() |
| self._patch_config() |
| return |
|
|
| print(f"[SYNC] Found {len(openclaw_files)} files under {DATASET_PATH}/ in dataset") |
|
|
| with tempfile.TemporaryDirectory() as tmpdir: |
| snapshot_download( |
| repo_id=HF_REPO_ID, |
| repo_type="dataset", |
| allow_patterns=f"{DATASET_PATH}/**", |
| local_dir=tmpdir, |
| token=HF_TOKEN, |
| ) |
| downloaded_root = Path(tmpdir) / DATASET_PATH |
| if downloaded_root.exists(): |
| for item in downloaded_root.rglob("*"): |
| if item.is_file(): |
| rel = item.relative_to(downloaded_root) |
| dest = OPENCLAW_HOME / rel |
| dest.parent.mkdir(parents=True, exist_ok=True) |
| shutil.copy2(str(item), str(dest)) |
| print("[SYNC] β Restore completed.") |
| else: |
| print("[SYNC] Downloaded snapshot but dir not found. Starting fresh.") |
|
|
| except Exception as e: |
| print(f"[SYNC] β Restore failed: {e}") |
| traceback.print_exc() |
|
|
| |
| self._patch_config() |
| self._debug_list_files() |
|
|
| |
|
|
| def save_to_repo(self): |
| """Upload entire ~/.openclaw directory β dataset (all files, no filtering)""" |
| if not self.enabled: |
| return |
| if not OPENCLAW_HOME.exists(): |
| print("[SYNC] ~/.openclaw does not exist, nothing to save.") |
| return |
|
|
| |
| if not self._ensure_repo_exists(): |
| print(f"[SYNC] Dataset {HF_REPO_ID} unavailable - skipping save") |
| return |
|
|
| print(f"[SYNC] βΆ Uploading ~/.openclaw β dataset {HF_REPO_ID}/{DATASET_PATH}/ ...") |
|
|
| try: |
| |
| total_size = 0 |
| file_count = 0 |
| for root, dirs, fls in os.walk(OPENCLAW_HOME): |
| for fn in fls: |
| fp = os.path.join(root, fn) |
| total_size += os.path.getsize(fp) |
| file_count += 1 |
| print(f"[SYNC] Uploading: {file_count} files, {total_size} bytes total") |
|
|
| if file_count == 0: |
| print("[SYNC] Nothing to upload.") |
| return |
|
|
| |
| self.api.upload_folder( |
| folder_path=str(OPENCLAW_HOME), |
| path_in_repo=DATASET_PATH, |
| repo_id=HF_REPO_ID, |
| repo_type="dataset", |
| token=HF_TOKEN, |
| commit_message=f"Sync .openclaw β {datetime.now().isoformat()}", |
| ignore_patterns=[ |
| "*.log", |
| "*.lock", |
| "*.tmp", |
| "*.pid", |
| "__pycache__", |
| ], |
| ) |
| print(f"[SYNC] β Upload completed at {datetime.now().isoformat()}") |
|
|
| |
| try: |
| files = self.api.list_repo_files(repo_id=HF_REPO_ID, repo_type="dataset") |
| oc_files = [f for f in files if f.startswith(f"{DATASET_PATH}/")] |
| print(f"[SYNC] Dataset now has {len(oc_files)} files under {DATASET_PATH}/") |
| except Exception: |
| pass |
|
|
| except Exception as e: |
| print(f"[SYNC] β Upload failed: {e}") |
| traceback.print_exc() |
|
|
| |
|
|
| def _ensure_default_config(self): |
| config_path = OPENCLAW_HOME / "openclaw.json" |
| if config_path.exists(): |
| return |
| default_src = Path(__file__).parent / "openclaw.json.default" |
| if default_src.exists(): |
| shutil.copy2(str(default_src), str(config_path)) |
| |
| try: |
| with open(config_path, "r") as f: |
| cfg = json.load(f) |
| |
| if "gateway" in cfg: |
| cfg["gateway"]["auth"] = {"token": GATEWAY_TOKEN} |
| |
| |
| |
| providers = cfg.get("models", {}).get("providers", {}) |
| for pid in list(providers.keys()): |
| if str(providers[pid].get("apiKey", "")).startswith("${"): |
| providers.pop(pid, None) |
| with open(config_path, "w") as f: |
| json.dump(cfg, f, indent=2) |
| except Exception as e: |
| print(f"[SYNC] Warning: failed to patch default config: {e}") |
| print("[SYNC] Created openclaw.json from default template") |
| else: |
| with open(config_path, "w") as f: |
| json.dump({ |
| "gateway": { |
| "mode": "local", "bind": "lan", "port": 7860, |
| "trustedProxies": ["0.0.0.0/0"], |
| "controlUi": { |
| "allowInsecureAuth": True, |
| "allowedOrigins": [ |
| "https://huggingface.co" |
| ] |
| } |
| }, |
| "session": {"scope": "global"}, |
| "agents": {"defaults": {"workspace": "~/.openclaw/workspace"}} |
| }, f) |
| print("[SYNC] Created minimal openclaw.json") |
|
|
| def _patch_config(self): |
| """Ensure critical settings after restore.""" |
| config_path = OPENCLAW_HOME / "openclaw.json" |
| if not config_path.exists(): |
| self._ensure_default_config() |
| return |
|
|
| print("[SYNC] Patching configuration...") |
| try: |
| with open(config_path, "r") as f: |
| data = json.load(f) |
| print("[SYNC] Config parsed OK.") |
| except (json.JSONDecodeError, Exception) as e: |
| |
| print(f"[SYNC] Config JSON is corrupt: {e}") |
| backup = config_path.with_suffix(f".corrupt_{int(time.time())}") |
| try: |
| shutil.copy2(config_path, backup) |
| print(f"[SYNC] Backed up corrupt config to {backup.name}") |
| except Exception: |
| pass |
| data = {} |
| print("[SYNC] Starting from clean config.") |
|
|
| try: |
| |
| if "plugins" in data and isinstance(data.get("plugins"), dict): |
| locs = data["plugins"].get("locations", []) |
| if isinstance(locs, list) and "/dev/null" in locs: |
| data["plugins"]["locations"] = [l for l in locs if l != "/dev/null"] |
|
|
| |
| if "auth" in data and isinstance(data.get("auth"), dict): |
| data["auth"].pop("defaultScope", None) |
| if not data["auth"]: |
| del data["auth"] |
| if "gateway" in data and isinstance(data.get("gateway"), dict): |
| auth = data["gateway"].get("auth", {}) |
| if isinstance(auth, dict): |
| auth.pop("scope", None) |
|
|
| |
| |
| allowed_origins = [ |
| "https://huggingface.co", |
| "https://*.hf.space", |
| ] |
| if SPACE_HOST: |
| allowed_origins.append(f"https://{SPACE_HOST}") |
| print(f"[SYNC] SPACE_HOST detected: {SPACE_HOST}") |
| data["gateway"] = { |
| "mode": "local", |
| "bind": "lan", |
| "port": 7860, |
| "auth": {"token": GATEWAY_TOKEN}, |
| "trustedProxies": ["0.0.0.0/0"], |
| "controlUi": { |
| "allowInsecureAuth": True, |
| "dangerouslyDisableDeviceAuth": True, |
| "allowedOrigins": allowed_origins |
| } |
| } |
| print(f"[SYNC] Set gateway config (port=7860, auth=token, origins={len(allowed_origins)})") |
|
|
| |
| data.setdefault("agents", {}).setdefault("defaults", {}).setdefault("model", {}) |
| data.setdefault("session", {})["scope"] = "global" |
| ''' |
| # ββ Provider cleanup βββββββββββββββββββββββββββββββββββββββββββββ |
| # Don't manage providers β let OpenClaw handle them natively from |
| # its own env var support. We only clean up stale providers that |
| # were restored from the dataset backup with hardcoded API keys |
| # that no longer exist in the current environment. |
| restored_providers = data.get("models", {}).get("providers", {}) |
| if restored_providers: |
| stale = [] |
| for pid, pcfg in list(restored_providers.items()): |
| api_key = pcfg.get("apiKey", "") |
| # Skip placeholder-style keys (${VAR}) β OpenClaw resolves these |
| if not api_key or str(api_key).startswith("${"): |
| continue |
| # Hardcoded key from backup β check if it's still in env |
| # We can't know which env var it came from, so check if |
| # the exact key value exists in any current env var |
| if api_key not in os.environ.values(): |
| stale.append(pid) |
| for pid in stale: |
| del restored_providers[pid] |
| print(f"[SYNC] Removed stale provider '{pid}' (API key no longer in environment)") |
| ''' |
| if OPENCLAW_DEFAULT_MODEL: |
| data["agents"]["defaults"]["model"]["primary"] = OPENCLAW_DEFAULT_MODEL |
|
|
| |
| data["acp"] = { |
| "enabled": False, |
| "backend": "acpx", |
| "defaultAgent": "claude", |
| "allowedAgents": ["claude"], |
| "maxConcurrentSessions": 4, |
| "dispatch": {"enabled": True}, |
| "runtime": {"ttlMinutes": 120} |
| } |
| print("[SYNC] ACP enabled: backend=acpx, agent=claude") |
|
|
| |
| data.setdefault("plugins", {}).setdefault("entries", {}) |
| plugin_allow = ["telegram", "whatsapp", "coding-agent", "acpx"] |
| if A2A_PEERS: |
| plugin_allow.append("a2a-gateway") |
| data["plugins"]["allow"] = plugin_allow |
|
|
| |
| data["plugins"]["entries"]["acpx"] = { |
| "enabled": True, |
| "config": { |
| "permissionMode": "approve-all" |
| } |
| } |
|
|
| |
| CODING_TARGET_SPACE = os.environ.get("CODING_AGENT_TARGET_SPACE", "") |
| CODING_TARGET_DATASET = os.environ.get("CODING_AGENT_TARGET_DATASET", "") |
| if CODING_TARGET_SPACE: |
| data["plugins"]["entries"]["coding-agent"] = { |
| "enabled": True, |
| "config": { |
| "targetSpace": CODING_TARGET_SPACE, |
| "targetDataset": CODING_TARGET_DATASET, |
| "hfToken": HF_TOKEN or "", |
| "zaiApiKey": ZAI_API_KEY or os.environ.get("ZHIPU_API_KEY", ""), |
| } |
| } |
| print(f"[SYNC] Coding agent configured: space={CODING_TARGET_SPACE}, dataset={CODING_TARGET_DATASET}, zaiKey={'set' if (ZAI_API_KEY or os.environ.get('ZHIPU_API_KEY')) else 'missing'}") |
| if "telegram" not in data["plugins"]["entries"]: |
| data["plugins"]["entries"]["telegram"] = {"enabled": True} |
| elif isinstance(data["plugins"]["entries"]["telegram"], dict): |
| data["plugins"]["entries"]["telegram"]["enabled"] = True |
|
|
| |
| if A2A_PEERS: |
| peers = [] |
| for peer_url in A2A_PEERS.split(","): |
| peer_url = peer_url.strip() |
| if not peer_url: |
| continue |
| name = peer_url.split("//")[-1].split(".")[0].split("-")[-1].capitalize() |
| peers.append({ |
| "name": name, |
| "agentCardUrl": f"{peer_url}/.well-known/agent-card.json" |
| }) |
| print(f"[SYNC] A2A peer: {name} β {peer_url}") |
|
|
| data["plugins"]["entries"]["a2a-gateway"] = { |
| "enabled": True, |
| "config": { |
| "agentCard": { |
| "name": AGENT_NAME, |
| "description": f"{AGENT_NAME} - HuggingClaw A2A Agent", |
| "skills": [{"id": "chat", "name": "chat", "description": "Chat bridge"}] |
| }, |
| "server": {"host": "0.0.0.0", "port": 18800}, |
| "security": {"inboundAuth": "none"}, |
| "routing": {"defaultAgentId": "main"}, |
| "peers": peers |
| } |
| } |
| print(f"[SYNC] A2A gateway configured: name={AGENT_NAME}, port=18800, peers={len(peers)}") |
|
|
| |
| |
| tg_ch = data.setdefault("channels", {}).setdefault("telegram", {}) |
| tg_ch["dmPolicy"] = "open" |
| tg_ch["allowFrom"] = ["*"] |
| tg_ch["configWrites"] = True |
| print("[SYNC] Set channels.telegram: dmPolicy=open, allowFrom=[*], configWrites=true") |
|
|
| |
| |
| |
|
|
| with open(config_path, "w") as f: |
| json.dump(data, f, indent=2) |
| print("[SYNC] Config patched and saved.") |
|
|
| |
| workspace_dir = Path(OPENCLAW_HOME) / "workspace" |
| workspace_dir.mkdir(parents=True, exist_ok=True) |
| templates_dir = Path("/home/node/workspace-templates") |
| if templates_dir.exists(): |
| for tmpl in templates_dir.glob("*.md"): |
| target = workspace_dir / tmpl.name |
| |
| should_write = not target.exists() |
| if target.exists(): |
| content = target.read_text() |
| should_write = "Fill this in" in content or len(content.strip()) < 50 |
| if should_write: |
| text = tmpl.read_text().replace("{{AGENT_NAME}}", AGENT_NAME) |
| target.write_text(text) |
| print(f"[SYNC] Deployed workspace template: {tmpl.name}") |
|
|
| |
| |
| devices_dir = Path(OPENCLAW_DIR) / "devices" |
| if devices_dir.exists(): |
| import shutil |
| shutil.rmtree(devices_dir, ignore_errors=True) |
| print("[SYNC] Deleted devices/ dir to force fresh auto-pair with operator.write/read scopes") |
|
|
| |
| with open(config_path, "r") as f: |
| verify_data = json.load(f) |
| gw = verify_data.get("gateway", {}) |
| providers = list(verify_data.get("models", {}).get("providers", {}).keys()) |
| primary = verify_data.get("agents", {}).get("defaults", {}).get("model", {}).get("primary") |
| print(f"[SYNC] VERIFY: gateway.port={gw.get('port')}, providers={providers}, primary={primary}") |
|
|
| except Exception as e: |
| print(f"[SYNC] Failed to patch config: {e}") |
| traceback.print_exc() |
|
|
| def _debug_list_files(self): |
| try: |
| count = sum(1 for _, _, files in os.walk(OPENCLAW_HOME) for _ in files) |
| print(f"[SYNC] Local ~/.openclaw: {count} files") |
| except Exception as e: |
| print(f"[SYNC] listing failed: {e}") |
|
|
| |
|
|
| def background_sync_loop(self, stop_event): |
| print(f"[SYNC] Background sync started (interval={SYNC_INTERVAL}s)") |
| while not stop_event.is_set(): |
| if stop_event.wait(timeout=SYNC_INTERVAL): |
| break |
| print(f"[SYNC] ββ Periodic sync triggered at {datetime.now().isoformat()} ββ") |
| self.save_to_repo() |
|
|
| |
|
|
| def run_openclaw(self): |
| log_file = OPENCLAW_HOME / "workspace" / "startup.log" |
| log_file.parent.mkdir(parents=True, exist_ok=True) |
|
|
| |
| if not Path(APP_DIR).exists(): |
| print(f"[SYNC] ERROR: App directory does not exist: {APP_DIR}") |
| return None |
|
|
| |
| entry_js = Path(APP_DIR) / "dist" / "entry.js" |
| openclaw_mjs = Path(APP_DIR) / "openclaw.mjs" |
| if entry_js.exists(): |
| entry_cmd = ["node", "dist/entry.js", "gateway"] |
| elif openclaw_mjs.exists(): |
| entry_cmd = ["node", "openclaw.mjs", "gateway", "--allow-unconfigured"] |
| else: |
| print(f"[SYNC] ERROR: No entry point found in {APP_DIR}") |
| print(f"[SYNC] Checked: dist/entry.js, openclaw.mjs") |
| |
| try: |
| print(f"[SYNC] Contents: {list(Path(APP_DIR).iterdir())[:20]}") |
| except: pass |
| return None |
|
|
| |
| print(f"[SYNC] Launching: {' '.join(entry_cmd)}") |
| print(f"[SYNC] Working directory: {APP_DIR}") |
| print(f"[SYNC] Log file: {log_file}") |
|
|
| |
| log_fh = open(log_file, "a") |
|
|
| |
| env = os.environ.copy() |
|
|
| |
| |
| |
| if TELEGRAM_API_BASE: |
| tg_root = TELEGRAM_API_BASE.rstrip("/") |
| print(f"[TELEGRAM] Using user-specified API base: {tg_root}") |
| else: |
| print("[TELEGRAM] Probing Telegram API endpoints...") |
| tg_root = probe_telegram_api() |
|
|
| if tg_root and tg_root != "https://api.telegram.org": |
| env["TELEGRAM_API_ROOT"] = tg_root |
| print(f"[TELEGRAM] Set TELEGRAM_API_ROOT={tg_root}") |
| print(f"[TELEGRAM] telegram-proxy.cjs will redirect fetch() calls") |
| elif tg_root: |
| print("[TELEGRAM] Official API reachable β no proxy needed") |
| else: |
| print("[TELEGRAM] No reachable endpoint found β Telegram will not work") |
| try: |
| |
| |
| process = subprocess.Popen( |
| entry_cmd, |
| cwd=str(APP_DIR), |
| stdout=subprocess.PIPE, |
| stderr=subprocess.STDOUT, |
| text=True, |
| bufsize=1, |
| env=env |
| ) |
|
|
| |
| def copy_output(): |
| try: |
| for line in process.stdout: |
| log_fh.write(line) |
| log_fh.flush() |
| |
| |
| stripped = line.strip() |
| if not stripped: |
| continue |
| |
| if any(skip in stripped for skip in [ |
| 'Downloading', 'Fetching', '%|', 'β', 'βββ', |
| 'Already cached', 'Using cache', 'tokenizer', |
| '.safetensors', 'model-', 'shard', |
| ]): |
| continue |
| print(line, end='') |
| except Exception as e: |
| print(f"[SYNC] Output copy error: {e}") |
| finally: |
| log_fh.close() |
|
|
| thread = threading.Thread(target=copy_output, daemon=True) |
| thread.start() |
|
|
| print(f"[SYNC] Process started with PID: {process.pid}") |
| return process |
|
|
| except Exception as e: |
| log_fh.close() |
| print(f"[SYNC] ERROR: Failed to start process: {e}") |
| traceback.print_exc() |
| return None |
|
|
| |
|
|
| def main(): |
| try: |
| t_main_start = time.time() |
|
|
| t0 = time.time() |
| sync = OpenClawFullSync() |
| print(f"[TIMER] sync_hf init: {time.time() - t0:.1f}s") |
|
|
| |
| t0 = time.time() |
| sync.load_from_repo() |
| print(f"[TIMER] load_from_repo (restore): {time.time() - t0:.1f}s") |
|
|
| |
| stop_event = threading.Event() |
| t = threading.Thread(target=sync.background_sync_loop, args=(stop_event,), daemon=True) |
| t.start() |
|
|
| |
| t0 = time.time() |
| process = sync.run_openclaw() |
| print(f"[TIMER] run_openclaw launch: {time.time() - t0:.1f}s") |
| print(f"[TIMER] Total startup (init β app launched): {time.time() - t_main_start:.1f}s") |
|
|
| |
| |
| conv_loop_proc = None |
| if os.environ.get("RUN_ORCHESTRATOR") == "1": |
| def run_conversation_loop_forever(): |
| """Launch conversation-loop with auto-restart on crash.""" |
| nonlocal conv_loop_proc |
| time.sleep(60) |
| |
| pip_result = subprocess.run( |
| [sys.executable, "-m", "pip", "install", "--user", |
| "--break-system-packages", "requests"], |
| capture_output=True, text=True, timeout=120) |
| if pip_result.returncode != 0: |
| print(f"[SYNC] pip install requests failed: {pip_result.stderr[:200]}") |
| else: |
| print("[SYNC] pip install requests OK") |
| script = os.path.join(os.path.dirname(__file__), "conversation-loop.py") |
| if not os.path.exists(script): |
| print(f"[SYNC] conversation-loop.py not found at {script}") |
| return |
| while not stop_event.is_set(): |
| print("[SYNC] Starting conversation-loop.py (Adam & Eve orchestrator)...") |
| log = open("/tmp/conversation-loop.log", "a") |
| conv_loop_proc = subprocess.Popen( |
| [sys.executable, "-u", script], |
| stdout=log, stderr=subprocess.STDOUT, |
| ) |
| print(f"[SYNC] conversation-loop.py started (PID {conv_loop_proc.pid})") |
| exit_code = conv_loop_proc.wait() |
| log.close() |
| if stop_event.is_set(): |
| break |
| print(f"[SYNC] conversation-loop.py exited ({exit_code}), restarting in 30s...") |
| time.sleep(30) |
|
|
| conv_thread = threading.Thread(target=run_conversation_loop_forever, daemon=True) |
| conv_thread.start() |
| else: |
| print("[SYNC] RUN_ORCHESTRATOR not set β skipping conversation-loop") |
|
|
| |
| def handle_signal(sig, frame): |
| print(f"\n[SYNC] Signal {sig} received. Shutting down...") |
| stop_event.set() |
| |
| t.join(timeout=10) |
| if conv_loop_proc: |
| print("[SYNC] Stopping conversation-loop...") |
| conv_loop_proc.terminate() |
| try: |
| conv_loop_proc.wait(timeout=5) |
| except subprocess.TimeoutExpired: |
| conv_loop_proc.kill() |
| if process: |
| process.terminate() |
| try: |
| process.wait(timeout=5) |
| except subprocess.TimeoutExpired: |
| process.kill() |
| print("[SYNC] Final sync...") |
| sync.save_to_repo() |
| sys.exit(0) |
|
|
| signal.signal(signal.SIGINT, handle_signal) |
| signal.signal(signal.SIGTERM, handle_signal) |
|
|
| |
| if process is None: |
| print("[SYNC] ERROR: Failed to start OpenClaw process. Exiting.") |
| stop_event.set() |
| t.join(timeout=5) |
| sys.exit(1) |
|
|
| exit_code = process.wait() |
| print(f"[SYNC] OpenClaw exited with code {exit_code}") |
| stop_event.set() |
| t.join(timeout=10) |
| print("[SYNC] Final sync...") |
| sync.save_to_repo() |
| sys.exit(exit_code) |
|
|
| except Exception as e: |
| print(f"[SYNC] FATAL ERROR in main: {e}") |
| traceback.print_exc() |
| sys.exit(1) |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|