8900 commited on
Commit
06ce069
Β·
verified Β·
1 Parent(s): 3a8abf5

Update hf-sync-manager.mjs

Browse files
Files changed (1) hide show
  1. hf-sync-manager.mjs +351 -72
hf-sync-manager.mjs CHANGED
@@ -1,101 +1,380 @@
1
- import fs from "node:fs";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  import path from "node:path";
3
  import { execSync } from "node:child_process";
4
 
5
- var HOME = process.env.OPENCLAW_HOME || process.env.HOME || "/home/user";
6
- var OPENCLAW_DIR = path.join(HOME,".openclaw");
7
- var MEMORY_DIR = path.join(OPENCLAW_DIR,"dataset-memory");
8
- var HF_TOKEN = (process.env.HF_TOKEN||"").trim();
9
- var DATASET_ID = (process.env.HF_DATASET_ID||"").trim();
10
- var REPO_DIR = "/tmp/hf-memory-sync";
11
- var UPLOAD_INTERVAL = 3*60*60*1000;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
 
13
- var TRACK_NAMES = ["conversations","memory","logs"];
14
- var botMemory = { conversations:[], memory:[], logs:[] };
15
 
16
- function log(msg){ console.log("[hf-sync] "+msg); }
17
- function repoUrl(){ return "https://user:"+HF_TOKEN+"@huggingface.co/datasets/"+DATASET_ID; }
18
- function git(args,cwd){ return execSync("git "+args,{ cwd:cwd||REPO_DIR, stdio:"pipe", timeout:120000, env:Object.assign({},process.env,{GIT_TERMINAL_PROMPT:"0"}) }).toString().trim(); }
19
- function sleep(ms){ return new Promise(resolve=>setTimeout(resolve,ms)); }
20
 
21
- function ensureRepo(){
22
- if(fs.existsSync(path.join(REPO_DIR,".git"))) return;
23
- execSync("rm -rf "+REPO_DIR,{stdio:"pipe"});
24
- try{ git("clone --depth 1 "+repoUrl()+" "+REPO_DIR,"/tmp"); } catch(e){ git("init",REPO_DIR); git("remote add origin "+repoUrl(),REPO_DIR); }
25
- try{ git("config user.email openclaw@hf.space"); git("config user.name OpenClaw-Memory-Sync"); git("config pull.rebase false"); } catch(e){ }
26
  }
27
 
28
- function pullLatest(){
29
- try{ git("fetch --quiet origin"); git("pull --quiet --no-rebase origin main"); }
30
- catch(e){ try{ git("pull --quiet --no-rebase origin master"); } catch(e2){ } }
31
  }
32
 
33
- function commitAndPush(){
34
- try{
35
- git("add -A");
36
- var changed = git("diff --cached --name-only");
37
- if(!changed) return 0;
38
- var ts = new Date().toISOString().replace("T"," ").substring(0,19);
39
- var count = changed.split("\n").filter(Boolean).length;
40
- git('commit -m "memory-sync '+ts+'"');
41
- try{ git("push --quiet origin HEAD:main"); } catch(e){ git("push --quiet origin HEAD:master"); }
42
- return count;
43
- }catch(e){ log("Push failed: "+e.message); return 0; }
44
  }
45
 
46
- function readJsonl(filePath,targetArray){ if(!fs.existsSync(filePath)) return; fs.readFileSync(filePath,"utf-8").split("\n").filter(Boolean).forEach(line=>{ try{ targetArray.push(JSON.parse(line)) }catch(e){} }); }
47
- function writeJsonl(filePath,dataArray){ fs.writeFileSync(filePath,dataArray.map(x=>JSON.stringify(x)).join("\n"),"utf-8"); }
 
 
 
 
 
 
 
 
48
 
49
- function todayDir(){ var d=new Date(); return d.getFullYear()+"-"+String(d.getMonth()+1).padStart(2,"0")+"-"+String(d.getDate()).padStart(2,"0"); }
50
- function getRepoDateDir(dateStr){ var dir=path.join(REPO_DIR,"dataset-memory",dateStr); fs.mkdirSync(dir,{recursive:true}); return dir; }
51
 
