@memtensor/memos-local-openclaw-plugin 0.3.20 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +239 -22
- package/dist/capture/index.d.ts +1 -1
- package/dist/capture/index.d.ts.map +1 -1
- package/dist/capture/index.js +33 -8
- package/dist/capture/index.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/ingest/providers/anthropic.d.ts.map +1 -1
- package/dist/ingest/providers/anthropic.js +22 -8
- package/dist/ingest/providers/anthropic.js.map +1 -1
- package/dist/ingest/providers/bedrock.d.ts.map +1 -1
- package/dist/ingest/providers/bedrock.js +22 -8
- package/dist/ingest/providers/bedrock.js.map +1 -1
- package/dist/ingest/providers/gemini.d.ts.map +1 -1
- package/dist/ingest/providers/gemini.js +22 -8
- package/dist/ingest/providers/gemini.js.map +1 -1
- package/dist/ingest/providers/index.d.ts +13 -18
- package/dist/ingest/providers/index.d.ts.map +1 -1
- package/dist/ingest/providers/index.js +213 -139
- package/dist/ingest/providers/index.js.map +1 -1
- package/dist/ingest/providers/openai.d.ts +1 -1
- package/dist/ingest/providers/openai.d.ts.map +1 -1
- package/dist/ingest/providers/openai.js +37 -17
- package/dist/ingest/providers/openai.js.map +1 -1
- package/dist/ingest/task-processor.d.ts +28 -3
- package/dist/ingest/task-processor.d.ts.map +1 -1
- package/dist/ingest/task-processor.js +166 -67
- package/dist/ingest/task-processor.js.map +1 -1
- package/dist/ingest/worker.d.ts.map +1 -1
- package/dist/ingest/worker.js +97 -75
- package/dist/ingest/worker.js.map +1 -1
- package/dist/shared/llm-call.d.ts +26 -0
- package/dist/shared/llm-call.d.ts.map +1 -0
- package/dist/shared/llm-call.js +163 -0
- package/dist/shared/llm-call.js.map +1 -0
- package/dist/skill/evaluator.d.ts +0 -3
- package/dist/skill/evaluator.d.ts.map +1 -1
- package/dist/skill/evaluator.js +34 -59
- package/dist/skill/evaluator.js.map +1 -1
- package/dist/skill/evolver.d.ts +22 -1
- package/dist/skill/evolver.d.ts.map +1 -1
- package/dist/skill/evolver.js +191 -32
- package/dist/skill/evolver.js.map +1 -1
- package/dist/skill/generator.d.ts +0 -3
- package/dist/skill/generator.d.ts.map +1 -1
- package/dist/skill/generator.js +15 -50
- package/dist/skill/generator.js.map +1 -1
- package/dist/skill/upgrader.d.ts +0 -2
- package/dist/skill/upgrader.d.ts.map +1 -1
- package/dist/skill/upgrader.js +4 -39
- package/dist/skill/upgrader.js.map +1 -1
- package/dist/skill/validator.d.ts +0 -2
- package/dist/skill/validator.d.ts.map +1 -1
- package/dist/skill/validator.js +14 -44
- package/dist/skill/validator.js.map +1 -1
- package/dist/storage/sqlite.d.ts +13 -2
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +92 -15
- package/dist/storage/sqlite.js.map +1 -1
- package/dist/tools/memory-get.d.ts.map +1 -1
- package/dist/tools/memory-get.js +5 -1
- package/dist/tools/memory-get.js.map +1 -1
- package/dist/tools/memory-search.d.ts.map +1 -1
- package/dist/tools/memory-search.js +5 -0
- package/dist/tools/memory-search.js.map +1 -1
- package/dist/tools/memory-timeline.d.ts.map +1 -1
- package/dist/tools/memory-timeline.js +11 -2
- package/dist/tools/memory-timeline.js.map +1 -1
- package/dist/types.d.ts +2 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -1
- package/dist/types.js.map +1 -1
- package/dist/viewer/html.d.ts +1 -1
- package/dist/viewer/html.d.ts.map +1 -1
- package/dist/viewer/html.js +380 -26
- package/dist/viewer/html.js.map +1 -1
- package/dist/viewer/server.d.ts +9 -0
- package/dist/viewer/server.d.ts.map +1 -1
- package/dist/viewer/server.js +549 -184
- package/dist/viewer/server.js.map +1 -1
- package/index.ts +9 -3
- package/package.json +2 -1
- package/src/capture/index.ts +39 -10
- package/src/index.ts +3 -2
- package/src/ingest/providers/anthropic.ts +22 -8
- package/src/ingest/providers/bedrock.ts +22 -8
- package/src/ingest/providers/gemini.ts +22 -8
- package/src/ingest/providers/index.ts +192 -142
- package/src/ingest/providers/openai.ts +37 -17
- package/src/ingest/task-processor.ts +183 -65
- package/src/ingest/worker.ts +98 -77
- package/src/shared/llm-call.ts +144 -0
- package/src/skill/evaluator.ts +35 -64
- package/src/skill/evolver.ts +201 -33
- package/src/skill/generator.ts +16 -59
- package/src/skill/upgrader.ts +5 -43
- package/src/skill/validator.ts +15 -47
- package/src/storage/sqlite.ts +107 -15
- package/src/tools/memory-get.ts +6 -1
- package/src/tools/memory-search.ts +6 -0
- package/src/tools/memory-timeline.ts +13 -1
- package/src/types.ts +2 -1
- package/src/viewer/html.ts +380 -26
- package/src/viewer/server.ts +535 -197
package/dist/viewer/server.js
CHANGED
|
@@ -168,6 +168,12 @@ class ViewerServer {
|
|
|
168
168
|
this.serveSearch(req, res, url);
|
|
169
169
|
else if (p === "/api/tasks" && req.method === "GET")
|
|
170
170
|
this.serveTasks(res, url);
|
|
171
|
+
else if (p.match(/^\/api\/task\/[^/]+\/retry-skill$/) && req.method === "POST")
|
|
172
|
+
this.handleTaskRetrySkill(req, res, p);
|
|
173
|
+
else if (p.startsWith("/api/task/") && req.method === "DELETE")
|
|
174
|
+
this.handleTaskDelete(res, p);
|
|
175
|
+
else if (p.startsWith("/api/task/") && req.method === "PUT")
|
|
176
|
+
this.handleTaskUpdate(req, res, p);
|
|
171
177
|
else if (p.startsWith("/api/task/") && req.method === "GET")
|
|
172
178
|
this.serveTaskDetail(res, p);
|
|
173
179
|
else if (p === "/api/skills" && req.method === "GET")
|
|
@@ -178,6 +184,10 @@ class ViewerServer {
|
|
|
178
184
|
this.serveSkillFiles(res, p);
|
|
179
185
|
else if (p.match(/^\/api\/skill\/[^/]+\/visibility$/) && req.method === "PUT")
|
|
180
186
|
this.handleSkillVisibility(req, res, p);
|
|
187
|
+
else if (p.startsWith("/api/skill/") && req.method === "DELETE")
|
|
188
|
+
this.handleSkillDelete(res, p);
|
|
189
|
+
else if (p.startsWith("/api/skill/") && req.method === "PUT")
|
|
190
|
+
this.handleSkillUpdate(req, res, p);
|
|
181
191
|
else if (p.startsWith("/api/skill/") && req.method === "GET")
|
|
182
192
|
this.serveSkillDetail(res, p);
|
|
183
193
|
else if (p === "/api/memory" && req.method === "POST")
|
|
@@ -200,6 +210,10 @@ class ViewerServer {
|
|
|
200
210
|
this.serveConfig(res);
|
|
201
211
|
else if (p === "/api/config" && req.method === "PUT")
|
|
202
212
|
this.handleSaveConfig(req, res);
|
|
213
|
+
else if (p === "/api/test-model" && req.method === "POST")
|
|
214
|
+
this.handleTestModel(req, res);
|
|
215
|
+
else if (p === "/api/fallback-model" && req.method === "GET")
|
|
216
|
+
this.serveFallbackModel(res);
|
|
203
217
|
else if (p === "/api/auth/logout" && req.method === "POST")
|
|
204
218
|
this.handleLogout(req, res);
|
|
205
219
|
else if (p === "/api/migrate/scan" && req.method === "GET")
|
|
@@ -401,16 +415,21 @@ class ViewerServer {
|
|
|
401
415
|
const limit = Math.min(100, Math.max(1, Number(url.searchParams.get("limit")) || 50));
|
|
402
416
|
const offset = Math.max(0, Number(url.searchParams.get("offset")) || 0);
|
|
403
417
|
const { tasks, total } = this.store.listTasks({ status, limit, offset });
|
|
404
|
-
const
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
418
|
+
const db = this.store.db;
|
|
419
|
+
const items = tasks.map((t) => {
|
|
420
|
+
const meta = db.prepare("SELECT skill_status FROM tasks WHERE id = ?").get(t.id);
|
|
421
|
+
return {
|
|
422
|
+
id: t.id,
|
|
423
|
+
sessionKey: t.sessionKey,
|
|
424
|
+
title: t.title,
|
|
425
|
+
summary: t.summary ? (t.summary.length > 300 ? t.summary.slice(0, 297) + "..." : t.summary) : "",
|
|
426
|
+
status: t.status,
|
|
427
|
+
startedAt: t.startedAt,
|
|
428
|
+
endedAt: t.endedAt,
|
|
429
|
+
chunkCount: this.store.countChunksByTask(t.id),
|
|
430
|
+
skillStatus: meta?.skill_status ?? null,
|
|
431
|
+
};
|
|
432
|
+
});
|
|
414
433
|
this.jsonResponse(res, { tasks: items, total, limit, offset });
|
|
415
434
|
}
|
|
416
435
|
serveTaskDetail(res, urlPath) {
|
|
@@ -538,18 +557,21 @@ class ViewerServer {
|
|
|
538
557
|
try {
|
|
539
558
|
ftsResults = db.prepare("SELECT c.* FROM chunks_fts f JOIN chunks c ON f.rowid = c.rowid WHERE chunks_fts MATCH ? ORDER BY rank LIMIT 100").all(q).filter(passesFilter);
|
|
540
559
|
}
|
|
541
|
-
catch {
|
|
560
|
+
catch { /* FTS syntax error, fall through */ }
|
|
561
|
+
if (ftsResults.length === 0) {
|
|
542
562
|
ftsResults = db.prepare("SELECT * FROM chunks WHERE content LIKE ? OR summary LIKE ? ORDER BY created_at DESC LIMIT 100").all(`%${q}%`, `%${q}%`).filter(passesFilter);
|
|
543
563
|
}
|
|
564
|
+
const SEMANTIC_THRESHOLD = 0.64;
|
|
544
565
|
let vectorResults = [];
|
|
566
|
+
let scoreMap = new Map();
|
|
545
567
|
try {
|
|
546
568
|
const queryVec = await this.embedder.embedQuery(q);
|
|
547
569
|
const hits = (0, vector_1.vectorSearch)(this.store, queryVec, 40);
|
|
548
|
-
|
|
570
|
+
scoreMap = new Map(hits.map(h => [h.chunkId, h.score]));
|
|
571
|
+
const hitIds = new Set(hits.filter(h => h.score >= SEMANTIC_THRESHOLD).map(h => h.chunkId));
|
|
549
572
|
if (hitIds.size > 0) {
|
|
550
573
|
const placeholders = [...hitIds].map(() => "?").join(",");
|
|
551
574
|
const rows = db.prepare(`SELECT * FROM chunks WHERE id IN (${placeholders})`).all(...hitIds).filter(passesFilter);
|
|
552
|
-
const scoreMap = new Map(hits.map(h => [h.chunkId, h.score]));
|
|
553
575
|
rows.forEach((r) => { r._vscore = scoreMap.get(r.id) ?? 0; });
|
|
554
576
|
rows.sort((a, b) => (b._vscore ?? 0) - (a._vscore ?? 0));
|
|
555
577
|
vectorResults = rows;
|
|
@@ -572,13 +594,14 @@ class ViewerServer {
|
|
|
572
594
|
merged.push(r);
|
|
573
595
|
}
|
|
574
596
|
}
|
|
597
|
+
const results = merged.length > 0 ? merged : ftsResults.slice(0, 20);
|
|
575
598
|
this.store.recordViewerEvent("search");
|
|
576
599
|
this.jsonResponse(res, {
|
|
577
|
-
results
|
|
600
|
+
results,
|
|
578
601
|
query: q,
|
|
579
602
|
vectorCount: vectorResults.length,
|
|
580
603
|
ftsCount: ftsResults.length,
|
|
581
|
-
total:
|
|
604
|
+
total: results.length,
|
|
582
605
|
});
|
|
583
606
|
}
|
|
584
607
|
// ─── Skills API ───
|
|
@@ -731,7 +754,123 @@ class ViewerServer {
|
|
|
731
754
|
this.jsonResponse(res, { ok: true, skillId, visibility });
|
|
732
755
|
}
|
|
733
756
|
catch (err) {
|
|
734
|
-
|
|
757
|
+
const errMsg = err instanceof Error ? `${err.name}: ${err.message}` : String(err);
|
|
758
|
+
this.log.error(`handleSkillVisibility error: skillId=${skillId}, body=${body}, err=${errMsg}`);
|
|
759
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
760
|
+
res.end(JSON.stringify({ error: errMsg }));
|
|
761
|
+
}
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
// ─── Task/Skill management ───
|
|
765
|
+
handleTaskRetrySkill(_req, res, urlPath) {
|
|
766
|
+
const taskId = urlPath.replace("/api/task/", "").replace("/retry-skill", "");
|
|
767
|
+
const task = this.store.getTask(taskId);
|
|
768
|
+
if (!task) {
|
|
769
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
770
|
+
res.end(JSON.stringify({ error: "Task not found" }));
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
if (task.status !== "completed") {
|
|
774
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
775
|
+
res.end(JSON.stringify({ error: "Only completed tasks can retry skill generation" }));
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
if (!this.ctx) {
|
|
779
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
780
|
+
res.end(JSON.stringify({ error: "Plugin context not available" }));
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
// Clean up stale task_skills references (e.g., skill was manually deleted)
|
|
784
|
+
const db = this.store.db;
|
|
785
|
+
db.prepare("DELETE FROM task_skills WHERE task_id = ? AND skill_id NOT IN (SELECT id FROM skills)").run(taskId);
|
|
786
|
+
this.store.setTaskSkillMeta(taskId, { skillStatus: "queued", skillReason: "手动重试中..." });
|
|
787
|
+
this.jsonResponse(res, { ok: true, taskId, status: "queued" });
|
|
788
|
+
const ctx = this.ctx;
|
|
789
|
+
const recallEngine = new engine_1.RecallEngine(this.store, this.embedder, ctx);
|
|
790
|
+
const evolver = new evolver_1.SkillEvolver(this.store, recallEngine, ctx, this.embedder);
|
|
791
|
+
evolver.onTaskCompleted(task).then(() => {
|
|
792
|
+
this.log.info(`Retry skill generation completed for task ${taskId}`);
|
|
793
|
+
}).catch((err) => {
|
|
794
|
+
this.log.error(`Retry skill generation failed for task ${taskId}: ${err}`);
|
|
795
|
+
this.store.setTaskSkillMeta(taskId, { skillStatus: "skipped", skillReason: `error: ${err}` });
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
handleTaskDelete(res, urlPath) {
|
|
799
|
+
const taskId = urlPath.replace("/api/task/", "");
|
|
800
|
+
const deleted = this.store.deleteTask(taskId);
|
|
801
|
+
if (!deleted) {
|
|
802
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
803
|
+
res.end(JSON.stringify({ error: "Task not found" }));
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
this.jsonResponse(res, { ok: true, taskId });
|
|
807
|
+
}
|
|
808
|
+
handleTaskUpdate(req, res, urlPath) {
|
|
809
|
+
const taskId = urlPath.replace("/api/task/", "");
|
|
810
|
+
this.readBody(req, (body) => {
|
|
811
|
+
try {
|
|
812
|
+
const data = JSON.parse(body);
|
|
813
|
+
const task = this.store.getTask(taskId);
|
|
814
|
+
if (!task) {
|
|
815
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
816
|
+
res.end(JSON.stringify({ error: "Task not found" }));
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
this.store.updateTask(taskId, {
|
|
820
|
+
title: data.title ?? task.title,
|
|
821
|
+
summary: data.summary ?? task.summary,
|
|
822
|
+
status: data.status ?? task.status,
|
|
823
|
+
endedAt: task.endedAt ?? undefined,
|
|
824
|
+
});
|
|
825
|
+
this.jsonResponse(res, { ok: true, taskId });
|
|
826
|
+
}
|
|
827
|
+
catch (err) {
|
|
828
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
829
|
+
res.end(JSON.stringify({ error: String(err) }));
|
|
830
|
+
}
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
handleSkillDelete(res, urlPath) {
|
|
834
|
+
const skillId = urlPath.replace("/api/skill/", "");
|
|
835
|
+
const skill = this.store.getSkill(skillId);
|
|
836
|
+
if (!skill) {
|
|
837
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
838
|
+
res.end(JSON.stringify({ error: "Skill not found" }));
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
// Remove skill directory from disk
|
|
842
|
+
try {
|
|
843
|
+
if (skill.dirPath && node_fs_1.default.existsSync(skill.dirPath)) {
|
|
844
|
+
node_fs_1.default.rmSync(skill.dirPath, { recursive: true, force: true });
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
catch (err) {
|
|
848
|
+
this.log.warn(`Failed to remove skill directory ${skill.dirPath}: ${err}`);
|
|
849
|
+
}
|
|
850
|
+
this.store.deleteSkill(skillId);
|
|
851
|
+
this.jsonResponse(res, { ok: true, skillId });
|
|
852
|
+
}
|
|
853
|
+
handleSkillUpdate(req, res, urlPath) {
|
|
854
|
+
const skillId = urlPath.replace("/api/skill/", "");
|
|
855
|
+
this.readBody(req, (body) => {
|
|
856
|
+
try {
|
|
857
|
+
const data = JSON.parse(body);
|
|
858
|
+
const skill = this.store.getSkill(skillId);
|
|
859
|
+
if (!skill) {
|
|
860
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
861
|
+
res.end(JSON.stringify({ error: "Skill not found" }));
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
this.store.updateSkill(skillId, {
|
|
865
|
+
description: data.description ?? skill.description,
|
|
866
|
+
version: skill.version,
|
|
867
|
+
status: data.status ?? skill.status,
|
|
868
|
+
installed: skill.installed,
|
|
869
|
+
qualityScore: skill.qualityScore,
|
|
870
|
+
});
|
|
871
|
+
this.jsonResponse(res, { ok: true, skillId });
|
|
872
|
+
}
|
|
873
|
+
catch (err) {
|
|
735
874
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
736
875
|
res.end(JSON.stringify({ error: String(err) }));
|
|
737
876
|
}
|
|
@@ -742,13 +881,18 @@ class ViewerServer {
|
|
|
742
881
|
this.readBody(req, (body) => {
|
|
743
882
|
try {
|
|
744
883
|
const data = JSON.parse(body);
|
|
884
|
+
if (!data.content || typeof data.content !== "string" || !data.content.trim()) {
|
|
885
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
886
|
+
res.end(JSON.stringify({ error: "content is required and must be a non-empty string" }));
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
745
889
|
const { v4: uuidv4 } = require("uuid");
|
|
746
890
|
const id = uuidv4();
|
|
747
891
|
const now = Date.now();
|
|
748
892
|
this.store.insertChunk({
|
|
749
893
|
id, sessionKey: data.session_key || "manual", turnId: `manual-${now}`, seq: 0,
|
|
750
|
-
role: data.role || "user", content: data.content
|
|
751
|
-
summary: data.summary || data.content
|
|
894
|
+
role: data.role || "user", content: data.content, kind: data.kind || "paragraph",
|
|
895
|
+
summary: data.summary || data.content.slice(0, 100),
|
|
752
896
|
taskId: null, skillId: null, owner: data.owner || "agent:main",
|
|
753
897
|
dedupStatus: "active", dedupTarget: null, dedupReason: null,
|
|
754
898
|
mergeCount: 0, lastHitAt: null, mergeHistory: "[]",
|
|
@@ -780,6 +924,11 @@ class ViewerServer {
|
|
|
780
924
|
this.readBody(req, (body) => {
|
|
781
925
|
try {
|
|
782
926
|
const data = JSON.parse(body);
|
|
927
|
+
if (data.content !== undefined && (typeof data.content !== "string" || !data.content.trim())) {
|
|
928
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
929
|
+
res.end(JSON.stringify({ error: "content must be a non-empty string" }));
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
783
932
|
const ok = this.store.updateChunk(chunkId, { summary: data.summary, content: data.content, role: data.role, kind: data.kind, owner: data.owner });
|
|
784
933
|
if (ok)
|
|
785
934
|
this.jsonResponse(res, { ok: true, message: "Memory updated" });
|
|
@@ -814,20 +963,27 @@ class ViewerServer {
|
|
|
814
963
|
this.jsonResponse(res, { ok: true, deleted: count });
|
|
815
964
|
}
|
|
816
965
|
handleDeleteAll(res) {
|
|
817
|
-
const result = this.store.deleteAll();
|
|
818
|
-
// Clean up skills-store directory
|
|
819
|
-
const skillsStoreDir = node_path_1.default.join(this.dataDir, "skills-store");
|
|
820
966
|
try {
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
967
|
+
const result = this.store.deleteAll();
|
|
968
|
+
const skillsStoreDir = node_path_1.default.join(this.dataDir, "skills-store");
|
|
969
|
+
try {
|
|
970
|
+
if (node_fs_1.default.existsSync(skillsStoreDir)) {
|
|
971
|
+
node_fs_1.default.rmSync(skillsStoreDir, { recursive: true });
|
|
972
|
+
node_fs_1.default.mkdirSync(skillsStoreDir, { recursive: true });
|
|
973
|
+
this.log.info("Cleared skills-store directory");
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
catch (err) {
|
|
977
|
+
this.log.warn(`Failed to clear skills-store: ${err}`);
|
|
825
978
|
}
|
|
979
|
+
this.jsonResponse(res, { ok: true, deleted: result });
|
|
826
980
|
}
|
|
827
981
|
catch (err) {
|
|
828
|
-
|
|
982
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
983
|
+
this.log.error(`handleDeleteAll error: ${msg}`);
|
|
984
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
985
|
+
res.end(JSON.stringify({ ok: false, error: msg }));
|
|
829
986
|
}
|
|
830
|
-
this.jsonResponse(res, { ok: true, deleted: result });
|
|
831
987
|
}
|
|
832
988
|
// ─── Helpers ───
|
|
833
989
|
// ─── Config API ───
|
|
@@ -910,6 +1066,157 @@ class ViewerServer {
|
|
|
910
1066
|
}
|
|
911
1067
|
});
|
|
912
1068
|
}
|
|
1069
|
+
handleTestModel(req, res) {
|
|
1070
|
+
this.readBody(req, async (body) => {
|
|
1071
|
+
try {
|
|
1072
|
+
const { type, provider, model, endpoint, apiKey } = JSON.parse(body);
|
|
1073
|
+
if (!provider) {
|
|
1074
|
+
this.jsonResponse(res, { ok: false, error: "provider is required" });
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1077
|
+
if (type === "embedding") {
|
|
1078
|
+
await this.testEmbeddingModel(provider, model, endpoint, apiKey);
|
|
1079
|
+
this.jsonResponse(res, { ok: true, detail: `${provider}/${model}` });
|
|
1080
|
+
}
|
|
1081
|
+
else {
|
|
1082
|
+
await this.testChatModel(provider, model, endpoint, apiKey);
|
|
1083
|
+
this.jsonResponse(res, { ok: true, detail: `${provider}/${model}` });
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
catch (e) {
|
|
1087
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1088
|
+
this.log.warn(`test-model failed: ${msg}`);
|
|
1089
|
+
this.jsonResponse(res, { ok: false, error: msg });
|
|
1090
|
+
}
|
|
1091
|
+
});
|
|
1092
|
+
}
|
|
1093
|
+
serveFallbackModel(res) {
|
|
1094
|
+
try {
|
|
1095
|
+
const cfgPath = this.getOpenClawConfigPath();
|
|
1096
|
+
if (!node_fs_1.default.existsSync(cfgPath)) {
|
|
1097
|
+
this.jsonResponse(res, { available: false });
|
|
1098
|
+
return;
|
|
1099
|
+
}
|
|
1100
|
+
const raw = JSON.parse(node_fs_1.default.readFileSync(cfgPath, "utf-8"));
|
|
1101
|
+
const agentModel = raw?.agents?.defaults?.model?.primary;
|
|
1102
|
+
if (!agentModel) {
|
|
1103
|
+
this.jsonResponse(res, { available: false });
|
|
1104
|
+
return;
|
|
1105
|
+
}
|
|
1106
|
+
const [providerKey, modelId] = agentModel.includes("/")
|
|
1107
|
+
? agentModel.split("/", 2)
|
|
1108
|
+
: [undefined, agentModel];
|
|
1109
|
+
const providerCfg = providerKey
|
|
1110
|
+
? raw?.models?.providers?.[providerKey]
|
|
1111
|
+
: Object.values(raw?.models?.providers ?? {})[0];
|
|
1112
|
+
if (!providerCfg || !providerCfg.baseUrl || !providerCfg.apiKey) {
|
|
1113
|
+
this.jsonResponse(res, { available: false });
|
|
1114
|
+
return;
|
|
1115
|
+
}
|
|
1116
|
+
this.jsonResponse(res, { available: true, model: modelId || agentModel, baseUrl: providerCfg.baseUrl });
|
|
1117
|
+
}
|
|
1118
|
+
catch {
|
|
1119
|
+
this.jsonResponse(res, { available: false });
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
async testEmbeddingModel(provider, model, endpoint, apiKey) {
|
|
1123
|
+
if (provider === "local") {
|
|
1124
|
+
return;
|
|
1125
|
+
}
|
|
1126
|
+
const baseUrl = (endpoint || "https://api.openai.com/v1").replace(/\/+$/, "");
|
|
1127
|
+
const embUrl = baseUrl.endsWith("/embeddings") ? baseUrl : `${baseUrl}/embeddings`;
|
|
1128
|
+
const headers = {
|
|
1129
|
+
"Content-Type": "application/json",
|
|
1130
|
+
"Authorization": `Bearer ${apiKey}`,
|
|
1131
|
+
};
|
|
1132
|
+
if (provider === "cohere") {
|
|
1133
|
+
headers["Authorization"] = `Bearer ${apiKey}`;
|
|
1134
|
+
const resp = await fetch(baseUrl.replace(/\/v\d+.*/, "/v2/embed"), {
|
|
1135
|
+
method: "POST",
|
|
1136
|
+
headers,
|
|
1137
|
+
body: JSON.stringify({ texts: ["test"], model: model || "embed-english-v3.0", input_type: "search_query", embedding_types: ["float"] }),
|
|
1138
|
+
signal: AbortSignal.timeout(15_000),
|
|
1139
|
+
});
|
|
1140
|
+
if (!resp.ok) {
|
|
1141
|
+
const txt = await resp.text();
|
|
1142
|
+
throw new Error(`Cohere embed ${resp.status}: ${txt}`);
|
|
1143
|
+
}
|
|
1144
|
+
return;
|
|
1145
|
+
}
|
|
1146
|
+
if (provider === "gemini") {
|
|
1147
|
+
const url = `https://generativelanguage.googleapis.com/v1/models/${model || "text-embedding-004"}:embedContent?key=${apiKey}`;
|
|
1148
|
+
const resp = await fetch(url, {
|
|
1149
|
+
method: "POST",
|
|
1150
|
+
headers: { "Content-Type": "application/json" },
|
|
1151
|
+
body: JSON.stringify({ content: { parts: [{ text: "test" }] } }),
|
|
1152
|
+
signal: AbortSignal.timeout(15_000),
|
|
1153
|
+
});
|
|
1154
|
+
if (!resp.ok) {
|
|
1155
|
+
const txt = await resp.text();
|
|
1156
|
+
throw new Error(`Gemini embed ${resp.status}: ${txt}`);
|
|
1157
|
+
}
|
|
1158
|
+
return;
|
|
1159
|
+
}
|
|
1160
|
+
const resp = await fetch(embUrl, {
|
|
1161
|
+
method: "POST",
|
|
1162
|
+
headers,
|
|
1163
|
+
body: JSON.stringify({ input: ["test"], model: model || "text-embedding-3-small" }),
|
|
1164
|
+
signal: AbortSignal.timeout(15_000),
|
|
1165
|
+
});
|
|
1166
|
+
if (!resp.ok) {
|
|
1167
|
+
const txt = await resp.text();
|
|
1168
|
+
throw new Error(`${resp.status}: ${txt}`);
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
async testChatModel(provider, model, endpoint, apiKey) {
|
|
1172
|
+
const baseUrl = (endpoint || "https://api.openai.com/v1").replace(/\/+$/, "");
|
|
1173
|
+
if (provider === "anthropic") {
|
|
1174
|
+
const url = endpoint || "https://api.anthropic.com/v1/messages";
|
|
1175
|
+
const resp = await fetch(url, {
|
|
1176
|
+
method: "POST",
|
|
1177
|
+
headers: {
|
|
1178
|
+
"Content-Type": "application/json",
|
|
1179
|
+
"x-api-key": apiKey,
|
|
1180
|
+
"anthropic-version": "2023-06-01",
|
|
1181
|
+
},
|
|
1182
|
+
body: JSON.stringify({ model: model || "claude-3-haiku-20240307", max_tokens: 5, messages: [{ role: "user", content: "hi" }] }),
|
|
1183
|
+
signal: AbortSignal.timeout(15_000),
|
|
1184
|
+
});
|
|
1185
|
+
if (!resp.ok) {
|
|
1186
|
+
const txt = await resp.text();
|
|
1187
|
+
throw new Error(`Anthropic ${resp.status}: ${txt}`);
|
|
1188
|
+
}
|
|
1189
|
+
return;
|
|
1190
|
+
}
|
|
1191
|
+
if (provider === "gemini") {
|
|
1192
|
+
const url = `https://generativelanguage.googleapis.com/v1/models/${model || "gemini-1.5-flash"}:generateContent?key=${apiKey}`;
|
|
1193
|
+
const resp = await fetch(url, {
|
|
1194
|
+
method: "POST",
|
|
1195
|
+
headers: { "Content-Type": "application/json" },
|
|
1196
|
+
body: JSON.stringify({ contents: [{ parts: [{ text: "hi" }] }], generationConfig: { maxOutputTokens: 5 } }),
|
|
1197
|
+
signal: AbortSignal.timeout(15_000),
|
|
1198
|
+
});
|
|
1199
|
+
if (!resp.ok) {
|
|
1200
|
+
const txt = await resp.text();
|
|
1201
|
+
throw new Error(`Gemini ${resp.status}: ${txt}`);
|
|
1202
|
+
}
|
|
1203
|
+
return;
|
|
1204
|
+
}
|
|
1205
|
+
const chatUrl = baseUrl.endsWith("/chat/completions") ? baseUrl : `${baseUrl}/chat/completions`;
|
|
1206
|
+
const resp = await fetch(chatUrl, {
|
|
1207
|
+
method: "POST",
|
|
1208
|
+
headers: {
|
|
1209
|
+
"Content-Type": "application/json",
|
|
1210
|
+
"Authorization": `Bearer ${apiKey}`,
|
|
1211
|
+
},
|
|
1212
|
+
body: JSON.stringify({ model: model || "gpt-4o-mini", max_tokens: 5, messages: [{ role: "user", content: "hi" }] }),
|
|
1213
|
+
signal: AbortSignal.timeout(15_000),
|
|
1214
|
+
});
|
|
1215
|
+
if (!resp.ok) {
|
|
1216
|
+
const txt = await resp.text();
|
|
1217
|
+
throw new Error(`${resp.status}: ${txt}`);
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
913
1220
|
serveLogs(res, url) {
|
|
914
1221
|
const limit = Math.min(Number(url.searchParams.get("limit") ?? 20), 200);
|
|
915
1222
|
const offset = Math.max(0, Number(url.searchParams.get("offset") ?? 0));
|
|
@@ -1108,6 +1415,7 @@ class ViewerServer {
|
|
|
1108
1415
|
opts = JSON.parse(body);
|
|
1109
1416
|
}
|
|
1110
1417
|
catch { /* defaults */ }
|
|
1418
|
+
const concurrency = Math.max(1, Math.min(opts.concurrency ?? 1, 8));
|
|
1111
1419
|
res.writeHead(200, {
|
|
1112
1420
|
"Content-Type": "text/event-stream",
|
|
1113
1421
|
"Cache-Control": "no-cache",
|
|
@@ -1144,7 +1452,7 @@ class ViewerServer {
|
|
|
1144
1452
|
this.broadcastSSE(event, data);
|
|
1145
1453
|
};
|
|
1146
1454
|
this.migrationRunning = true;
|
|
1147
|
-
this.runMigration(send, opts.sources).finally(() => {
|
|
1455
|
+
this.runMigration(send, opts.sources, concurrency).finally(() => {
|
|
1148
1456
|
this.migrationRunning = false;
|
|
1149
1457
|
this.migrationState.done = true;
|
|
1150
1458
|
if (this.migrationAbort) {
|
|
@@ -1165,7 +1473,7 @@ class ViewerServer {
|
|
|
1165
1473
|
});
|
|
1166
1474
|
});
|
|
1167
1475
|
}
|
|
1168
|
-
async runMigration(send, sources) {
|
|
1476
|
+
async runMigration(send, sources, concurrency = 1) {
|
|
1169
1477
|
const ocHome = this.getOpenClawHome();
|
|
1170
1478
|
const importSqlite = !sources || sources.includes("sqlite");
|
|
1171
1479
|
const importSessions = !sources || sources.includes("sessions");
|
|
@@ -1175,15 +1483,17 @@ class ViewerServer {
|
|
|
1175
1483
|
let totalErrors = 0;
|
|
1176
1484
|
const cfgPath = this.getOpenClawConfigPath();
|
|
1177
1485
|
let summarizerCfg;
|
|
1486
|
+
let strongCfg;
|
|
1178
1487
|
try {
|
|
1179
1488
|
const raw = JSON.parse(node_fs_1.default.readFileSync(cfgPath, "utf-8"));
|
|
1180
1489
|
const pluginCfg = raw?.plugins?.entries?.["memos-local-openclaw-plugin"]?.config ??
|
|
1181
1490
|
raw?.plugins?.entries?.["memos-lite"]?.config ??
|
|
1182
1491
|
raw?.plugins?.entries?.["memos-lite-openclaw-plugin"]?.config ?? {};
|
|
1183
1492
|
summarizerCfg = pluginCfg.summarizer;
|
|
1493
|
+
strongCfg = pluginCfg.skillEvolution?.summarizer;
|
|
1184
1494
|
}
|
|
1185
1495
|
catch { /* no config */ }
|
|
1186
|
-
const summarizer = new providers_1.Summarizer(summarizerCfg, this.log);
|
|
1496
|
+
const summarizer = new providers_1.Summarizer(summarizerCfg, this.log, strongCfg);
|
|
1187
1497
|
// Phase 1: Import SQLite memory chunks
|
|
1188
1498
|
if (importSqlite) {
|
|
1189
1499
|
const memoryDir = node_path_1.default.join(ocHome, "memory");
|
|
@@ -1218,6 +1528,21 @@ class ViewerServer {
|
|
|
1218
1528
|
});
|
|
1219
1529
|
continue;
|
|
1220
1530
|
}
|
|
1531
|
+
const importOwner = `agent:${agentId}`;
|
|
1532
|
+
// Exact hash dedup within same agent
|
|
1533
|
+
const existingByHash = this.store.findActiveChunkByHash(row.text, importOwner);
|
|
1534
|
+
if (existingByHash) {
|
|
1535
|
+
totalSkipped++;
|
|
1536
|
+
send("item", {
|
|
1537
|
+
index: i + 1,
|
|
1538
|
+
total: rows.length,
|
|
1539
|
+
status: "skipped",
|
|
1540
|
+
preview: row.text.slice(0, 120),
|
|
1541
|
+
source: file,
|
|
1542
|
+
reason: "exact duplicate within agent",
|
|
1543
|
+
});
|
|
1544
|
+
continue;
|
|
1545
|
+
}
|
|
1221
1546
|
try {
|
|
1222
1547
|
const summary = await summarizer.summarize(row.text);
|
|
1223
1548
|
let embedding = null;
|
|
@@ -1231,7 +1556,9 @@ class ViewerServer {
|
|
|
1231
1556
|
let dedupTarget = null;
|
|
1232
1557
|
let dedupReason = null;
|
|
1233
1558
|
if (embedding) {
|
|
1234
|
-
const
|
|
1559
|
+
const importThreshold = this.ctx?.config?.dedup?.similarityThreshold ?? 0.60;
|
|
1560
|
+
const dedupOwnerFilter = [importOwner];
|
|
1561
|
+
const topSimilar = (0, dedup_1.findTopSimilar)(this.store, embedding, importThreshold, 5, this.log, dedupOwnerFilter);
|
|
1235
1562
|
if (topSimilar.length > 0) {
|
|
1236
1563
|
const candidates = topSimilar.map((s, idx) => {
|
|
1237
1564
|
const chunk = this.store.getChunk(s.chunkId);
|
|
@@ -1322,17 +1649,32 @@ class ViewerServer {
|
|
|
1322
1649
|
}
|
|
1323
1650
|
}
|
|
1324
1651
|
}
|
|
1325
|
-
// Phase 2: Import session JSONL files
|
|
1652
|
+
// Phase 2: Import session JSONL files from ALL agents (supports parallel by agent)
|
|
1326
1653
|
if (importSessions) {
|
|
1327
|
-
const
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1654
|
+
const agentsDir = node_path_1.default.join(ocHome, "agents");
|
|
1655
|
+
const agentGroups = new Map();
|
|
1656
|
+
if (node_fs_1.default.existsSync(agentsDir)) {
|
|
1657
|
+
for (const entry of node_fs_1.default.readdirSync(agentsDir, { withFileTypes: true })) {
|
|
1658
|
+
if (entry.isDirectory()) {
|
|
1659
|
+
const sessDir = node_path_1.default.join(agentsDir, entry.name, "sessions");
|
|
1660
|
+
if (node_fs_1.default.existsSync(sessDir)) {
|
|
1661
|
+
const jsonlFiles = node_fs_1.default.readdirSync(sessDir).filter(f => f.includes(".jsonl")).sort();
|
|
1662
|
+
if (jsonlFiles.length > 0) {
|
|
1663
|
+
agentGroups.set(entry.name, jsonlFiles.map(f => ({ file: f, filePath: node_path_1.default.join(sessDir, f) })));
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
const agentIds = Array.from(agentGroups.keys());
|
|
1670
|
+
const allFileCount = Array.from(agentGroups.values()).reduce((s, g) => s + g.length, 0);
|
|
1671
|
+
send("phase", { phase: "sessions", files: allFileCount, agents: agentIds, concurrency });
|
|
1672
|
+
// Count total messages across all agents
|
|
1673
|
+
let totalMsgs = 0;
|
|
1674
|
+
for (const files of agentGroups.values()) {
|
|
1675
|
+
for (const { filePath } of files) {
|
|
1334
1676
|
try {
|
|
1335
|
-
const raw = node_fs_1.default.readFileSync(
|
|
1677
|
+
const raw = node_fs_1.default.readFileSync(filePath, "utf-8");
|
|
1336
1678
|
for (const line of raw.split("\n")) {
|
|
1337
1679
|
if (!line.trim())
|
|
1338
1680
|
continue;
|
|
@@ -1361,12 +1703,17 @@ class ViewerServer {
|
|
|
1361
1703
|
}
|
|
1362
1704
|
catch { /* skip */ }
|
|
1363
1705
|
}
|
|
1364
|
-
|
|
1706
|
+
}
|
|
1707
|
+
// Thread-safe counters for parallel execution
|
|
1708
|
+
let globalMsgIdx = 0;
|
|
1709
|
+
const incIdx = () => ++globalMsgIdx;
|
|
1710
|
+
// Import one agent's sessions sequentially
|
|
1711
|
+
const importAgent = async (agentId, files) => {
|
|
1712
|
+
const agentOwner = `agent:${agentId}`;
|
|
1713
|
+
for (const { file, filePath } of files) {
|
|
1365
1714
|
if (this.migrationAbort)
|
|
1366
1715
|
break;
|
|
1367
1716
|
const sessionId = file.replace(/\.jsonl.*$/, "");
|
|
1368
|
-
const filePath = node_path_1.default.join(sessionsDir, file);
|
|
1369
|
-
send("progress", { total: totalMsgs, processed: globalMsgIdx, phase: "sessions", file });
|
|
1370
1717
|
try {
|
|
1371
1718
|
const fileStream = node_fs_1.default.createReadStream(filePath, { encoding: "utf-8" });
|
|
1372
1719
|
const rl = node_readline_1.default.createInterface({ input: fileStream, crlfDelay: Infinity });
|
|
@@ -1406,20 +1753,18 @@ class ViewerServer {
|
|
|
1406
1753
|
}
|
|
1407
1754
|
if (!content || content.length < 10)
|
|
1408
1755
|
continue;
|
|
1409
|
-
|
|
1756
|
+
const idx = incIdx();
|
|
1410
1757
|
totalProcessed++;
|
|
1411
1758
|
const sessionKey = `openclaw-session-${sessionId}`;
|
|
1412
1759
|
if (this.store.chunkExistsByContent(sessionKey, msgRole, content)) {
|
|
1413
1760
|
totalSkipped++;
|
|
1414
|
-
send("item", {
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
reason: "duplicate",
|
|
1422
|
-
});
|
|
1761
|
+
send("item", { index: idx, total: totalMsgs, status: "skipped", preview: content.slice(0, 120), source: file, agent: agentId, role: msgRole, reason: "duplicate" });
|
|
1762
|
+
continue;
|
|
1763
|
+
}
|
|
1764
|
+
const existingByHash = this.store.findActiveChunkByHash(content, agentOwner);
|
|
1765
|
+
if (existingByHash) {
|
|
1766
|
+
totalSkipped++;
|
|
1767
|
+
send("item", { index: idx, total: totalMsgs, status: "skipped", preview: content.slice(0, 120), source: file, agent: agentId, role: msgRole, reason: "exact duplicate within agent" });
|
|
1423
1768
|
continue;
|
|
1424
1769
|
}
|
|
1425
1770
|
try {
|
|
@@ -1435,11 +1780,13 @@ class ViewerServer {
|
|
|
1435
1780
|
let dedupTarget = null;
|
|
1436
1781
|
let dedupReason = null;
|
|
1437
1782
|
if (embedding) {
|
|
1438
|
-
const
|
|
1783
|
+
const importThreshold = this.ctx?.config?.dedup?.similarityThreshold ?? 0.60;
|
|
1784
|
+
const dedupOwnerFilter = [agentOwner];
|
|
1785
|
+
const topSimilar = (0, dedup_1.findTopSimilar)(this.store, embedding, importThreshold, 5, this.log, dedupOwnerFilter);
|
|
1439
1786
|
if (topSimilar.length > 0) {
|
|
1440
|
-
const candidates = topSimilar.map((s,
|
|
1787
|
+
const candidates = topSimilar.map((s, i) => {
|
|
1441
1788
|
const chunk = this.store.getChunk(s.chunkId);
|
|
1442
|
-
return { index:
|
|
1789
|
+
return { index: i + 1, summary: chunk?.summary ?? "", chunkId: s.chunkId };
|
|
1443
1790
|
}).filter(c => c.summary);
|
|
1444
1791
|
if (candidates.length > 0) {
|
|
1445
1792
|
const dedupResult = await summarizer.judgeDedup(summary, candidates);
|
|
@@ -1473,60 +1820,55 @@ class ViewerServer {
|
|
|
1473
1820
|
const msgTs = obj.message?.timestamp ?? obj.timestamp;
|
|
1474
1821
|
const ts = msgTs ? new Date(msgTs).getTime() : Date.now();
|
|
1475
1822
|
const chunk = {
|
|
1476
|
-
id: chunkId,
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
role: msgRole,
|
|
1481
|
-
content,
|
|
1482
|
-
kind: "paragraph",
|
|
1483
|
-
summary,
|
|
1484
|
-
embedding: null,
|
|
1485
|
-
taskId: null,
|
|
1486
|
-
skillId: null,
|
|
1487
|
-
owner: "agent:main",
|
|
1488
|
-
dedupStatus,
|
|
1489
|
-
dedupTarget,
|
|
1490
|
-
dedupReason,
|
|
1491
|
-
mergeCount: 0,
|
|
1492
|
-
lastHitAt: null,
|
|
1493
|
-
mergeHistory: "[]",
|
|
1494
|
-
createdAt: ts,
|
|
1495
|
-
updatedAt: ts,
|
|
1823
|
+
id: chunkId, sessionKey, turnId: `import-${agentId}-${sessionId}-${idx}`, seq: 0,
|
|
1824
|
+
role: msgRole, content, kind: "paragraph", summary, embedding: null,
|
|
1825
|
+
taskId: null, skillId: null, owner: agentOwner, dedupStatus, dedupTarget, dedupReason,
|
|
1826
|
+
mergeCount: 0, lastHitAt: null, mergeHistory: "[]", createdAt: ts, updatedAt: ts,
|
|
1496
1827
|
};
|
|
1497
1828
|
this.store.insertChunk(chunk);
|
|
1498
|
-
if (embedding && dedupStatus === "active")
|
|
1829
|
+
if (embedding && dedupStatus === "active")
|
|
1499
1830
|
this.store.upsertEmbedding(chunkId, embedding);
|
|
1500
|
-
}
|
|
1501
1831
|
totalStored++;
|
|
1502
|
-
send("item", {
|
|
1503
|
-
index: globalMsgIdx,
|
|
1504
|
-
total: totalMsgs,
|
|
1505
|
-
status: dedupStatus === "active" ? "stored" : dedupStatus,
|
|
1506
|
-
preview: content.slice(0, 120),
|
|
1507
|
-
summary: summary.slice(0, 80),
|
|
1508
|
-
source: file,
|
|
1509
|
-
role: msgRole,
|
|
1510
|
-
});
|
|
1832
|
+
send("item", { index: idx, total: totalMsgs, status: dedupStatus === "active" ? "stored" : dedupStatus, preview: content.slice(0, 120), summary: summary.slice(0, 80), source: file, agent: agentId, role: msgRole });
|
|
1511
1833
|
}
|
|
1512
1834
|
catch (err) {
|
|
1513
1835
|
totalErrors++;
|
|
1514
|
-
send("item", {
|
|
1515
|
-
index: globalMsgIdx,
|
|
1516
|
-
total: totalMsgs,
|
|
1517
|
-
status: "error",
|
|
1518
|
-
preview: content.slice(0, 120),
|
|
1519
|
-
source: file,
|
|
1520
|
-
error: String(err).slice(0, 200),
|
|
1521
|
-
});
|
|
1836
|
+
send("item", { index: idx, total: totalMsgs, status: "error", preview: content.slice(0, 120), source: file, agent: agentId, error: String(err).slice(0, 200) });
|
|
1522
1837
|
}
|
|
1523
1838
|
}
|
|
1524
1839
|
}
|
|
1525
1840
|
catch (err) {
|
|
1526
|
-
send("error", { file, error: String(err) });
|
|
1841
|
+
send("error", { file, agent: agentId, error: String(err) });
|
|
1527
1842
|
totalErrors++;
|
|
1528
1843
|
}
|
|
1529
1844
|
}
|
|
1845
|
+
};
|
|
1846
|
+
// Execute agents with concurrency control
|
|
1847
|
+
const agentEntries = Array.from(agentGroups.entries());
|
|
1848
|
+
if (concurrency <= 1 || agentEntries.length <= 1) {
|
|
1849
|
+
for (const [agentId, files] of agentEntries) {
|
|
1850
|
+
if (this.migrationAbort)
|
|
1851
|
+
break;
|
|
1852
|
+
send("progress", { total: totalMsgs, processed: globalMsgIdx, phase: "sessions", agent: agentId });
|
|
1853
|
+
await importAgent(agentId, files);
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
else {
|
|
1857
|
+
// Parallel: run up to `concurrency` agents at once
|
|
1858
|
+
let cursor = 0;
|
|
1859
|
+
const runBatch = async () => {
|
|
1860
|
+
while (cursor < agentEntries.length && !this.migrationAbort) {
|
|
1861
|
+
const batch = [];
|
|
1862
|
+
const batchStart = cursor;
|
|
1863
|
+
while (batch.length < concurrency && cursor < agentEntries.length) {
|
|
1864
|
+
const [agentId, files] = agentEntries[cursor++];
|
|
1865
|
+
send("progress", { total: totalMsgs, processed: globalMsgIdx, phase: "sessions", agent: agentId, parallel: true });
|
|
1866
|
+
batch.push(importAgent(agentId, files));
|
|
1867
|
+
}
|
|
1868
|
+
await Promise.all(batch);
|
|
1869
|
+
}
|
|
1870
|
+
};
|
|
1871
|
+
await runBatch();
|
|
1530
1872
|
}
|
|
1531
1873
|
}
|
|
1532
1874
|
send("progress", { total: totalProcessed, processed: totalProcessed, phase: "done" });
|
|
@@ -1550,6 +1892,7 @@ class ViewerServer {
|
|
|
1550
1892
|
opts = JSON.parse(body);
|
|
1551
1893
|
}
|
|
1552
1894
|
catch { /* defaults */ }
|
|
1895
|
+
const concurrency = Math.max(1, Math.min(opts.concurrency ?? 1, 8));
|
|
1553
1896
|
res.writeHead(200, {
|
|
1554
1897
|
"Content-Type": "text/event-stream",
|
|
1555
1898
|
"Cache-Control": "no-cache",
|
|
@@ -1564,7 +1907,7 @@ class ViewerServer {
|
|
|
1564
1907
|
this.broadcastPPSSE(event, data);
|
|
1565
1908
|
};
|
|
1566
1909
|
this.ppRunning = true;
|
|
1567
|
-
this.runPostprocess(send, !!opts.enableTasks, !!opts.enableSkills).finally(() => {
|
|
1910
|
+
this.runPostprocess(send, !!opts.enableTasks, !!opts.enableSkills, concurrency).finally(() => {
|
|
1568
1911
|
this.ppRunning = false;
|
|
1569
1912
|
this.ppState.running = false;
|
|
1570
1913
|
this.ppState.done = true;
|
|
@@ -1623,126 +1966,148 @@ class ViewerServer {
|
|
|
1623
1966
|
catch { /* */ }
|
|
1624
1967
|
}
|
|
1625
1968
|
}
|
|
1626
|
-
async runPostprocess(send, enableTasks, enableSkills) {
|
|
1969
|
+
async runPostprocess(send, enableTasks, enableSkills, concurrency = 1) {
|
|
1627
1970
|
const ctx = this.ctx;
|
|
1628
|
-
const taskProcessor = new task_processor_1.TaskProcessor(this.store, ctx);
|
|
1629
|
-
let skillEvolver = null;
|
|
1630
|
-
if (enableSkills) {
|
|
1631
|
-
const recallEngine = new engine_1.RecallEngine(this.store, this.embedder, ctx);
|
|
1632
|
-
skillEvolver = new evolver_1.SkillEvolver(this.store, recallEngine, ctx);
|
|
1633
|
-
taskProcessor.onTaskCompleted(async (task) => {
|
|
1634
|
-
try {
|
|
1635
|
-
await skillEvolver.onTaskCompleted(task);
|
|
1636
|
-
this.ppState.skillsCreated++;
|
|
1637
|
-
send("skill", { taskId: task.id, title: task.title });
|
|
1638
|
-
}
|
|
1639
|
-
catch (err) {
|
|
1640
|
-
this.log.warn(`Postprocess skill evolution error: ${err}`);
|
|
1641
|
-
}
|
|
1642
|
-
});
|
|
1643
|
-
}
|
|
1644
1971
|
const importSessions = this.store.getDistinctSessionKeys()
|
|
1645
1972
|
.filter((sk) => sk.startsWith("openclaw-import-") || sk.startsWith("openclaw-session-"));
|
|
1646
1973
|
const pendingItems = [];
|
|
1647
1974
|
let skippedCount = 0;
|
|
1975
|
+
const ownerMap = this.store.getSessionOwnerMap(importSessions);
|
|
1648
1976
|
for (const sk of importSessions) {
|
|
1649
1977
|
const hasTask = this.store.hasTaskForSession(sk);
|
|
1650
1978
|
const hasSkill = this.store.hasSkillForSessionTask(sk);
|
|
1979
|
+
const owner = ownerMap.get(sk) ?? "agent:main";
|
|
1651
1980
|
if (enableTasks && !hasTask) {
|
|
1652
|
-
pendingItems.push({ sessionKey: sk, action: "full" });
|
|
1981
|
+
pendingItems.push({ sessionKey: sk, action: "full", owner });
|
|
1653
1982
|
}
|
|
1654
1983
|
else if (enableSkills && hasTask && !hasSkill) {
|
|
1655
|
-
pendingItems.push({ sessionKey: sk, action: "skill-only" });
|
|
1984
|
+
pendingItems.push({ sessionKey: sk, action: "skill-only", owner });
|
|
1656
1985
|
}
|
|
1657
1986
|
else {
|
|
1658
1987
|
skippedCount++;
|
|
1659
1988
|
}
|
|
1660
1989
|
}
|
|
1990
|
+
// Group pending items by agent (owner)
|
|
1991
|
+
const agentGroups = new Map();
|
|
1992
|
+
for (const item of pendingItems) {
|
|
1993
|
+
const group = agentGroups.get(item.owner) ?? [];
|
|
1994
|
+
group.push(item);
|
|
1995
|
+
agentGroups.set(item.owner, group);
|
|
1996
|
+
}
|
|
1661
1997
|
this.ppState.total = pendingItems.length;
|
|
1662
1998
|
send("info", {
|
|
1663
1999
|
totalSessions: importSessions.length,
|
|
1664
2000
|
alreadyProcessed: skippedCount,
|
|
1665
2001
|
pending: pendingItems.length,
|
|
2002
|
+
agents: Array.from(agentGroups.keys()),
|
|
2003
|
+
concurrency,
|
|
1666
2004
|
});
|
|
1667
2005
|
send("progress", { processed: 0, total: pendingItems.length });
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
await taskProcessor.onChunksIngested(sessionKey, Date.now());
|
|
1683
|
-
const activeTask = this.store.getActiveTask(sessionKey);
|
|
1684
|
-
if (activeTask) {
|
|
1685
|
-
await taskProcessor.finalizeTask(activeTask);
|
|
1686
|
-
const finalized = this.store.getTask(activeTask.id);
|
|
1687
|
-
this.ppState.tasksCreated++;
|
|
1688
|
-
send("item", {
|
|
1689
|
-
index: i + 1,
|
|
1690
|
-
total: pendingItems.length,
|
|
1691
|
-
session: sessionKey,
|
|
1692
|
-
step: "done",
|
|
1693
|
-
taskTitle: finalized?.title || "",
|
|
1694
|
-
taskStatus: finalized?.status || "",
|
|
1695
|
-
});
|
|
2006
|
+
let globalIdx = 0;
|
|
2007
|
+
const incIdx = () => ++globalIdx;
|
|
2008
|
+
// Process one agent's sessions sequentially
|
|
2009
|
+
const processAgent = async (agentOwner, items) => {
|
|
2010
|
+
const taskProcessor = new task_processor_1.TaskProcessor(this.store, ctx);
|
|
2011
|
+
let skillEvolver = null;
|
|
2012
|
+
if (enableSkills) {
|
|
2013
|
+
const recallEngine = new engine_1.RecallEngine(this.store, this.embedder, ctx);
|
|
2014
|
+
skillEvolver = new evolver_1.SkillEvolver(this.store, recallEngine, ctx);
|
|
2015
|
+
taskProcessor.onTaskCompleted(async (task) => {
|
|
2016
|
+
try {
|
|
2017
|
+
await skillEvolver.onTaskCompleted(task);
|
|
2018
|
+
this.ppState.skillsCreated++;
|
|
2019
|
+
send("skill", { taskId: task.id, title: task.title, agent: agentOwner });
|
|
1696
2020
|
}
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
index: i + 1,
|
|
1700
|
-
total: pendingItems.length,
|
|
1701
|
-
session: sessionKey,
|
|
1702
|
-
step: "done",
|
|
1703
|
-
taskTitle: "(no chunks)",
|
|
1704
|
-
});
|
|
2021
|
+
catch (err) {
|
|
2022
|
+
this.log.warn(`Postprocess skill evolution error (${agentOwner}): ${err}`);
|
|
1705
2023
|
}
|
|
1706
|
-
}
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
2024
|
+
});
|
|
2025
|
+
}
|
|
2026
|
+
for (const { sessionKey, action } of items) {
|
|
2027
|
+
if (this.ppAbort)
|
|
2028
|
+
break;
|
|
2029
|
+
const idx = incIdx();
|
|
2030
|
+
this.ppState.processed = globalIdx;
|
|
2031
|
+
send("item", {
|
|
2032
|
+
index: idx,
|
|
2033
|
+
total: pendingItems.length,
|
|
2034
|
+
session: sessionKey,
|
|
2035
|
+
agent: agentOwner,
|
|
2036
|
+
step: "processing",
|
|
2037
|
+
action,
|
|
2038
|
+
});
|
|
2039
|
+
try {
|
|
2040
|
+
if (action === "full") {
|
|
2041
|
+
await taskProcessor.onChunksIngested(sessionKey, Date.now());
|
|
2042
|
+
const activeTask = this.store.getActiveTask(sessionKey);
|
|
2043
|
+
if (activeTask) {
|
|
2044
|
+
await taskProcessor.finalizeTask(activeTask);
|
|
2045
|
+
const finalized = this.store.getTask(activeTask.id);
|
|
2046
|
+
this.ppState.tasksCreated++;
|
|
2047
|
+
send("item", {
|
|
2048
|
+
index: idx, total: pendingItems.length, session: sessionKey, agent: agentOwner,
|
|
2049
|
+
step: "done", taskTitle: finalized?.title || "", taskStatus: finalized?.status || "",
|
|
2050
|
+
});
|
|
2051
|
+
}
|
|
2052
|
+
else {
|
|
2053
|
+
send("item", {
|
|
2054
|
+
index: idx, total: pendingItems.length, session: sessionKey, agent: agentOwner,
|
|
2055
|
+
step: "done", taskTitle: "(no chunks)",
|
|
2056
|
+
});
|
|
1718
2057
|
}
|
|
1719
|
-
|
|
1720
|
-
|
|
2058
|
+
}
|
|
2059
|
+
else if (action === "skill-only" && skillEvolver) {
|
|
2060
|
+
const completedTasks = this.store.getCompletedTasksForSession(sessionKey);
|
|
2061
|
+
let skillGenerated = false;
|
|
2062
|
+
for (const task of completedTasks) {
|
|
2063
|
+
if (this.ppAbort)
|
|
2064
|
+
break;
|
|
2065
|
+
try {
|
|
2066
|
+
await skillEvolver.onTaskCompleted(task);
|
|
2067
|
+
this.ppState.skillsCreated++;
|
|
2068
|
+
skillGenerated = true;
|
|
2069
|
+
send("skill", { taskId: task.id, title: task.title, agent: agentOwner });
|
|
2070
|
+
}
|
|
2071
|
+
catch (err) {
|
|
2072
|
+
this.log.warn(`Skill evolution error (${agentOwner}) task=${task.id}: ${err}`);
|
|
2073
|
+
}
|
|
1721
2074
|
}
|
|
2075
|
+
send("item", {
|
|
2076
|
+
index: idx, total: pendingItems.length, session: sessionKey, agent: agentOwner,
|
|
2077
|
+
step: "done", taskTitle: completedTasks[0]?.title || sessionKey, action: "skill-only", skillGenerated,
|
|
2078
|
+
});
|
|
1722
2079
|
}
|
|
2080
|
+
}
|
|
2081
|
+
catch (err) {
|
|
2082
|
+
this.ppState.errors++;
|
|
2083
|
+
this.log.warn(`Postprocess error (${agentOwner}) ${sessionKey}: ${err}`);
|
|
1723
2084
|
send("item", {
|
|
1724
|
-
index:
|
|
1725
|
-
|
|
1726
|
-
session: sessionKey,
|
|
1727
|
-
step: "done",
|
|
1728
|
-
taskTitle: completedTasks[0]?.title || sessionKey,
|
|
1729
|
-
action: "skill-only",
|
|
1730
|
-
skillGenerated,
|
|
2085
|
+
index: idx, total: pendingItems.length, session: sessionKey, agent: agentOwner,
|
|
2086
|
+
step: "error", error: String(err).slice(0, 200),
|
|
1731
2087
|
});
|
|
1732
2088
|
}
|
|
2089
|
+
send("progress", { processed: globalIdx, total: pendingItems.length });
|
|
1733
2090
|
}
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
2091
|
+
};
|
|
2092
|
+
// Execute agents with concurrency control
|
|
2093
|
+
const agentEntries = Array.from(agentGroups.entries());
|
|
2094
|
+
if (concurrency <= 1 || agentEntries.length <= 1) {
|
|
2095
|
+
for (const [agentOwner, items] of agentEntries) {
|
|
2096
|
+
if (this.ppAbort)
|
|
2097
|
+
break;
|
|
2098
|
+
await processAgent(agentOwner, items);
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
else {
|
|
2102
|
+
let cursor = 0;
|
|
2103
|
+
while (cursor < agentEntries.length && !this.ppAbort) {
|
|
2104
|
+
const batch = [];
|
|
2105
|
+
while (batch.length < concurrency && cursor < agentEntries.length) {
|
|
2106
|
+
const [agentOwner, items] = agentEntries[cursor++];
|
|
2107
|
+
batch.push(processAgent(agentOwner, items));
|
|
2108
|
+
}
|
|
2109
|
+
await Promise.all(batch);
|
|
1744
2110
|
}
|
|
1745
|
-
send("progress", { processed: i + 1, total: pendingItems.length });
|
|
1746
2111
|
}
|
|
1747
2112
|
}
|
|
1748
2113
|
readBody(req, cb) {
|