8900 commited on
Commit
e76aaa9
·
verified ·
1 Parent(s): ae72878

Update hf-sync-manager.mjs

Browse files
Files changed (1) hide show
  1. hf-sync-manager.mjs +62 -156
hf-sync-manager.mjs CHANGED
@@ -3,33 +3,34 @@
3
  //
4
  // TRACK 1 - Settings (openclaw.json):
5
  // Boot: repo/settings/openclaw.json -> local disk
6
- // Upload: local disk -> repo/settings/openclaw.json (overwrite, always single copy)
7
- // Rule: repo always has exactly ONE copy, no versioning
 
 
 
8
  //
9
  // TRACK 2 - Memory (conversations, history, context, agents, logs):
10
  // Boot: repo/data/* -> local disk (full restore, all sessions)
11
- // Upload: local disk -> repo/data/* every 3h (upload only, NEVER download mid-run)
12
- // Rule: runtime is write-only to prevent memory flooding
13
  // ============================================================
14
 
15
  import fs from "node:fs";
16
  import path from "node:path";
17
  import { execSync } from "node:child_process";
18
 
19
- // -- config -----------------------------------
20
 
21
  var HOME = process.env.OPENCLAW_HOME || process.env.HOME || "/home/user";
22
  var OPENCLAW_DIR = path.join(HOME, ".openclaw");
23
  var HF_TOKEN = (process.env.HF_TOKEN || "").trim();
24
  var DATASET_ID = (process.env.HF_DATASET_ID || "").trim();
25
  var REPO_DIR = "/tmp/hf-memory-sync";
26
- var UPLOAD_INTERVAL = 3 * 60 * 60 * 1000; // 3 hours
27
 
28
- // Settings track: only this file
29
- var SETTINGS_FILE = "openclaw.json";
30
- var REPO_SETTINGS = "settings"; // repo subfolder
31
 
