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