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