somratpro commited on
Commit
eec7304
Β·
1 Parent(s): 1557f14

refactor: modularize workspace sync logic and optimize state snapshotting with reduced sync interval

Browse files
Files changed (6) hide show
  1. .env.example +2 -2
  2. Dockerfile +29 -0
  3. README.md +3 -2
  4. health-server.js +1 -2
  5. start.sh +78 -14
  6. workspace-sync.py +118 -36
.env.example CHANGED
@@ -162,8 +162,8 @@ WORKSPACE_GIT_NAME=OpenClaw Bot
162
  # Keep-alive ping interval (seconds). Default: 300. Set 0 to disable.
163
  KEEP_ALIVE_INTERVAL=300
164
 
165
- # Workspace auto-sync interval (seconds). Default: 600.
166
- SYNC_INTERVAL=600
167
 
168
  # Webhooks: Standard POST notifications for lifecycle events
169
  # WEBHOOK_URL=https://your-webhook-endpoint.com/log
 
162
  # Keep-alive ping interval (seconds). Default: 300. Set 0 to disable.
163
  KEEP_ALIVE_INTERVAL=300
164
 
165
+ # Workspace auto-sync interval (seconds). Default: 180.
166
+ SYNC_INTERVAL=180
167
 
168
  # Webhooks: Standard POST notifications for lifecycle events
169
  # WEBHOOK_URL=https://your-webhook-endpoint.com/log
Dockerfile CHANGED
@@ -19,6 +19,27 @@ RUN apt-get update && apt-get install -y \
19
  curl \
20
  python3 \
21
  python3-pip \
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  --no-install-recommends && \
23
  pip3 install --no-cache-dir --break-system-packages huggingface_hub && \