52
- function loadMemoryFromRepo(){
53
- var repoBase = path.join(REPO_DIR,"dataset-memory");
54
- if(!fs.existsSync(repoBase)) return;
55
- var dateDirs = fs.readdirSync(repoBase).filter(f=>fs.statSync(path.join(repoBase,f)).isDirectory());
56
- dateDirs.forEach(dateDir=>{ TRACK_NAMES.forEach(track=>{ readJsonl(path.join(repoBase,dateDir,track+".jsonl"),botMemory[track]); }); });
57
- log("Loaded: conversations="+botMemory.conversations.length+" memory="+botMemory.memory.length+" logs="+botMemory.logs.length);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
  }
59
 
60
- function saveMemoryToRepo(){
61
- var dateStr=todayDir(); var destDir=getRepoDateDir(dateStr); TRACK_NAMES.forEach(track=>writeJsonl(path.join(destDir,track+".jsonl"),botMemory[track]));
 
 
 
 
 
62
  }
63
 
64
- function bootRestore(){
65
- if(!HF_TOKEN || !DATASET_ID){ log("HF_TOKEN or HF_DATASET_ID not set, skipping restore"); return; }
66
- log("Boot restore starting from "+DATASET_ID+"...");
67
- try{ ensureRepo(); pullLatest(); loadMemoryFromRepo(); log("Boot restore done."); }catch(e){ log("Boot restore failed (non-fatal): "+e.message); }
 
 
 
 
 
 
 
 
 
 
 
68
  }
69
 