32
- // Memory track: these dirs/files inside OPENCLAW_DIR
33
  var MEMORY_TARGETS = [
34
  "memory",
35
  "history",
@@ -39,8 +40,8 @@ var MEMORY_TARGETS = [
39
  "agents"
40
  ];
41
 
42
- // Never touch these in either track
43
  var NEVER_SYNC = [
 
44
  "openclaw.json.bak",
45
  ".sync-state.json",
46
  "settings.json",
@@ -52,7 +53,7 @@ var NEVER_SYNC = [
52
  "canvas"
53
  ];
54
 
55
- // -- helpers -----------------------------------
56
 
57
  function log(msg) {
58
  console.log("[hf-sync] " + msg);
@@ -64,7 +65,7 @@ function sleep(ms) {
64
 
65
  function repoUrl() {
66
  return "https://user:" + HF_TOKEN +
67
- "@huggingface.co/datasets/" + DATASET_ID;
68
  }
69
 
70
  function git(args) {
@@ -77,16 +78,40 @@ function git(args) {
77
  }
78
 
79
  function isMemoryTarget(name) {
80
- for (let i = 0; i < NEVER_SYNC.length; i++) {
 
81
  if (name === NEVER_SYNC[i]) return false;
82
  }
83
- for (let i = 0; i < MEMORY_TARGETS.length; i++) {
84
  if (name === MEMORY_TARGETS[i]) return true;
85
  }
86
  return false;
87
  }
88
 
89
- // -- git repo setup ----------------------------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
 
91
  function ensureRepo() {
92
  if (fs.existsSync(path.join(REPO_DIR, ".git"))) return;
@@ -121,7 +146,6 @@ function pullLatest() {
121
  }
122
  }
123
 
124
- // Single commit+push for both tracks
125
  function commitAndPush(label) {
126
  try {
127
  git("add -A");
@@ -142,17 +166,6 @@ function commitAndPush(label) {
142
  }
143
  }
144
 
145
- // -- copy helper ------------------------------
146
-
147
- function copyItem(src, dst) {
148
- if (fs.statSync(src).isDirectory()) {
149
- execSync("cp -r " + src + " " + dst, { stdio: "pipe" });
150
- } else {
151
- fs.mkdirSync(path.dirname(dst), { recursive: true });
152
- fs.copyFileSync(src, dst);
153
- }
154
- }
155
-
156
  // ============================================================
157
  // TRACK 1: Settings
158
  // ============================================================
@@ -162,14 +175,25 @@ function settingsBootRestore() {
162
  var localFile = path.join(OPENCLAW_DIR, SETTINGS_FILE);
163
 
164
  if (!fs.existsSync(repoFile)) {
165
- log("[settings] No settings in repo yet, using local defaults");
 
 
 
 
 
 
 
166
  return false;
167
  }
168
 
169
  try {
170
  fs.mkdirSync(OPENCLAW_DIR, { recursive: true });
 
 
 
 
171
  fs.copyFileSync(repoFile, localFile);
172
- log("[settings] Boot restore done: openclaw.json <- repo");
173
  return true;
174
  } catch (e) {
175
  log("[settings] Boot restore failed: " + e.message);
@@ -179,8 +203,10 @@ function settingsBootRestore() {
179
 
180
  function settingsUpload() {
181
  var localFile = path.join(OPENCLAW_DIR, SETTINGS_FILE);
182
- if (!fs.existsSync(localFile)) {
183
- log("[settings] No local openclaw.json to upload");
 
 
184
  return 0;
185
  }
186
 
@@ -188,7 +214,9 @@ function settingsUpload() {
188
  var repoFile = path.join(repoDir, SETTINGS_FILE);
189
 
190
  fs.mkdirSync(repoDir, { recursive: true });
 
191
  fs.copyFileSync(localFile, repoFile);
 
192
  return 1;
193
  }
194
 
@@ -198,10 +226,7 @@ function settingsUpload() {
198
 
199
  function memoryBootRestore() {
200
  var src = path.join(REPO_DIR, "data");
201
- if (!fs.existsSync(src)) {
202
- log("[memory] No memory data in repo yet");
203
- return 0;
204
- }
205
 
206
  var items = fs.readdirSync(src);
207
  var copied = 0;
@@ -249,123 +274,4 @@ function memoryUpload() {
249
  function scanMemoryDisk() {
250
  if (!fs.existsSync(OPENCLAW_DIR)) return [];
251
  return fs.readdirSync(OPENCLAW_DIR).filter(isMemoryTarget);
252
- }
253
-
254
- // ============================================================
255
- // Boot sequence (runs once at startup)
256
- // ============================================================
257
-
258
- function bootRestore() {
259
- if (!HF_TOKEN || !DATASET_ID) {
260
- log("HF_TOKEN or HF_DATASET_ID not set, skipping all restore");
261
- return;
262
- }
263
-
264
- log("Boot restore starting from " + DATASET_ID + "...");
265
- try {
266
- ensureRepo();
267
- pullLatest();
268
- } catch (e) {
269
- log("Repo init failed (non-fatal): " + e.message);
270
- return;
271
- }
272
-
273
- settingsBootRestore();
274
- var restored = memoryBootRestore();
275
- if (restored > 0) {
276
- log("[memory] Boot restore done: " + restored + " item(s) -> " + OPENCLAW_DIR);
277
- } else {
278
- log("[memory] Boot restore: no memory data in repository yet");
279
- }
280
- }
281
-
282
- // ============================================================
283
- // Upload cycle (runs every 3h, NEVER downloads)
284
- // ============================================================
285
-
286
- function runUploadCycle() {
287
- if (!HF_TOKEN || !DATASET_ID) return;
288
-
289
- var ts = new Date().toISOString().replace("T", " ").substring(0, 19);
290
- log("Upload cycle starting at " + ts + "...");
291
-
292
- try {
293
- ensureRepo();
294
- pullLatest(); // pull repo meta only, then we overwrite with our data
295
-
296
- // Track 1: settings (overwrite single copy)
297
- var sCopied = settingsUpload();
298
- log("[settings] Copied " + sCopied + " file(s) to repo");
299
-
300
- // Track 2: memory (upload from disk, never read back)
301
- var targets = scanMemoryDisk();
302
- if (targets.length > 0) {
303
- var mCopied = memoryUpload();
304
- log("[memory] Copied " + mCopied + " item(s) to repo: " + targets.join(", "));
305
- } else {
306
- log("[memory] No memory targets found on disk yet");
307
- }
308
-
309
- var changed = commitAndPush("sync");
310
- if (changed > 0) {
311
- log("Upload done: " + changed + " file(s) committed at " + ts);
312
- } else {
313
- log("Upload done: no changes since last sync");
314
- }
315
- } catch (e) {
316
- log("Upload cycle failed (will retry in 3h): " + e.message);
317
- }
318
- }
319
-
320
- // ============================================================
321
- // Upload loop
322
- // ============================================================
323
-
324
- async function startUploadLoop() {
325
- log("First upload scheduled in 10 min...");
326
- await sleep(10 * 60 * 1000);
327
- runUploadCycle();
328
-
329
- while (true) {
330
- await sleep(UPLOAD_INTERVAL);
331
- runUploadCycle();
332
- }
333
- }
334
-
335
- // ============================================================
336
- // Main
337
- // ============================================================
338
-
339
- async function main() {
340
- log("Starting...");
341
- log("OPENCLAW_DIR : " + OPENCLAW_DIR);
342
- log("HF_DATASET_ID : " + (DATASET_ID || "NOT SET"));
343
- log("Settings target : " + SETTINGS_FILE + " (single copy, overwrite)");
344
- log("Memory targets : " + MEMORY_TARGETS.join(", "));
345
- log("Upload every : " + (UPLOAD_INTERVAL / 3600000) + "h");
346
-
347
- fs.mkdirSync(OPENCLAW_DIR, { recursive: true });
348
-
349
- try {
350
- execSync("git config --global user.email openclaw@hf.space", { stdio: "pipe" });
351
- execSync("git config --global user.name OpenClaw-Memory-Sync", { stdio: "pipe" });
352
- execSync("git config --global http.postBuffer 52428800", { stdio: "pipe" });
353
- execSync("git config --global pull.rebase false", { stdio: "pipe" });
354
- } catch (e) { /* non-fatal */ }
355
-
356
- bootRestore();
357
-
358
- if (HF_TOKEN && DATASET_ID) {
359
- startUploadLoop().catch(function(e) {
360
- log("Upload loop crashed: " + e.message);
361
- });
362
- } else {
363
- log("Upload loop disabled: set HF_TOKEN and HF_DATASET_ID in Space Secrets");
364
- }
365
-
366
- log("Ready.");
367
- }
368
-
369
- main().catch(function(e) {
370
- console.error("[hf-sync] Fatal: " + e.message);
371
- });
 
3
  //
4
  // TRACK 1 - Settings (openclaw.json):
5
  // Boot: repo/settings/openclaw.json -> local disk
6
+ // ONLY if repo file passes JSON validation
7
+ // If invalid: keep local file, log warning
8
+ // Upload: local disk -> repo/settings/openclaw.json
9
+ // ONLY if local file passes JSON validation
10
+ // Single copy, always overwrite, never accumulate
11
  //
12
  // TRACK 2 - Memory (conversations, history, context, agents, logs):
13
  // Boot: repo/data/* -> local disk (full restore, all sessions)
14
+ // Upload: local disk -> repo/data/* every 3h
15
+ // Upload only, NEVER download during runtime
16
  // ============================================================
17
 
18
  import fs from "node:fs";
19
  import path from "node:path";
20
  import { execSync } from "node:child_process";
21
 
22
+ // -- config ---------------------------------------------------
23
 
24
  var HOME = process.env.OPENCLAW_HOME || process.env.HOME || "/home/user";
25
  var OPENCLAW_DIR = path.join(HOME, ".openclaw");
26
  var HF_TOKEN = (process.env.HF_TOKEN || "").trim();
27
  var DATASET_ID = (process.env.HF_DATASET_ID || "").trim();
28
  var REPO_DIR = "/tmp/hf-memory-sync";
29
+ var UPLOAD_INTERVAL = 3 * 60 * 60 * 1000;
30
 
31
+ var SETTINGS_FILE = "openclaw.json";
32
+ var REPO_SETTINGS = "settings";
 
33
 
 
34
  var MEMORY_TARGETS = [
35
  "memory",
36
  "history",
 
40
  "agents"
41
  ];
42
 
 
43
  var NEVER_SYNC = [
44
+ "openclaw.json",
45
  "openclaw.json.bak",
46
  ".sync-state.json",
47
  "settings.json",
 
53
  "canvas"
54
  ];
55
 
56
+ // -- helpers --------------------------------------------------
57
 
58
  function log(msg) {
59
  console.log("[hf-sync] " + msg);
 
65
 
66
  function repoUrl() {
67
  return "https://user:" + HF_TOKEN +
68
+ "@huggingface.co/datasets/" + DATASET_ID;
69
  }
70
 
71
  function git(args) {
 
78
  }
79
 
80
  function isMemoryTarget(name) {
81
+ var i;
82
+ for (i = 0; i < NEVER_SYNC.length; i++) {
83
  if (name === NEVER_SYNC[i]) return false;
84
  }
85
+ for (i = 0; i < MEMORY_TARGETS.length; i++) {
86
  if (name === MEMORY_TARGETS[i]) return true;
87
  }
88
  return false;
89
  }
90
 
91
+ // FIX: validate JSON before any config file operation
92
+ // Returns true if file exists and contains valid JSON
93
+ function isValidJson(filePath) {
94
+ try {
95
+ if (!fs.existsSync(filePath)) return false;
96
+ var content = fs.readFileSync(filePath, "utf-8").trim();
97
+ if (!content) return false;
98
+ JSON.parse(content);
99
+ return true;
100
+ } catch (e) {
101
+ return false;
102
+ }
103
+ }
104
+
105
+ function copyItem(src, dst) {
106
+ if (fs.statSync(src).isDirectory()) {
107
+ execSync("cp -r " + src + " " + dst, { stdio: "pipe" });
108
+ } else {
109
+ fs.mkdirSync(path.dirname(dst), { recursive: true });
110
+ fs.copyFileSync(src, dst);
111
+ }
112
+ }
113
+
114
+ // -- git repo setup ------------------------------------------
115
 
116
  function ensureRepo() {
117
  if (fs.existsSync(path.join(REPO_DIR, ".git"))) return;
 
146
  }
147
  }
148
 
 
149
  function commitAndPush(label) {
150
  try {
151
  git("add -A");
 
166
  }
167
  }
168
 
 
 
 
 
 
 
 
 
 
 
 
169
  // ============================================================
170
  // TRACK 1: Settings
171
  // ============================================================
 
175
  var localFile = path.join(OPENCLAW_DIR, SETTINGS_FILE);
176
 
177
  if (!fs.existsSync(repoFile)) {
178
+ log("[settings] No settings in repo yet, keeping local defaults");
179
+ return false;
180
+ }
181
+
182
+ // FIX: validate repo file before overwriting local config
183
+ if (!isValidJson(repoFile)) {
184
+ log("[settings] WARN: repo openclaw.json is invalid JSON, skipping restore to protect local config");
185
+ log("[settings] Corrupted repo file will be overwritten on next upload");
186
  return false;
187
  }
188
 
189
  try {
190
  fs.mkdirSync(OPENCLAW_DIR, { recursive: true });
191
+ // Backup local before overwriting
192
+ if (fs.existsSync(localFile)) {
193
+ fs.copyFileSync(localFile, localFile + ".pre-restore-bak");
194
+ }
195
  fs.copyFileSync(repoFile, localFile);
196
+ log("[settings] Boot restore done: openclaw.json <- repo (validated OK)");
197
  return true;
198
  } catch (e) {
199
  log("[settings] Boot restore failed: " + e.message);
 
203
 
204
  function settingsUpload() {
205
  var localFile = path.join(OPENCLAW_DIR, SETTINGS_FILE);
206
+
207
+ // FIX: validate local file before uploading to repo
208
+ if (!isValidJson(localFile)) {
209
+ log("[settings] WARN: local openclaw.json is invalid JSON, skipping upload");
210
  return 0;
211
  }
212
 
 
214
  var repoFile = path.join(repoDir, SETTINGS_FILE);
215
 
216
  fs.mkdirSync(repoDir, { recursive: true });
217
+ // Single copy, direct overwrite - no version accumulation
218
  fs.copyFileSync(localFile, repoFile);
219
+ log("[settings] Uploaded openclaw.json -> repo (validated OK)");
220
  return 1;
221
  }
222
 
 
226
 
227
  function memoryBootRestore() {
228
  var src = path.join(REPO_DIR, "data");
229
+ if (!fs.existsSync(src)) return 0;
 
 
 
230
 
231
  var items = fs.readdirSync(src);
232
  var copied = 0;
 
274
  function scanMemoryDisk() {
275
  if (!fs.existsSync(OPENCLAW_DIR)) return [];
276
  return fs.readdirSync(OPENCLAW_DIR).filter(isMemoryTarget);
277
+ }