24
  rm -rf /var/lib/apt/lists/*
@@ -30,6 +51,13 @@ RUN mkdir -p /home/node/app /home/node/.openclaw && \
30
  # Copy pre-built OpenClaw (skips npm install entirely β€” much faster!)
31
  COPY --from=openclaw --chown=1000:1000 /app /home/node/.openclaw/openclaw-app
32
 
 
 
 
 
 
 
 
33
  # Symlink openclaw CLI so it's available globally
34
  RUN ln -s /home/node/.openclaw/openclaw-app/openclaw.mjs /usr/local/bin/openclaw 2>/dev/null || \
35
  npm install -g openclaw@${OPENCLAW_VERSION}
@@ -48,6 +76,7 @@ USER node
48
  ENV HOME=/home/node \
49
  OPENCLAW_VERSION=${OPENCLAW_VERSION} \
50
  PATH=/home/node/.local/bin:/usr/local/bin:$PATH \
 
51
  NODE_OPTIONS="--require /opt/dns-fix.js"
52
 
53
  WORKDIR /home/node/app
 
19
  curl \
20
  python3 \
21
  python3-pip \
22
+ chromium \
23
+ libnss3 \
24
+ libatk1.0-0 \
25
+ libatk-bridge2.0-0 \
26
+ libdrm2 \
27
+ libgbm1 \
28
+ libxcomposite1 \
29
+ libxdamage1 \
30
+ libxrandr2 \
31
+ libxkbcommon0 \
32
+ libx11-6 \
33
+ libxext6 \
34
+ libxfixes3 \
35
+ libasound2 \
36
+ fonts-dejavu-core \
37
+ fonts-liberation \
38
+ fonts-noto-color-emoji \
39
+ fonts-freefont-ttf \
40
+ fonts-ipafont-gothic \
41
+ fonts-wqy-zenhei \
42
+ xfonts-scalable \
43
  --no-install-recommends && \
44
  pip3 install --no-cache-dir --break-system-packages huggingface_hub && \
45
  rm -rf /var/lib/apt/lists/*
 
51
  # Copy pre-built OpenClaw (skips npm install entirely β€” much faster!)
52
  COPY --from=openclaw --chown=1000:1000 /app /home/node/.openclaw/openclaw-app
53
 
54
+ # Add Playwright in an isolated sidecar node_modules so we do not mutate the
55
+ # bundled OpenClaw app dependency tree.
56
+ RUN mkdir -p /home/node/browser-deps && \
57
+ cd /home/node/browser-deps && \
58
+ npm init -y && \
59
+ PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 npm install --omit=dev playwright@1.59.1
60
+
61
  # Symlink openclaw CLI so it's available globally
62
  RUN ln -s /home/node/.openclaw/openclaw-app/openclaw.mjs /usr/local/bin/openclaw 2>/dev/null || \
63
  npm install -g openclaw@${OPENCLAW_VERSION}
 
76
  ENV HOME=/home/node \
77
  OPENCLAW_VERSION=${OPENCLAW_VERSION} \
78
  PATH=/home/node/.local/bin:/usr/local/bin:$PATH \
79
+ NODE_PATH=/home/node/browser-deps/node_modules \
80
  NODE_OPTIONS="--require /opt/dns-fix.js"
81
 
82
  WORKDIR /home/node/app
README.md CHANGED
@@ -42,6 +42,7 @@ license: mit
42
  - πŸ”Œ **Any LLM:** Use Claude, OpenAI GPT, Google Gemini, Grok, DeepSeek, Qwen, and 40+ providers (set `LLM_API_KEY` and `LLM_MODEL` accordingly).
43
  - ⚑ **Zero Config:** Duplicate this Space and set **just three** secrets (LLM_API_KEY, LLM_MODEL, GATEWAY_TOKEN) – no other setup needed.
44
  - 🐳 **Fast Builds:** Uses a pre-built OpenClaw Docker image to deploy in minutes.
 
45
  - πŸ’Ύ **Workspace Backup:** Chats, settings, and WhatsApp session state sync to a private HF Dataset via the `huggingface_hub` (Git fallback), preserving data automatically.
46
  - ⏰ **External Keep-Alive:** Set up a one-time UptimeRobot monitor from the dashboard to help keep free HF Spaces awake.
47
  - πŸ‘₯ **Multi-User Messaging:** Support for Telegram (multi-user) and WhatsApp (pairing).
@@ -60,7 +61,7 @@ Watch a quick walkthrough on YouTube: [Deploying HuggingClaw on HF Spaces](https
60
 
61
  [![Duplicate this Space](https://huggingface.co/datasets/huggingface/badges/resolve/main/duplicate-this-space-xl.svg)](https://huggingface.co/spaces/somratpro/HuggingClaw?duplicate=true)
62
 
63
- Click the button above to duplicate the template.
64
 
65
  ### Step 2: Add Your Secrets
66
 
@@ -119,7 +120,7 @@ For persistent chat history and configuration, HuggingClaw can sync your workspa
119
  | `HF_USERNAME` | β€” | Your HuggingFace username |
120
  | `HF_TOKEN` | β€” | HF token with write access |
121
  | `BACKUP_DATASET_NAME` | `huggingclaw-backup` | Dataset name for backup repo |
122
- | `SYNC_INTERVAL` | `600` | Sync interval in seconds |
123
  | `WORKSPACE_GIT_USER` | `openclaw@example.com` | Git commit email for syncs |
124
  | `WORKSPACE_GIT_NAME` | `OpenClaw Bot` | Git commit name for syncs |
125
 
 
42
  - πŸ”Œ **Any LLM:** Use Claude, OpenAI GPT, Google Gemini, Grok, DeepSeek, Qwen, and 40+ providers (set `LLM_API_KEY` and `LLM_MODEL` accordingly).
43
  - ⚑ **Zero Config:** Duplicate this Space and set **just three** secrets (LLM_API_KEY, LLM_MODEL, GATEWAY_TOKEN) – no other setup needed.
44
  - 🐳 **Fast Builds:** Uses a pre-built OpenClaw Docker image to deploy in minutes.
45
+ - 🌐 **Built-In Browser:** Headless Chromium is included in the Space, so browser actions work from the start.
46
  - πŸ’Ύ **Workspace Backup:** Chats, settings, and WhatsApp session state sync to a private HF Dataset via the `huggingface_hub` (Git fallback), preserving data automatically.
47
  - ⏰ **External Keep-Alive:** Set up a one-time UptimeRobot monitor from the dashboard to help keep free HF Spaces awake.
48
  - πŸ‘₯ **Multi-User Messaging:** Support for Telegram (multi-user) and WhatsApp (pairing).
 
61
 
62
  [![Duplicate this Space](https://huggingface.co/datasets/huggingface/badges/resolve/main/duplicate-this-space-xl.svg)](https://huggingface.co/spaces/somratpro/HuggingClaw?duplicate=true)
63
 
64
+ Click the button above to duplicate the template.
65
 
66
  ### Step 2: Add Your Secrets
67
 
 
120
  | `HF_USERNAME` | β€” | Your HuggingFace username |
121
  | `HF_TOKEN` | β€” | HF token with write access |
122
  | `BACKUP_DATASET_NAME` | `huggingclaw-backup` | Dataset name for backup repo |
123
+ | `SYNC_INTERVAL` | `180` | Sync interval in seconds |
124
  | `WORKSPACE_GIT_USER` | `openclaw@example.com` | Git commit email for syncs |
125
  | `WORKSPACE_GIT_NAME` | `OpenClaw Bot` | Git commit name for syncs |
126
 
health-server.js CHANGED
@@ -196,8 +196,7 @@ async function resolveSpaceIsPrivate(parsedUrl) {
196
  const statusCode = await fetchStatusCode(
197
  `https://huggingface.co/api/spaces/${ref.owner}/${ref.repo}`,
198
  );
199
- const isPrivate =
200
- statusCode === 401 || statusCode === 403 || statusCode === 404;
201
  spaceVisibilityCache.set(cacheKey, { isPrivate, timestamp: Date.now() });
202
  return isPrivate;
203
  } catch {
 
196
  const statusCode = await fetchStatusCode(
197
  `https://huggingface.co/api/spaces/${ref.owner}/${ref.repo}`,
198
  );
199
+ const isPrivate = statusCode === 401 || statusCode === 403 || statusCode === 404;
 
200
  spaceVisibilityCache.set(cacheKey, { isPrivate, timestamp: Date.now() });
201
  return isPrivate;
202
  } catch {
start.sh CHANGED
@@ -9,6 +9,7 @@ set -e
9
  OPENCLAW_VERSION="${OPENCLAW_VERSION:-latest}"
10
  WHATSAPP_ENABLED="${WHATSAPP_ENABLED:-false}"
11
  WHATSAPP_ENABLED_NORMALIZED=$(printf '%s' "$WHATSAPP_ENABLED" | tr '[:upper:]' '[:lower:]')
 
12
  echo ""
13
  echo " ╔══════════════════════════════════════════╗"
14
  echo " β•‘ 🦞 HuggingClaw Gateway β•‘"
@@ -91,6 +92,8 @@ esac
91
  # ── Setup directories ──
92
  mkdir -p /home/node/.openclaw/agents/main/sessions
93
  mkdir -p /home/node/.openclaw/credentials
 
 
94
  mkdir -p /home/node/.openclaw/workspace
95
  chmod 700 /home/node/.openclaw
96
  chmod 700 /home/node/.openclaw/credentials
@@ -165,6 +168,22 @@ if [ -n "$HF_USERNAME" ] && [ -n "$HF_TOKEN" ]; then
165
  cd /
166
  fi
167
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
168
  # ── Restore persisted WhatsApp credentials (if present) ──
169
  WA_BACKUP_DIR="/home/node/.openclaw/workspace/.huggingclaw-state/credentials/whatsapp/default"
170
  WA_CREDS_DIR="/home/node/.openclaw/credentials/whatsapp/default"
@@ -212,6 +231,31 @@ CONFIG_JSON=$(echo "$CONFIG_JSON" | jq ".gateway.auth.token = \"$GATEWAY_TOKEN\"
212
  # Model configuration at top level
213
  CONFIG_JSON=$(echo "$CONFIG_JSON" | jq ".agents.defaults.model = \"$LLM_MODEL\"")
214
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
215
  # Control UI origin (allow HF Space URL for web UI access)
216
  if [ -n "$SPACE_HOST" ]; then
217
  CONFIG_JSON=$(echo "$CONFIG_JSON" | jq ".gateway.controlUi.allowedOrigins = [\"https://${SPACE_HOST}\"]")
@@ -321,6 +365,11 @@ printf " β”‚ %-40s β”‚\n" "WhatsApp: βœ… enabled"
321
  else
322
  printf " β”‚ %-40s β”‚\n" "WhatsApp: ❌ disabled"
323
  fi
 
 
 
 
 
324
  if [ -n "$HF_USERNAME" ] && [ -n "$HF_TOKEN" ]; then
325
  printf " β”‚ %-40s β”‚\n" "Backup: βœ… ${HF_USERNAME}/${BACKUP_DATASET:-huggingclaw-backup}"
326
  else
@@ -337,7 +386,7 @@ printf " β”‚ %-40s β”‚\n" "Dashboard: https://${SPACE_HOST}"
337
  fi
338
  SYNC_STATUS="❌ disabled"
339
  if [ -n "$HF_USERNAME" ] && [ -n "$HF_TOKEN" ]; then
340
- SYNC_STATUS="βœ… every ${SYNC_INTERVAL:-600}s"
341
  fi
342
  printf " β”‚ %-40s β”‚\n" "Auto-sync: $SYNC_STATUS"
343
  if [ -n "$WEBHOOK_URL" ]; then
@@ -358,19 +407,11 @@ fi
358
  graceful_shutdown() {
359
  echo ""
360
  echo "πŸ›‘ Shutting down gracefully..."
361
-
362
- # Commit any unsaved workspace changes
363
- if [ -d "/home/node/.openclaw/workspace/.git" ]; then
364
- echo "πŸ’Ύ Saving workspace before exit..."
365
- cd /home/node/.openclaw/workspace
366
- git add -A 2>/dev/null
367
- if ! git diff --cached --quiet 2>/dev/null; then
368
- TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)
369
- git commit -m "Shutdown sync ${TIMESTAMP}" 2>/dev/null
370
- git push origin main 2>/dev/null && echo " βœ… Workspace saved!" || echo " ⚠️ Push failed"
371
- else
372
- echo " βœ… No unsaved changes"
373
- fi
374
  fi
375
 
376
  # Kill background processes
@@ -380,6 +421,26 @@ graceful_shutdown() {
380
  }
381
  trap graceful_shutdown SIGTERM SIGINT
382
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
383
  # ── Start background services ──
384
  export LLM_MODEL="$LLM_MODEL"
385
  # 10. Start Health Server & Dashboard
@@ -416,6 +477,9 @@ if [ "$WHATSAPP_ENABLED_NORMALIZED" = "true" ]; then
416
  echo "πŸ›‘οΈ WhatsApp Guardian started (PID: $GUARDIAN_PID)"
417
  fi
418
 
 
 
 
419
  # 12. Start Workspace Sync after startup settles
420
  python3 -u /home/node/app/workspace-sync.py &
421
 
 
9
  OPENCLAW_VERSION="${OPENCLAW_VERSION:-latest}"
10
  WHATSAPP_ENABLED="${WHATSAPP_ENABLED:-false}"
11
  WHATSAPP_ENABLED_NORMALIZED=$(printf '%s' "$WHATSAPP_ENABLED" | tr '[:upper:]' '[:lower:]')
12
+ SYNC_INTERVAL="${SYNC_INTERVAL:-180}"
13
  echo ""
14
  echo " ╔══════════════════════════════════════════╗"
15
  echo " β•‘ 🦞 HuggingClaw Gateway β•‘"
 
92
  # ── Setup directories ──
93
  mkdir -p /home/node/.openclaw/agents/main/sessions
94
  mkdir -p /home/node/.openclaw/credentials
95
+ mkdir -p /home/node/.openclaw/memory
96
+ mkdir -p /home/node/.openclaw/extensions
97
  mkdir -p /home/node/.openclaw/workspace
98
  chmod 700 /home/node/.openclaw
99
  chmod 700 /home/node/.openclaw/credentials
 
168
  cd /
169
  fi
170
 
171
+ # ── Restore persisted OpenClaw state (if present) ──
172
+ STATE_BACKUP_ROOT="/home/node/.openclaw/workspace/.huggingclaw-state/openclaw"
173
+ if [ -d "$STATE_BACKUP_ROOT" ]; then
174
+ echo "🧠 Restoring OpenClaw state..."
175
+ for source_path in "$STATE_BACKUP_ROOT"/*; do
176
+ [ -e "$source_path" ] || continue
177
+ name="$(basename "$source_path")"
178
+ target_path="/home/node/.openclaw/${name}"
179
+
180
+ rm -rf "$target_path"
181
+ mkdir -p "$(dirname "$target_path")"
182
+ cp -R "$source_path" "$target_path"
183
+ done
184
+ echo " βœ… OpenClaw state restored"
185
+ fi
186
+
187
  # ── Restore persisted WhatsApp credentials (if present) ──
188
  WA_BACKUP_DIR="/home/node/.openclaw/workspace/.huggingclaw-state/credentials/whatsapp/default"
189
  WA_CREDS_DIR="/home/node/.openclaw/credentials/whatsapp/default"
 
231
  # Model configuration at top level
232
  CONFIG_JSON=$(echo "$CONFIG_JSON" | jq ".agents.defaults.model = \"$LLM_MODEL\"")
233
 
234
+ # Browser configuration (managed local Chromium in HF/Docker)
235
+ BROWSER_EXECUTABLE_PATH=""
236
+ for candidate in /usr/bin/chromium /usr/bin/chromium-browser /snap/bin/chromium; do
237
+ if [ -x "$candidate" ]; then
238
+ BROWSER_EXECUTABLE_PATH="$candidate"
239
+ break
240
+ fi
241
+ done
242
+
243
+ BROWSER_SHOULD_ENABLE=false
244
+ if [ -n "$BROWSER_EXECUTABLE_PATH" ] && [ -x "$BROWSER_EXECUTABLE_PATH" ]; then
245
+ BROWSER_SHOULD_ENABLE=true
246
+ fi
247
+
248
+ if [ "$BROWSER_SHOULD_ENABLE" = "true" ]; then
249
+ CONFIG_JSON=$(echo "$CONFIG_JSON" | jq \
250
+ ".browser = {
251
+ \"enabled\": true,
252
+ \"defaultProfile\": \"openclaw\",
253
+ \"headless\": true,
254
+ \"noSandbox\": true,
255
+ \"executablePath\": \"$BROWSER_EXECUTABLE_PATH\"
256
+ } | .agents.defaults.sandbox.browser.allowHostControl = true")
257
+ fi
258
+
259
  # Control UI origin (allow HF Space URL for web UI access)
260
  if [ -n "$SPACE_HOST" ]; then
261
  CONFIG_JSON=$(echo "$CONFIG_JSON" | jq ".gateway.controlUi.allowedOrigins = [\"https://${SPACE_HOST}\"]")
 
365
  else
366
  printf " β”‚ %-40s β”‚\n" "WhatsApp: ❌ disabled"
367
  fi
368
+ if [ "$BROWSER_SHOULD_ENABLE" = "true" ]; then
369
+ printf " β”‚ %-40s β”‚\n" "Browser: βœ… ${BROWSER_EXECUTABLE_PATH}"
370
+ else
371
+ printf " β”‚ %-40s β”‚\n" "Browser: ❌ unavailable"
372
+ fi
373
  if [ -n "$HF_USERNAME" ] && [ -n "$HF_TOKEN" ]; then
374
  printf " β”‚ %-40s β”‚\n" "Backup: βœ… ${HF_USERNAME}/${BACKUP_DATASET:-huggingclaw-backup}"
375
  else
 
386
  fi
387
  SYNC_STATUS="❌ disabled"
388
  if [ -n "$HF_USERNAME" ] && [ -n "$HF_TOKEN" ]; then
389
+ SYNC_STATUS="βœ… every ${SYNC_INTERVAL:-180}s"
390
  fi
391
  printf " β”‚ %-40s β”‚\n" "Auto-sync: $SYNC_STATUS"
392
  if [ -n "$WEBHOOK_URL" ]; then
 
407
  graceful_shutdown() {
408
  echo ""
409
  echo "πŸ›‘ Shutting down gracefully..."
410
+
411
+ if [ -f "/home/node/app/workspace-sync.py" ]; then
412
+ echo "πŸ’Ύ Saving OpenClaw state before exit..."
413
+ python3 /home/node/app/workspace-sync.py --sync-once || \
414
+ echo " ⚠️ Could not complete shutdown sync"
 
 
 
 
 
 
 
 
415
  fi
416
 
417
  # Kill background processes
 
421
  }
422
  trap graceful_shutdown SIGTERM SIGINT
423
 
424
+ warmup_browser() {
425
+ [ "$BROWSER_SHOULD_ENABLE" = "true" ] || return 0
426
+
427
+ (
428
+ sleep 5
429
+
430
+ local attempt
431
+ for attempt in 1 2 3 4 5; do
432
+ if openclaw browser --browser-profile openclaw start >/dev/null 2>&1; then
433
+ openclaw browser --browser-profile openclaw open about:blank >/dev/null 2>&1 || true
434
+ echo " βœ… Managed browser ready"
435
+ return 0
436
+ fi
437
+ sleep 2
438
+ done
439
+
440
+ echo " ⚠️ Managed browser warm-up did not complete; first browser action may need a retry"
441
+ ) &
442
+ }
443
+
444
  # ── Start background services ──
445
  export LLM_MODEL="$LLM_MODEL"
446
  # 10. Start Health Server & Dashboard
 
477
  echo "πŸ›‘οΈ WhatsApp Guardian started (PID: $GUARDIAN_PID)"
478
  fi
479
 
480
+ # 11.5 Warm up the managed browser so first browser actions have a live tab
481
+ warmup_browser
482
+
483
  # 12. Start Workspace Sync after startup settles
484
  python3 -u /home/node/app/workspace-sync.py &
485
 
workspace-sync.py CHANGED
@@ -15,12 +15,20 @@ import shutil
15
  import subprocess
16
  from pathlib import Path
17
 
18
- WORKSPACE = Path("/home/node/.openclaw/workspace")
 
19
  STATE_DIR = WORKSPACE / ".huggingclaw-state"
 
 
 
 
 
 
 
20
  WHATSAPP_CREDS_DIR = Path("/home/node/.openclaw/credentials/whatsapp/default")
21
  WHATSAPP_BACKUP_DIR = STATE_DIR / "credentials" / "whatsapp" / "default"
22
  RESET_MARKER = WORKSPACE / ".reset_credentials"
23
- INTERVAL = int(os.environ.get("SYNC_INTERVAL", "600"))
24
  INITIAL_DELAY = int(os.environ.get("SYNC_START_DELAY", "10"))
25
  HF_TOKEN = os.environ.get("HF_TOKEN", "")
26
  HF_USERNAME = os.environ.get("HF_USERNAME", "")
@@ -52,6 +60,24 @@ def snapshot_state_into_workspace() -> None:
52
  This keeps WhatsApp credentials in a hidden folder that is synced together
53
  with the workspace, without changing the live credentials location.
54
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  try:
56
  if not WHATSAPP_ENABLED:
57
  return
@@ -183,7 +209,94 @@ def sync_with_git():
183
  return False
184
 
185
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
186
  def main():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
187
  if not WORKSPACE.exists():
188
  print("πŸ“ Workspace sync: workspace not found, exiting.")
189
  return
@@ -204,50 +317,19 @@ def main():
204
  # Give the gateway a short head start before the first sync probe.
205
  time.sleep(INITIAL_DELAY)
206
 
207
- snapshot_state_into_workspace()
208
-
209
  if use_hf_hub:
210
  print(f"πŸ”„ Workspace sync started (huggingface_hub): every {INTERVAL}s β†’ {HF_USERNAME}/{BACKUP_DATASET}")
211
  else:
212
  print(f"πŸ”„ Workspace sync started (git): every {INTERVAL}s")
213
 
 
 
214
  while running:
215
  time.sleep(INTERVAL)
216
  if not running:
217
  break
218
 
219
- snapshot_state_into_workspace()
220
-
221
- if not has_changes():
222
- continue
223
-
224
- ts = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
225
-
226
- write_sync_status("syncing", f"Starting sync at {ts}")
227
-
228
- if use_hf_hub:
229
- if sync_with_hf_hub():
230
- print(f"πŸ”„ Workspace sync (hf_hub): pushed changes ({ts})")
231
- write_sync_status("success", "Successfully pushed to HF Hub")
232
- else:
233
- # Fallback to git
234
- if sync_with_git():
235
- print(f"πŸ”„ Workspace sync (git fallback): pushed changes ({ts})")
236
- write_sync_status("success", "Successfully pushed via git fallback")
237
- else:
238
- msg = f"Workspace sync: failed ({ts}), will retry"
239
- print(f"πŸ”„ {msg}")
240
- write_sync_status("error", msg)
241
- trigger_webhook("sync", "error", msg)
242
- else:
243
- if sync_with_git():
244
- print(f"πŸ”„ Workspace sync (git): pushed changes ({ts})")
245
- write_sync_status("success", "Successfully pushed via git")
246
- else:
247
- msg = f"Workspace sync: push failed ({ts}), will retry"
248
- print(f"πŸ”„ {msg}")
249
- write_sync_status("error", msg)
250
- trigger_webhook("sync", "error", msg)
251
 
252
 
253
  if __name__ == "__main__":
 
15
  import subprocess
16
  from pathlib import Path
17
 
18
+ OPENCLAW_HOME = Path("/home/node/.openclaw")
19
+ WORKSPACE = OPENCLAW_HOME / "workspace"
20
  STATE_DIR = WORKSPACE / ".huggingclaw-state"
21
+ OPENCLAW_STATE_BACKUP_DIR = STATE_DIR / "openclaw"
22
+ EXCLUDED_STATE_NAMES = {
23
+ "workspace",
24
+ "openclaw-app",
25
+ "gateway.log",
26
+ "browser",
27
+ }
28
  WHATSAPP_CREDS_DIR = Path("/home/node/.openclaw/credentials/whatsapp/default")
29
  WHATSAPP_BACKUP_DIR = STATE_DIR / "credentials" / "whatsapp" / "default"
30
  RESET_MARKER = WORKSPACE / ".reset_credentials"
31
+ INTERVAL = int(os.environ.get("SYNC_INTERVAL", "180"))
32
  INITIAL_DELAY = int(os.environ.get("SYNC_START_DELAY", "10"))
33
  HF_TOKEN = os.environ.get("HF_TOKEN", "")
34
  HF_USERNAME = os.environ.get("HF_USERNAME", "")
 
60
  This keeps WhatsApp credentials in a hidden folder that is synced together
61
  with the workspace, without changing the live credentials location.
62
  """
63
+ try:
64
+ STATE_DIR.mkdir(parents=True, exist_ok=True)
65
+ if OPENCLAW_STATE_BACKUP_DIR.exists():
66
+ shutil.rmtree(OPENCLAW_STATE_BACKUP_DIR, ignore_errors=True)
67
+ OPENCLAW_STATE_BACKUP_DIR.mkdir(parents=True, exist_ok=True)
68
+
69
+ for source_path in OPENCLAW_HOME.iterdir():
70
+ if source_path.name in EXCLUDED_STATE_NAMES:
71
+ continue
72
+
73
+ backup_path = OPENCLAW_STATE_BACKUP_DIR / source_path.name
74
+ if source_path.is_dir():
75
+ shutil.copytree(source_path, backup_path)
76
+ elif source_path.is_file():
77
+ shutil.copy2(source_path, backup_path)
78
+ except Exception as e:
79
+ print(f" ⚠️ Could not snapshot OpenClaw state: {e}")
80
+
81
  try:
82
  if not WHATSAPP_ENABLED:
83
  return
 
209
  return False
210
 
211
 
212
+ def run_sync_pass(use_hf_hub: bool) -> None:
213
+ """Snapshot state and push it if anything changed."""
214
+ snapshot_state_into_workspace()
215
+
216
+ if not has_changes():
217
+ return
218
+
219
+ ts = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
220
+ write_sync_status("syncing", f"Starting sync at {ts}")
221
+
222
+ if use_hf_hub:
223
+ if sync_with_hf_hub():
224
+ print(f"πŸ”„ Workspace sync (hf_hub): pushed changes ({ts})")
225
+ write_sync_status("success", "Successfully pushed to HF Hub")
226
+ return
227
+
228
+ if sync_with_git():
229
+ print(f"πŸ”„ Workspace sync (git fallback): pushed changes ({ts})")
230
+ write_sync_status("success", "Successfully pushed via git fallback")
231
+ return
232
+
233
+ msg = f"Workspace sync: failed ({ts}), will retry"
234
+ print(f"πŸ”„ {msg}")
235
+ write_sync_status("error", msg)
236
+ trigger_webhook("sync", "error", msg)
237
+ return
238
+
239
+ if sync_with_git():
240
+ print(f"πŸ”„ Workspace sync (git): pushed changes ({ts})")
241
+ write_sync_status("success", "Successfully pushed via git")
242
+ return
243
+
244
+ msg = f"Workspace sync: push failed ({ts}), will retry"
245
+ print(f"πŸ”„ {msg}")
246
+ write_sync_status("error", msg)
247
+ trigger_webhook("sync", "error", msg)
248
+
249
+
250
  def main():
251
+ if "--snapshot-once" in sys.argv:
252
+ snapshot_state_into_workspace()
253
+ write_sync_status("configured", "State snapshot refreshed during shutdown.")
254
+ return
255
+
256
+ if "--sync-once" in sys.argv:
257
+ if not WORKSPACE.exists():
258
+ print("πŸ“ Workspace sync: workspace not found, exiting.")
259
+ return
260
+
261
+ use_hf_hub = bool(HF_TOKEN and HF_USERNAME)
262
+ git_dir = WORKSPACE / ".git"
263
+
264
+ if not use_hf_hub and not git_dir.exists():
265
+ print("πŸ“ Workspace sync: no git repo and no HF credentials, skipping.")
266
+ return
267
+
268
+ snapshot_state_into_workspace()
269
+
270
+ if not has_changes():
271
+ print("πŸ“ Workspace sync: no changes to persist.")
272
+ write_sync_status("configured", "No new state changes to sync.")
273
+ return
274
+
275
+ ts = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
276
+ write_sync_status("syncing", f"Shutdown sync started at {ts}")
277
+
278
+ if use_hf_hub:
279
+ if sync_with_hf_hub():
280
+ print(f"πŸ”„ Workspace sync (hf_hub): pushed changes ({ts})")
281
+ write_sync_status("success", "Shutdown sync pushed to HF Hub")
282
+ return
283
+ if sync_with_git():
284
+ print(f"πŸ”„ Workspace sync (git fallback): pushed changes ({ts})")
285
+ write_sync_status("success", "Shutdown sync pushed via git fallback")
286
+ return
287
+ write_sync_status("error", "Shutdown sync failed")
288
+ print("πŸ“ Workspace sync: shutdown sync failed.")
289
+ return
290
+
291
+ if sync_with_git():
292
+ print(f"πŸ”„ Workspace sync (git): pushed changes ({ts})")
293
+ write_sync_status("success", "Shutdown sync pushed via git")
294
+ return
295
+
296
+ write_sync_status("error", "Shutdown sync failed")
297
+ print("πŸ“ Workspace sync: shutdown sync failed.")
298
+ return
299
+
300
  if not WORKSPACE.exists():
301
  print("πŸ“ Workspace sync: workspace not found, exiting.")
302
  return
 
317
  # Give the gateway a short head start before the first sync probe.
318
  time.sleep(INITIAL_DELAY)
319
 
 
 
320
  if use_hf_hub:
321
  print(f"πŸ”„ Workspace sync started (huggingface_hub): every {INTERVAL}s β†’ {HF_USERNAME}/{BACKUP_DATASET}")
322
  else:
323
  print(f"πŸ”„ Workspace sync started (git): every {INTERVAL}s")
324
 
325
+ run_sync_pass(use_hf_hub)
326
+
327
  while running:
328
  time.sleep(INTERVAL)
329
  if not running:
330
  break
331
 
332
+ run_sync_pass(use_hf_hub)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
333
 
334
 
335
  if __name__ == "__main__":