anurag008w commited on
Commit
c3c95c3
·
1 Parent(s): 6b45d1f

Preserve restored OpenClaw config defaults

Browse files
Files changed (4) hide show
  1. .env.example +3 -0
  2. README.md +2 -1
  3. openclaw-sync.py +80 -6
  4. start.sh +26 -8
.env.example CHANGED
@@ -285,6 +285,9 @@ KEEP_ALIVE_INTERVAL=300
285
  # Workspace auto-sync interval (seconds). Default: 180.
286
  SYNC_INTERVAL=180
287
 
 
 
 
288
  # Webhooks: Standard POST notifications for lifecycle events
289
  # WEBHOOK_URL=https://your-webhook-endpoint.com/log
290
 
 
285
  # Workspace auto-sync interval (seconds). Default: 180.
286
  SYNC_INTERVAL=180
287
 
288
+ # Check openclaw.json for settings changes this often (seconds). Default: 1.
289
+ OPENCLAW_CONFIG_WATCH_INTERVAL=1
290
+
291
  # Webhooks: Standard POST notifications for lifecycle events
292
  # WEBHOOK_URL=https://your-webhook-endpoint.com/log
293
 
README.md CHANGED
@@ -162,7 +162,8 @@ HuggingClaw automatically syncs your workspace (chats, settings, sessions) to a
162
  | Variable | Default | Description |
163
  | :--- | :--- | :--- |
164
  | `HF_TOKEN` | — | HF token with **Write** access |
165
- | `SYNC_INTERVAL` | `180` | Backup frequency in seconds |
 
166
 
167
  ## 📦 Ephemeral Package Re-install *(Optional)*
168
 
 
162
  | Variable | Default | Description |
163
  | :--- | :--- | :--- |
164
  | `HF_TOKEN` | — | HF token with **Write** access |
165
+ | `SYNC_INTERVAL` | `180` | Full backup frequency in seconds |
166
+ | `OPENCLAW_CONFIG_WATCH_INTERVAL` | `1` | How often to check `openclaw.json` for immediate settings sync |
167
 
168
  ## 📦 Ephemeral Package Re-install *(Optional)*
169
 
openclaw-sync.py CHANGED
@@ -34,10 +34,15 @@ from huggingface_hub.errors import HfHubHTTPError, RepositoryNotFoundError
34
  logging.getLogger("huggingface_hub").setLevel(logging.ERROR)
35
 
36
  OPENCLAW_HOME = Path("/home/node/.openclaw")
 
37
  WORKSPACE = OPENCLAW_HOME / "workspace"
38
  STATUS_FILE = Path("/tmp/sync-status.json")
39
  INTERVAL = int(os.environ.get("SYNC_INTERVAL", "180"))
40
  INITIAL_DELAY = int(os.environ.get("SYNC_START_DELAY", "10"))
 
 
 
 
41
  HF_TOKEN = os.environ.get("HF_TOKEN", "").strip()
42
  HF_USERNAME = os.environ.get("HF_USERNAME", "").strip()
43
  SPACE_AUTHOR_NAME = os.environ.get("SPACE_AUTHOR_NAME", "").strip()
@@ -78,6 +83,13 @@ def write_status(status: str, message: str) -> None:
78
  tmp_path.replace(STATUS_FILE)
79
 
80
 
 
 
 
 
 
 
 
81
  def count_files(path: Path) -> int:
82
  if not path.exists():
83
  return 0
@@ -250,6 +262,18 @@ def _should_exclude(rel_posix: str, path: Path) -> bool:
250
  return False
251
 
252
 
 
 
 
 
 
 
 
 
 
 
 
 
253
  def metadata_marker(root: Path) -> tuple[int, int, int]:
254
  if not root.exists():
255
  return (0, 0, 0)
@@ -416,10 +440,31 @@ def handle_signal(_sig, _frame) -> None:
416
  STOP_EVENT.set()
417
 
418
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
419
  def loop() -> int:
420
  signal.signal(signal.SIGTERM, handle_signal)
421
  signal.signal(signal.SIGINT, handle_signal)
422
 
 
 
423
  try:
424
  repo_id = resolve_backup_namespace()
425
  write_status("configured", f"Backup loop active for {repo_id} with {INTERVAL}s interval.")
@@ -431,24 +476,53 @@ def loop() -> int:
431
  time.sleep(INITIAL_DELAY)
432
  print(f"Workspace sync started: every {INTERVAL}s -> {repo_id}")
433
 
