@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.
Files changed (106) hide show
  1. package/README.md +239 -22
  2. package/dist/capture/index.d.ts +1 -1
  3. package/dist/capture/index.d.ts.map +1 -1
  4. package/dist/capture/index.js +33 -8
  5. package/dist/capture/index.js.map +1 -1
  6. package/dist/index.d.ts +1 -1
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +2 -1
  9. package/dist/index.js.map +1 -1
  10. package/dist/ingest/providers/anthropic.d.ts.map +1 -1
  11. package/dist/ingest/providers/anthropic.js +22 -8
  12. package/dist/ingest/providers/anthropic.js.map +1 -1
  13. package/dist/ingest/providers/bedrock.d.ts.map +1 -1
  14. package/dist/ingest/providers/bedrock.js +22 -8
  15. package/dist/ingest/providers/bedrock.js.map +1 -1
  16. package/dist/ingest/providers/gemini.d.ts.map +1 -1
  17. package/dist/ingest/providers/gemini.js +22 -8
  18. package/dist/ingest/providers/gemini.js.map +1 -1
  19. package/dist/ingest/providers/index.d.ts +13 -18
  20. package/dist/ingest/providers/index.d.ts.map +1 -1
  21. package/dist/ingest/providers/index.js +213 -139
  22. package/dist/ingest/providers/index.js.map +1 -1
  23. package/dist/ingest/providers/openai.d.ts +1 -1
  24. package/dist/ingest/providers/openai.d.ts.map +1 -1
  25. package/dist/ingest/providers/openai.js +37 -17
  26. package/dist/ingest/providers/openai.js.map +1 -1
  27. package/dist/ingest/task-processor.d.ts +28 -3
  28. package/dist/ingest/task-processor.d.ts.map +1 -1
  29. package/dist/ingest/task-processor.js +166 -67
  30. package/dist/ingest/task-processor.js.map +1 -1
  31. package/dist/ingest/worker.d.ts.map +1 -1
  32. package/dist/ingest/worker.js +97 -75
  33. package/dist/ingest/worker.js.map +1 -1
  34. package/dist/shared/llm-call.d.ts +26 -0
  35. package/dist/shared/llm-call.d.ts.map +1 -0
  36. package/dist/shared/llm-call.js +163 -0
  37. package/dist/shared/llm-call.js.map +1 -0
  38. package/dist/skill/evaluator.d.ts +0 -3
  39. package/dist/skill/evaluator.d.ts.map +1 -1
  40. package/dist/skill/evaluator.js +34 -59
  41. package/dist/skill/evaluator.js.map +1 -1
  42. package/dist/skill/evolver.d.ts +22 -1
  43. package/dist/skill/evolver.d.ts.map +1 -1
  44. package/dist/skill/evolver.js +191 -32
  45. package/dist/skill/evolver.js.map +1 -1
  46. package/dist/skill/generator.d.ts +0 -3
  47. package/dist/skill/generator.d.ts.map +1 -1
  48. package/dist/skill/generator.js +15 -50
  49. package/dist/skill/generator.js.map +1 -1
  50. package/dist/skill/upgrader.d.ts +0 -2
  51. package/dist/skill/upgrader.d.ts.map +1 -1
  52. package/dist/skill/upgrader.js +4 -39
  53. package/dist/skill/upgrader.js.map +1 -1
  54. package/dist/skill/validator.d.ts +0 -2
  55. package/dist/skill/validator.d.ts.map +1 -1
  56. package/dist/skill/validator.js +14 -44
  57. package/dist/skill/validator.js.map +1 -1
  58. package/dist/storage/sqlite.d.ts +13 -2
  59. package/dist/storage/sqlite.d.ts.map +1 -1
  60. package/dist/storage/sqlite.js +92 -15
  61. package/dist/storage/sqlite.js.map +1 -1
  62. package/dist/tools/memory-get.d.ts.map +1 -1
  63. package/dist/tools/memory-get.js +5 -1
  64. package/dist/tools/memory-get.js.map +1 -1
  65. package/dist/tools/memory-search.d.ts.map +1 -1
  66. package/dist/tools/memory-search.js +5 -0
  67. package/dist/tools/memory-search.js.map +1 -1
  68. package/dist/tools/memory-timeline.d.ts.map +1 -1
  69. package/dist/tools/memory-timeline.js +11 -2
  70. package/dist/tools/memory-timeline.js.map +1 -1
  71. package/dist/types.d.ts +2 -1
  72. package/dist/types.d.ts.map +1 -1
  73. package/dist/types.js +1 -1
  74. package/dist/types.js.map +1 -1
  75. package/dist/viewer/html.d.ts +1 -1
  76. package/dist/viewer/html.d.ts.map +1 -1
  77. package/dist/viewer/html.js +380 -26
  78. package/dist/viewer/html.js.map +1 -1
  79. package/dist/viewer/server.d.ts +9 -0
  80. package/dist/viewer/server.d.ts.map +1 -1
  81. package/dist/viewer/server.js +549 -184
  82. package/dist/viewer/server.js.map +1 -1
  83. package/index.ts +9 -3
  84. package/package.json +2 -1
  85. package/src/capture/index.ts +39 -10
  86. package/src/index.ts +3 -2
  87. package/src/ingest/providers/anthropic.ts +22 -8
  88. package/src/ingest/providers/bedrock.ts +22 -8
  89. package/src/ingest/providers/gemini.ts +22 -8
  90. package/src/ingest/providers/index.ts +192 -142
  91. package/src/ingest/providers/openai.ts +37 -17
  92. package/src/ingest/task-processor.ts +183 -65
  93. package/src/ingest/worker.ts +98 -77
  94. package/src/shared/llm-call.ts +144 -0
  95. package/src/skill/evaluator.ts +35 -64
  96. package/src/skill/evolver.ts +201 -33
  97. package/src/skill/generator.ts +16 -59
  98. package/src/skill/upgrader.ts +5 -43
  99. package/src/skill/validator.ts +15 -47
  100. package/src/storage/sqlite.ts +107 -15
  101. package/src/tools/memory-get.ts +6 -1
  102. package/src/tools/memory-search.ts +6 -0
  103. package/src/tools/memory-timeline.ts +13 -1
  104. package/src/types.ts +2 -1
  105. package/src/viewer/html.ts +380 -26
  106. package/src/viewer/server.ts +535 -197
