@echomem/echo-memory-cloud-openclaw-plugin 0.1.1 → 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/lib/api-client.js +125 -44
- package/lib/local-server.js +554 -472
- package/lib/local-ui/src/App.jsx +131 -23
- 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 +6 -5
- package/lib/sync.js +480 -207
- package/moltbot.plugin.json +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/lib/sync.js
CHANGED
|
@@ -8,47 +8,184 @@ function resolveRuntimeStateDir(api, fallbackStateDir = null) {
|
|
|
8
8
|
}
|
|
9
9
|
return fallbackStateDir;
|
|
10
10
|
}
|
|
11
|
-
|
|
12
|
-
function
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
return
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
failed_file_count: 0,
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
|
|
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
|
+
|
|
31
27
|
function mergeSummary(target, next) {
|
|
32
|
-
target.file_count += next.file_count ?? 0;
|
|
33
|
-
target.skipped_count += next.skipped_count ?? 0;
|
|
34
|
-
target.new_source_count += next.new_source_count ?? 0;
|
|
35
|
-
target.new_memory_count += next.new_memory_count ?? 0;
|
|
36
|
-
target.duplicate_count += next.duplicate_count ?? 0;
|
|
37
|
-
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;
|
|
38
34
|
return target;
|
|
39
35
|
}
|
|
40
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
|
+
|
|
41
175
|
function buildProgressPayload({
|
|
42
176
|
phase,
|
|
177
|
+
runId,
|
|
43
178
|
trigger,
|
|
44
179
|
startedAt,
|
|
45
180
|
totalFiles,
|
|
46
181
|
completedFiles,
|
|
47
|
-
|
|
48
|
-
|
|
182
|
+
currentFileIndex = 0,
|
|
183
|
+
currentFilePath = null,
|
|
184
|
+
currentStage = null,
|
|
49
185
|
currentFilePaths = [],
|
|
50
186
|
completedFilePaths = [],
|
|
51
187
|
failedFilePaths = [],
|
|
188
|
+
runResults = [],
|
|
52
189
|
error = null,
|
|
53
190
|
}) {
|
|
54
191
|
const startedAtMs = new Date(startedAt).getTime();
|
|
@@ -58,63 +195,73 @@ function buildProgressPayload({
|
|
|
58
195
|
completedFiles > 0 && remainingFiles > 0
|
|
59
196
|
? Math.round((elapsedMs / completedFiles) * remainingFiles)
|
|
60
197
|
: null;
|
|
198
|
+
const counts = countStatuses(runResults);
|
|
199
|
+
const recentFileResult = runResults.length > 0 ? runResults[runResults.length - 1] : null;
|
|
61
200
|
|
|
62
201
|
return {
|
|
63
202
|
phase,
|
|
203
|
+
runId,
|
|
64
204
|
trigger,
|
|
65
205
|
startedAt,
|
|
66
206
|
totalFiles,
|
|
67
207
|
completedFiles,
|
|
68
208
|
remainingFiles,
|
|
69
|
-
|
|
70
|
-
|
|
209
|
+
currentFileIndex,
|
|
210
|
+
currentFilePath,
|
|
211
|
+
currentStage,
|
|
71
212
|
currentFilePaths,
|
|
72
213
|
completedFilePaths,
|
|
73
214
|
failedFilePaths,
|
|
74
215
|
elapsedMs,
|
|
75
216
|
etaMs,
|
|
217
|
+
successCount: counts.successCount,
|
|
218
|
+
failedCount: counts.failedCount,
|
|
219
|
+
skippedCount: counts.skippedCount,
|
|
220
|
+
duplicateCount: counts.duplicateCount,
|
|
221
|
+
recentFileResult,
|
|
76
222
|
error,
|
|
77
223
|
};
|
|
78
224
|
}
|
|
79
|
-
|
|
80
|
-
export function formatStatusText(localState, remoteStatus = null) {
|
|
81
|
-
const lines = [];
|
|
82
|
-
lines.push("Echo Memory status:");
|
|
83
|
-
|
|
84
|
-
if (localState) {
|
|
85
|
-
lines.push(`- last_sync_at: ${localState.finished_at || "(unknown)"}`);
|
|
86
|
-
lines.push(`- last_sync_mode: ${localState.trigger || "(unknown)"}`);
|
|
87
|
-
lines.push(`- files_scanned: ${localState.summary?.file_count ?? 0}`);
|
|
88
|
-
lines.push(`- skipped: ${localState.summary?.skipped_count ?? 0}`);
|
|
89
|
-
lines.push(`- new_sources: ${localState.summary?.new_source_count ?? 0}`);
|
|
90
|
-
lines.push(`- new_memories: ${localState.summary?.new_memory_count ?? 0}`);
|
|
91
|
-
lines.push(`- duplicates: ${localState.summary?.duplicate_count ?? 0}`);
|
|
92
|
-
lines.push(`- failed_files: ${localState.summary?.failed_file_count ?? 0}`);
|
|
93
|
-
if (localState.error) {
|
|
94
|
-
lines.push(`- last_error: ${localState.error}`);
|
|
95
|
-
}
|
|
96
|
-
} else {
|
|
97
|
-
lines.push("- last_sync_at: (none)");
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
if (remoteStatus) {
|
|
101
|
-
lines.push("");
|
|
102
|
-
lines.push("Echo backend:");
|
|
103
|
-
lines.push(`- total_sources: ${remoteStatus.total_source_versions ?? 0}`);
|
|
104
|
-
lines.push(`- processed_sources: ${remoteStatus.processed_source_versions ?? 0}`);
|
|
105
|
-
lines.push(`- recent_memories: ${remoteStatus.recent_memory_count ?? 0}`);
|
|
106
|
-
lines.push(`- latest_imported_at: ${remoteStatus.latest_imported_at || "(none)"}`);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
return lines.join("\n");
|
|
110
|
-
}
|
|
111
|
-
|
|
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
|
+
|
|
112
258
|
export function createSyncRunner({ api, cfg, client, fallbackStateDir = null }) {
|
|
113
259
|
let intervalHandle = null;
|
|
114
260
|
let statePath = null;
|
|
115
261
|
let activeRun = null;
|
|
262
|
+
let activeRunInfo = null;
|
|
116
263
|
const progressListeners = new Set();
|
|
117
|
-
|
|
264
|
+
|
|
118
265
|
async function initialize(stateDir) {
|
|
119
266
|
const resolvedStateDir = stateDir || resolveRuntimeStateDir(api, fallbackStateDir);
|
|
120
267
|
if (!resolvedStateDir) {
|
|
@@ -148,43 +295,71 @@ export function createSyncRunner({ api, cfg, client, fallbackStateDir = null })
|
|
|
148
295
|
progressListeners.add(listener);
|
|
149
296
|
return () => progressListeners.delete(listener);
|
|
150
297
|
}
|
|
151
|
-
|
|
152
|
-
async function runSync(trigger = "manual", filterPaths = null) {
|
|
153
|
-
if (activeRun) {
|
|
154
|
-
return activeRun;
|
|
155
|
-
}
|
|
156
|
-
|
|
298
|
+
|
|
299
|
+
async function runSync(trigger = "manual", filterPaths = null) {
|
|
300
|
+
if (activeRun) {
|
|
301
|
+
return activeRun;
|
|
302
|
+
}
|
|
303
|
+
|
|
157
304
|
activeRun = (async () => {
|
|
305
|
+
const runId = buildRunId();
|
|
158
306
|
const startedAt = new Date().toISOString();
|
|
307
|
+
activeRunInfo = { runId, trigger, startedAt };
|
|
308
|
+
|
|
159
309
|
let totalFiles = 0;
|
|
160
|
-
let batchCount = 0;
|
|
161
310
|
let completedFiles = 0;
|
|
162
|
-
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
|
+
|
|
163
334
|
try {
|
|
164
335
|
if (!cfg.apiKey) {
|
|
336
|
+
const message = "Missing Echo API key";
|
|
165
337
|
emitProgress(buildProgressPayload({
|
|
166
338
|
phase: "failed",
|
|
339
|
+
runId,
|
|
167
340
|
trigger,
|
|
168
341
|
startedAt,
|
|
169
342
|
totalFiles: 0,
|
|
170
343
|
completedFiles: 0,
|
|
171
|
-
|
|
344
|
+
runResults,
|
|
345
|
+
error: message,
|
|
172
346
|
}));
|
|
173
347
|
const state = {
|
|
174
|
-
trigger,
|
|
175
|
-
started_at: startedAt,
|
|
176
|
-
finished_at: new Date().toISOString(),
|
|
177
|
-
error:
|
|
178
|
-
summary: buildEmptySummary(0),
|
|
179
|
-
results:
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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 = [];
|
|
188
363
|
try {
|
|
189
364
|
files = await scanOpenClawMemoryDir(cfg.memoryDir);
|
|
190
365
|
} catch (error) {
|
|
@@ -193,182 +368,280 @@ export function createSyncRunner({ api, cfg, client, fallbackStateDir = null })
|
|
|
193
368
|
: String(error?.message ?? error);
|
|
194
369
|
emitProgress(buildProgressPayload({
|
|
195
370
|
phase: "failed",
|
|
371
|
+
runId,
|
|
196
372
|
trigger,
|
|
197
373
|
startedAt,
|
|
198
374
|
totalFiles: 0,
|
|
199
375
|
completedFiles: 0,
|
|
376
|
+
runResults,
|
|
200
377
|
error: message,
|
|
201
378
|
}));
|
|
202
379
|
const state = {
|
|
203
|
-
trigger,
|
|
204
|
-
started_at: startedAt,
|
|
205
|
-
finished_at: new Date().toISOString(),
|
|
206
|
-
error: message,
|
|
207
|
-
summary: buildEmptySummary(0),
|
|
208
|
-
results:
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
if (filterPaths instanceof Set && filterPaths.size > 0) {
|
|
216
|
-
files = files.filter(
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
|
|
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) {
|
|
220
398
|
emitProgress(buildProgressPayload({
|
|
221
399
|
phase: "finished",
|
|
400
|
+
runId,
|
|
222
401
|
trigger,
|
|
223
402
|
startedAt,
|
|
224
403
|
totalFiles: 0,
|
|
225
404
|
completedFiles: 0,
|
|
405
|
+
runResults,
|
|
226
406
|
}));
|
|
227
407
|
const state = {
|
|
228
|
-
trigger,
|
|
229
|
-
started_at: startedAt,
|
|
230
|
-
finished_at: new Date().toISOString(),
|
|
231
|
-
summary: buildEmptySummary(0),
|
|
232
|
-
results:
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
const summary = buildEmptySummary();
|
|
239
|
-
const batches = chunk(files, cfg.batchSize);
|
|
240
|
-
totalFiles = files.length;
|
|
241
|
-
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
|
+
}
|
|
242
418
|
|
|
243
419
|
emitProgress(buildProgressPayload({
|
|
244
420
|
phase: "started",
|
|
421
|
+
runId,
|
|
245
422
|
trigger,
|
|
246
423
|
startedAt,
|
|
247
424
|
totalFiles,
|
|
248
425
|
completedFiles: 0,
|
|
249
|
-
batchCount,
|
|
250
426
|
currentFilePaths: files.map((file) => file.filePath),
|
|
427
|
+
runResults,
|
|
251
428
|
}));
|
|
252
429
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
startedAt,
|
|
259
|
-
totalFiles,
|
|
260
|
-
completedFiles,
|
|
261
|
-
batchIndex: index + 1,
|
|
262
|
-
batchCount,
|
|
263
|
-
currentFilePaths: currentBatchPaths,
|
|
264
|
-
}));
|
|
265
|
-
const response = await client.importMarkdown(batch);
|
|
266
|
-
mergeSummary(summary, response.summary ?? {});
|
|
267
|
-
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;
|
|
268
435
|
emitProgress(buildProgressPayload({
|
|
269
|
-
phase: "
|
|
436
|
+
phase: "file-started",
|
|
437
|
+
runId,
|
|
270
438
|
trigger,
|
|
271
439
|
startedAt,
|
|
272
440
|
totalFiles,
|
|
273
441
|
completedFiles,
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
442
|
+
currentFileIndex,
|
|
443
|
+
currentFilePath,
|
|
444
|
+
currentStage: "parse",
|
|
445
|
+
currentFilePaths: [file.filePath],
|
|
446
|
+
completedFilePaths: successfulPaths,
|
|
447
|
+
failedFilePaths: failedPaths,
|
|
448
|
+
runResults,
|
|
277
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
|
+
}
|
|
278
543
|
}
|
|
279
|
-
|
|
280
|
-
const
|
|
281
|
-
|
|
282
|
-
// Merge with existing state — preserve previously synced files
|
|
283
|
-
let mergedResults = newResults;
|
|
284
|
-
if (filterPaths instanceof Set && filterPaths.size > 0) {
|
|
285
|
-
const prevState = await readLastSyncState(getStatePath());
|
|
286
|
-
const prevResults = Array.isArray(prevState?.results) ? prevState.results : [];
|
|
287
|
-
// Keep previous results that weren't in this batch, add new ones
|
|
288
|
-
const newPathSet = new Set(newResults.map(r => r.filePath));
|
|
289
|
-
mergedResults = [
|
|
290
|
-
...prevResults.filter(r => !newPathSet.has(r.filePath || r.file_path)),
|
|
291
|
-
...newResults,
|
|
292
|
-
];
|
|
293
|
-
}
|
|
294
|
-
|
|
544
|
+
|
|
545
|
+
const mergedResults = [...resultMap.values()];
|
|
295
546
|
const state = {
|
|
296
|
-
trigger,
|
|
297
|
-
started_at: startedAt,
|
|
298
|
-
finished_at: new Date().toISOString(),
|
|
299
|
-
summary,
|
|
300
|
-
results: mergedResults,
|
|
547
|
+
trigger,
|
|
548
|
+
started_at: startedAt,
|
|
549
|
+
finished_at: new Date().toISOString(),
|
|
550
|
+
summary,
|
|
551
|
+
results: mergedResults,
|
|
552
|
+
run_results: runResults,
|
|
301
553
|
};
|
|
302
554
|
await writeLastSyncState(getStatePath(), state);
|
|
555
|
+
|
|
303
556
|
emitProgress(buildProgressPayload({
|
|
304
557
|
phase: "finished",
|
|
558
|
+
runId,
|
|
305
559
|
trigger,
|
|
306
560
|
startedAt,
|
|
307
561
|
totalFiles,
|
|
308
562
|
completedFiles,
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
completedFilePaths:
|
|
563
|
+
currentFileIndex,
|
|
564
|
+
currentFilePath,
|
|
565
|
+
completedFilePaths: successfulPaths,
|
|
566
|
+
failedFilePaths: failedPaths,
|
|
567
|
+
runResults,
|
|
312
568
|
}));
|
|
569
|
+
|
|
313
570
|
api.logger?.info?.(
|
|
314
|
-
`[echo-memory] sync complete: files=${summary.file_count} new_memories=${summary.new_memory_count} skipped=${summary.skipped_count} failed=${summary.failed_file_count}`,
|
|
315
|
-
);
|
|
316
|
-
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;
|
|
317
574
|
} catch (error) {
|
|
318
575
|
const message = String(error?.message ?? error);
|
|
319
576
|
emitProgress(buildProgressPayload({
|
|
320
577
|
phase: "failed",
|
|
578
|
+
runId,
|
|
321
579
|
trigger,
|
|
322
580
|
startedAt,
|
|
323
581
|
totalFiles,
|
|
324
582
|
completedFiles,
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
583
|
+
currentFileIndex,
|
|
584
|
+
currentFilePath,
|
|
585
|
+
currentFilePaths: currentFilePath ? [currentFilePath] : [],
|
|
586
|
+
failedFilePaths: currentFilePath ? [currentFilePath] : [],
|
|
587
|
+
runResults,
|
|
328
588
|
error: message,
|
|
329
589
|
}));
|
|
590
|
+
|
|
330
591
|
const state = {
|
|
331
|
-
trigger,
|
|
332
|
-
started_at: startedAt,
|
|
333
|
-
finished_at: new Date().toISOString(),
|
|
334
|
-
error: message,
|
|
335
|
-
summary: buildEmptySummary(0),
|
|
336
|
-
results: [],
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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
|
+
|
|
366
637
|
return {
|
|
367
638
|
initialize,
|
|
368
639
|
getStatePath,
|
|
369
640
|
onProgress,
|
|
370
641
|
runSync,
|
|
371
|
-
startInterval,
|
|
372
|
-
stopInterval,
|
|
373
|
-
|
|
374
|
-
|
|
642
|
+
startInterval,
|
|
643
|
+
stopInterval,
|
|
644
|
+
isRunning,
|
|
645
|
+
getActiveRunInfo,
|
|
646
|
+
};
|
|
647
|
+
}
|