@memtensor/memos-local-openclaw-plugin 0.1.10 → 0.2.0

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 (49) hide show
  1. package/.env.example +6 -0
  2. package/README.md +90 -25
  3. package/dist/config.d.ts.map +1 -1
  4. package/dist/config.js +8 -0
  5. package/dist/config.js.map +1 -1
  6. package/dist/embedding/local.d.ts.map +1 -1
  7. package/dist/embedding/local.js +3 -2
  8. package/dist/embedding/local.js.map +1 -1
  9. package/dist/skill/evaluator.js +1 -1
  10. package/dist/skill/evaluator.js.map +1 -1
  11. package/dist/skill/generator.js +1 -1
  12. package/dist/skill/generator.js.map +1 -1
  13. package/dist/skill/upgrader.js +1 -1
  14. package/dist/skill/upgrader.js.map +1 -1
  15. package/dist/skill/validator.js +1 -1
  16. package/dist/skill/validator.js.map +1 -1
  17. package/dist/storage/sqlite.d.ts +4 -0
  18. package/dist/storage/sqlite.d.ts.map +1 -1
  19. package/dist/storage/sqlite.js +16 -0
  20. package/dist/storage/sqlite.js.map +1 -1
  21. package/dist/telemetry.d.ts +37 -0
  22. package/dist/telemetry.d.ts.map +1 -0
  23. package/dist/telemetry.js +179 -0
  24. package/dist/telemetry.js.map +1 -0
  25. package/dist/types.d.ts +6 -1
  26. package/dist/types.d.ts.map +1 -1
  27. package/dist/types.js.map +1 -1
  28. package/dist/viewer/html.d.ts +1 -1
  29. package/dist/viewer/html.d.ts.map +1 -1
  30. package/dist/viewer/html.js +828 -52
  31. package/dist/viewer/html.js.map +1 -1
  32. package/dist/viewer/server.d.ts +25 -1
  33. package/dist/viewer/server.d.ts.map +1 -1
  34. package/dist/viewer/server.js +807 -0
  35. package/dist/viewer/server.js.map +1 -1
  36. package/index.ts +31 -3
  37. package/openclaw.plugin.json +3 -3
  38. package/package.json +4 -3
  39. package/src/config.ts +11 -0
  40. package/src/embedding/local.ts +3 -2
  41. package/src/skill/evaluator.ts +1 -1
  42. package/src/skill/generator.ts +1 -1
  43. package/src/skill/upgrader.ts +1 -1
  44. package/src/skill/validator.ts +1 -1
  45. package/src/storage/sqlite.ts +26 -0
  46. package/src/telemetry.ts +160 -0
  47. package/src/types.ts +7 -1
  48. package/src/viewer/html.ts +828 -52
  49. package/src/viewer/server.ts +818 -1
@@ -9,8 +9,15 @@ const node_crypto_1 = __importDefault(require("node:crypto"));
9
9
  const node_child_process_1 = require("node:child_process");
10
10
  const node_fs_1 = __importDefault(require("node:fs"));
11
11
  const node_path_1 = __importDefault(require("node:path"));
12
+ const node_readline_1 = __importDefault(require("node:readline"));
13
+ const providers_1 = require("../ingest/providers");
14
+ const dedup_1 = require("../ingest/dedup");
12
15
  const vector_1 = require("../storage/vector");
16
+ const task_processor_1 = require("../ingest/task-processor");
17
+ const engine_1 = require("../recall/engine");
18
+ const evolver_1 = require("../skill/evolver");
13
19
  const html_1 = require("./html");