70
- async function startUploadLoop(){
71
- while(true){
72
- await sleep(UPLOAD_INTERVAL);
73
- try{ log("Uploading memory to repo..."); ensureRepo(); pullLatest(); saveMemoryToRepo(); var count=commitAndPush(); log("Upload done: "+count+" file(s) changed"); }catch(e){ log("Upload error: "+e.message); }
74
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
  }
76
 
77
- async function main(){
78
- log("Starting...");
79
- log("OPENCLAW_DIR : "+OPENCLAW_DIR);
80
- log("HF_DATASET_ID: "+(DATASET_ID||"NOT SET"));
81
- log("Upload interval: "+(UPLOAD_INTERVAL/3600000)+"h");
82
 
83
- fs.mkdirSync(MEMORY_DIR,{recursive:true});
84
- try{
85
- execSync("git config --global user.email openclaw@hf.space",{stdio:"pipe"});
86
- execSync("git config --global user.name OpenClaw-Memory-Sync",{stdio:"pipe"});
87
- execSync("git config --global http.postBuffer 52428800",{stdio:"pipe"});
88
- execSync("git config --global pull.rebase false",{stdio:"pipe"});
89
- }catch(e){}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
 
91
- bootRestore();
92
 
93
- if(HF_TOKEN && DATASET_ID){
94
- log("Upload loop started (first run in "+(UPLOAD_INTERVAL/3600000)+"h)");
95
- startUploadLoop().catch(e=>{ log("Upload loop error: "+e.message); });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
  }
 
 
 
97
 
98
- log("Ready.");
 
 
 
 
99
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
 
101
- main().catch(e=>{ console.error("[hf-sync] Fatal: "+e.message); });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ============================================================
2
+ // HF Sync Manager - Full Mirror Edition
3
+ //
4
+ // Mirrors ~/.openclaw/ to HF Dataset every 30 min.
5
+ //
6
+ // CRITICAL RULES:
7
+ // 1. NEVER restore openclaw.json from Dataset.
8
+ // openclaw.json is owned by setup-hf-config.mjs only.
9
+ // Restoring it causes an infinite restart loop:
10
+ // user saves -> gateway restarts -> we overwrite their
11
+ // settings with old Dataset copy -> OpenClaw restarts again.
12
+ //
13
+ // 2. RUNNING_FLAG is never deleted on process exit.
14
+ // It lives in /tmp which is only cleared on true container
15
+ // restart. Gateway restarts do NOT clear /tmp.
16
+ // This correctly distinguishes the two restart types.
17
+ //
18
+ // Excluded from sync:
19
+ // openclaw.json (owned by setup, never touch)
20
+ // openclaw.json.bak (backup files)
21
+ // agents/*/sessions/ (session transcripts, runtime only)
22
+ // agents/*/qmd/ (vector index, rebuilt on boot)
23
+ // canvas/ (UI runtime files)
24
+ // *.bak *.tmp *.log
25
+ // ============================================================
26
+
27
+ import fs from "node:fs";
28
  import path from "node:path";
29
  import { execSync } from "node:child_process";
30
 
31
+ // –– config β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”
32
+
33
+ var HOME = process.env.OPENCLAW_HOME || process.env.HOME || "/home/user";
34
+ var OPENCLAW_DIR = path.join(HOME, ".openclaw");
35
+ var WORKSPACE = path.join(OPENCLAW_DIR, "workspace");
36
+ var HF_TOKEN = (process.env.HF_TOKEN || "").trim();
37
+ var DATASET_ID = (process.env.HF_DATASET_ID || "").trim();
38
+ var REPO_DIR = "/tmp/oc-dataset";
39
+ var INTERVAL = 30 * 60 * 1000; // 30 minutes
40
+
41
+ // This flag file distinguishes container restart from gateway restart.
42
+ // Container restart: /tmp is wiped, flag is gone -> do full restore.
43
+ // Gateway restart: /tmp survives, flag exists -> skip restore.
44
+ // IMPORTANT: we never delete this flag on process exit.
45
+ var RUNNING_FLAG = "/tmp/.hf-sync-running";
46
+
47
+ // Files/dirs to EXCLUDE from sync in any direction
48
+ var EXCLUDE_NAMES = [
49
+ "openclaw.json", // NEVER touch - owned by setup-hf-config.mjs
50
+ "openclaw.json.bak", // backup files
51
+ "sessions", // session transcripts inside agents/*/sessions/
52
+ "qmd", // sqlite vector index inside agents/*/qmd/
53
+ "canvas", // UI runtime files
54
+ ".git"
55
+ ];
56
+ var EXCLUDE_EXT = [".bak", ".tmp", ".log"];
57
 
58
+ // –– helpers ———————————————–
 
59
 
60
+ function log(msg) { console.log("[hf-sync] " + msg); }
 
 
 
61
 
62
+ function sleep(ms) {
63
+ return new Promise(function(resolve) { setTimeout(resolve, ms); });
 
 
 
64
  }
65
 
66
+ function repoUrl() {
67
+ return "https://user:" + HF_TOKEN +
68
+ "@huggingface.co/datasets/" + DATASET_ID;
69
  }
70
 
71
+ function git(args) {
72
+ return execSync("git " + args, {
73
+ cwd: REPO_DIR,
74
+ stdio: "pipe",
75
+ timeout: 120000,
76
+ env: Object.assign({}, process.env, { GIT_TERMINAL_PROMPT: "0" })
77
+ }).toString().trim();
 
 
 
 
78
  }
79
 
80
+ function shouldExclude(name) {
81
+ var i;
82
+ for (i = 0; i < EXCLUDE_NAMES.length; i++) {
83
+ if (name === EXCLUDE_NAMES[i]) return true;
84
+ }
85
+ for (i = 0; i < EXCLUDE_EXT.length; i++) {
86
+ if (name.indexOf(EXCLUDE_EXT[i]) >= 0) return true;
87
+ }
88
+ return false;
89
+ }
90
 
91
+ // –– git repo setup ––––––––––––––––––––
 
92
 
93
+ function ensureRepo() {
94
+ if (fs.existsSync(path.join(REPO_DIR, ".git"))) return;
95
+ execSync("rm -rf " + REPO_DIR, { stdio: "pipe" });
96
+ try {
97
+ execSync("git clone --depth 1 " + repoUrl() + " " + REPO_DIR, {
98
+ stdio: "pipe", timeout: 120000,
99
+ env: Object.assign({}, process.env, { GIT_TERMINAL_PROMPT: "0" })
100
+ });
101
+ log("Repo cloned");
102
+ } catch (e) {
103
+ log("Clone failed, init empty: " + e.message);
104
+ fs.mkdirSync(REPO_DIR, { recursive: true });
105
+ execSync("git init " + REPO_DIR, { stdio: "pipe" });
106
+ git("remote add origin " + repoUrl());
107
+ }
108
+ try {
109
+ git("config user.email openclaw@hf.space");
110
+ git("config user.name OpenClaw-Sync");
111
+ git("config pull.rebase false");
112
+ } catch (e) { /* non-fatal */ }
113
  }
114
 
115
+ function pull() {
116
+ try { git("fetch --quiet origin"); } catch (e) { /* ignore */ }
117
+ try { git("pull --quiet --no-rebase origin main"); }
118
+ catch (e) {
119
+ try { git("pull --quiet --no-rebase origin master"); }
120
+ catch (e2) { /* empty repo */ }
121
+ }
122
  }
123
 
124
+ function push() {
125
+ try {
126
+ git("add -A");
127
+ var changed = git("diff --cached --name-only");
128
+ if (!changed) return 0;
129
+ var n = changed.split("\n").filter(Boolean).length;
130
+ var ts = new Date().toISOString().replace("T", " ").substring(0, 19);
131
+ git('commit -m "sync ' + ts + '"');
132
+ try { git("push --quiet origin HEAD:main"); }
133
+ catch (e) { git("push --quiet origin HEAD:master"); }
134
+ return n;
135
+ } catch (e) {
136
+ log("Push failed: " + e.message);
137
+ return 0;
138
+ }
139
  }
140
 
141
+ // –– directory mirror: local -> repo ———————–
142
+
143
+ function mirrorToRepo(srcDir, dstDir) {
144
+ if (!fs.existsSync(srcDir)) return 0;
145
+ fs.mkdirSync(dstDir, { recursive: true });
146
+ var count = 0;
147
+ fs.readdirSync(srcDir).forEach(function(name) {
148
+ if (shouldExclude(name)) return;
149
+ var src = path.join(srcDir, name);
150
+ var dst = path.join(dstDir, name);
151
+ try {
152
+ if (fs.statSync(src).isDirectory()) {
153
+ count += mirrorToRepo(src, dst);
154
+ } else {
155
+ fs.copyFileSync(src, dst);
156
+ count++;
157
+ }
158
+ } catch (e) {
159
+ log("Copy error [" + name + "]: " + e.message);
160
+ }
161
+ });
162
+ return count;
163
  }
164
 
165
+ // –– directory mirror: repo -> local ———————–
166
+ // Note: shouldExclude ensures openclaw.json is never restored
 
 
 
167
 
168
+ function mirrorFromRepo(srcDir, dstDir) {
169
+ if (!fs.existsSync(srcDir)) return 0;
170
+ fs.mkdirSync(dstDir, { recursive: true });
171
+ var count = 0;
172
+ fs.readdirSync(srcDir).forEach(function(name) {
173
+ if (shouldExclude(name)) return; // never restore openclaw.json
174
+ var src = path.join(srcDir, name);
175
+ var dst = path.join(dstDir, name);
176
+ try {
177
+ if (fs.statSync(src).isDirectory()) {
178
+ count += mirrorFromRepo(src, dst);
179
+ } else {
180
+ fs.copyFileSync(src, dst);
181
+ count++;
182
+ }
183
+ } catch (e) {
184
+ log("Restore error [" + name + "]: " + e.message);
185
+ }
186
+ });
187
+ return count;
188
+ }
189
+
190
+ // –– seed default workspace files –––––––––––––
191
+
192
+ function seedWorkspaceIfNeeded() {
193
+ fs.mkdirSync(WORKSPACE, { recursive: true });
194
+ fs.mkdirSync(path.join(WORKSPACE, "memory"), { recursive: true });
195
+
196
+ var soul = path.join(WORKSPACE, "SOUL.md");
197
+ if (!fs.existsSync(soul)) {
198
+ fs.writeFileSync(soul, [
199
+ "# Soul",
200
+ "",
201
+ "You are a helpful, warm, concise AI assistant.",
202
+ "",
203
+ "## Language",
204
+ "",
205
+ "Default language: Simplified Chinese.",
206
+ "Always reply in Chinese unless the user writes in another language first.",
207
+ "",
208
+ "## Tone",
209
+ "",
210
+ "- Natural and friendly, not overly formal",
211
+ "- Concise and to the point",
212
+ "- Ask one clarifying question at a time when needed"
213
+ ].join("\n") + "\n", "utf-8");
214
+ log("Seeded workspace/SOUL.md");
215
+ }
216
+
217
+ var agents = path.join(WORKSPACE, "AGENTS.md");
218
+ if (!fs.existsSync(agents)) {
219
+ fs.writeFileSync(agents, [
220
+ "# Agent Instructions",
221
+ "",
222
+ "## Boot sequence",
223
+ "",
224
+ "1. Read SOUL.md - language and persona",
225
+ "2. Read USER.md if present - user profile",
226
+ "3. Read MEMORY.md - long-term facts and rules",
227
+ "4. Read today and yesterday memory/YYYY-MM-DD.md if present",
228
+ "",
229
+ "## Memory rules",
230
+ "",
231
+ "- Write important facts to MEMORY.md when asked",
232
+ "- Log daily context to memory/YYYY-MM-DD.md",
233
+ "- Do not ask for information already provided"
234
+ ].join("\n") + "\n", "utf-8");
235
+ log("Seeded workspace/AGENTS.md");
236
+ }
237
+
238
+ var mem = path.join(WORKSPACE, "MEMORY.md");
239
+ if (!fs.existsSync(mem)) {
240
+ fs.writeFileSync(mem, [
241
+ "# Long-term Memory",
242
+ "",
243
+ "<!-- OpenClaw writes important facts here. -->",
244
+ "<!-- Loaded at the start of every session. -->"
245
+ ].join("\n") + "\n", "utf-8");
246
+ log("Seeded workspace/MEMORY.md");
247
+ }
248
+ }
249
 
250
+ // –– boot restore (container restart only) —————–
251
 
252
+ function bootRestore() {
253
+ if (!HF_TOKEN || !DATASET_ID) {
254
+ log("No HF_TOKEN/HF_DATASET_ID - skipping Dataset restore");
255
+ seedWorkspaceIfNeeded();
256
+ return;
257
+ }
258
+
259
+ log("Boot restore from " + DATASET_ID + "…");
260
+ try {
261
+ ensureRepo();
262
+ pull();
263
+ } catch (e) {
264
+ log("Repo init failed (" + e.message + ") - starting fresh");
265
+ seedWorkspaceIfNeeded();
266
+ return;
267
+ }
268
+
269
+ var repoOC = path.join(REPO_DIR, "openclaw");
270
+ if (fs.existsSync(repoOC)) {
271
+ // mirrorFromRepo skips openclaw.json automatically via shouldExclude
272
+ var n = mirrorFromRepo(repoOC, OPENCLAW_DIR);
273
+ log("Boot restore done: " + n + " file(s) restored (openclaw.json excluded)");
274
+ } else {
275
+ log("No data in Dataset yet - starting fresh");
276
+ }
277
+
278
+ seedWorkspaceIfNeeded();
279
+ log("Boot restore complete");
280
+ }
281
+
282
+ // –– sync cycle (every 30 min) —————————–
283
+
284
+ function runSync() {
285
+ if (!HF_TOKEN || !DATASET_ID) return;
286
+
287
+ var ts = new Date().toISOString().replace("T", " ").substring(0, 19);
288
+ log("Sync at " + ts + "…");
289
+
290
+ try {
291
+ ensureRepo();
292
+ pull();
293
+
294
+ var repoOC = path.join(REPO_DIR, "openclaw");
295
+ if (fs.existsSync(repoOC)) {
296
+ execSync("rm -rf " + repoOC, { stdio: "pipe" });
297
+ }
298
+ fs.mkdirSync(repoOC, { recursive: true });
299
+
300
+ // mirrorToRepo also skips openclaw.json via shouldExclude
301
+ // We explicitly copy openclaw.json separately only for backup
302
+ var n = mirrorToRepo(OPENCLAW_DIR, repoOC);
303
+
304
+ // Upload openclaw.json separately without excluding it from upload
305
+ // (we exclude it from RESTORE, not from upload)
306
+ var localCfg = path.join(OPENCLAW_DIR, "openclaw.json");
307
+ if (fs.existsSync(localCfg)) {
308
+ try {
309
+ var content = fs.readFileSync(localCfg, "utf-8").trim();
310
+ JSON.parse(content); // validate
311
+ fs.copyFileSync(localCfg, path.join(repoOC, "openclaw.json"));
312
+ n++;
313
+ log("openclaw.json uploaded (validated)");
314
+ } catch (e) {
315
+ log("openclaw.json skipped (invalid JSON): " + e.message);
316
  }
317
+ }
318
+
319
+ log("Mirrored " + n + " file(s) to Dataset");
320
 
321
+ var pushed = push();
322
+ if (pushed > 0) {
323
+ log("Sync done: " + pushed + " file(s) pushed");
324
+ } else {
325
+ log("Sync done: no changes");
326
  }
327
+ } catch (e) {
328
+ log("Sync failed (retry in 30 min): " + e.message);
329
+ }
330
+ }
331
+
332
+ // –– main –––––––––––––––––––––––––
333
+
334
+ (async function() {
335
+ log("Starting…");
336
+ log("OPENCLAW_DIR : " + OPENCLAW_DIR);
337
+ log("DATASET : " + (DATASET_ID || "NOT SET"));
338
+ log("RUNNING_FLAG : " + RUNNING_FLAG);
339
+
340
+ fs.mkdirSync(OPENCLAW_DIR, { recursive: true });
341
+ fs.mkdirSync(WORKSPACE, { recursive: true });
342
+
343
+ try {
344
+ execSync("git config --global user.email openclaw@hf.space", { stdio: "pipe" });
345
+ execSync("git config --global user.name OpenClaw-Sync", { stdio: "pipe" });
346
+ execSync("git config --global http.postBuffer 52428800", { stdio: "pipe" });
347
+ execSync("git config --global pull.rebase false", { stdio: "pipe" });
348
+ } catch (e) { /* non-fatal */ }
349
+
350
+ // Detect restart type:
351
+ // /tmp survives gateway restart but is wiped on container restart.
352
+ // We NEVER delete this flag - only container restart clears it.
353
+ var isGatewayRestart = fs.existsSync(RUNNING_FLAG);
354
 
355
+ if (isGatewayRestart) {
356
+ log("Gateway restart detected - skipping boot restore");
357
+ log("(openclaw.json and workspace already on disk, nothing to restore)");
358
+ } else {
359
+ log("Container start detected - running full boot restore");
360
+ bootRestore();
361
+ }
362
+
363
+ // Write flag. Never delete it (not on SIGTERM, not on exit).
364
+ // Container restart is the only thing that clears /tmp.
365
+ fs.writeFileSync(RUNNING_FLAG, String(process.pid), "utf-8");
366
+ log("Running flag set: " + RUNNING_FLAG);
367
+
368
+ if (!HF_TOKEN || !DATASET_ID) {
369
+ log("Sync disabled: set HF_TOKEN and HF_DATASET_ID in Space Secrets");
370
+ return;
371
+ }
372
+
373
+ log("Sync loop starting (first run in 10 min)…");
374
+ await sleep(10 * 60 * 1000);
375
+
376
+ while (true) {
377
+ runSync();
378
+ await sleep(INTERVAL);
379
+ }
380
+ })().catch(function(e) { console.error("[hf-sync] Fatal: " + e.message); });