@@ -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 if (p === "/api/skills" && req.method === "GET") this.serveSkills(res, url);
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 items = tasks.map((t) => ({
398
- id: t.id,
399
- sessionKey: t.sessionKey,
400
- title: t.title,
401
- summary: t.summary ? (t.summary.length > 300 ? t.summary.slice(0, 297) + "..." : t.summary) : "",
402
- status: t.status,
403
- startedAt: t.startedAt,
404
- endedAt: t.endedAt,
405
- chunkCount: this.store.countChunksByTask(t.id),
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
- const hitIds = new Set(hits.filter(h => h.score > 0.3).map(h => h.chunkId));
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: merged,
589
+ results,
573
590
  query: q,
574
591
  vectorCount: vectorResults.length,
575
592
  ftsCount: ftsResults.length,
576
- total: merged.length,
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
- this.log.error(`handleSkillVisibility error: skillId=${skillId}, body=${body}, err=${err}`);
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 || "", kind: data.kind || "paragraph",
754
- summary: data.summary || data.content?.slice(0, 100) || "",
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
- if (fs.existsSync(skillsStoreDir)) {
816
- fs.rmSync(skillsStoreDir, { recursive: true });
817
- fs.mkdirSync(skillsStoreDir, { recursive: true });
818
- this.log.info("Cleared skills-store directory");
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
- this.log.warn(`Failed to clear skills-store: ${err}`);
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 topSimilar = findTopSimilar(this.store, embedding, 0.85, 3, this.log);
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 sessionsDir = path.join(ocHome, "agents", "main", "sessions");
1321
- if (fs.existsSync(sessionsDir)) {
1322
- const jsonlFiles = fs.readdirSync(sessionsDir).filter(f => f.includes(".jsonl")).sort();
1323
- send("phase", { phase: "sessions", files: jsonlFiles.length });
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
- let globalMsgIdx = 0;
1326
- let totalMsgs = 0;
1327
- for (const f of jsonlFiles) {
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(path.join(sessionsDir, f), "utf-8");
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
- for (const file of jsonlFiles) {
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
- globalMsgIdx++;
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
- index: globalMsgIdx,
1395
- total: totalMsgs,
1396
- status: "skipped",
1397
- preview: content.slice(0, 120),
1398
- source: file,
1399
- role: msgRole,
1400
- reason: "duplicate",
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 topSimilar = findTopSimilar(this.store, embedding, 0.85, 3, this.log);
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, idx) => {
1747
+ const candidates = topSimilar.map((s, i) => {
1422
1748
  const chunk = this.store.getChunk(s.chunkId);
1423
- return { index: idx + 1, summary: chunk?.summary ?? "", chunkId: s.chunkId };
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
- const [newEmb] = await this.embedder.embed([dedupResult.mergedSummary]);
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
- sessionKey,
1458
- turnId: `import-${sessionId}-${globalMsgIdx}`,
1459
- seq: 0,
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
- for (let i = 0; i < pendingItems.length; i++) {
1659
- if (this.ppAbort) break;
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
- try {
1672
- if (action === "full") {
1673
- await taskProcessor.onChunksIngested(sessionKey, Date.now());
1674
- const activeTask = this.store.getActiveTask(sessionKey);
1675
- if (activeTask) {
1676
- await taskProcessor.finalizeTask(activeTask);
1677
- const finalized = this.store.getTask(activeTask.id);
1678
- this.ppState.tasksCreated++;
1679
- send("item", {
1680
- index: i + 1,
1681
- total: pendingItems.length,
1682
- session: sessionKey,
1683
- step: "done",
1684
- taskTitle: finalized?.title || "",
1685
- taskStatus: finalized?.status || "",
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
- } else if (action === "skill-only" && skillEvolver) {
1697
- const completedTasks = this.store.getCompletedTasksForSession(sessionKey);
1698
- let skillGenerated = false;
1699
- for (const task of completedTasks) {
1700
- if (this.ppAbort) break;
1701
- try {
1702
- await skillEvolver.onTaskCompleted(task);
1703
- this.ppState.skillsCreated++;
1704
- skillGenerated = true;
1705
- send("skill", { taskId: task.id, title: task.title });
1706
- } catch (err) {
1707
- this.log.warn(`Skill evolution error for task=${task.id}: ${err}`);
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: i + 1,
1712
- total: pendingItems.length,
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
- } catch (err) {
1721
- this.ppState.errors++;
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
- send("progress", { processed: i + 1, total: pendingItems.length });
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