@echomem/echo-memory-cloud-openclaw-plugin 0.1.0 → 0.1.2
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/clawdbot.plugin.json +1 -1
- package/index.js +254 -216
- package/lib/api-client.js +125 -44
- package/lib/local-server.js +537 -381
- package/lib/local-ui/src/App.jsx +133 -25
- package/lib/local-ui/src/canvas/Viewport.jsx +10 -8
- package/lib/local-ui/src/cards/Card.css +13 -0
- package/lib/local-ui/src/cards/Card.jsx +10 -1
- package/lib/local-ui/src/styles/global.css +8 -0
- package/lib/local-ui/src/sync/api.js +16 -14
- package/lib/sync.js +507 -215
- package/moltbot.plugin.json +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/lib/sync.js
CHANGED
|
@@ -1,46 +1,191 @@
|
|
|
1
|
-
import { scanOpenClawMemoryDir } from "./openclaw-memory-scan.js";
|
|
2
|
-
import { resolveStatePath, readLastSyncState, writeLastSyncState } from "./state.js";
|
|
3
|
-
|
|
4
|
-
function
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
}
|
|
9
|
-
return
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function
|
|
13
|
-
return {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
1
|
+
import { scanOpenClawMemoryDir } from "./openclaw-memory-scan.js";
|
|
2
|
+
import { resolveStatePath, readLastSyncState, writeLastSyncState } from "./state.js";
|
|
3
|
+
|
|
4
|
+
function resolveRuntimeStateDir(api, fallbackStateDir = null) {
|
|
5
|
+
const runtimeStateDir = api?.runtime?.state?.resolveStateDir?.();
|
|
6
|
+
if (runtimeStateDir) {
|
|
7
|
+
return runtimeStateDir;
|
|
8
|
+
}
|
|
9
|
+
return fallbackStateDir;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function buildRunId() {
|
|
13
|
+
return `echo-sync-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function buildEmptySummary(fileCount = 0) {
|
|
17
|
+
return {
|
|
18
|
+
file_count: fileCount,
|
|
19
|
+
skipped_count: 0,
|
|
20
|
+
new_source_count: 0,
|
|
21
|
+
new_memory_count: 0,
|
|
22
|
+
duplicate_count: 0,
|
|
23
|
+
failed_file_count: 0,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
23
27
|
function mergeSummary(target, next) {
|
|
24
|
-
target.file_count += next.file_count ?? 0;
|
|
25
|
-
target.skipped_count += next.skipped_count ?? 0;
|
|
26
|
-
target.new_source_count += next.new_source_count ?? 0;
|
|
27
|
-
target.new_memory_count += next.new_memory_count ?? 0;
|
|
28
|
-
target.duplicate_count += next.duplicate_count ?? 0;
|
|
29
|
-
target.failed_file_count += next.failed_file_count ?? 0;
|
|
28
|
+
target.file_count += next.file_count ?? 0;
|
|
29
|
+
target.skipped_count += next.skipped_count ?? 0;
|
|
30
|
+
target.new_source_count += next.new_source_count ?? 0;
|
|
31
|
+
target.new_memory_count += next.new_memory_count ?? 0;
|
|
32
|
+
target.duplicate_count += next.duplicate_count ?? 0;
|
|
33
|
+
target.failed_file_count += next.failed_file_count ?? 0;
|
|
30
34
|
return target;
|
|
31
35
|
}
|
|
32
36
|
|
|
37
|
+
function countStatuses(results = []) {
|
|
38
|
+
const counts = {
|
|
39
|
+
successCount: 0,
|
|
40
|
+
failedCount: 0,
|
|
41
|
+
skippedCount: 0,
|
|
42
|
+
duplicateCount: 0,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
for (const result of results) {
|
|
46
|
+
if (!result?.status) continue;
|
|
47
|
+
if (result.status === "failed") {
|
|
48
|
+
counts.failedCount += 1;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (result.status === "skipped") {
|
|
52
|
+
counts.skippedCount += 1;
|
|
53
|
+
counts.successCount += 1;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (result.status === "duplicate") {
|
|
57
|
+
counts.duplicateCount += 1;
|
|
58
|
+
counts.successCount += 1;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
counts.successCount += 1;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return counts;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function applyManualFileSummary(summary, status) {
|
|
68
|
+
summary.file_count += 1;
|
|
69
|
+
if (status === "failed") {
|
|
70
|
+
summary.failed_file_count += 1;
|
|
71
|
+
} else if (status === "skipped") {
|
|
72
|
+
summary.skipped_count += 1;
|
|
73
|
+
} else if (status === "duplicate") {
|
|
74
|
+
summary.duplicate_count += 1;
|
|
75
|
+
} else {
|
|
76
|
+
summary.new_source_count += 1;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function normalizeStatus(rawStatus, fallback = "imported") {
|
|
81
|
+
const normalized = String(rawStatus || "").trim().toLowerCase();
|
|
82
|
+
if (!normalized) return fallback;
|
|
83
|
+
if (["failed", "error"].includes(normalized)) return "failed";
|
|
84
|
+
if (["skipped", "unchanged"].includes(normalized)) return "skipped";
|
|
85
|
+
if (["duplicate", "deduped"].includes(normalized)) return "duplicate";
|
|
86
|
+
if (["imported", "processed", "success", "saved", "synced"].includes(normalized)) return "imported";
|
|
87
|
+
return fallback;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function selectMatchingResult(response, filePath) {
|
|
91
|
+
const results = Array.isArray(response?.results)
|
|
92
|
+
? response.results
|
|
93
|
+
: Array.isArray(response?.file_results)
|
|
94
|
+
? response.file_results
|
|
95
|
+
: [];
|
|
96
|
+
if (results.length === 0) {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const exactMatch = results.find((result) => {
|
|
101
|
+
const candidate = result?.file_path || result?.filePath || result?.path;
|
|
102
|
+
return candidate === filePath;
|
|
103
|
+
});
|
|
104
|
+
if (exactMatch) {
|
|
105
|
+
return exactMatch;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return results.length === 1 ? results[0] : null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function buildFileResult({
|
|
112
|
+
file,
|
|
113
|
+
response = null,
|
|
114
|
+
attemptAt,
|
|
115
|
+
previousResult = null,
|
|
116
|
+
error = null,
|
|
117
|
+
}) {
|
|
118
|
+
const matchedResult = response ? selectMatchingResult(response, file.filePath) : null;
|
|
119
|
+
const rawError =
|
|
120
|
+
error
|
|
121
|
+
?? matchedResult?.error
|
|
122
|
+
?? matchedResult?.error_message
|
|
123
|
+
?? matchedResult?.reason
|
|
124
|
+
?? response?.error
|
|
125
|
+
?? response?.message
|
|
126
|
+
?? null;
|
|
127
|
+
const responseSummary = response?.summary ?? null;
|
|
128
|
+
|
|
129
|
+
let status = matchedResult?.status ? normalizeStatus(matchedResult.status) : null;
|
|
130
|
+
if (!status) {
|
|
131
|
+
if ((responseSummary?.failed_file_count ?? 0) > 0) {
|
|
132
|
+
status = "failed";
|
|
133
|
+
} else if ((responseSummary?.duplicate_count ?? 0) > 0) {
|
|
134
|
+
status = "duplicate";
|
|
135
|
+
} else if ((responseSummary?.skipped_count ?? 0) > 0) {
|
|
136
|
+
status = "skipped";
|
|
137
|
+
} else {
|
|
138
|
+
status = error ? "failed" : "imported";
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const lastSuccessAt =
|
|
143
|
+
status === "failed"
|
|
144
|
+
? previousResult?.lastSuccessAt ?? previousResult?.last_success_at ?? null
|
|
145
|
+
: attemptAt;
|
|
146
|
+
const lastSuccessfulContentHash =
|
|
147
|
+
status === "failed"
|
|
148
|
+
? previousResult?.lastSuccessfulContentHash
|
|
149
|
+
?? previousResult?.last_successful_content_hash
|
|
150
|
+
?? previousResult?.contentHash
|
|
151
|
+
?? previousResult?.content_hash
|
|
152
|
+
?? null
|
|
153
|
+
: file.contentHash;
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
filePath: file.filePath,
|
|
157
|
+
contentHash: file.contentHash,
|
|
158
|
+
status,
|
|
159
|
+
lastAttemptAt: attemptAt,
|
|
160
|
+
lastSuccessAt,
|
|
161
|
+
lastSuccessfulContentHash,
|
|
162
|
+
lastError: status === "failed" ? String(rawError || "Unknown import failure") : null,
|
|
163
|
+
stage: matchedResult?.stage ?? matchedResult?.stage_reached ?? null,
|
|
164
|
+
summary: {
|
|
165
|
+
file_count: responseSummary?.file_count ?? 1,
|
|
166
|
+
skipped_count: responseSummary?.skipped_count ?? (status === "skipped" ? 1 : 0),
|
|
167
|
+
new_source_count: responseSummary?.new_source_count ?? (status === "imported" ? 1 : 0),
|
|
168
|
+
new_memory_count: responseSummary?.new_memory_count ?? 0,
|
|
169
|
+
duplicate_count: responseSummary?.duplicate_count ?? (status === "duplicate" ? 1 : 0),
|
|
170
|
+
failed_file_count: responseSummary?.failed_file_count ?? (status === "failed" ? 1 : 0),
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
33
175
|
function buildProgressPayload({
|
|
34
176
|
phase,
|
|
177
|
+
runId,
|
|
35
178
|
trigger,
|
|
36
179
|
startedAt,
|
|
37
180
|
totalFiles,
|
|
38
181
|
completedFiles,
|
|
39
|
-
|
|
40
|
-
|
|
182
|
+
currentFileIndex = 0,
|
|
183
|
+
currentFilePath = null,
|
|
184
|
+
currentStage = null,
|
|
41
185
|
currentFilePaths = [],
|
|
42
186
|
completedFilePaths = [],
|
|
43
187
|
failedFilePaths = [],
|
|
188
|
+
runResults = [],
|
|
44
189
|
error = null,
|
|
45
190
|
}) {
|
|
46
191
|
const startedAtMs = new Date(startedAt).getTime();
|
|
@@ -50,69 +195,90 @@ function buildProgressPayload({
|
|
|
50
195
|
completedFiles > 0 && remainingFiles > 0
|
|
51
196
|
? Math.round((elapsedMs / completedFiles) * remainingFiles)
|
|
52
197
|
: null;
|
|
198
|
+
const counts = countStatuses(runResults);
|
|
199
|
+
const recentFileResult = runResults.length > 0 ? runResults[runResults.length - 1] : null;
|
|
53
200
|
|
|
54
201
|
return {
|
|
55
202
|
phase,
|
|
203
|
+
runId,
|
|
56
204
|
trigger,
|
|
57
205
|
startedAt,
|
|
58
206
|
totalFiles,
|
|
59
207
|
completedFiles,
|
|
60
208
|
remainingFiles,
|
|
61
|
-
|
|
62
|
-
|
|
209
|
+
currentFileIndex,
|
|
210
|
+
currentFilePath,
|
|
211
|
+
currentStage,
|
|
63
212
|
currentFilePaths,
|
|
64
213
|
completedFilePaths,
|
|
65
214
|
failedFilePaths,
|
|
66
215
|
elapsedMs,
|
|
67
216
|
etaMs,
|
|
217
|
+
successCount: counts.successCount,
|
|
218
|
+
failedCount: counts.failedCount,
|
|
219
|
+
skippedCount: counts.skippedCount,
|
|
220
|
+
duplicateCount: counts.duplicateCount,
|
|
221
|
+
recentFileResult,
|
|
68
222
|
error,
|
|
69
223
|
};
|
|
70
224
|
}
|
|
71
|
-
|
|
72
|
-
export function formatStatusText(localState, remoteStatus = null) {
|
|
73
|
-
const lines = [];
|
|
74
|
-
lines.push("Echo Memory status:");
|
|
75
|
-
|
|
76
|
-
if (localState) {
|
|
77
|
-
lines.push(`- last_sync_at: ${localState.finished_at || "(unknown)"}`);
|
|
78
|
-
lines.push(`- last_sync_mode: ${localState.trigger || "(unknown)"}`);
|
|
79
|
-
lines.push(`- files_scanned: ${localState.summary?.file_count ?? 0}`);
|
|
80
|
-
lines.push(`- skipped: ${localState.summary?.skipped_count ?? 0}`);
|
|
81
|
-
lines.push(`- new_sources: ${localState.summary?.new_source_count ?? 0}`);
|
|
82
|
-
lines.push(`- new_memories: ${localState.summary?.new_memory_count ?? 0}`);
|
|
83
|
-
lines.push(`- duplicates: ${localState.summary?.duplicate_count ?? 0}`);
|
|
84
|
-
lines.push(`- failed_files: ${localState.summary?.failed_file_count ?? 0}`);
|
|
85
|
-
if (localState.error) {
|
|
86
|
-
lines.push(`- last_error: ${localState.error}`);
|
|
87
|
-
}
|
|
88
|
-
} else {
|
|
89
|
-
lines.push("- last_sync_at: (none)");
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
if (remoteStatus) {
|
|
93
|
-
lines.push("");
|
|
94
|
-
lines.push("Echo backend:");
|
|
95
|
-
lines.push(`- total_sources: ${remoteStatus.total_source_versions ?? 0}`);
|
|
96
|
-
lines.push(`- processed_sources: ${remoteStatus.processed_source_versions ?? 0}`);
|
|
97
|
-
lines.push(`- recent_memories: ${remoteStatus.recent_memory_count ?? 0}`);
|
|
98
|
-
lines.push(`- latest_imported_at: ${remoteStatus.latest_imported_at || "(none)"}`);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
return lines.join("\n");
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
export function createSyncRunner({ api, cfg, client }) {
|
|
225
|
+
|
|
226
|
+
export function formatStatusText(localState, remoteStatus = null) {
|
|
227
|
+
const lines = [];
|
|
228
|
+
lines.push("Echo Memory status:");
|
|
229
|
+
|
|
230
|
+
if (localState) {
|
|
231
|
+
lines.push(`- last_sync_at: ${localState.finished_at || "(unknown)"}`);
|
|
232
|
+
lines.push(`- last_sync_mode: ${localState.trigger || "(unknown)"}`);
|
|
233
|
+
lines.push(`- files_scanned: ${localState.summary?.file_count ?? 0}`);
|
|
234
|
+
lines.push(`- skipped: ${localState.summary?.skipped_count ?? 0}`);
|
|
235
|
+
lines.push(`- new_sources: ${localState.summary?.new_source_count ?? 0}`);
|
|
236
|
+
lines.push(`- new_memories: ${localState.summary?.new_memory_count ?? 0}`);
|
|
237
|
+
lines.push(`- duplicates: ${localState.summary?.duplicate_count ?? 0}`);
|
|
238
|
+
lines.push(`- failed_files: ${localState.summary?.failed_file_count ?? 0}`);
|
|
239
|
+
if (localState.error) {
|
|
240
|
+
lines.push(`- last_error: ${localState.error}`);
|
|
241
|
+
}
|
|
242
|
+
} else {
|
|
243
|
+
lines.push("- last_sync_at: (none)");
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (remoteStatus) {
|
|
247
|
+
lines.push("");
|
|
248
|
+
lines.push("Echo backend:");
|
|
249
|
+
lines.push(`- total_sources: ${remoteStatus.total_source_versions ?? 0}`);
|
|
250
|
+
lines.push(`- processed_sources: ${remoteStatus.processed_source_versions ?? 0}`);
|
|
251
|
+
lines.push(`- recent_memories: ${remoteStatus.recent_memory_count ?? 0}`);
|
|
252
|
+
lines.push(`- latest_imported_at: ${remoteStatus.latest_imported_at || "(none)"}`);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return lines.join("\n");
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export function createSyncRunner({ api, cfg, client, fallbackStateDir = null }) {
|
|
105
259
|
let intervalHandle = null;
|
|
106
260
|
let statePath = null;
|
|
107
261
|
let activeRun = null;
|
|
262
|
+
let activeRunInfo = null;
|
|
108
263
|
const progressListeners = new Set();
|
|
109
|
-
|
|
110
|
-
async function initialize(stateDir) {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
264
|
+
|
|
265
|
+
async function initialize(stateDir) {
|
|
266
|
+
const resolvedStateDir = stateDir || resolveRuntimeStateDir(api, fallbackStateDir);
|
|
267
|
+
if (!resolvedStateDir) {
|
|
268
|
+
throw new Error("Echo memory state directory is unavailable");
|
|
269
|
+
}
|
|
270
|
+
statePath = resolveStatePath(resolvedStateDir);
|
|
271
|
+
}
|
|
272
|
+
|
|
114
273
|
function getStatePath() {
|
|
115
|
-
|
|
274
|
+
if (statePath) {
|
|
275
|
+
return statePath;
|
|
276
|
+
}
|
|
277
|
+
const resolvedStateDir = resolveRuntimeStateDir(api, fallbackStateDir);
|
|
278
|
+
if (!resolvedStateDir) {
|
|
279
|
+
throw new Error("Echo memory state directory is unavailable");
|
|
280
|
+
}
|
|
281
|
+
return resolveStatePath(resolvedStateDir);
|
|
116
282
|
}
|
|
117
283
|
|
|
118
284
|
function emitProgress(event) {
|
|
@@ -129,43 +295,71 @@ export function createSyncRunner({ api, cfg, client }) {
|
|
|
129
295
|
progressListeners.add(listener);
|
|
130
296
|
return () => progressListeners.delete(listener);
|
|
131
297
|
}
|
|
132
|
-
|
|
133
|
-
async function runSync(trigger = "manual", filterPaths = null) {
|
|
134
|
-
if (activeRun) {
|
|
135
|
-
return activeRun;
|
|
136
|
-
}
|
|
137
|
-
|
|
298
|
+
|
|
299
|
+
async function runSync(trigger = "manual", filterPaths = null) {
|
|
300
|
+
if (activeRun) {
|
|
301
|
+
return activeRun;
|
|
302
|
+
}
|
|
303
|
+
|
|
138
304
|
activeRun = (async () => {
|
|
305
|
+
const runId = buildRunId();
|
|
139
306
|
const startedAt = new Date().toISOString();
|
|
307
|
+
activeRunInfo = { runId, trigger, startedAt };
|
|
308
|
+
|
|
140
309
|
let totalFiles = 0;
|
|
141
|
-
let batchCount = 0;
|
|
142
310
|
let completedFiles = 0;
|
|
143
|
-
let
|
|
311
|
+
let currentFilePath = null;
|
|
312
|
+
let currentFileIndex = 0;
|
|
313
|
+
const successfulPaths = [];
|
|
314
|
+
const failedPaths = [];
|
|
315
|
+
const runResults = [];
|
|
316
|
+
|
|
317
|
+
let prevState = null;
|
|
318
|
+
let prevResults = [];
|
|
319
|
+
const resultMap = new Map();
|
|
320
|
+
|
|
321
|
+
try {
|
|
322
|
+
prevState = await readLastSyncState(getStatePath());
|
|
323
|
+
prevResults = Array.isArray(prevState?.results) ? prevState.results : [];
|
|
324
|
+
for (const entry of prevResults) {
|
|
325
|
+
const key = entry?.filePath || entry?.file_path;
|
|
326
|
+
if (!key) continue;
|
|
327
|
+
resultMap.set(key, entry);
|
|
328
|
+
}
|
|
329
|
+
} catch {
|
|
330
|
+
prevState = null;
|
|
331
|
+
prevResults = [];
|
|
332
|
+
}
|
|
333
|
+
|
|
144
334
|
try {
|
|
145
335
|
if (!cfg.apiKey) {
|
|
336
|
+
const message = "Missing Echo API key";
|
|
146
337
|
emitProgress(buildProgressPayload({
|
|
147
338
|
phase: "failed",
|
|
339
|
+
runId,
|
|
148
340
|
trigger,
|
|
149
341
|
startedAt,
|
|
150
342
|
totalFiles: 0,
|
|
151
343
|
completedFiles: 0,
|
|
152
|
-
|
|
344
|
+
runResults,
|
|
345
|
+
error: message,
|
|
153
346
|
}));
|
|
154
347
|
const state = {
|
|
155
|
-
trigger,
|
|
156
|
-
started_at: startedAt,
|
|
157
|
-
finished_at: new Date().toISOString(),
|
|
158
|
-
error:
|
|
159
|
-
summary: buildEmptySummary(0),
|
|
160
|
-
results:
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
348
|
+
trigger,
|
|
349
|
+
started_at: startedAt,
|
|
350
|
+
finished_at: new Date().toISOString(),
|
|
351
|
+
error: message,
|
|
352
|
+
summary: buildEmptySummary(0),
|
|
353
|
+
results: prevResults,
|
|
354
|
+
run_results: [],
|
|
355
|
+
};
|
|
356
|
+
await writeLastSyncState(getStatePath(), state);
|
|
357
|
+
return state;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
await client.whoami();
|
|
361
|
+
|
|
362
|
+
let files = [];
|
|
169
363
|
try {
|
|
170
364
|
files = await scanOpenClawMemoryDir(cfg.memoryDir);
|
|
171
365
|
} catch (error) {
|
|
@@ -174,182 +368,280 @@ export function createSyncRunner({ api, cfg, client }) {
|
|
|
174
368
|
: String(error?.message ?? error);
|
|
175
369
|
emitProgress(buildProgressPayload({
|
|
176
370
|
phase: "failed",
|
|
371
|
+
runId,
|
|
177
372
|
trigger,
|
|
178
373
|
startedAt,
|
|
179
374
|
totalFiles: 0,
|
|
180
375
|
completedFiles: 0,
|
|
376
|
+
runResults,
|
|
181
377
|
error: message,
|
|
182
378
|
}));
|
|
183
379
|
const state = {
|
|
184
|
-
trigger,
|
|
185
|
-
started_at: startedAt,
|
|
186
|
-
finished_at: new Date().toISOString(),
|
|
187
|
-
error: message,
|
|
188
|
-
summary: buildEmptySummary(0),
|
|
189
|
-
results:
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
if (filterPaths instanceof Set && filterPaths.size > 0) {
|
|
197
|
-
files = files.filter(
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
|
|
380
|
+
trigger,
|
|
381
|
+
started_at: startedAt,
|
|
382
|
+
finished_at: new Date().toISOString(),
|
|
383
|
+
error: message,
|
|
384
|
+
summary: buildEmptySummary(0),
|
|
385
|
+
results: prevResults,
|
|
386
|
+
run_results: [],
|
|
387
|
+
};
|
|
388
|
+
await writeLastSyncState(getStatePath(), state);
|
|
389
|
+
return state;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (filterPaths instanceof Set && filterPaths.size > 0) {
|
|
393
|
+
files = files.filter((file) => filterPaths.has(file.filePath));
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
totalFiles = files.length;
|
|
397
|
+
if (totalFiles === 0) {
|
|
201
398
|
emitProgress(buildProgressPayload({
|
|
202
399
|
phase: "finished",
|
|
400
|
+
runId,
|
|
203
401
|
trigger,
|
|
204
402
|
startedAt,
|
|
205
403
|
totalFiles: 0,
|
|
206
404
|
completedFiles: 0,
|
|
405
|
+
runResults,
|
|
207
406
|
}));
|
|
208
407
|
const state = {
|
|
209
|
-
trigger,
|
|
210
|
-
started_at: startedAt,
|
|
211
|
-
finished_at: new Date().toISOString(),
|
|
212
|
-
summary: buildEmptySummary(0),
|
|
213
|
-
results:
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
const summary = buildEmptySummary();
|
|
220
|
-
const batches = chunk(files, cfg.batchSize);
|
|
221
|
-
totalFiles = files.length;
|
|
222
|
-
batchCount = batches.length;
|
|
408
|
+
trigger,
|
|
409
|
+
started_at: startedAt,
|
|
410
|
+
finished_at: new Date().toISOString(),
|
|
411
|
+
summary: buildEmptySummary(0),
|
|
412
|
+
results: prevResults,
|
|
413
|
+
run_results: [],
|
|
414
|
+
};
|
|
415
|
+
await writeLastSyncState(getStatePath(), state);
|
|
416
|
+
return state;
|
|
417
|
+
}
|
|
223
418
|
|
|
224
419
|
emitProgress(buildProgressPayload({
|
|
225
420
|
phase: "started",
|
|
421
|
+
runId,
|
|
226
422
|
trigger,
|
|
227
423
|
startedAt,
|
|
228
424
|
totalFiles,
|
|
229
425
|
completedFiles: 0,
|
|
230
|
-
batchCount,
|
|
231
426
|
currentFilePaths: files.map((file) => file.filePath),
|
|
427
|
+
runResults,
|
|
232
428
|
}));
|
|
233
429
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
startedAt,
|
|
240
|
-
totalFiles,
|
|
241
|
-
completedFiles,
|
|
242
|
-
batchIndex: index + 1,
|
|
243
|
-
batchCount,
|
|
244
|
-
currentFilePaths: currentBatchPaths,
|
|
245
|
-
}));
|
|
246
|
-
const response = await client.importMarkdown(batch);
|
|
247
|
-
mergeSummary(summary, response.summary ?? {});
|
|
248
|
-
completedFiles += batch.length;
|
|
430
|
+
const summary = buildEmptySummary(0);
|
|
431
|
+
|
|
432
|
+
for (const [index, file] of files.entries()) {
|
|
433
|
+
currentFileIndex = index + 1;
|
|
434
|
+
currentFilePath = file.filePath;
|
|
249
435
|
emitProgress(buildProgressPayload({
|
|
250
|
-
phase: "
|
|
436
|
+
phase: "file-started",
|
|
437
|
+
runId,
|
|
251
438
|
trigger,
|
|
252
439
|
startedAt,
|
|
253
440
|
totalFiles,
|
|
254
441
|
completedFiles,
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
442
|
+
currentFileIndex,
|
|
443
|
+
currentFilePath,
|
|
444
|
+
currentStage: "parse",
|
|
445
|
+
currentFilePaths: [file.filePath],
|
|
446
|
+
completedFilePaths: successfulPaths,
|
|
447
|
+
failedFilePaths: failedPaths,
|
|
448
|
+
runResults,
|
|
258
449
|
}));
|
|
450
|
+
|
|
451
|
+
const attemptAt = new Date().toISOString();
|
|
452
|
+
const previousResult = resultMap.get(file.filePath) ?? null;
|
|
453
|
+
|
|
454
|
+
try {
|
|
455
|
+
const response = await client.importMarkdown([file], {
|
|
456
|
+
onStageEvent: (stageEvent) => {
|
|
457
|
+
emitProgress(buildProgressPayload({
|
|
458
|
+
phase: "file-stage",
|
|
459
|
+
runId,
|
|
460
|
+
trigger,
|
|
461
|
+
startedAt,
|
|
462
|
+
totalFiles,
|
|
463
|
+
completedFiles,
|
|
464
|
+
currentFileIndex,
|
|
465
|
+
currentFilePath,
|
|
466
|
+
currentStage: stageEvent?.stage || null,
|
|
467
|
+
currentFilePaths: [file.filePath],
|
|
468
|
+
completedFilePaths: successfulPaths,
|
|
469
|
+
failedFilePaths: failedPaths,
|
|
470
|
+
runResults,
|
|
471
|
+
}));
|
|
472
|
+
},
|
|
473
|
+
});
|
|
474
|
+
if (response?.summary) {
|
|
475
|
+
mergeSummary(summary, response.summary);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const fileResult = buildFileResult({
|
|
479
|
+
file,
|
|
480
|
+
response,
|
|
481
|
+
attemptAt,
|
|
482
|
+
previousResult,
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
if (!response?.summary) {
|
|
486
|
+
applyManualFileSummary(summary, fileResult.status);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
runResults.push(fileResult);
|
|
490
|
+
resultMap.set(file.filePath, fileResult);
|
|
491
|
+
completedFiles += 1;
|
|
492
|
+
|
|
493
|
+
if (fileResult.status === "failed") {
|
|
494
|
+
failedPaths.push(file.filePath);
|
|
495
|
+
} else {
|
|
496
|
+
successfulPaths.push(file.filePath);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
emitProgress(buildProgressPayload({
|
|
500
|
+
phase: "file-finished",
|
|
501
|
+
runId,
|
|
502
|
+
trigger,
|
|
503
|
+
startedAt,
|
|
504
|
+
totalFiles,
|
|
505
|
+
completedFiles,
|
|
506
|
+
currentFileIndex,
|
|
507
|
+
currentFilePath,
|
|
508
|
+
currentStage: fileResult.stage,
|
|
509
|
+
currentFilePaths: [file.filePath],
|
|
510
|
+
completedFilePaths: fileResult.status === "failed" ? [] : [file.filePath],
|
|
511
|
+
failedFilePaths: fileResult.status === "failed" ? [file.filePath] : [],
|
|
512
|
+
runResults,
|
|
513
|
+
}));
|
|
514
|
+
} catch (error) {
|
|
515
|
+
const fileResult = buildFileResult({
|
|
516
|
+
file,
|
|
517
|
+
attemptAt,
|
|
518
|
+
previousResult,
|
|
519
|
+
error: String(error?.message ?? error),
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
applyManualFileSummary(summary, fileResult.status);
|
|
523
|
+
runResults.push(fileResult);
|
|
524
|
+
resultMap.set(file.filePath, fileResult);
|
|
525
|
+
completedFiles += 1;
|
|
526
|
+
failedPaths.push(file.filePath);
|
|
527
|
+
|
|
528
|
+
emitProgress(buildProgressPayload({
|
|
529
|
+
phase: "file-finished",
|
|
530
|
+
runId,
|
|
531
|
+
trigger,
|
|
532
|
+
startedAt,
|
|
533
|
+
totalFiles,
|
|
534
|
+
completedFiles,
|
|
535
|
+
currentFileIndex,
|
|
536
|
+
currentFilePath,
|
|
537
|
+
currentStage: fileResult.stage,
|
|
538
|
+
currentFilePaths: [file.filePath],
|
|
539
|
+
failedFilePaths: [file.filePath],
|
|
540
|
+
runResults,
|
|
541
|
+
}));
|
|
542
|
+
}
|
|
259
543
|
}
|
|
260
|
-
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
// Merge with existing state — preserve previously synced files
|
|
264
|
-
let mergedResults = newResults;
|
|
265
|
-
if (filterPaths instanceof Set && filterPaths.size > 0) {
|
|
266
|
-
const prevState = await readLastSyncState(getStatePath());
|
|
267
|
-
const prevResults = Array.isArray(prevState?.results) ? prevState.results : [];
|
|
268
|
-
// Keep previous results that weren't in this batch, add new ones
|
|
269
|
-
const newPathSet = new Set(newResults.map(r => r.filePath));
|
|
270
|
-
mergedResults = [
|
|
271
|
-
...prevResults.filter(r => !newPathSet.has(r.filePath || r.file_path)),
|
|
272
|
-
...newResults,
|
|
273
|
-
];
|
|
274
|
-
}
|
|
275
|
-
|
|
544
|
+
|
|
545
|
+
const mergedResults = [...resultMap.values()];
|
|
276
546
|
const state = {
|
|
277
|
-
trigger,
|
|
278
|
-
started_at: startedAt,
|
|
279
|
-
finished_at: new Date().toISOString(),
|
|
280
|
-
summary,
|
|
281
|
-
results: mergedResults,
|
|
547
|
+
trigger,
|
|
548
|
+
started_at: startedAt,
|
|
549
|
+
finished_at: new Date().toISOString(),
|
|
550
|
+
summary,
|
|
551
|
+
results: mergedResults,
|
|
552
|
+
run_results: runResults,
|
|
282
553
|
};
|
|
283
554
|
await writeLastSyncState(getStatePath(), state);
|
|
555
|
+
|
|
284
556
|
emitProgress(buildProgressPayload({
|
|
285
557
|
phase: "finished",
|
|
558
|
+
runId,
|
|
286
559
|
trigger,
|
|
287
560
|
startedAt,
|
|
288
561
|
totalFiles,
|
|
289
562
|
completedFiles,
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
completedFilePaths:
|
|
563
|
+
currentFileIndex,
|
|
564
|
+
currentFilePath,
|
|
565
|
+
completedFilePaths: successfulPaths,
|
|
566
|
+
failedFilePaths: failedPaths,
|
|
567
|
+
runResults,
|
|
293
568
|
}));
|
|
569
|
+
|
|
294
570
|
api.logger?.info?.(
|
|
295
|
-
`[echo-memory] sync complete: files=${summary.file_count} new_memories=${summary.new_memory_count} skipped=${summary.skipped_count} failed=${summary.failed_file_count}`,
|
|
296
|
-
);
|
|
297
|
-
return state;
|
|
571
|
+
`[echo-memory] sync complete: files=${summary.file_count} new_memories=${summary.new_memory_count} skipped=${summary.skipped_count} failed=${summary.failed_file_count}`,
|
|
572
|
+
);
|
|
573
|
+
return state;
|
|
298
574
|
} catch (error) {
|
|
299
575
|
const message = String(error?.message ?? error);
|
|
300
576
|
emitProgress(buildProgressPayload({
|
|
301
577
|
phase: "failed",
|
|
578
|
+
runId,
|
|
302
579
|
trigger,
|
|
303
580
|
startedAt,
|
|
304
581
|
totalFiles,
|
|
305
582
|
completedFiles,
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
583
|
+
currentFileIndex,
|
|
584
|
+
currentFilePath,
|
|
585
|
+
currentFilePaths: currentFilePath ? [currentFilePath] : [],
|
|
586
|
+
failedFilePaths: currentFilePath ? [currentFilePath] : [],
|
|
587
|
+
runResults,
|
|
309
588
|
error: message,
|
|
310
589
|
}));
|
|
590
|
+
|
|
311
591
|
const state = {
|
|
312
|
-
trigger,
|
|
313
|
-
started_at: startedAt,
|
|
314
|
-
finished_at: new Date().toISOString(),
|
|
315
|
-
error: message,
|
|
316
|
-
summary: buildEmptySummary(0),
|
|
317
|
-
results: [],
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
592
|
+
trigger,
|
|
593
|
+
started_at: startedAt,
|
|
594
|
+
finished_at: new Date().toISOString(),
|
|
595
|
+
error: message,
|
|
596
|
+
summary: buildEmptySummary(0),
|
|
597
|
+
results: [...resultMap.values()],
|
|
598
|
+
run_results: runResults,
|
|
599
|
+
};
|
|
600
|
+
await writeLastSyncState(getStatePath(), state);
|
|
601
|
+
return state;
|
|
602
|
+
}
|
|
603
|
+
})().finally(() => {
|
|
604
|
+
activeRun = null;
|
|
605
|
+
activeRunInfo = null;
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
return activeRun;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function startInterval() {
|
|
612
|
+
stopInterval();
|
|
613
|
+
const intervalMs = cfg.syncIntervalMinutes * 60 * 1000;
|
|
614
|
+
intervalHandle = setInterval(() => {
|
|
615
|
+
runSync("scheduled").catch((error) => {
|
|
616
|
+
api.logger?.warn?.(`[echo-memory] scheduled sync failed: ${String(error?.message ?? error)}`);
|
|
617
|
+
});
|
|
618
|
+
}, intervalMs);
|
|
619
|
+
intervalHandle.unref?.();
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function stopInterval() {
|
|
623
|
+
if (intervalHandle) {
|
|
624
|
+
clearInterval(intervalHandle);
|
|
625
|
+
intervalHandle = null;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function isRunning() {
|
|
630
|
+
return Boolean(activeRun);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function getActiveRunInfo() {
|
|
634
|
+
return activeRunInfo;
|
|
635
|
+
}
|
|
636
|
+
|
|
347
637
|
return {
|
|
348
638
|
initialize,
|
|
349
639
|
getStatePath,
|
|
350
640
|
onProgress,
|
|
351
641
|
runSync,
|
|
352
|
-
startInterval,
|
|
353
|
-
stopInterval,
|
|
354
|
-
|
|
355
|
-
|
|
642
|
+
startInterval,
|
|
643
|
+
stopInterval,
|
|
644
|
+
isRunning,
|
|
645
|
+
getActiveRunInfo,
|
|
646
|
+
};
|
|
647
|
+
}
|