@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/src/viewer/server.ts
CHANGED
|
@@ -194,11 +194,16 @@ export class ViewerServer {
|
|
|
194
194
|
else if (p === "/api/tool-metrics") this.serveToolMetrics(res, url);
|
|
195
195
|
else if (p === "/api/search") this.serveSearch(req, res, url);
|
|
196
196
|
else if (p === "/api/tasks" && req.method === "GET") this.serveTasks(res, url);
|
|
197
|
+
else if (p.match(/^\/api\/task\/[^/]+\/retry-skill$/) && req.method === "POST") this.handleTaskRetrySkill(req, res, p);
|
|
198
|
+
else if (p.startsWith("/api/task/") && req.method === "DELETE") this.handleTaskDelete(res, p);
|
|
199
|
+
else if (p.startsWith("/api/task/") && req.method === "PUT") this.handleTaskUpdate(req, res, p);
|
|
197
200
|
else if (p.startsWith("/api/task/") && req.method === "GET") this.serveTaskDetail(res, p);
|
|
198
|
-
else
|
|
201
|
+
else if (p === "/api/skills" && req.method === "GET") this.serveSkills(res, url);
|
|
199
202
|
else if (p.match(/^\/api\/skill\/[^/]+\/download$/) && req.method === "GET") this.serveSkillDownload(res, p);
|
|
200
203
|
else if (p.match(/^\/api\/skill\/[^/]+\/files$/) && req.method === "GET") this.serveSkillFiles(res, p);
|
|
201
204
|
else if (p.match(/^\/api\/skill\/[^/]+\/visibility$/) && req.method === "PUT") this.handleSkillVisibility(req, res, p);
|
|
205
|
+
else if (p.startsWith("/api/skill/") && req.method === "DELETE") this.handleSkillDelete(res, p);
|
|
206
|
+
else if (p.startsWith("/api/skill/") && req.method === "PUT") this.handleSkillUpdate(req, res, p);
|
|
202
207
|
else if (p.startsWith("/api/skill/") && req.method === "GET") this.serveSkillDetail(res, p);
|
|
203
208
|
else if (p === "/api/memory" && req.method === "POST") this.handleCreate(req, res);
|
|
204
209
|
else if (p.startsWith("/api/memory/") && req.method === "GET") this.serveMemoryDetail(res, p);
|
|
@@ -210,6 +215,8 @@ export class ViewerServer {
|
|
|
210
215
|
else if (p === "/api/log-tools" && req.method === "GET") this.serveLogTools(res);
|
|
211
216
|
else if (p === "/api/config" && req.method === "GET") this.serveConfig(res);
|
|
212
217
|
else if (p === "/api/config" && req.method === "PUT") this.handleSaveConfig(req, res);
|
|
218
|
+
else if (p === "/api/test-model" && req.method === "POST") this.handleTestModel(req, res);
|
|
219
|
+
else if (p === "/api/fallback-model" && req.method === "GET") this.serveFallbackModel(res);
|
|
213
220
|
else if (p === "/api/auth/logout" && req.method === "POST") this.handleLogout(req, res);
|
|
214
221
|
else if (p === "/api/migrate/scan" && req.method === "GET") this.handleMigrateScan(res);
|
|
215
222
|
else if (p === "/api/migrate/start" && req.method === "POST") this.handleMigrateStart(req, res);
|
|
@@ -394,16 +401,21 @@ export class ViewerServer {
|
|
|
394
401
|
const offset = Math.max(0, Number(url.searchParams.get("offset")) || 0);
|
|
395
402
|
const { tasks, total } = this.store.listTasks({ status, limit, offset });
|
|
396
403
|
|
|
397
|
-
const
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
404
|
+
const db = (this.store as any).db;
|
|
405
|
+
const items = tasks.map((t) => {
|
|
406
|
+
const meta = db.prepare("SELECT skill_status FROM tasks WHERE id = ?").get(t.id) as { skill_status: string | null } | undefined;
|
|
407
|
+
return {
|
|
408
|
+
id: t.id,
|
|
409
|
+
sessionKey: t.sessionKey,
|
|
410
|
+
title: t.title,
|
|
411
|
+
summary: t.summary ? (t.summary.length > 300 ? t.summary.slice(0, 297) + "..." : t.summary) : "",
|
|
412
|
+
status: t.status,
|
|
413
|
+
startedAt: t.startedAt,
|
|
414
|
+
endedAt: t.endedAt,
|
|
415
|
+
chunkCount: this.store.countChunksByTask(t.id),
|
|
416
|
+
skillStatus: meta?.skill_status ?? null,
|
|
417
|
+
};
|
|
418
|
+
});
|
|
407
419
|
|
|
408
420
|
this.jsonResponse(res, { tasks: items, total, limit, offset });
|
|
409
421
|
}
|
|
@@ -535,21 +547,24 @@ export class ViewerServer {
|
|
|
535
547
|
ftsResults = db.prepare(
|
|
536
548
|
"SELECT c.* FROM chunks_fts f JOIN chunks c ON f.rowid = c.rowid WHERE chunks_fts MATCH ? ORDER BY rank LIMIT 100",
|
|
537
549
|
).all(q).filter(passesFilter);
|
|
538
|
-
} catch {
|
|
550
|
+
} catch { /* FTS syntax error, fall through */ }
|
|
551
|
+
if (ftsResults.length === 0) {
|
|
539
552
|
ftsResults = db.prepare(
|
|
540
553
|
"SELECT * FROM chunks WHERE content LIKE ? OR summary LIKE ? ORDER BY created_at DESC LIMIT 100",
|
|
541
554
|
).all(`%${q}%`, `%${q}%`).filter(passesFilter);
|
|
542
555
|
}
|
|
543
556
|
|
|
557
|
+
const SEMANTIC_THRESHOLD = 0.64;
|
|
544
558
|
let vectorResults: any[] = [];
|
|
559
|
+
let scoreMap = new Map<string, number>();
|
|
545
560
|
try {
|
|
546
561
|
const queryVec = await this.embedder.embedQuery(q);
|
|
547
562
|
const hits = vectorSearch(this.store, queryVec, 40);
|
|
548
|
-
|
|
563
|
+
scoreMap = new Map(hits.map(h => [h.chunkId, h.score]));
|
|
564
|
+
const hitIds = new Set(hits.filter(h => h.score >= SEMANTIC_THRESHOLD).map(h => h.chunkId));
|
|
549
565
|
if (hitIds.size > 0) {
|
|
550
566
|
const placeholders = [...hitIds].map(() => "?").join(",");
|
|
551
567
|
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
568
|
rows.forEach((r: any) => { r._vscore = scoreMap.get(r.id) ?? 0; });
|
|
554
569
|
rows.sort((a: any, b: any) => (b._vscore ?? 0) - (a._vscore ?? 0));
|
|
555
570
|
vectorResults = rows;
|
|
@@ -567,13 +582,15 @@ export class ViewerServer {
|
|
|
567
582
|
if (!seenIds.has(r.id)) { seenIds.add(r.id); merged.push(r); }
|
|
568
583
|
}
|
|
569
584
|
|
|
585
|
+
const results = merged.length > 0 ? merged : ftsResults.slice(0, 20);
|
|
586
|
+
|
|
570
587
|
this.store.recordViewerEvent("search");
|
|
571
588
|
this.jsonResponse(res, {
|
|
572
|
-
results
|
|
589
|
+
results,
|
|
573
590
|
query: q,
|
|
574
591
|
vectorCount: vectorResults.length,
|
|
575
592
|
ftsCount: ftsResults.length,
|
|
576
|
-
total:
|
|
593
|
+
total: results.length,
|
|
577
594
|
});
|
|
578
595
|
}
|
|
579
596
|
|
|
@@ -732,7 +749,101 @@ export class ViewerServer {
|
|
|
732
749
|
this.store.setSkillVisibility(skillId, visibility);
|
|
733
750
|
this.jsonResponse(res, { ok: true, skillId, visibility });
|
|
734
751
|
} catch (err) {
|
|
735
|
-
|
|
752
|
+
const errMsg = err instanceof Error ? `${err.name}: ${err.message}` : String(err);
|
|
753
|
+
this.log.error(`handleSkillVisibility error: skillId=${skillId}, body=${body}, err=${errMsg}`);
|
|
754
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
755
|
+
res.end(JSON.stringify({ error: errMsg }));
|
|
756
|
+
}
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// ─── Task/Skill management ───
|
|
761
|
+
|
|
762
|
+
private handleTaskRetrySkill(_req: http.IncomingMessage, res: http.ServerResponse, urlPath: string): void {
|
|
763
|
+
const taskId = urlPath.replace("/api/task/", "").replace("/retry-skill", "");
|
|
764
|
+
const task = this.store.getTask(taskId);
|
|
765
|
+
if (!task) { res.writeHead(404, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "Task not found" })); return; }
|
|
766
|
+
if (task.status !== "completed") { res.writeHead(400, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "Only completed tasks can retry skill generation" })); return; }
|
|
767
|
+
if (!this.ctx) { res.writeHead(500, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "Plugin context not available" })); return; }
|
|
768
|
+
|
|
769
|
+
// Clean up stale task_skills references (e.g., skill was manually deleted)
|
|
770
|
+
const db = (this.store as any).db;
|
|
771
|
+
db.prepare("DELETE FROM task_skills WHERE task_id = ? AND skill_id NOT IN (SELECT id FROM skills)").run(taskId);
|
|
772
|
+
|
|
773
|
+
this.store.setTaskSkillMeta(taskId, { skillStatus: "queued", skillReason: "手动重试中..." });
|
|
774
|
+
this.jsonResponse(res, { ok: true, taskId, status: "queued" });
|
|
775
|
+
|
|
776
|
+
const ctx = this.ctx;
|
|
777
|
+
const recallEngine = new RecallEngine(this.store, this.embedder, ctx);
|
|
778
|
+
const evolver = new SkillEvolver(this.store, recallEngine, ctx, this.embedder);
|
|
779
|
+
evolver.onTaskCompleted(task).then(() => {
|
|
780
|
+
this.log.info(`Retry skill generation completed for task ${taskId}`);
|
|
781
|
+
}).catch((err) => {
|
|
782
|
+
this.log.error(`Retry skill generation failed for task ${taskId}: ${err}`);
|
|
783
|
+
this.store.setTaskSkillMeta(taskId, { skillStatus: "skipped", skillReason: `error: ${err}` });
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
private handleTaskDelete(res: http.ServerResponse, urlPath: string): void {
|
|
788
|
+
const taskId = urlPath.replace("/api/task/", "");
|
|
789
|
+
const deleted = this.store.deleteTask(taskId);
|
|
790
|
+
if (!deleted) { res.writeHead(404, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "Task not found" })); return; }
|
|
791
|
+
this.jsonResponse(res, { ok: true, taskId });
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
private handleTaskUpdate(req: http.IncomingMessage, res: http.ServerResponse, urlPath: string): void {
|
|
795
|
+
const taskId = urlPath.replace("/api/task/", "");
|
|
796
|
+
this.readBody(req, (body) => {
|
|
797
|
+
try {
|
|
798
|
+
const data = JSON.parse(body);
|
|
799
|
+
const task = this.store.getTask(taskId);
|
|
800
|
+
if (!task) { res.writeHead(404, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "Task not found" })); return; }
|
|
801
|
+
this.store.updateTask(taskId, {
|
|
802
|
+
title: data.title ?? task.title,
|
|
803
|
+
summary: data.summary ?? task.summary,
|
|
804
|
+
status: data.status ?? task.status,
|
|
805
|
+
endedAt: task.endedAt ?? undefined,
|
|
806
|
+
});
|
|
807
|
+
this.jsonResponse(res, { ok: true, taskId });
|
|
808
|
+
} catch (err) {
|
|
809
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
810
|
+
res.end(JSON.stringify({ error: String(err) }));
|
|
811
|
+
}
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
private handleSkillDelete(res: http.ServerResponse, urlPath: string): void {
|
|
816
|
+
const skillId = urlPath.replace("/api/skill/", "");
|
|
817
|
+
const skill = this.store.getSkill(skillId);
|
|
818
|
+
if (!skill) { res.writeHead(404, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "Skill not found" })); return; }
|
|
819
|
+
// Remove skill directory from disk
|
|
820
|
+
try {
|
|
821
|
+
if (skill.dirPath && fs.existsSync(skill.dirPath)) {
|
|
822
|
+
fs.rmSync(skill.dirPath, { recursive: true, force: true });
|
|
823
|
+
}
|
|
824
|
+
} catch (err) {
|
|
825
|
+
this.log.warn(`Failed to remove skill directory ${skill.dirPath}: ${err}`);
|
|
826
|
+
}
|
|
827
|
+
this.store.deleteSkill(skillId);
|
|
828
|
+
this.jsonResponse(res, { ok: true, skillId });
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
private handleSkillUpdate(req: http.IncomingMessage, res: http.ServerResponse, urlPath: string): void {
|
|
832
|
+
const skillId = urlPath.replace("/api/skill/", "");
|
|
833
|
+
this.readBody(req, (body) => {
|
|
834
|
+
try {
|
|
835
|
+
const data = JSON.parse(body);
|
|
836
|
+
const skill = this.store.getSkill(skillId);
|
|
837
|
+
if (!skill) { res.writeHead(404, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "Skill not found" })); return; }
|
|
838
|
+
this.store.updateSkill(skillId, {
|
|
839
|
+
description: data.description ?? skill.description,
|
|
840
|
+
version: skill.version,
|
|
841
|
+
status: data.status ?? skill.status,
|
|
842
|
+
installed: skill.installed,
|
|
843
|
+
qualityScore: skill.qualityScore,
|
|
844
|
+
});
|
|
845
|
+
this.jsonResponse(res, { ok: true, skillId });
|
|
846
|
+
} catch (err) {
|
|
736
847
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
737
848
|
res.end(JSON.stringify({ error: String(err) }));
|
|
738
849
|
}
|
|
@@ -745,13 +856,18 @@ export class ViewerServer {
|
|
|
745
856
|
this.readBody(req, (body) => {
|
|
746
857
|
try {
|
|
747
858
|
const data = JSON.parse(body);
|
|
859
|
+
if (!data.content || typeof data.content !== "string" || !data.content.trim()) {
|
|
860
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
861
|
+
res.end(JSON.stringify({ error: "content is required and must be a non-empty string" }));
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
748
864
|
const { v4: uuidv4 } = require("uuid");
|
|
749
865
|
const id = uuidv4();
|
|
750
866
|
const now = Date.now();
|
|
751
867
|
this.store.insertChunk({
|
|
752
868
|
id, sessionKey: data.session_key || "manual", turnId: `manual-${now}`, seq: 0,
|
|
753
|
-
role: data.role || "user", content: data.content
|
|
754
|
-
summary: data.summary || data.content
|
|
869
|
+
role: data.role || "user", content: data.content, kind: data.kind || "paragraph",
|
|
870
|
+
summary: data.summary || data.content.slice(0, 100),
|
|
755
871
|
taskId: null, skillId: null, owner: data.owner || "agent:main",
|
|
756
872
|
dedupStatus: "active", dedupTarget: null, dedupReason: null,
|
|
757
873
|
mergeCount: 0, lastHitAt: null, mergeHistory: "[]",
|
|
@@ -784,6 +900,11 @@ export class ViewerServer {
|
|
|
784
900
|
this.readBody(req, (body) => {
|
|
785
901
|
try {
|
|
786
902
|
const data = JSON.parse(body);
|
|
903
|
+
if (data.content !== undefined && (typeof data.content !== "string" || !data.content.trim())) {
|
|
904
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
905
|
+
res.end(JSON.stringify({ error: "content must be a non-empty string" }));
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
787
908
|
const ok = this.store.updateChunk(chunkId, { summary: data.summary, content: data.content, role: data.role, kind: data.kind, owner: data.owner });
|
|
788
909
|
if (ok) this.jsonResponse(res, { ok: true, message: "Memory updated" });
|
|
789
910
|
else { res.writeHead(404, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "Not found" })); }
|
|
@@ -808,19 +929,25 @@ export class ViewerServer {
|
|
|
808
929
|
}
|
|
809
930
|
|
|
810
931
|
private handleDeleteAll(res: http.ServerResponse): void {
|
|
811
|
-
const result = this.store.deleteAll();
|
|
812
|
-
// Clean up skills-store directory
|
|
813
|
-
const skillsStoreDir = path.join(this.dataDir, "skills-store");
|
|
814
932
|
try {
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
933
|
+
const result = this.store.deleteAll();
|
|
934
|
+
const skillsStoreDir = path.join(this.dataDir, "skills-store");
|
|
935
|
+
try {
|
|
936
|
+
if (fs.existsSync(skillsStoreDir)) {
|
|
937
|
+
fs.rmSync(skillsStoreDir, { recursive: true });
|
|
938
|
+
fs.mkdirSync(skillsStoreDir, { recursive: true });
|
|
939
|
+
this.log.info("Cleared skills-store directory");
|
|
940
|
+
}
|
|
941
|
+
} catch (err) {
|
|
942
|
+
this.log.warn(`Failed to clear skills-store: ${err}`);
|
|
819
943
|
}
|
|
944
|
+
this.jsonResponse(res, { ok: true, deleted: result });
|
|
820
945
|
} catch (err) {
|
|
821
|
-
|
|
946
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
947
|
+
this.log.error(`handleDeleteAll error: ${msg}`);
|
|
948
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
949
|
+
res.end(JSON.stringify({ ok: false, error: msg }));
|
|
822
950
|
}
|
|
823
|
-
this.jsonResponse(res, { ok: true, deleted: result });
|
|
824
951
|
}
|
|
825
952
|
|
|
826
953
|
// ─── Helpers ───
|
|
@@ -901,6 +1028,158 @@ export class ViewerServer {
|
|
|
901
1028
|
});
|
|
902
1029
|
}
|
|
903
1030
|
|
|
1031
|
+
private handleTestModel(req: http.IncomingMessage, res: http.ServerResponse): void {
|
|
1032
|
+
this.readBody(req, async (body) => {
|
|
1033
|
+
try {
|
|
1034
|
+
const { type, provider, model, endpoint, apiKey } = JSON.parse(body);
|
|
1035
|
+
if (!provider) {
|
|
1036
|
+
this.jsonResponse(res, { ok: false, error: "provider is required" });
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
1039
|
+
if (type === "embedding") {
|
|
1040
|
+
await this.testEmbeddingModel(provider, model, endpoint, apiKey);
|
|
1041
|
+
this.jsonResponse(res, { ok: true, detail: `${provider}/${model}` });
|
|
1042
|
+
} else {
|
|
1043
|
+
await this.testChatModel(provider, model, endpoint, apiKey);
|
|
1044
|
+
this.jsonResponse(res, { ok: true, detail: `${provider}/${model}` });
|
|
1045
|
+
}
|
|
1046
|
+
} catch (e: unknown) {
|
|
1047
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1048
|
+
this.log.warn(`test-model failed: ${msg}`);
|
|
1049
|
+
this.jsonResponse(res, { ok: false, error: msg });
|
|
1050
|
+
}
|
|
1051
|
+
});
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
private serveFallbackModel(res: http.ServerResponse): void {
|
|
1055
|
+
try {
|
|
1056
|
+
const cfgPath = this.getOpenClawConfigPath();
|
|
1057
|
+
if (!fs.existsSync(cfgPath)) {
|
|
1058
|
+
this.jsonResponse(res, { available: false });
|
|
1059
|
+
return;
|
|
1060
|
+
}
|
|
1061
|
+
const raw = JSON.parse(fs.readFileSync(cfgPath, "utf-8"));
|
|
1062
|
+
const agentModel: string | undefined = raw?.agents?.defaults?.model?.primary;
|
|
1063
|
+
if (!agentModel) {
|
|
1064
|
+
this.jsonResponse(res, { available: false });
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
1067
|
+
const [providerKey, modelId] = agentModel.includes("/")
|
|
1068
|
+
? agentModel.split("/", 2)
|
|
1069
|
+
: [undefined, agentModel];
|
|
1070
|
+
const providerCfg = providerKey
|
|
1071
|
+
? raw?.models?.providers?.[providerKey]
|
|
1072
|
+
: Object.values(raw?.models?.providers ?? {})[0] as Record<string, unknown> | undefined;
|
|
1073
|
+
if (!providerCfg || !providerCfg.baseUrl || !providerCfg.apiKey) {
|
|
1074
|
+
this.jsonResponse(res, { available: false });
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1077
|
+
this.jsonResponse(res, { available: true, model: modelId || agentModel, baseUrl: providerCfg.baseUrl });
|
|
1078
|
+
} catch {
|
|
1079
|
+
this.jsonResponse(res, { available: false });
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
private async testEmbeddingModel(provider: string, model: string, endpoint: string, apiKey: string): Promise<void> {
|
|
1084
|
+
if (provider === "local") {
|
|
1085
|
+
return;
|
|
1086
|
+
}
|
|
1087
|
+
const baseUrl = (endpoint || "https://api.openai.com/v1").replace(/\/+$/, "");
|
|
1088
|
+
const embUrl = baseUrl.endsWith("/embeddings") ? baseUrl : `${baseUrl}/embeddings`;
|
|
1089
|
+
const headers: Record<string, string> = {
|
|
1090
|
+
"Content-Type": "application/json",
|
|
1091
|
+
"Authorization": `Bearer ${apiKey}`,
|
|
1092
|
+
};
|
|
1093
|
+
if (provider === "cohere") {
|
|
1094
|
+
headers["Authorization"] = `Bearer ${apiKey}`;
|
|
1095
|
+
const resp = await fetch(baseUrl.replace(/\/v\d+.*/, "/v2/embed"), {
|
|
1096
|
+
method: "POST",
|
|
1097
|
+
headers,
|
|
1098
|
+
body: JSON.stringify({ texts: ["test"], model: model || "embed-english-v3.0", input_type: "search_query", embedding_types: ["float"] }),
|
|
1099
|
+
signal: AbortSignal.timeout(15_000),
|
|
1100
|
+
});
|
|
1101
|
+
if (!resp.ok) {
|
|
1102
|
+
const txt = await resp.text();
|
|
1103
|
+
throw new Error(`Cohere embed ${resp.status}: ${txt}`);
|
|
1104
|
+
}
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
if (provider === "gemini") {
|
|
1108
|
+
const url = `https://generativelanguage.googleapis.com/v1/models/${model || "text-embedding-004"}:embedContent?key=${apiKey}`;
|
|
1109
|
+
const resp = await fetch(url, {
|
|
1110
|
+
method: "POST",
|
|
1111
|
+
headers: { "Content-Type": "application/json" },
|
|
1112
|
+
body: JSON.stringify({ content: { parts: [{ text: "test" }] } }),
|
|
1113
|
+
signal: AbortSignal.timeout(15_000),
|
|
1114
|
+
});
|
|
1115
|
+
if (!resp.ok) {
|
|
1116
|
+
const txt = await resp.text();
|
|
1117
|
+
throw new Error(`Gemini embed ${resp.status}: ${txt}`);
|
|
1118
|
+
}
|
|
1119
|
+
return;
|
|
1120
|
+
}
|
|
1121
|
+
const resp = await fetch(embUrl, {
|
|
1122
|
+
method: "POST",
|
|
1123
|
+
headers,
|
|
1124
|
+
body: JSON.stringify({ input: ["test"], model: model || "text-embedding-3-small" }),
|
|
1125
|
+
signal: AbortSignal.timeout(15_000),
|
|
1126
|
+
});
|
|
1127
|
+
if (!resp.ok) {
|
|
1128
|
+
const txt = await resp.text();
|
|
1129
|
+
throw new Error(`${resp.status}: ${txt}`);
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
private async testChatModel(provider: string, model: string, endpoint: string, apiKey: string): Promise<void> {
|
|
1134
|
+
const baseUrl = (endpoint || "https://api.openai.com/v1").replace(/\/+$/, "");
|
|
1135
|
+
if (provider === "anthropic") {
|
|
1136
|
+
const url = endpoint || "https://api.anthropic.com/v1/messages";
|
|
1137
|
+
const resp = await fetch(url, {
|
|
1138
|
+
method: "POST",
|
|
1139
|
+
headers: {
|
|
1140
|
+
"Content-Type": "application/json",
|
|
1141
|
+
"x-api-key": apiKey,
|
|
1142
|
+
"anthropic-version": "2023-06-01",
|
|
1143
|
+
},
|
|
1144
|
+
body: JSON.stringify({ model: model || "claude-3-haiku-20240307", max_tokens: 5, messages: [{ role: "user", content: "hi" }] }),
|
|
1145
|
+
signal: AbortSignal.timeout(15_000),
|
|
1146
|
+
});
|
|
1147
|
+
if (!resp.ok) {
|
|
1148
|
+
const txt = await resp.text();
|
|
1149
|
+
throw new Error(`Anthropic ${resp.status}: ${txt}`);
|
|
1150
|
+
}
|
|
1151
|
+
return;
|
|
1152
|
+
}
|
|
1153
|
+
if (provider === "gemini") {
|
|
1154
|
+
const url = `https://generativelanguage.googleapis.com/v1/models/${model || "gemini-1.5-flash"}:generateContent?key=${apiKey}`;
|
|
1155
|
+
const resp = await fetch(url, {
|
|
1156
|
+
method: "POST",
|
|
1157
|
+
headers: { "Content-Type": "application/json" },
|
|
1158
|
+
body: JSON.stringify({ contents: [{ parts: [{ text: "hi" }] }], generationConfig: { maxOutputTokens: 5 } }),
|
|
1159
|
+
signal: AbortSignal.timeout(15_000),
|
|
1160
|
+
});
|
|
1161
|
+
if (!resp.ok) {
|
|
1162
|
+
const txt = await resp.text();
|
|
1163
|
+
throw new Error(`Gemini ${resp.status}: ${txt}`);
|
|
1164
|
+
}
|
|
1165
|
+
return;
|
|
1166
|
+
}
|
|
1167
|
+
const chatUrl = baseUrl.endsWith("/chat/completions") ? baseUrl : `${baseUrl}/chat/completions`;
|
|
1168
|
+
const resp = await fetch(chatUrl, {
|
|
1169
|
+
method: "POST",
|
|
1170
|
+
headers: {
|
|
1171
|
+
"Content-Type": "application/json",
|
|
1172
|
+
"Authorization": `Bearer ${apiKey}`,
|
|
1173
|
+
},
|
|
1174
|
+
body: JSON.stringify({ model: model || "gpt-4o-mini", max_tokens: 5, messages: [{ role: "user", content: "hi" }] }),
|
|
1175
|
+
signal: AbortSignal.timeout(15_000),
|
|
1176
|
+
});
|
|
1177
|
+
if (!resp.ok) {
|
|
1178
|
+
const txt = await resp.text();
|
|
1179
|
+
throw new Error(`${resp.status}: ${txt}`);
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
|
|
904
1183
|
private serveLogs(res: http.ServerResponse, url: URL): void {
|
|
905
1184
|
const limit = Math.min(Number(url.searchParams.get("limit") ?? 20), 200);
|
|
906
1185
|
const offset = Math.max(0, Number(url.searchParams.get("offset") ?? 0));
|
|
@@ -1092,9 +1371,11 @@ export class ViewerServer {
|
|
|
1092
1371
|
}
|
|
1093
1372
|
|
|
1094
1373
|
this.readBody(req, (body) => {
|
|
1095
|
-
let opts: { sources?: string[] } = {};
|
|
1374
|
+
let opts: { sources?: string[]; concurrency?: number } = {};
|
|
1096
1375
|
try { opts = JSON.parse(body); } catch { /* defaults */ }
|
|
1097
1376
|
|
|
1377
|
+
const concurrency = Math.max(1, Math.min(opts.concurrency ?? 1, 8));
|
|
1378
|
+
|
|
1098
1379
|
res.writeHead(200, {
|
|
1099
1380
|
"Content-Type": "text/event-stream",
|
|
1100
1381
|
"Cache-Control": "no-cache",
|
|
@@ -1129,7 +1410,7 @@ export class ViewerServer {
|
|
|
1129
1410
|
};
|
|
1130
1411
|
|
|
1131
1412
|
this.migrationRunning = true;
|
|
1132
|
-
this.runMigration(send, opts.sources).finally(() => {
|
|
1413
|
+
this.runMigration(send, opts.sources, concurrency).finally(() => {
|
|
1133
1414
|
this.migrationRunning = false;
|
|
1134
1415
|
this.migrationState.done = true;
|
|
1135
1416
|
if (this.migrationAbort) {
|
|
@@ -1150,6 +1431,7 @@ export class ViewerServer {
|
|
|
1150
1431
|
private async runMigration(
|
|
1151
1432
|
send: (event: string, data: unknown) => void,
|
|
1152
1433
|
sources?: string[],
|
|
1434
|
+
concurrency: number = 1,
|
|
1153
1435
|
): Promise<void> {
|
|
1154
1436
|
const ocHome = this.getOpenClawHome();
|
|
1155
1437
|
const importSqlite = !sources || sources.includes("sqlite");
|
|
@@ -1162,15 +1444,17 @@ export class ViewerServer {
|
|
|
1162
1444
|
|
|
1163
1445
|
const cfgPath = this.getOpenClawConfigPath();
|
|
1164
1446
|
let summarizerCfg: any;
|
|
1447
|
+
let strongCfg: any;
|
|
1165
1448
|
try {
|
|
1166
1449
|
const raw = JSON.parse(fs.readFileSync(cfgPath, "utf-8"));
|
|
1167
1450
|
const pluginCfg = raw?.plugins?.entries?.["memos-local-openclaw-plugin"]?.config ??
|
|
1168
1451
|
raw?.plugins?.entries?.["memos-lite"]?.config ??
|
|
1169
1452
|
raw?.plugins?.entries?.["memos-lite-openclaw-plugin"]?.config ?? {};
|
|
1170
1453
|
summarizerCfg = pluginCfg.summarizer;
|
|
1454
|
+
strongCfg = pluginCfg.skillEvolution?.summarizer;
|
|
1171
1455
|
} catch { /* no config */ }
|
|
1172
1456
|
|
|
1173
|
-
const summarizer = new Summarizer(summarizerCfg, this.log);
|
|
1457
|
+
const summarizer = new Summarizer(summarizerCfg, this.log, strongCfg);
|
|
1174
1458
|
|
|
1175
1459
|
// Phase 1: Import SQLite memory chunks
|
|
1176
1460
|
if (importSqlite) {
|
|
@@ -1210,6 +1494,23 @@ export class ViewerServer {
|
|
|
1210
1494
|
continue;
|
|
1211
1495
|
}
|
|
1212
1496
|
|
|
1497
|
+
const importOwner = `agent:${agentId}`;
|
|
1498
|
+
|
|
1499
|
+
// Exact hash dedup within same agent
|
|
1500
|
+
const existingByHash = this.store.findActiveChunkByHash(row.text, importOwner);
|
|
1501
|
+
if (existingByHash) {
|
|
1502
|
+
totalSkipped++;
|
|
1503
|
+
send("item", {
|
|
1504
|
+
index: i + 1,
|
|
1505
|
+
total: rows.length,
|
|
1506
|
+
status: "skipped",
|
|
1507
|
+
preview: row.text.slice(0, 120),
|
|
1508
|
+
source: file,
|
|
1509
|
+
reason: "exact duplicate within agent",
|
|
1510
|
+
});
|
|
1511
|
+
continue;
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1213
1514
|
try {
|
|
1214
1515
|
const summary = await summarizer.summarize(row.text);
|
|
1215
1516
|
let embedding: number[] | null = null;
|
|
@@ -1224,7 +1525,9 @@ export class ViewerServer {
|
|
|
1224
1525
|
let dedupReason: string | null = null;
|
|
1225
1526
|
|
|
1226
1527
|
if (embedding) {
|
|
1227
|
-
const
|
|
1528
|
+
const importThreshold = this.ctx?.config?.dedup?.similarityThreshold ?? 0.60;
|
|
1529
|
+
const dedupOwnerFilter = [importOwner];
|
|
1530
|
+
const topSimilar = findTopSimilar(this.store, embedding, importThreshold, 5, this.log, dedupOwnerFilter);
|
|
1228
1531
|
if (topSimilar.length > 0) {
|
|
1229
1532
|
const candidates = topSimilar.map((s, idx) => {
|
|
1230
1533
|
const chunk = this.store.getChunk(s.chunkId);
|
|
@@ -1315,18 +1618,34 @@ export class ViewerServer {
|
|
|
1315
1618
|
}
|
|
1316
1619
|
}
|
|
1317
1620
|
|
|
1318
|
-
// Phase 2: Import session JSONL files
|
|
1621
|
+
// Phase 2: Import session JSONL files from ALL agents (supports parallel by agent)
|
|
1319
1622
|
if (importSessions) {
|
|
1320
|
-
const
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1623
|
+
const agentsDir = path.join(ocHome, "agents");
|
|
1624
|
+
const agentGroups: Map<string, Array<{ file: string; filePath: string }>> = new Map();
|
|
1625
|
+
if (fs.existsSync(agentsDir)) {
|
|
1626
|
+
for (const entry of fs.readdirSync(agentsDir, { withFileTypes: true })) {
|
|
1627
|
+
if (entry.isDirectory()) {
|
|
1628
|
+
const sessDir = path.join(agentsDir, entry.name, "sessions");
|
|
1629
|
+
if (fs.existsSync(sessDir)) {
|
|
1630
|
+
const jsonlFiles = fs.readdirSync(sessDir).filter(f => f.includes(".jsonl")).sort();
|
|
1631
|
+
if (jsonlFiles.length > 0) {
|
|
1632
|
+
agentGroups.set(entry.name, jsonlFiles.map(f => ({ file: f, filePath: path.join(sessDir, f) })));
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1324
1638
|
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1639
|
+
const agentIds = Array.from(agentGroups.keys());
|
|
1640
|
+
const allFileCount = Array.from(agentGroups.values()).reduce((s, g) => s + g.length, 0);
|
|
1641
|
+
send("phase", { phase: "sessions", files: allFileCount, agents: agentIds, concurrency });
|
|
1642
|
+
|
|
1643
|
+
// Count total messages across all agents
|
|
1644
|
+
let totalMsgs = 0;
|
|
1645
|
+
for (const files of agentGroups.values()) {
|
|
1646
|
+
for (const { filePath } of files) {
|
|
1328
1647
|
try {
|
|
1329
|
-
const raw = fs.readFileSync(
|
|
1648
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
1330
1649
|
for (const line of raw.split("\n")) {
|
|
1331
1650
|
if (!line.trim()) continue;
|
|
1332
1651
|
try {
|
|
@@ -1347,12 +1666,18 @@ export class ViewerServer {
|
|
|
1347
1666
|
}
|
|
1348
1667
|
} catch { /* skip */ }
|
|
1349
1668
|
}
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
// Thread-safe counters for parallel execution
|
|
1672
|
+
let globalMsgIdx = 0;
|
|
1673
|
+
const incIdx = () => ++globalMsgIdx;
|
|
1350
1674
|
|
|
1351
|
-
|
|
1675
|
+
// Import one agent's sessions sequentially
|
|
1676
|
+
const importAgent = async (agentId: string, files: Array<{ file: string; filePath: string }>) => {
|
|
1677
|
+
const agentOwner = `agent:${agentId}`;
|
|
1678
|
+
for (const { file, filePath } of files) {
|
|
1352
1679
|
if (this.migrationAbort) break;
|
|
1353
1680
|
const sessionId = file.replace(/\.jsonl.*$/, "");
|
|
1354
|
-
const filePath = path.join(sessionsDir, file);
|
|
1355
|
-
send("progress", { total: totalMsgs, processed: globalMsgIdx, phase: "sessions", file });
|
|
1356
1681
|
|
|
1357
1682
|
try {
|
|
1358
1683
|
const fileStream = fs.createReadStream(filePath, { encoding: "utf-8" });
|
|
@@ -1384,21 +1709,20 @@ export class ViewerServer {
|
|
|
1384
1709
|
}
|
|
1385
1710
|
if (!content || content.length < 10) continue;
|
|
1386
1711
|
|
|
1387
|
-
|
|
1712
|
+
const idx = incIdx();
|
|
1388
1713
|
totalProcessed++;
|
|
1389
1714
|
|
|
1390
1715
|
const sessionKey = `openclaw-session-${sessionId}`;
|
|
1391
1716
|
if (this.store.chunkExistsByContent(sessionKey, msgRole, content)) {
|
|
1392
1717
|
totalSkipped++;
|
|
1393
|
-
send("item", {
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
});
|
|
1718
|
+
send("item", { index: idx, total: totalMsgs, status: "skipped", preview: content.slice(0, 120), source: file, agent: agentId, role: msgRole, reason: "duplicate" });
|
|
1719
|
+
continue;
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
const existingByHash = this.store.findActiveChunkByHash(content, agentOwner);
|
|
1723
|
+
if (existingByHash) {
|
|
1724
|
+
totalSkipped++;
|
|
1725
|
+
send("item", { index: idx, total: totalMsgs, status: "skipped", preview: content.slice(0, 120), source: file, agent: agentId, role: msgRole, reason: "exact duplicate within agent" });
|
|
1402
1726
|
continue;
|
|
1403
1727
|
}
|
|
1404
1728
|
|
|
@@ -1416,33 +1740,26 @@ export class ViewerServer {
|
|
|
1416
1740
|
let dedupReason: string | null = null;
|
|
1417
1741
|
|
|
1418
1742
|
if (embedding) {
|
|
1419
|
-
const
|
|
1743
|
+
const importThreshold = this.ctx?.config?.dedup?.similarityThreshold ?? 0.60;
|
|
1744
|
+
const dedupOwnerFilter = [agentOwner];
|
|
1745
|
+
const topSimilar = findTopSimilar(this.store, embedding, importThreshold, 5, this.log, dedupOwnerFilter);
|
|
1420
1746
|
if (topSimilar.length > 0) {
|
|
1421
|
-
const candidates = topSimilar.map((s,
|
|
1747
|
+
const candidates = topSimilar.map((s, i) => {
|
|
1422
1748
|
const chunk = this.store.getChunk(s.chunkId);
|
|
1423
|
-
return { index:
|
|
1749
|
+
return { index: i + 1, summary: chunk?.summary ?? "", chunkId: s.chunkId };
|
|
1424
1750
|
}).filter(c => c.summary);
|
|
1425
1751
|
|
|
1426
1752
|
if (candidates.length > 0) {
|
|
1427
1753
|
const dedupResult = await summarizer.judgeDedup(summary, candidates);
|
|
1428
1754
|
if (dedupResult?.action === "DUPLICATE" && dedupResult.targetIndex) {
|
|
1429
1755
|
const targetId = candidates[dedupResult.targetIndex - 1]?.chunkId;
|
|
1430
|
-
if (targetId) {
|
|
1431
|
-
dedupStatus = "duplicate";
|
|
1432
|
-
dedupTarget = targetId;
|
|
1433
|
-
dedupReason = dedupResult.reason;
|
|
1434
|
-
}
|
|
1756
|
+
if (targetId) { dedupStatus = "duplicate"; dedupTarget = targetId; dedupReason = dedupResult.reason; }
|
|
1435
1757
|
} else if (dedupResult?.action === "UPDATE" && dedupResult.targetIndex && dedupResult.mergedSummary) {
|
|
1436
1758
|
const targetId = candidates[dedupResult.targetIndex - 1]?.chunkId;
|
|
1437
1759
|
if (targetId) {
|
|
1438
1760
|
this.store.updateChunkSummaryAndContent(targetId, dedupResult.mergedSummary, content);
|
|
1439
|
-
try {
|
|
1440
|
-
|
|
1441
|
-
if (newEmb) this.store.upsertEmbedding(targetId, newEmb);
|
|
1442
|
-
} catch { /* best-effort */ }
|
|
1443
|
-
dedupStatus = "merged";
|
|
1444
|
-
dedupTarget = targetId;
|
|
1445
|
-
dedupReason = dedupResult.reason;
|
|
1761
|
+
try { const [newEmb] = await this.embedder.embed([dedupResult.mergedSummary]); if (newEmb) this.store.upsertEmbedding(targetId, newEmb); } catch { /* best-effort */ }
|
|
1762
|
+
dedupStatus = "merged"; dedupTarget = targetId; dedupReason = dedupResult.reason;
|
|
1446
1763
|
}
|
|
1447
1764
|
}
|
|
1448
1765
|
}
|
|
@@ -1453,60 +1770,53 @@ export class ViewerServer {
|
|
|
1453
1770
|
const msgTs = obj.message?.timestamp ?? obj.timestamp;
|
|
1454
1771
|
const ts = msgTs ? new Date(msgTs).getTime() : Date.now();
|
|
1455
1772
|
const chunk: Chunk = {
|
|
1456
|
-
id: chunkId,
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
role: msgRole as any,
|
|
1461
|
-
content,
|
|
1462
|
-
kind: "paragraph",
|
|
1463
|
-
summary,
|
|
1464
|
-
embedding: null,
|
|
1465
|
-
taskId: null,
|
|
1466
|
-
skillId: null,
|
|
1467
|
-
owner: "agent:main",
|
|
1468
|
-
dedupStatus,
|
|
1469
|
-
dedupTarget,
|
|
1470
|
-
dedupReason,
|
|
1471
|
-
mergeCount: 0,
|
|
1472
|
-
lastHitAt: null,
|
|
1473
|
-
mergeHistory: "[]",
|
|
1474
|
-
createdAt: ts,
|
|
1475
|
-
updatedAt: ts,
|
|
1773
|
+
id: chunkId, sessionKey, turnId: `import-${agentId}-${sessionId}-${idx}`, seq: 0,
|
|
1774
|
+
role: msgRole as any, content, kind: "paragraph", summary, embedding: null,
|
|
1775
|
+
taskId: null, skillId: null, owner: agentOwner, dedupStatus, dedupTarget, dedupReason,
|
|
1776
|
+
mergeCount: 0, lastHitAt: null, mergeHistory: "[]", createdAt: ts, updatedAt: ts,
|
|
1476
1777
|
};
|
|
1477
1778
|
|
|
1478
1779
|
this.store.insertChunk(chunk);
|
|
1479
|
-
if (embedding && dedupStatus === "active")
|
|
1480
|
-
this.store.upsertEmbedding(chunkId, embedding);
|
|
1481
|
-
}
|
|
1780
|
+
if (embedding && dedupStatus === "active") this.store.upsertEmbedding(chunkId, embedding);
|
|
1482
1781
|
|
|
1483
1782
|
totalStored++;
|
|
1484
|
-
send("item", {
|
|
1485
|
-
index: globalMsgIdx,
|
|
1486
|
-
total: totalMsgs,
|
|
1487
|
-
status: dedupStatus === "active" ? "stored" : dedupStatus,
|
|
1488
|
-
preview: content.slice(0, 120),
|
|
1489
|
-
summary: summary.slice(0, 80),
|
|
1490
|
-
source: file,
|
|
1491
|
-
role: msgRole,
|
|
1492
|
-
});
|
|
1783
|
+
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 });
|
|
1493
1784
|
} catch (err) {
|
|
1494
1785
|
totalErrors++;
|
|
1495
|
-
send("item", {
|
|
1496
|
-
index: globalMsgIdx,
|
|
1497
|
-
total: totalMsgs,
|
|
1498
|
-
status: "error",
|
|
1499
|
-
preview: content.slice(0, 120),
|
|
1500
|
-
source: file,
|
|
1501
|
-
error: String(err).slice(0, 200),
|
|
1502
|
-
});
|
|
1786
|
+
send("item", { index: idx, total: totalMsgs, status: "error", preview: content.slice(0, 120), source: file, agent: agentId, error: String(err).slice(0, 200) });
|
|
1503
1787
|
}
|
|
1504
1788
|
}
|
|
1505
1789
|
} catch (err) {
|
|
1506
|
-
send("error", { file, error: String(err) });
|
|
1790
|
+
send("error", { file, agent: agentId, error: String(err) });
|
|
1507
1791
|
totalErrors++;
|
|
1508
1792
|
}
|
|
1509
1793
|
}
|
|
1794
|
+
};
|
|
1795
|
+
|
|
1796
|
+
// Execute agents with concurrency control
|
|
1797
|
+
const agentEntries = Array.from(agentGroups.entries());
|
|
1798
|
+
if (concurrency <= 1 || agentEntries.length <= 1) {
|
|
1799
|
+
for (const [agentId, files] of agentEntries) {
|
|
1800
|
+
if (this.migrationAbort) break;
|
|
1801
|
+
send("progress", { total: totalMsgs, processed: globalMsgIdx, phase: "sessions", agent: agentId });
|
|
1802
|
+
await importAgent(agentId, files);
|
|
1803
|
+
}
|
|
1804
|
+
} else {
|
|
1805
|
+
// Parallel: run up to `concurrency` agents at once
|
|
1806
|
+
let cursor = 0;
|
|
1807
|
+
const runBatch = async () => {
|
|
1808
|
+
while (cursor < agentEntries.length && !this.migrationAbort) {
|
|
1809
|
+
const batch: Promise<void>[] = [];
|
|
1810
|
+
const batchStart = cursor;
|
|
1811
|
+
while (batch.length < concurrency && cursor < agentEntries.length) {
|
|
1812
|
+
const [agentId, files] = agentEntries[cursor++];
|
|
1813
|
+
send("progress", { total: totalMsgs, processed: globalMsgIdx, phase: "sessions", agent: agentId, parallel: true });
|
|
1814
|
+
batch.push(importAgent(agentId, files));
|
|
1815
|
+
}
|
|
1816
|
+
await Promise.all(batch);
|
|
1817
|
+
}
|
|
1818
|
+
};
|
|
1819
|
+
await runBatch();
|
|
1510
1820
|
}
|
|
1511
1821
|
}
|
|
1512
1822
|
|
|
@@ -1529,9 +1839,11 @@ export class ViewerServer {
|
|
|
1529
1839
|
}
|
|
1530
1840
|
|
|
1531
1841
|
this.readBody(req, (body) => {
|
|
1532
|
-
let opts: { enableTasks?: boolean; enableSkills?: boolean } = {};
|
|
1842
|
+
let opts: { enableTasks?: boolean; enableSkills?: boolean; concurrency?: number } = {};
|
|
1533
1843
|
try { opts = JSON.parse(body); } catch { /* defaults */ }
|
|
1534
1844
|
|
|
1845
|
+
const concurrency = Math.max(1, Math.min(opts.concurrency ?? 1, 8));
|
|
1846
|
+
|
|
1535
1847
|
res.writeHead(200, {
|
|
1536
1848
|
"Content-Type": "text/event-stream",
|
|
1537
1849
|
"Cache-Control": "no-cache",
|
|
@@ -1550,7 +1862,7 @@ export class ViewerServer {
|
|
|
1550
1862
|
};
|
|
1551
1863
|
|
|
1552
1864
|
this.ppRunning = true;
|
|
1553
|
-
this.runPostprocess(send, !!opts.enableTasks, !!opts.enableSkills).finally(() => {
|
|
1865
|
+
this.runPostprocess(send, !!opts.enableTasks, !!opts.enableSkills, concurrency).finally(() => {
|
|
1554
1866
|
this.ppRunning = false;
|
|
1555
1867
|
this.ppState.running = false;
|
|
1556
1868
|
this.ppState.done = true;
|
|
@@ -1608,128 +1920,154 @@ export class ViewerServer {
|
|
|
1608
1920
|
send: (event: string, data: unknown) => void,
|
|
1609
1921
|
enableTasks: boolean,
|
|
1610
1922
|
enableSkills: boolean,
|
|
1923
|
+
concurrency: number = 1,
|
|
1611
1924
|
): Promise<void> {
|
|
1612
1925
|
const ctx = this.ctx!;
|
|
1613
|
-
const taskProcessor = new TaskProcessor(this.store, ctx);
|
|
1614
|
-
let skillEvolver: SkillEvolver | null = null;
|
|
1615
|
-
|
|
1616
|
-
if (enableSkills) {
|
|
1617
|
-
const recallEngine = new RecallEngine(this.store, this.embedder, ctx);
|
|
1618
|
-
skillEvolver = new SkillEvolver(this.store, recallEngine, ctx);
|
|
1619
|
-
taskProcessor.onTaskCompleted(async (task) => {
|
|
1620
|
-
try {
|
|
1621
|
-
await skillEvolver!.onTaskCompleted(task);
|
|
1622
|
-
this.ppState.skillsCreated++;
|
|
1623
|
-
send("skill", { taskId: task.id, title: task.title });
|
|
1624
|
-
} catch (err) {
|
|
1625
|
-
this.log.warn(`Postprocess skill evolution error: ${err}`);
|
|
1626
|
-
}
|
|
1627
|
-
});
|
|
1628
|
-
}
|
|
1629
1926
|
|
|
1630
1927
|
const importSessions = this.store.getDistinctSessionKeys()
|
|
1631
1928
|
.filter((sk: string) => sk.startsWith("openclaw-import-") || sk.startsWith("openclaw-session-"));
|
|
1632
1929
|
|
|
1633
|
-
type PendingItem = { sessionKey: string; action: "full" | "skill-only" };
|
|
1930
|
+
type PendingItem = { sessionKey: string; action: "full" | "skill-only"; owner: string };
|
|
1634
1931
|
const pendingItems: PendingItem[] = [];
|
|
1635
1932
|
let skippedCount = 0;
|
|
1636
1933
|
|
|
1934
|
+
const ownerMap = this.store.getSessionOwnerMap(importSessions);
|
|
1935
|
+
|
|
1637
1936
|
for (const sk of importSessions) {
|
|
1638
1937
|
const hasTask = this.store.hasTaskForSession(sk);
|
|
1639
1938
|
const hasSkill = this.store.hasSkillForSessionTask(sk);
|
|
1939
|
+
const owner = ownerMap.get(sk) ?? "agent:main";
|
|
1640
1940
|
|
|
1641
1941
|
if (enableTasks && !hasTask) {
|
|
1642
|
-
pendingItems.push({ sessionKey: sk, action: "full" });
|
|
1942
|
+
pendingItems.push({ sessionKey: sk, action: "full", owner });
|
|
1643
1943
|
} else if (enableSkills && hasTask && !hasSkill) {
|
|
1644
|
-
pendingItems.push({ sessionKey: sk, action: "skill-only" });
|
|
1944
|
+
pendingItems.push({ sessionKey: sk, action: "skill-only", owner });
|
|
1645
1945
|
} else {
|
|
1646
1946
|
skippedCount++;
|
|
1647
1947
|
}
|
|
1648
1948
|
}
|
|
1649
1949
|
|
|
1950
|
+
// Group pending items by agent (owner)
|
|
1951
|
+
const agentGroups = new Map<string, PendingItem[]>();
|
|
1952
|
+
for (const item of pendingItems) {
|
|
1953
|
+
const group = agentGroups.get(item.owner) ?? [];
|
|
1954
|
+
group.push(item);
|
|
1955
|
+
agentGroups.set(item.owner, group);
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1650
1958
|
this.ppState.total = pendingItems.length;
|
|
1651
1959
|
send("info", {
|
|
1652
1960
|
totalSessions: importSessions.length,
|
|
1653
1961
|
alreadyProcessed: skippedCount,
|
|
1654
1962
|
pending: pendingItems.length,
|
|
1963
|
+
agents: Array.from(agentGroups.keys()),
|
|
1964
|
+
concurrency,
|
|
1655
1965
|
});
|
|
1656
1966
|
send("progress", { processed: 0, total: pendingItems.length });
|
|
1657
1967
|
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
const { sessionKey, action } = pendingItems[i];
|
|
1661
|
-
this.ppState.processed = i + 1;
|
|
1662
|
-
|
|
1663
|
-
send("item", {
|
|
1664
|
-
index: i + 1,
|
|
1665
|
-
total: pendingItems.length,
|
|
1666
|
-
session: sessionKey,
|
|
1667
|
-
step: "processing",
|
|
1668
|
-
action,
|
|
1669
|
-
});
|
|
1968
|
+
let globalIdx = 0;
|
|
1969
|
+
const incIdx = () => ++globalIdx;
|
|
1670
1970
|
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
});
|
|
1687
|
-
} else {
|
|
1688
|
-
send("item", {
|
|
1689
|
-
index: i + 1,
|
|
1690
|
-
total: pendingItems.length,
|
|
1691
|
-
session: sessionKey,
|
|
1692
|
-
step: "done",
|
|
1693
|
-
taskTitle: "(no chunks)",
|
|
1694
|
-
});
|
|
1971
|
+
// Process one agent's sessions sequentially
|
|
1972
|
+
const processAgent = async (agentOwner: string, items: PendingItem[]) => {
|
|
1973
|
+
const taskProcessor = new TaskProcessor(this.store, ctx);
|
|
1974
|
+
let skillEvolver: SkillEvolver | null = null;
|
|
1975
|
+
|
|
1976
|
+
if (enableSkills) {
|
|
1977
|
+
const recallEngine = new RecallEngine(this.store, this.embedder, ctx);
|
|
1978
|
+
skillEvolver = new SkillEvolver(this.store, recallEngine, ctx);
|
|
1979
|
+
taskProcessor.onTaskCompleted(async (task) => {
|
|
1980
|
+
try {
|
|
1981
|
+
await skillEvolver!.onTaskCompleted(task);
|
|
1982
|
+
this.ppState.skillsCreated++;
|
|
1983
|
+
send("skill", { taskId: task.id, title: task.title, agent: agentOwner });
|
|
1984
|
+
} catch (err) {
|
|
1985
|
+
this.log.warn(`Postprocess skill evolution error (${agentOwner}): ${err}`);
|
|
1695
1986
|
}
|
|
1696
|
-
}
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1987
|
+
});
|
|
1988
|
+
}
|
|
1989
|
+
|
|
1990
|
+
for (const { sessionKey, action } of items) {
|
|
1991
|
+
if (this.ppAbort) break;
|
|
1992
|
+
const idx = incIdx();
|
|
1993
|
+
this.ppState.processed = globalIdx;
|
|
1994
|
+
|
|
1995
|
+
send("item", {
|
|
1996
|
+
index: idx,
|
|
1997
|
+
total: pendingItems.length,
|
|
1998
|
+
session: sessionKey,
|
|
1999
|
+
agent: agentOwner,
|
|
2000
|
+
step: "processing",
|
|
2001
|
+
action,
|
|
2002
|
+
});
|
|
2003
|
+
|
|
2004
|
+
try {
|
|
2005
|
+
if (action === "full") {
|
|
2006
|
+
await taskProcessor.onChunksIngested(sessionKey, Date.now());
|
|
2007
|
+
const activeTask = this.store.getActiveTask(sessionKey);
|
|
2008
|
+
if (activeTask) {
|
|
2009
|
+
await taskProcessor.finalizeTask(activeTask);
|
|
2010
|
+
const finalized = this.store.getTask(activeTask.id);
|
|
2011
|
+
this.ppState.tasksCreated++;
|
|
2012
|
+
send("item", {
|
|
2013
|
+
index: idx, total: pendingItems.length, session: sessionKey, agent: agentOwner,
|
|
2014
|
+
step: "done", taskTitle: finalized?.title || "", taskStatus: finalized?.status || "",
|
|
2015
|
+
});
|
|
2016
|
+
} else {
|
|
2017
|
+
send("item", {
|
|
2018
|
+
index: idx, total: pendingItems.length, session: sessionKey, agent: agentOwner,
|
|
2019
|
+
step: "done", taskTitle: "(no chunks)",
|
|
2020
|
+
});
|
|
2021
|
+
}
|
|
2022
|
+
} else if (action === "skill-only" && skillEvolver) {
|
|
2023
|
+
const completedTasks = this.store.getCompletedTasksForSession(sessionKey);
|
|
2024
|
+
let skillGenerated = false;
|
|
2025
|
+
for (const task of completedTasks) {
|
|
2026
|
+
if (this.ppAbort) break;
|
|
2027
|
+
try {
|
|
2028
|
+
await skillEvolver.onTaskCompleted(task);
|
|
2029
|
+
this.ppState.skillsCreated++;
|
|
2030
|
+
skillGenerated = true;
|
|
2031
|
+
send("skill", { taskId: task.id, title: task.title, agent: agentOwner });
|
|
2032
|
+
} catch (err) {
|
|
2033
|
+
this.log.warn(`Skill evolution error (${agentOwner}) task=${task.id}: ${err}`);
|
|
2034
|
+
}
|
|
1708
2035
|
}
|
|
2036
|
+
send("item", {
|
|
2037
|
+
index: idx, total: pendingItems.length, session: sessionKey, agent: agentOwner,
|
|
2038
|
+
step: "done", taskTitle: completedTasks[0]?.title || sessionKey, action: "skill-only", skillGenerated,
|
|
2039
|
+
});
|
|
1709
2040
|
}
|
|
2041
|
+
} catch (err) {
|
|
2042
|
+
this.ppState.errors++;
|
|
2043
|
+
this.log.warn(`Postprocess error (${agentOwner}) ${sessionKey}: ${err}`);
|
|
1710
2044
|
send("item", {
|
|
1711
|
-
index:
|
|
1712
|
-
|
|
1713
|
-
session: sessionKey,
|
|
1714
|
-
step: "done",
|
|
1715
|
-
taskTitle: completedTasks[0]?.title || sessionKey,
|
|
1716
|
-
action: "skill-only",
|
|
1717
|
-
skillGenerated,
|
|
2045
|
+
index: idx, total: pendingItems.length, session: sessionKey, agent: agentOwner,
|
|
2046
|
+
step: "error", error: String(err).slice(0, 200),
|
|
1718
2047
|
});
|
|
1719
2048
|
}
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
this.log.warn(`Postprocess error for ${sessionKey}: ${err}`);
|
|
1723
|
-
send("item", {
|
|
1724
|
-
index: i + 1,
|
|
1725
|
-
total: pendingItems.length,
|
|
1726
|
-
session: sessionKey,
|
|
1727
|
-
step: "error",
|
|
1728
|
-
error: String(err).slice(0, 200),
|
|
1729
|
-
});
|
|
2049
|
+
|
|
2050
|
+
send("progress", { processed: globalIdx, total: pendingItems.length });
|
|
1730
2051
|
}
|
|
2052
|
+
};
|
|
1731
2053
|
|
|
1732
|
-
|
|
2054
|
+
// Execute agents with concurrency control
|
|
2055
|
+
const agentEntries = Array.from(agentGroups.entries());
|
|
2056
|
+
if (concurrency <= 1 || agentEntries.length <= 1) {
|
|
2057
|
+
for (const [agentOwner, items] of agentEntries) {
|
|
2058
|
+
if (this.ppAbort) break;
|
|
2059
|
+
await processAgent(agentOwner, items);
|
|
2060
|
+
}
|
|
2061
|
+
} else {
|
|
2062
|
+
let cursor = 0;
|
|
2063
|
+
while (cursor < agentEntries.length && !this.ppAbort) {
|
|
2064
|
+
const batch: Promise<void>[] = [];
|
|
2065
|
+
while (batch.length < concurrency && cursor < agentEntries.length) {
|
|
2066
|
+
const [agentOwner, items] = agentEntries[cursor++];
|
|
2067
|
+
batch.push(processAgent(agentOwner, items));
|
|
2068
|
+
}
|
|
2069
|
+
await Promise.all(batch);
|
|
2070
|
+
}
|
|
1733
2071
|
}
|
|
1734
2072
|
}
|
|
1735
2073
|
|