434
- # Take a fingerprint of the workspace AS RESTORED (after snapshotting state)
435
- # so the first loop iteration only uploads if something genuinely changed.
436
- # Previously this was None, which forced an unconditional upload every restart
437
- # even when restore had failed silently and the workspace was empty.
 
 
 
 
 
438
  snapshot_state_into_workspace()
439
  last_fingerprint = fingerprint_dir(WORKSPACE)
440
  last_marker = metadata_marker(WORKSPACE)
441
- print("Initial workspace fingerprint captured.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
442
 
443
  while not STOP_EVENT.is_set():
444
  try:
 
445
  last_fingerprint, last_marker = sync_once(last_fingerprint, last_marker)
 
 
 
 
 
446
  except Exception as exc:
447
  write_status("error", f"Sync failed: {exc}")
448
  print(f"Workspace sync failed: {exc}")
 
449
 
450
- if STOP_EVENT.wait(INTERVAL):
 
451
  break
 
 
452
 
453
  return 0
454
 
 
34
  logging.getLogger("huggingface_hub").setLevel(logging.ERROR)
35
 
36
  OPENCLAW_HOME = Path("/home/node/.openclaw")
37
+ OPENCLAW_CONFIG_FILE = OPENCLAW_HOME / "openclaw.json"
38
  WORKSPACE = OPENCLAW_HOME / "workspace"
39
  STATUS_FILE = Path("/tmp/sync-status.json")
40
  INTERVAL = int(os.environ.get("SYNC_INTERVAL", "180"))
41
  INITIAL_DELAY = int(os.environ.get("SYNC_START_DELAY", "10"))
42
+ CONFIG_WATCH_INTERVAL = max(
43
+ 0.5,
44
+ float(os.environ.get("OPENCLAW_CONFIG_WATCH_INTERVAL", "1")),
45
+ )
46
  HF_TOKEN = os.environ.get("HF_TOKEN", "").strip()
47
  HF_USERNAME = os.environ.get("HF_USERNAME", "").strip()
48
  SPACE_AUTHOR_NAME = os.environ.get("SPACE_AUTHOR_NAME", "").strip()
 
83
  tmp_path.replace(STATUS_FILE)
84
 
85
 
86
+ def read_status() -> dict[str, str]:
87
+ try:
88
+ return json.loads(STATUS_FILE.read_text(encoding="utf-8"))
89
+ except Exception:
90
+ return {}
91
+
92
+
93
  def count_files(path: Path) -> int:
94
  if not path.exists():
95
  return 0
 
262
  return False
263
 
264
 
265
+ def file_marker(path: Path) -> tuple[int, int, int]:
266
+ try:
267
+ stat = path.stat()
268
+ except OSError:
269
+ return (0, 0, 0)
270
+
271
+ if not path.is_file():
272
+ return (0, 0, 0)
273
+
274
+ return (1, int(stat.st_size), int(stat.st_mtime_ns))
275
+
276
+
277
  def metadata_marker(root: Path) -> tuple[int, int, int]:
278
  if not root.exists():
279
  return (0, 0, 0)
 
440
  STOP_EVENT.set()
441
 
442
 
443
+ def wait_for_sync_trigger(config_marker: tuple[int, int, int]) -> tuple[str, tuple[int, int, int]]:
444
+ deadline = time.monotonic() + max(0, INTERVAL)
445
+
446
+ while not STOP_EVENT.is_set():
447
+ current_config_marker = file_marker(OPENCLAW_CONFIG_FILE)
448
+ if current_config_marker != config_marker:
449
+ return ("openclaw_config_changed", current_config_marker)
450
+
451
+ remaining = deadline - time.monotonic()
452
+ if remaining <= 0:
453
+ return ("interval", current_config_marker)
454
+
455
+ wait_seconds = min(CONFIG_WATCH_INTERVAL, remaining)
456
+ if STOP_EVENT.wait(wait_seconds):
457
+ return ("stopped", current_config_marker)
458
+
459
+ return ("stopped", config_marker)
460
+
461
+
462
  def loop() -> int:
463
  signal.signal(signal.SIGTERM, handle_signal)
464
  signal.signal(signal.SIGINT, handle_signal)
465
 
466
+ previous_status = read_status().get("status", "")
467
+
468
  try:
469
  repo_id = resolve_backup_namespace()
470
  write_status("configured", f"Backup loop active for {repo_id} with {INTERVAL}s interval.")
 
476
  time.sleep(INITIAL_DELAY)
477
  print(f"Workspace sync started: every {INTERVAL}s -> {repo_id}")
478
 
479
+ # Capture the restored dataset state before refreshing the embedded
480
+ # /home/node/.openclaw backup. Startup may have patched openclaw.json
481
+ # after restore (token/model/logging/channel toggles), and that patch only
482
+ # becomes part of the dataset once snapshot_state_into_workspace() copies it
483
+ # into workspace/huggingclaw-state/openclaw/. If the snapshot changes the
484
+ # workspace, seed the first sync with the pre-snapshot fingerprint so the
485
+ # updated openclaw.json is uploaded instead of being treated as the baseline.
486
+ pre_snapshot_fingerprint = fingerprint_dir(WORKSPACE)
487
+ pre_snapshot_marker = metadata_marker(WORKSPACE)
488
  snapshot_state_into_workspace()
489
  last_fingerprint = fingerprint_dir(WORKSPACE)
490
  last_marker = metadata_marker(WORKSPACE)
491
+
492
+ if last_fingerprint != pre_snapshot_fingerprint:
493
+ if previous_status == "error":
494
+ print(
495
+ "Initial state snapshot changed, but restore previously failed; "
496
+ "keeping current state as baseline to avoid overwriting the remote backup."
497
+ )
498
+ else:
499
+ last_fingerprint = pre_snapshot_fingerprint
500
+ last_marker = pre_snapshot_marker
501
+ print("Initial state snapshot changed; first sync will upload refreshed OpenClaw state.")
502
+ else:
503
+ print("Initial workspace fingerprint captured.")
504
+
505
+ config_marker = file_marker(OPENCLAW_CONFIG_FILE)
506
 
507
  while not STOP_EVENT.is_set():
508
  try:
509
+ sync_started_config_marker = file_marker(OPENCLAW_CONFIG_FILE)
510
  last_fingerprint, last_marker = sync_once(last_fingerprint, last_marker)
511
+ config_marker = file_marker(OPENCLAW_CONFIG_FILE)
512
+
513
+ if config_marker != sync_started_config_marker:
514
+ print("OpenClaw config changed during sync; syncing again immediately.")
515
+ continue
516
  except Exception as exc:
517
  write_status("error", f"Sync failed: {exc}")
518
  print(f"Workspace sync failed: {exc}")
519
+ config_marker = file_marker(OPENCLAW_CONFIG_FILE)
520
 
521
+ trigger, config_marker = wait_for_sync_trigger(config_marker)
522
+ if trigger == "stopped":
523
  break
524
+ if trigger == "openclaw_config_changed":
525
+ print("OpenClaw config changed; syncing immediately.")
526
 
527
  return 0
528
 
start.sh CHANGED
@@ -11,6 +11,14 @@ umask 0077
11
  OPENCLAW_VERSION="${OPENCLAW_VERSION:-latest}"
12
  OPENCLAW_APP_DIR="/home/node/.openclaw/openclaw-app"
13
  OPENCLAW_RUNTIME_VERSION=""
 
 
 
 
 
 
 
 
14
  WHATSAPP_ENABLED="${WHATSAPP_ENABLED:-false}"
15
  WHATSAPP_ENABLED_NORMALIZED=$(printf '%s' "$WHATSAPP_ENABLED" | tr '[:upper:]' '[:lower:]')
16
  SYNC_INTERVAL="${SYNC_INTERVAL:-180}"
@@ -445,22 +453,32 @@ if [ -f "$EXISTING_CONFIG" ]; then
445
  --arg consoleLevel "$OPENCLAW_CONSOLE_LOG_LEVEL" \
446
  --arg consoleStyle "$OPENCLAW_CONSOLE_LOG_STYLE" \
447
  --argjson desired "$CONFIG_JSON" \
 
 
 
 
448
  --argjson whatsappEnabled "$WHATSAPP_CONFIG_ENABLED" \
449
- '.gateway.auth.token = $token
 
450
  | .agents.defaults.model = $model
451
- | .logging.level = $fileLevel
452
- | .logging.consoleLevel = $consoleLevel
453
- | .logging.consoleStyle = $consoleStyle
454
  | .channels = ((.channels // {}) * ($desired.channels // {}))
455
  | .plugins.allow = (((.plugins.allow // []) + ($desired.plugins.allow // [])) | unique)
456
  | .plugins.deny = (((.plugins.deny // []) + ($desired.plugins.deny // [])) | unique)
457
- | .plugins.entries = ((.plugins.entries // {}) * ($desired.plugins.entries // {}))
458
  | if $whatsappEnabled then
459
- .plugins.entries.whatsapp.enabled = true
460
- | .channels.whatsapp = ($desired.channels.whatsapp // {"dmPolicy": "pairing"})
461
- else
 
 
 
462
  .plugins.entries.whatsapp.enabled = false
463
  | del(.channels.whatsapp)
 
 
464
  end' \
465
  "$EXISTING_CONFIG" 2>/dev/null)
466
 
 
11
  OPENCLAW_VERSION="${OPENCLAW_VERSION:-latest}"
12
  OPENCLAW_APP_DIR="/home/node/.openclaw/openclaw-app"
13
  OPENCLAW_RUNTIME_VERSION=""
14
+ OPENCLAW_FILE_LOG_LEVEL_CONFIGURED=false
15
+ OPENCLAW_CONSOLE_LOG_LEVEL_CONFIGURED=false
16
+ OPENCLAW_CONSOLE_LOG_STYLE_CONFIGURED=false
17
+ WHATSAPP_ENABLED_CONFIGURED=false
18
+ [ "${OPENCLAW_FILE_LOG_LEVEL+x}" = "x" ] && OPENCLAW_FILE_LOG_LEVEL_CONFIGURED=true
19
+ [ "${OPENCLAW_CONSOLE_LOG_LEVEL+x}" = "x" ] && OPENCLAW_CONSOLE_LOG_LEVEL_CONFIGURED=true
20
+ [ "${OPENCLAW_CONSOLE_LOG_STYLE+x}" = "x" ] && OPENCLAW_CONSOLE_LOG_STYLE_CONFIGURED=true
21
+ [ "${WHATSAPP_ENABLED+x}" = "x" ] && WHATSAPP_ENABLED_CONFIGURED=true
22
  WHATSAPP_ENABLED="${WHATSAPP_ENABLED:-false}"
23
  WHATSAPP_ENABLED_NORMALIZED=$(printf '%s' "$WHATSAPP_ENABLED" | tr '[:upper:]' '[:lower:]')
24
  SYNC_INTERVAL="${SYNC_INTERVAL:-180}"
 
453
  --arg consoleLevel "$OPENCLAW_CONSOLE_LOG_LEVEL" \
454
  --arg consoleStyle "$OPENCLAW_CONSOLE_LOG_STYLE" \
455
  --argjson desired "$CONFIG_JSON" \
456
+ --argjson fileLogConfigured "$OPENCLAW_FILE_LOG_LEVEL_CONFIGURED" \
457
+ --argjson consoleLogConfigured "$OPENCLAW_CONSOLE_LOG_LEVEL_CONFIGURED" \
458
+ --argjson consoleStyleConfigured "$OPENCLAW_CONSOLE_LOG_STYLE_CONFIGURED" \
459
+ --argjson whatsappConfigured "$WHATSAPP_ENABLED_CONFIGURED" \
460
  --argjson whatsappEnabled "$WHATSAPP_CONFIG_ENABLED" \
461
+ '(.channels.whatsapp // {}) as $existingWhatsapp
462
+ | .gateway.auth.token = $token
463
  | .agents.defaults.model = $model
464
+ | if $fileLogConfigured then .logging.level = $fileLevel else . end
465
+ | if $consoleLogConfigured then .logging.consoleLevel = $consoleLevel else . end
466
+ | if $consoleStyleConfigured then .logging.consoleStyle = $consoleStyle else . end
467
  | .channels = ((.channels // {}) * ($desired.channels // {}))
468
  | .plugins.allow = (((.plugins.allow // []) + ($desired.plugins.allow // [])) | unique)
469
  | .plugins.deny = (((.plugins.deny // []) + ($desired.plugins.deny // [])) | unique)
470
+ | .plugins.entries = (($desired.plugins.entries // {}) * (.plugins.entries // {}))
471
  | if $whatsappEnabled then
472
+ ($desired.channels.whatsapp // {"dmPolicy": "pairing"}) as $desiredWhatsapp
473
+ | .plugins.entries.whatsapp.enabled = true
474
+ | .channels.whatsapp = (($existingWhatsapp * $desiredWhatsapp)
475
+ | if ($existingWhatsapp | has("dmPolicy")) then .dmPolicy = $existingWhatsapp.dmPolicy else . end
476
+ | if ($existingWhatsapp | has("allowFrom")) then .allowFrom = $existingWhatsapp.allowFrom else . end)
477
+ elif $whatsappConfigured then
478
  .plugins.entries.whatsapp.enabled = false
479
  | del(.channels.whatsapp)
480
+ else
481
+ .
482
  end' \
483
  "$EXISTING_CONFIG" 2>/dev/null)
484