20
+ const uuid_1 = require("uuid");
14
21
  class ViewerServer {
15
22
  server = null;
16
23
  store;
@@ -20,14 +27,24 @@ class ViewerServer {
20
27
  dataDir;
21
28
  authFile;
22
29
  auth;
30
+ ctx;
23
31
  static SESSION_TTL = 24 * 60 * 60 * 1000;
24
32
  resetToken;
33
+ migrationRunning = false;
34
+ migrationAbort = false;
35
+ migrationState = { phase: "", stored: 0, skipped: 0, merged: 0, errors: 0, processed: 0, total: 0, lastItem: null, done: false, stopped: false };
36
+ migrationSSEClients = [];
37
+ ppRunning = false;
38
+ ppAbort = false;
39
+ ppState = { running: false, done: false, stopped: false, processed: 0, total: 0, tasksCreated: 0, skillsCreated: 0, errors: 0 };
40
+ ppSSEClients = [];
25
41
  constructor(opts) {
26
42
  this.store = opts.store;
27
43
  this.embedder = opts.embedder;
28
44
  this.port = opts.port;
29
45
  this.log = opts.log;
30
46
  this.dataDir = opts.dataDir;
47
+ this.ctx = opts.ctx;
31
48
  this.authFile = node_path_1.default.join(opts.dataDir, "viewer-auth.json");
32
49
  this.auth = { passwordHash: null, sessions: new Map() };
33
50
  this.resetToken = node_crypto_1.default.randomBytes(16).toString("hex");
@@ -182,6 +199,24 @@ class ViewerServer {
182
199
  this.handleSaveConfig(req, res);
183
200
  else if (p === "/api/auth/logout" && req.method === "POST")
184
201
  this.handleLogout(req, res);
202
+ else if (p === "/api/migrate/scan" && req.method === "GET")
203
+ this.handleMigrateScan(res);
204
+ else if (p === "/api/migrate/start" && req.method === "POST")
205
+ this.handleMigrateStart(req, res);
206
+ else if (p === "/api/migrate/status" && req.method === "GET")
207
+ this.handleMigrateStatus(res);
208
+ else if (p === "/api/migrate/stream" && req.method === "GET")
209
+ this.handleMigrateStream(res);
210
+ else if (p === "/api/migrate/stop" && req.method === "POST")
211
+ this.handleMigrateStop(res);
212
+ else if (p === "/api/migrate/postprocess" && req.method === "POST")
213
+ this.handlePostprocess(req, res);
214
+ else if (p === "/api/migrate/postprocess/stream" && req.method === "GET")
215
+ this.handlePostprocessStream(res);
216
+ else if (p === "/api/migrate/postprocess/stop" && req.method === "POST")
217
+ this.handlePostprocessStop(res);
218
+ else if (p === "/api/migrate/postprocess/status" && req.method === "GET")
219
+ this.handlePostprocessStatus(res);
185
220
  else {
186
221
  res.writeHead(404, { "Content-Type": "application/json" });
187
222
  res.end(JSON.stringify({ error: "not found" }));
@@ -774,6 +809,8 @@ class ViewerServer {
774
809
  config.skillEvolution = newCfg.skillEvolution;
775
810
  if (newCfg.viewerPort)
776
811
  config.viewerPort = newCfg.viewerPort;
812
+ if (newCfg.telemetry !== undefined)
813
+ config.telemetry = newCfg.telemetry;
777
814
  node_fs_1.default.mkdirSync(node_path_1.default.dirname(cfgPath), { recursive: true });
778
815
  node_fs_1.default.writeFileSync(cfgPath, JSON.stringify(raw, null, 2), "utf-8");
779
816
  this.log.info("Plugin config updated via Viewer");
@@ -799,6 +836,776 @@ class ViewerServer {
799
836
  const tools = this.store.getApiLogToolNames();
800
837
  this.jsonResponse(res, { tools });
801
838
  }
839
+ // ─── Migration: scan OpenClaw built-in memory ───
840
+ getOpenClawHome() {
841
+ const home = process.env.HOME || process.env.USERPROFILE || "";
842
+ return node_path_1.default.join(home, ".openclaw");
843
+ }
844
+ handleMigrateScan(res) {
845
+ try {
846
+ const ocHome = this.getOpenClawHome();
847
+ const memoryDir = node_path_1.default.join(ocHome, "memory");
848
+ const sessionsDir = node_path_1.default.join(ocHome, "agents", "main", "sessions");
849
+ const sqliteFiles = [];
850
+ if (node_fs_1.default.existsSync(memoryDir)) {
851
+ for (const f of node_fs_1.default.readdirSync(memoryDir)) {
852
+ if (f.endsWith(".sqlite")) {
853
+ try {
854
+ const Database = require("better-sqlite3");
855
+ const db = new Database(node_path_1.default.join(memoryDir, f), { readonly: true });
856
+ const row = db.prepare("SELECT COUNT(*) as cnt FROM chunks").get();
857
+ sqliteFiles.push({ file: f, chunks: row.cnt });
858
+ db.close();
859
+ }
860
+ catch { /* skip unreadable */ }
861
+ }
862
+ }
863
+ }
864
+ let sessionCount = 0;
865
+ let messageCount = 0;
866
+ if (node_fs_1.default.existsSync(sessionsDir)) {
867
+ const jsonlFiles = node_fs_1.default.readdirSync(sessionsDir).filter(f => f.includes(".jsonl"));
868
+ sessionCount = jsonlFiles.length;
869
+ for (const f of jsonlFiles) {
870
+ try {
871
+ const content = node_fs_1.default.readFileSync(node_path_1.default.join(sessionsDir, f), "utf-8");
872
+ const lines = content.split("\n").filter(l => l.trim());
873
+ for (const line of lines) {
874
+ try {
875
+ const obj = JSON.parse(line);
876
+ if (obj.type === "message") {
877
+ const role = obj.message?.role ?? obj.role;
878
+ if (role === "user" || role === "assistant")
879
+ messageCount++;
880
+ }
881
+ }
882
+ catch { /* skip bad lines */ }
883
+ }
884
+ }
885
+ catch { /* skip unreadable */ }
886
+ }
887
+ }
888
+ const cfgPath = this.getOpenClawConfigPath();
889
+ let hasEmbedding = false;
890
+ let hasSummarizer = false;
891
+ if (node_fs_1.default.existsSync(cfgPath)) {
892
+ try {
893
+ const raw = JSON.parse(node_fs_1.default.readFileSync(cfgPath, "utf-8"));
894
+ const pluginCfg = raw?.plugins?.entries?.["memos-local"]?.config ??
895
+ raw?.plugins?.entries?.["memos-local-openclaw-plugin"]?.config ?? {};
896
+ const emb = pluginCfg.embedding;
897
+ hasEmbedding = !!(emb && emb.provider);
898
+ const sum = pluginCfg.summarizer;
899
+ hasSummarizer = !!(sum && sum.provider);
900
+ }
901
+ catch { /* ignore */ }
902
+ }
903
+ const importedSessions = this.store.getDistinctSessionKeys()
904
+ .filter((sk) => sk.startsWith("openclaw-import-") || sk.startsWith("openclaw-session-"));
905
+ this.jsonResponse(res, {
906
+ sqliteFiles,
907
+ sessions: { count: sessionCount, messages: messageCount },
908
+ totalItems: sqliteFiles.reduce((s, f) => s + f.chunks, 0) + messageCount,
909
+ configReady: hasEmbedding && hasSummarizer,
910
+ hasEmbedding,
911
+ hasSummarizer,
912
+ hasImportedData: importedSessions.length > 0,
913
+ importedSessionCount: importedSessions.length,
914
+ });
915
+ }
916
+ catch (e) {
917
+ this.log.warn(`migrate/scan error: ${e}`);
918
+ res.writeHead(500, { "Content-Type": "application/json" });
919
+ res.end(JSON.stringify({ error: String(e) }));
920
+ }
921
+ }
922
+ // ─── Migration: start import with SSE progress ───
923
+ broadcastSSE(event, data) {
924
+ const msg = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
925
+ this.migrationSSEClients = this.migrationSSEClients.filter(c => {
926
+ try {
927
+ c.write(msg);
928
+ return true;
929
+ }
930
+ catch {
931
+ return false;
932
+ }
933
+ });
934
+ }
935
+ handleMigrateStatus(res) {
936
+ this.jsonResponse(res, {
937
+ running: this.migrationRunning,
938
+ ...this.migrationState,
939
+ });
940
+ }
941
+ handleMigrateStop(res) {
942
+ if (!this.migrationRunning) {
943
+ this.jsonResponse(res, { ok: false, error: "not_running" });
944
+ return;
945
+ }
946
+ this.migrationAbort = true;
947
+ this.jsonResponse(res, { ok: true });
948
+ }
949
+ handleMigrateStream(res) {
950
+ res.writeHead(200, {
951
+ "Content-Type": "text/event-stream",
952
+ "Cache-Control": "no-cache",
953
+ "Connection": "keep-alive",
954
+ "X-Accel-Buffering": "no",
955
+ });
956
+ if (this.migrationRunning) {
957
+ res.write(`event: state\ndata: ${JSON.stringify(this.migrationState)}\n\n`);
958
+ }
959
+ else if (this.migrationState.done) {
960
+ res.write(`event: state\ndata: ${JSON.stringify(this.migrationState)}\n\n`);
961
+ res.write(`event: done\ndata: ${JSON.stringify({ ok: true })}\n\n`);
962
+ res.end();
963
+ return;
964
+ }
965
+ this.migrationSSEClients.push(res);
966
+ res.on("close", () => {
967
+ this.migrationSSEClients = this.migrationSSEClients.filter(c => c !== res);
968
+ });
969
+ }
970
+ handleMigrateStart(req, res) {
971
+ if (this.migrationRunning) {
972
+ res.writeHead(200, {
973
+ "Content-Type": "text/event-stream",
974
+ "Cache-Control": "no-cache",
975
+ "Connection": "keep-alive",
976
+ "X-Accel-Buffering": "no",
977
+ });
978
+ res.write(`event: state\ndata: ${JSON.stringify(this.migrationState)}\n\n`);
979
+ this.migrationSSEClients.push(res);
980
+ res.on("close", () => {
981
+ this.migrationSSEClients = this.migrationSSEClients.filter(c => c !== res);
982
+ });
983
+ return;
984
+ }
985
+ this.readBody(req, (body) => {
986
+ let opts = {};
987
+ try {
988
+ opts = JSON.parse(body);
989
+ }
990
+ catch { /* defaults */ }
991
+ res.writeHead(200, {
992
+ "Content-Type": "text/event-stream",
993
+ "Cache-Control": "no-cache",
994
+ "Connection": "keep-alive",
995
+ "X-Accel-Buffering": "no",
996
+ });
997
+ this.migrationSSEClients.push(res);
998
+ res.on("close", () => {
999
+ this.migrationSSEClients = this.migrationSSEClients.filter(c => c !== res);
1000
+ });
1001
+ this.migrationAbort = false;
1002
+ this.migrationState = { phase: "", stored: 0, skipped: 0, merged: 0, errors: 0, processed: 0, total: 0, lastItem: null, done: false, stopped: false };
1003
+ const send = (event, data) => {
1004
+ if (event === "item") {
1005
+ const d = data;
1006
+ if (d.status === "stored")
1007
+ this.migrationState.stored++;
1008
+ else if (d.status === "skipped" || d.status === "duplicate")
1009
+ this.migrationState.skipped++;
1010
+ else if (d.status === "merged")
1011
+ this.migrationState.merged++;
1012
+ else if (d.status === "error")
1013
+ this.migrationState.errors++;
1014
+ this.migrationState.processed = d.index ?? this.migrationState.processed + 1;
1015
+ this.migrationState.total = d.total ?? this.migrationState.total;
1016
+ this.migrationState.lastItem = d;
1017
+ }
1018
+ else if (event === "phase") {
1019
+ this.migrationState.phase = data.phase;
1020
+ }
1021
+ else if (event === "progress") {
1022
+ this.migrationState.total = data.total ?? this.migrationState.total;
1023
+ }
1024
+ this.broadcastSSE(event, data);
1025
+ };
1026
+ this.migrationRunning = true;
1027
+ this.runMigration(send, opts.sources).finally(() => {
1028
+ this.migrationRunning = false;
1029
+ this.migrationState.done = true;
1030
+ if (this.migrationAbort) {
1031
+ this.migrationState.stopped = true;
1032
+ this.broadcastSSE("stopped", { ok: true, ...this.migrationState });
1033
+ }
1034
+ else {
1035
+ this.broadcastSSE("done", { ok: true });
1036
+ }
1037
+ for (const c of this.migrationSSEClients) {
1038
+ try {
1039
+ c.end();
1040
+ }
1041
+ catch { /* ignore */ }
1042
+ }
1043
+ this.migrationSSEClients = [];
1044
+ this.migrationAbort = false;
1045
+ });
1046
+ });
1047
+ }
1048
+ async runMigration(send, sources) {
1049
+ const ocHome = this.getOpenClawHome();
1050
+ const importSqlite = !sources || sources.includes("sqlite");
1051
+ const importSessions = !sources || sources.includes("sessions");
1052
+ let totalProcessed = 0;
1053
+ let totalStored = 0;
1054
+ let totalSkipped = 0;
1055
+ let totalErrors = 0;
1056
+ const cfgPath = this.getOpenClawConfigPath();
1057
+ let summarizerCfg;
1058
+ try {
1059
+ const raw = JSON.parse(node_fs_1.default.readFileSync(cfgPath, "utf-8"));
1060
+ const pluginCfg = raw?.plugins?.entries?.["memos-local"]?.config ??
1061
+ raw?.plugins?.entries?.["memos-local-openclaw-plugin"]?.config ?? {};
1062
+ summarizerCfg = pluginCfg.summarizer;
1063
+ }
1064
+ catch { /* no config */ }
1065
+ const summarizer = new providers_1.Summarizer(summarizerCfg, this.log);
1066
+ // Phase 1: Import SQLite memory chunks
1067
+ if (importSqlite) {
1068
+ const memoryDir = node_path_1.default.join(ocHome, "memory");
1069
+ if (node_fs_1.default.existsSync(memoryDir)) {
1070
+ const files = node_fs_1.default.readdirSync(memoryDir).filter(f => f.endsWith(".sqlite"));
1071
+ for (const file of files) {
1072
+ if (this.migrationAbort)
1073
+ break;
1074
+ send("phase", { phase: "sqlite", file });
1075
+ try {
1076
+ const Database = require("better-sqlite3");
1077
+ const db = new Database(node_path_1.default.join(memoryDir, file), { readonly: true });
1078
+ const rows = db.prepare("SELECT id, path, text, updated_at FROM chunks ORDER BY updated_at ASC").all();
1079
+ db.close();
1080
+ const agentId = file.replace(".sqlite", "");
1081
+ send("progress", { total: rows.length, processed: 0, phase: "sqlite", file });
1082
+ for (let i = 0; i < rows.length; i++) {
1083
+ if (this.migrationAbort)
1084
+ break;
1085
+ const row = rows[i];
1086
+ totalProcessed++;
1087
+ const contentHash = node_crypto_1.default.createHash("sha256").update(row.text).digest("hex");
1088
+ if (this.store.chunkExistsByContent(`openclaw-import-${agentId}`, "assistant", row.text)) {
1089
+ totalSkipped++;
1090
+ send("item", {
1091
+ index: i + 1,
1092
+ total: rows.length,
1093
+ status: "skipped",
1094
+ preview: row.text.slice(0, 120),
1095
+ source: file,
1096
+ reason: "duplicate",
1097
+ });
1098
+ continue;
1099
+ }
1100
+ try {
1101
+ const summary = await summarizer.summarize(row.text);
1102
+ let embedding = null;
1103
+ try {
1104
+ [embedding] = await this.embedder.embed([summary]);
1105
+ }
1106
+ catch (err) {
1107
+ this.log.warn(`Migration embed failed: ${err}`);
1108
+ }
1109
+ let dedupStatus = "active";
1110
+ let dedupTarget = null;
1111
+ let dedupReason = null;
1112
+ if (embedding) {
1113
+ const topSimilar = (0, dedup_1.findTopSimilar)(this.store, embedding, 0.85, 3, this.log);
1114
+ if (topSimilar.length > 0) {
1115
+ const candidates = topSimilar.map((s, idx) => {
1116
+ const chunk = this.store.getChunk(s.chunkId);
1117
+ return { index: idx + 1, summary: chunk?.summary ?? "", chunkId: s.chunkId };
1118
+ }).filter(c => c.summary);
1119
+ if (candidates.length > 0) {
1120
+ const dedupResult = await summarizer.judgeDedup(summary, candidates);
1121
+ if (dedupResult?.action === "DUPLICATE" && dedupResult.targetIndex) {
1122
+ const targetId = candidates[dedupResult.targetIndex - 1]?.chunkId;
1123
+ if (targetId) {
1124
+ dedupStatus = "duplicate";
1125
+ dedupTarget = targetId;
1126
+ dedupReason = dedupResult.reason;
1127
+ }
1128
+ }
1129
+ else if (dedupResult?.action === "UPDATE" && dedupResult.targetIndex && dedupResult.mergedSummary) {
1130
+ const targetId = candidates[dedupResult.targetIndex - 1]?.chunkId;
1131
+ if (targetId) {
1132
+ this.store.updateChunkSummaryAndContent(targetId, dedupResult.mergedSummary, row.text);
1133
+ try {
1134
+ const [newEmb] = await this.embedder.embed([dedupResult.mergedSummary]);
1135
+ if (newEmb)
1136
+ this.store.upsertEmbedding(targetId, newEmb);
1137
+ }
1138
+ catch { /* best-effort */ }
1139
+ dedupStatus = "merged";
1140
+ dedupTarget = targetId;
1141
+ dedupReason = dedupResult.reason;
1142
+ }
1143
+ }
1144
+ }
1145
+ }
1146
+ }
1147
+ const chunkId = (0, uuid_1.v4)();
1148
+ const chunk = {
1149
+ id: chunkId,
1150
+ sessionKey: `openclaw-import-${agentId}`,
1151
+ turnId: `import-${row.id}`,
1152
+ seq: 0,
1153
+ role: "assistant",
1154
+ content: row.text,
1155
+ kind: "paragraph",
1156
+ summary,
1157
+ embedding: null,
1158
+ taskId: null,
1159
+ skillId: null,
1160
+ dedupStatus,
1161
+ dedupTarget,
1162
+ dedupReason,
1163
+ mergeCount: 0,
1164
+ lastHitAt: null,
1165
+ mergeHistory: "[]",
1166
+ createdAt: row.updated_at * 1000,
1167
+ updatedAt: row.updated_at * 1000,
1168
+ };
1169
+ this.store.insertChunk(chunk);
1170
+ if (embedding && dedupStatus === "active") {
1171
+ this.store.upsertEmbedding(chunkId, embedding);
1172
+ }
1173
+ totalStored++;
1174
+ send("item", {
1175
+ index: i + 1,
1176
+ total: rows.length,
1177
+ status: dedupStatus === "active" ? "stored" : dedupStatus,
1178
+ preview: row.text.slice(0, 120),
1179
+ summary: summary.slice(0, 80),
1180
+ source: file,
1181
+ });
1182
+ }
1183
+ catch (err) {
1184
+ totalErrors++;
1185
+ send("item", {
1186
+ index: i + 1,
1187
+ total: rows.length,
1188
+ status: "error",
1189
+ preview: row.text.slice(0, 120),
1190
+ source: file,
1191
+ error: String(err).slice(0, 200),
1192
+ });
1193
+ }
1194
+ }
1195
+ }
1196
+ catch (err) {
1197
+ send("error", { file, error: String(err) });
1198
+ totalErrors++;
1199
+ }
1200
+ }
1201
+ }
1202
+ }
1203
+ // Phase 2: Import session JSONL files
1204
+ if (importSessions) {
1205
+ const sessionsDir = node_path_1.default.join(ocHome, "agents", "main", "sessions");
1206
+ if (node_fs_1.default.existsSync(sessionsDir)) {
1207
+ const jsonlFiles = node_fs_1.default.readdirSync(sessionsDir).filter(f => f.includes(".jsonl")).sort();
1208
+ send("phase", { phase: "sessions", files: jsonlFiles.length });
1209
+ let globalMsgIdx = 0;
1210
+ let totalMsgs = 0;
1211
+ for (const f of jsonlFiles) {
1212
+ try {
1213
+ const raw = node_fs_1.default.readFileSync(node_path_1.default.join(sessionsDir, f), "utf-8");
1214
+ for (const line of raw.split("\n")) {
1215
+ if (!line.trim())
1216
+ continue;
1217
+ try {
1218
+ const obj = JSON.parse(line);
1219
+ if (obj.type === "message") {
1220
+ const role = obj.message?.role ?? obj.role;
1221
+ if (role === "user" || role === "assistant")
1222
+ totalMsgs++;
1223
+ }
1224
+ }
1225
+ catch { /* skip */ }
1226
+ }
1227
+ }
1228
+ catch { /* skip */ }
1229
+ }
1230
+ for (const file of jsonlFiles) {
1231
+ if (this.migrationAbort)
1232
+ break;
1233
+ const sessionId = file.replace(/\.jsonl.*$/, "");
1234
+ const filePath = node_path_1.default.join(sessionsDir, file);
1235
+ send("progress", { total: totalMsgs, processed: globalMsgIdx, phase: "sessions", file });
1236
+ try {
1237
+ const fileStream = node_fs_1.default.createReadStream(filePath, { encoding: "utf-8" });
1238
+ const rl = node_readline_1.default.createInterface({ input: fileStream, crlfDelay: Infinity });
1239
+ for await (const line of rl) {
1240
+ if (this.migrationAbort)
1241
+ break;
1242
+ if (!line.trim())
1243
+ continue;
1244
+ let obj;
1245
+ try {
1246
+ obj = JSON.parse(line);
1247
+ }
1248
+ catch {
1249
+ continue;
1250
+ }
1251
+ if (obj.type !== "message")
1252
+ continue;
1253
+ const msgRole = obj.message?.role ?? obj.role;
1254
+ if (msgRole !== "user" && msgRole !== "assistant")
1255
+ continue;
1256
+ const msgContent = obj.message?.content ?? obj.content;
1257
+ let content;
1258
+ if (typeof msgContent === "string") {
1259
+ content = msgContent;
1260
+ }
1261
+ else if (Array.isArray(msgContent)) {
1262
+ content = msgContent
1263
+ .filter((p) => p.type === "text" && p.text)
1264
+ .map((p) => p.text)
1265
+ .join("\n");
1266
+ }
1267
+ else {
1268
+ content = JSON.stringify(msgContent);
1269
+ }
1270
+ if (!content || content.length < 10)
1271
+ continue;
1272
+ globalMsgIdx++;
1273
+ totalProcessed++;
1274
+ const sessionKey = `openclaw-session-${sessionId}`;
1275
+ if (this.store.chunkExistsByContent(sessionKey, msgRole, content)) {
1276
+ totalSkipped++;
1277
+ send("item", {
1278
+ index: globalMsgIdx,
1279
+ total: totalMsgs,
1280
+ status: "skipped",
1281
+ preview: content.slice(0, 120),
1282
+ source: file,
1283
+ role: msgRole,
1284
+ reason: "duplicate",
1285
+ });
1286
+ continue;
1287
+ }
1288
+ try {
1289
+ const summary = await summarizer.summarize(content);
1290
+ let embedding = null;
1291
+ try {
1292
+ [embedding] = await this.embedder.embed([summary]);
1293
+ }
1294
+ catch (err) {
1295
+ this.log.warn(`Migration embed failed: ${err}`);
1296
+ }
1297
+ let dedupStatus = "active";
1298
+ let dedupTarget = null;
1299
+ let dedupReason = null;
1300
+ if (embedding) {
1301
+ const topSimilar = (0, dedup_1.findTopSimilar)(this.store, embedding, 0.85, 3, this.log);
1302
+ if (topSimilar.length > 0) {
1303
+ const candidates = topSimilar.map((s, idx) => {
1304
+ const chunk = this.store.getChunk(s.chunkId);
1305
+ return { index: idx + 1, summary: chunk?.summary ?? "", chunkId: s.chunkId };
1306
+ }).filter(c => c.summary);
1307
+ if (candidates.length > 0) {
1308
+ const dedupResult = await summarizer.judgeDedup(summary, candidates);
1309
+ if (dedupResult?.action === "DUPLICATE" && dedupResult.targetIndex) {
1310
+ const targetId = candidates[dedupResult.targetIndex - 1]?.chunkId;
1311
+ if (targetId) {
1312
+ dedupStatus = "duplicate";
1313
+ dedupTarget = targetId;
1314
+ dedupReason = dedupResult.reason;
1315
+ }
1316
+ }
1317
+ else if (dedupResult?.action === "UPDATE" && dedupResult.targetIndex && dedupResult.mergedSummary) {
1318
+ const targetId = candidates[dedupResult.targetIndex - 1]?.chunkId;
1319
+ if (targetId) {
1320
+ this.store.updateChunkSummaryAndContent(targetId, dedupResult.mergedSummary, content);
1321
+ try {
1322
+ const [newEmb] = await this.embedder.embed([dedupResult.mergedSummary]);
1323
+ if (newEmb)
1324
+ this.store.upsertEmbedding(targetId, newEmb);
1325
+ }
1326
+ catch { /* best-effort */ }
1327
+ dedupStatus = "merged";
1328
+ dedupTarget = targetId;
1329
+ dedupReason = dedupResult.reason;
1330
+ }
1331
+ }
1332
+ }
1333
+ }
1334
+ }
1335
+ const chunkId = (0, uuid_1.v4)();
1336
+ const msgTs = obj.message?.timestamp ?? obj.timestamp;
1337
+ const ts = msgTs ? new Date(msgTs).getTime() : Date.now();
1338
+ const chunk = {
1339
+ id: chunkId,
1340
+ sessionKey,
1341
+ turnId: `import-${sessionId}-${globalMsgIdx}`,
1342
+ seq: 0,
1343
+ role: msgRole,
1344
+ content,
1345
+ kind: "paragraph",
1346
+ summary,
1347
+ embedding: null,
1348
+ taskId: null,
1349
+ skillId: null,
1350
+ dedupStatus,
1351
+ dedupTarget,
1352
+ dedupReason,
1353
+ mergeCount: 0,
1354
+ lastHitAt: null,
1355
+ mergeHistory: "[]",
1356
+ createdAt: ts,
1357
+ updatedAt: ts,
1358
+ };
1359
+ this.store.insertChunk(chunk);
1360
+ if (embedding && dedupStatus === "active") {
1361
+ this.store.upsertEmbedding(chunkId, embedding);
1362
+ }
1363
+ totalStored++;
1364
+ send("item", {
1365
+ index: globalMsgIdx,
1366
+ total: totalMsgs,
1367
+ status: dedupStatus === "active" ? "stored" : dedupStatus,
1368
+ preview: content.slice(0, 120),
1369
+ summary: summary.slice(0, 80),
1370
+ source: file,
1371
+ role: msgRole,
1372
+ });
1373
+ }
1374
+ catch (err) {
1375
+ totalErrors++;
1376
+ send("item", {
1377
+ index: globalMsgIdx,
1378
+ total: totalMsgs,
1379
+ status: "error",
1380
+ preview: content.slice(0, 120),
1381
+ source: file,
1382
+ error: String(err).slice(0, 200),
1383
+ });
1384
+ }
1385
+ }
1386
+ }
1387
+ catch (err) {
1388
+ send("error", { file, error: String(err) });
1389
+ totalErrors++;
1390
+ }
1391
+ }
1392
+ }
1393
+ }
1394
+ send("summary", { totalProcessed, totalStored, totalSkipped, totalErrors });
1395
+ }
1396
+ // ─── Post-processing: independent task/skill generation ───
1397
+ handlePostprocess(req, res) {
1398
+ if (this.ppRunning) {
1399
+ res.writeHead(409, { "Content-Type": "application/json" });
1400
+ res.end(JSON.stringify({ error: "postprocess already running" }));
1401
+ return;
1402
+ }
1403
+ if (!this.ctx) {
1404
+ res.writeHead(500, { "Content-Type": "application/json" });
1405
+ res.end(JSON.stringify({ error: "plugin context not available — please restart the gateway" }));
1406
+ return;
1407
+ }
1408
+ this.readBody(req, (body) => {
1409
+ let opts = {};
1410
+ try {
1411
+ opts = JSON.parse(body);
1412
+ }
1413
+ catch { /* defaults */ }
1414
+ res.writeHead(200, {
1415
+ "Content-Type": "text/event-stream",
1416
+ "Cache-Control": "no-cache",
1417
+ "Connection": "keep-alive",
1418
+ "X-Accel-Buffering": "no",
1419
+ });
1420
+ this.ppSSEClients.push(res);
1421
+ res.on("close", () => { this.ppSSEClients = this.ppSSEClients.filter(c => c !== res); });
1422
+ this.ppAbort = false;
1423
+ this.ppState = { running: true, done: false, stopped: false, processed: 0, total: 0, tasksCreated: 0, skillsCreated: 0, errors: 0 };
1424
+ const send = (event, data) => {
1425
+ this.broadcastPPSSE(event, data);
1426
+ };
1427
+ this.ppRunning = true;
1428
+ this.runPostprocess(send, !!opts.enableTasks, !!opts.enableSkills).finally(() => {
1429
+ this.ppRunning = false;
1430
+ this.ppState.running = false;
1431
+ this.ppState.done = true;
1432
+ if (this.ppAbort) {
1433
+ this.ppState.stopped = true;
1434
+ this.broadcastPPSSE("stopped", { ...this.ppState });
1435
+ }
1436
+ else {
1437
+ this.broadcastPPSSE("done", { ...this.ppState });
1438
+ }
1439
+ for (const c of this.ppSSEClients) {
1440
+ try {
1441
+ c.end();
1442
+ }
1443
+ catch { /* */ }
1444
+ }
1445
+ this.ppSSEClients = [];
1446
+ this.ppAbort = false;
1447
+ });
1448
+ });
1449
+ }
1450
+ handlePostprocessStream(res) {
1451
+ res.writeHead(200, {
1452
+ "Content-Type": "text/event-stream",
1453
+ "Cache-Control": "no-cache",
1454
+ "Connection": "keep-alive",
1455
+ "X-Accel-Buffering": "no",
1456
+ });
1457
+ if (this.ppRunning) {
1458
+ res.write(`event: state\ndata: ${JSON.stringify(this.ppState)}\n\n`);
1459
+ this.ppSSEClients.push(res);
1460
+ res.on("close", () => { this.ppSSEClients = this.ppSSEClients.filter(c => c !== res); });
1461
+ }
1462
+ else if (this.ppState.done) {
1463
+ const evt = this.ppState.stopped ? "stopped" : "done";
1464
+ res.write(`event: ${evt}\ndata: ${JSON.stringify(this.ppState)}\n\n`);
1465
+ res.end();
1466
+ }
1467
+ else {
1468
+ res.end();
1469
+ }
1470
+ }
1471
+ handlePostprocessStop(res) {
1472
+ this.ppAbort = true;
1473
+ this.jsonResponse(res, { ok: true });
1474
+ }
1475
+ handlePostprocessStatus(res) {
1476
+ this.jsonResponse(res, this.ppState);
1477
+ }
1478
+ broadcastPPSSE(event, data) {
1479
+ const payload = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
1480
+ for (const c of this.ppSSEClients) {
1481
+ try {
1482
+ c.write(payload);
1483
+ }
1484
+ catch { /* */ }
1485
+ }
1486
+ }
1487
+ async runPostprocess(send, enableTasks, enableSkills) {
1488
+ const ctx = this.ctx;
1489
+ const taskProcessor = new task_processor_1.TaskProcessor(this.store, ctx);
1490
+ let skillEvolver = null;
1491
+ if (enableSkills) {
1492
+ const recallEngine = new engine_1.RecallEngine(this.store, this.embedder, ctx);
1493
+ skillEvolver = new evolver_1.SkillEvolver(this.store, recallEngine, ctx);
1494
+ taskProcessor.onTaskCompleted(async (task) => {
1495
+ try {
1496
+ await skillEvolver.onTaskCompleted(task);
1497
+ this.ppState.skillsCreated++;
1498
+ send("skill", { taskId: task.id, title: task.title });
1499
+ }
1500
+ catch (err) {
1501
+ this.log.warn(`Postprocess skill evolution error: ${err}`);
1502
+ }
1503
+ });
1504
+ }
1505
+ const importSessions = this.store.getDistinctSessionKeys()
1506
+ .filter((sk) => sk.startsWith("openclaw-import-") || sk.startsWith("openclaw-session-"));
1507
+ const pendingItems = [];
1508
+ let skippedCount = 0;
1509
+ for (const sk of importSessions) {
1510
+ const hasTask = this.store.hasTaskForSession(sk);
1511
+ const hasSkill = this.store.hasSkillForSessionTask(sk);
1512
+ if (enableTasks && !hasTask) {
1513
+ pendingItems.push({ sessionKey: sk, action: "full" });
1514
+ }
1515
+ else if (enableSkills && hasTask && !hasSkill) {
1516
+ pendingItems.push({ sessionKey: sk, action: "skill-only" });
1517
+ }
1518
+ else {
1519
+ skippedCount++;
1520
+ }
1521
+ }
1522
+ this.ppState.total = pendingItems.length;
1523
+ send("info", {
1524
+ totalSessions: importSessions.length,
1525
+ alreadyProcessed: skippedCount,
1526
+ pending: pendingItems.length,
1527
+ });
1528
+ send("progress", { processed: 0, total: pendingItems.length });
1529
+ for (let i = 0; i < pendingItems.length; i++) {
1530
+ if (this.ppAbort)
1531
+ break;
1532
+ const { sessionKey, action } = pendingItems[i];
1533
+ this.ppState.processed = i + 1;
1534
+ send("item", {
1535
+ index: i + 1,
1536
+ total: pendingItems.length,
1537
+ session: sessionKey,
1538
+ step: "processing",
1539
+ action,
1540
+ });
1541
+ try {
1542
+ if (action === "full") {
1543
+ await taskProcessor.onChunksIngested(sessionKey, Date.now());
1544
+ const activeTask = this.store.getActiveTask(sessionKey);
1545
+ if (activeTask) {
1546
+ await taskProcessor.finalizeTask(activeTask);
1547
+ const finalized = this.store.getTask(activeTask.id);
1548
+ this.ppState.tasksCreated++;
1549
+ send("item", {
1550
+ index: i + 1,
1551
+ total: pendingItems.length,
1552
+ session: sessionKey,
1553
+ step: "done",
1554
+ taskTitle: finalized?.title || "",
1555
+ taskStatus: finalized?.status || "",
1556
+ });
1557
+ }
1558
+ else {
1559
+ send("item", {
1560
+ index: i + 1,
1561
+ total: pendingItems.length,
1562
+ session: sessionKey,
1563
+ step: "done",
1564
+ taskTitle: "(no chunks)",
1565
+ });
1566
+ }
1567
+ }
1568
+ else if (action === "skill-only" && skillEvolver) {
1569
+ const completedTasks = this.store.getCompletedTasksForSession(sessionKey);
1570
+ let skillGenerated = false;
1571
+ for (const task of completedTasks) {
1572
+ if (this.ppAbort)
1573
+ break;
1574
+ try {
1575
+ await skillEvolver.onTaskCompleted(task);
1576
+ this.ppState.skillsCreated++;
1577
+ skillGenerated = true;
1578
+ send("skill", { taskId: task.id, title: task.title });
1579
+ }
1580
+ catch (err) {
1581
+ this.log.warn(`Skill evolution error for task=${task.id}: ${err}`);
1582
+ }
1583
+ }
1584
+ send("item", {
1585
+ index: i + 1,
1586
+ total: pendingItems.length,
1587
+ session: sessionKey,
1588
+ step: "done",
1589
+ taskTitle: completedTasks[0]?.title || sessionKey,
1590
+ action: "skill-only",
1591
+ skillGenerated,
1592
+ });
1593
+ }
1594
+ }
1595
+ catch (err) {
1596
+ this.ppState.errors++;
1597
+ this.log.warn(`Postprocess error for ${sessionKey}: ${err}`);
1598
+ send("item", {
1599
+ index: i + 1,
1600
+ total: pendingItems.length,
1601
+ session: sessionKey,
1602
+ step: "error",
1603
+ error: String(err).slice(0, 200),
1604
+ });
1605
+ }
1606
+ send("progress", { processed: i + 1, total: pendingItems.length });
1607
+ }
1608
+ }
802
1609
  readBody(req, cb) {
803
1610
  let body = "";
804
1611
  req.on("data", (chunk) => { body += chunk.toString(); });