codesesh 0.13.0 → 0.15.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/README.md +2 -0
- package/dist/{chunk-BV65IEWZ.js → chunk-NBCLV4CX.js} +1255 -1220
- package/dist/chunk-NBCLV4CX.js.map +1 -0
- package/dist/{dist-VD54GDPO.js → dist-5356XOFP.js} +17 -10
- package/dist/index.js +959 -867
- package/dist/index.js.map +1 -1
- package/dist/scan-refresh-worker.js +1 -1
- package/dist/search-index-worker.js +1 -1
- package/dist/smart-tag-worker.js +1 -1
- package/dist/web/assets/index-B6p64k_Q.js +113 -0
- package/dist/web/assets/index-DiFr4_5-.css +2 -0
- package/dist/web/assets/react-3acXkZNw.js +1821 -0
- package/dist/web/assets/vendor-CShgXaOi.js +40 -0
- package/dist/web/index.html +4 -4
- package/package.json +1 -1
- package/dist/chunk-BV65IEWZ.js.map +0 -1
- package/dist/web/assets/index-ZikfLnUo.js +0 -113
- package/dist/web/assets/index-y6Zx3LT1.css +0 -2
- package/dist/web/assets/react-DBHwgW7V.js +0 -1821
- package/dist/web/assets/vendor-bXsb4Kz4.js +0 -40
- /package/dist/{dist-VD54GDPO.js.map → dist-5356XOFP.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
BookmarkStorageUnavailableError,
|
|
4
3
|
FileSystemSessionSource,
|
|
4
|
+
StateStorageUnavailableError,
|
|
5
5
|
attachMissingProjectIdentities,
|
|
6
6
|
buildAgentCacheMeta,
|
|
7
7
|
buildDashboard,
|
|
@@ -11,9 +11,9 @@ import {
|
|
|
11
11
|
createProjectScopeMatcher,
|
|
12
12
|
createRegisteredAgents,
|
|
13
13
|
deleteBookmark,
|
|
14
|
+
deleteSessionAlias,
|
|
14
15
|
executeSessionSearch,
|
|
15
16
|
extractSessionFileActivity,
|
|
16
|
-
filterSessions,
|
|
17
17
|
getAgentInfoMap,
|
|
18
18
|
getAgentLastFullSyncAt,
|
|
19
19
|
getCursorDataPath,
|
|
@@ -27,12 +27,14 @@ import {
|
|
|
27
27
|
listBookmarks,
|
|
28
28
|
listCachedProjectGroups,
|
|
29
29
|
listFileActivity,
|
|
30
|
+
listSessionAliases,
|
|
30
31
|
listSessionFileActivity,
|
|
31
32
|
loadCachedSessionData,
|
|
32
33
|
loadCachedSessions,
|
|
33
34
|
markAgentFullSyncCompleted,
|
|
34
35
|
matchesProjectIdentity,
|
|
35
36
|
matchesProjectScope,
|
|
37
|
+
mergeSearchQueryOptions,
|
|
36
38
|
perf,
|
|
37
39
|
realFs,
|
|
38
40
|
refreshPricingCache,
|
|
@@ -41,8 +43,9 @@ import {
|
|
|
41
43
|
sessionSignature,
|
|
42
44
|
sortSessions,
|
|
43
45
|
startOfLocalDay,
|
|
44
|
-
upsertBookmark
|
|
45
|
-
|
|
46
|
+
upsertBookmark,
|
|
47
|
+
upsertSessionAlias
|
|
48
|
+
} from "./chunk-NBCLV4CX.js";
|
|
46
49
|
|
|
47
50
|
// src/index.ts
|
|
48
51
|
import { defineCommand, runMain } from "citty";
|
|
@@ -208,7 +211,131 @@ function logSearchIndexSync(context, result, data = {}) {
|
|
|
208
211
|
});
|
|
209
212
|
}
|
|
210
213
|
|
|
214
|
+
// src/time-window-resolution.ts
|
|
215
|
+
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
216
|
+
var DEFAULT_DASHBOARD_DAYS = 30;
|
|
217
|
+
function resolveTimeWindow(request) {
|
|
218
|
+
return request.mode === "cli" ? resolveCliWindow(request) : resolveDashboardWindow(request);
|
|
219
|
+
}
|
|
220
|
+
function resolveCliWindow(request) {
|
|
221
|
+
const now = request.now ?? Date.now();
|
|
222
|
+
const from = parseRequiredDate(request.from);
|
|
223
|
+
const to = parseRequiredDate(request.to);
|
|
224
|
+
if (from != null) return { from, to };
|
|
225
|
+
const days = parseDays(request.days);
|
|
226
|
+
if (days === 0) return { to, days };
|
|
227
|
+
if (days == null || days < 0) return { to };
|
|
228
|
+
const rollingFrom = now - days * DAY_MS;
|
|
229
|
+
return Number.isFinite(rollingFrom) ? { from: rollingFrom, to, days } : { to };
|
|
230
|
+
}
|
|
231
|
+
function resolveDashboardWindow(request) {
|
|
232
|
+
const now = request.now ?? Date.now();
|
|
233
|
+
const defaults = request.defaults ?? {};
|
|
234
|
+
const to = parseOptionalDate(request.query.to) ?? defaults.to ?? now;
|
|
235
|
+
const hasQueryDays = Boolean(request.query.days?.trim());
|
|
236
|
+
const parsedDays = hasQueryDays ? parseDays(request.query.days) : void 0;
|
|
237
|
+
let days = parsedDays != null && parsedDays > 0 ? parsedDays : defaults.days;
|
|
238
|
+
const queryFrom = parseOptionalDate(request.query.from);
|
|
239
|
+
if (queryFrom != null) {
|
|
240
|
+
days ??= elapsedDays(queryFrom, to);
|
|
241
|
+
return { from: queryFrom, to, days };
|
|
242
|
+
}
|
|
243
|
+
if (parsedDays === 0 || !hasQueryDays && defaults.days === 0) {
|
|
244
|
+
return { to, days: 0 };
|
|
245
|
+
}
|
|
246
|
+
if (defaults.from != null) {
|
|
247
|
+
days ??= elapsedDays(defaults.from, to);
|
|
248
|
+
return { from: defaults.from, to, days };
|
|
249
|
+
}
|
|
250
|
+
const resolvedDays = days != null && days > 0 ? days : DEFAULT_DASHBOARD_DAYS;
|
|
251
|
+
return {
|
|
252
|
+
from: startOfLocalDay(to) - (resolvedDays - 1) * DAY_MS,
|
|
253
|
+
to,
|
|
254
|
+
days: resolvedDays
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
function parseRequiredDate(value) {
|
|
258
|
+
if (!value) return void 0;
|
|
259
|
+
const timestamp = new Date(value).getTime();
|
|
260
|
+
if (Number.isNaN(timestamp)) throw new Error(`Invalid date: ${value}`);
|
|
261
|
+
return timestamp;
|
|
262
|
+
}
|
|
263
|
+
function parseOptionalDate(value) {
|
|
264
|
+
if (value == null) return void 0;
|
|
265
|
+
const timestamp = new Date(value).getTime();
|
|
266
|
+
return Number.isNaN(timestamp) ? void 0 : timestamp;
|
|
267
|
+
}
|
|
268
|
+
function parseDays(value) {
|
|
269
|
+
if (value == null) return void 0;
|
|
270
|
+
const days = Number.parseInt(value, 10);
|
|
271
|
+
return Number.isSafeInteger(days) && Number.isSafeInteger(days * DAY_MS) ? days : void 0;
|
|
272
|
+
}
|
|
273
|
+
function elapsedDays(from, to) {
|
|
274
|
+
return Math.max(1, Math.ceil((to - from) / DAY_MS));
|
|
275
|
+
}
|
|
276
|
+
|
|
211
277
|
// src/api/handlers.ts
|
|
278
|
+
function getSessionAliasKey(agentKey, sessionId) {
|
|
279
|
+
return `${agentKey.toLowerCase()}\0${sessionId}`;
|
|
280
|
+
}
|
|
281
|
+
function getSessionAgentKey(session) {
|
|
282
|
+
return session.slug.split("/")[0]?.toLowerCase() ?? "";
|
|
283
|
+
}
|
|
284
|
+
function loadSessionAliasMap() {
|
|
285
|
+
try {
|
|
286
|
+
return new Map(
|
|
287
|
+
listSessionAliases().map((alias) => [
|
|
288
|
+
getSessionAliasKey(alias.agentKey, alias.sessionId),
|
|
289
|
+
alias.alias
|
|
290
|
+
])
|
|
291
|
+
);
|
|
292
|
+
} catch (error) {
|
|
293
|
+
if (!(error instanceof StateStorageUnavailableError)) {
|
|
294
|
+
appLogger.warn("api.session_aliases.load_failed", {
|
|
295
|
+
error: error instanceof Error ? error.message : String(error)
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
return /* @__PURE__ */ new Map();
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
function isStateStorageUnavailable(error) {
|
|
302
|
+
return error instanceof StateStorageUnavailableError;
|
|
303
|
+
}
|
|
304
|
+
function withDisplayTitle(session, agentKey, aliases) {
|
|
305
|
+
const alias = aliases.get(getSessionAliasKey(agentKey, session.id));
|
|
306
|
+
return alias ? { ...session, display_title: alias } : session;
|
|
307
|
+
}
|
|
308
|
+
function withBookmarkDisplayTitle(bookmark, aliases) {
|
|
309
|
+
const alias = aliases.get(getSessionAliasKey(bookmark.agentKey, bookmark.sessionId));
|
|
310
|
+
return alias ? { ...bookmark, display_title: alias } : bookmark;
|
|
311
|
+
}
|
|
312
|
+
function withFileActivityDisplayTitle(activity, aliases) {
|
|
313
|
+
return {
|
|
314
|
+
...activity,
|
|
315
|
+
session: withDisplayTitle(activity.session, activity.agent_name, aliases)
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
function findAliasSearchResults(query, options, scanResult, aliases) {
|
|
319
|
+
const search = mergeSearchQueryOptions(query, options);
|
|
320
|
+
const needle = search.text.trim().toLowerCase();
|
|
321
|
+
if (!needle || aliases.size === 0) return [];
|
|
322
|
+
return executeSessionSearch(
|
|
323
|
+
"",
|
|
324
|
+
{ ...search.options, limit: Math.max(scanResult.sessions.length, 1) },
|
|
325
|
+
scanResult
|
|
326
|
+
).flatMap((result) => {
|
|
327
|
+
const alias = aliases.get(getSessionAliasKey(result.agentName, result.session.id));
|
|
328
|
+
if (!alias || !alias.toLowerCase().includes(needle)) return [];
|
|
329
|
+
return [
|
|
330
|
+
{
|
|
331
|
+
agentName: result.agentName,
|
|
332
|
+
session: withDisplayTitle(result.session, result.agentName, aliases),
|
|
333
|
+
snippet: `Alias \xB7 ${result.session.directory}`,
|
|
334
|
+
matchType: "title"
|
|
335
|
+
}
|
|
336
|
+
];
|
|
337
|
+
});
|
|
338
|
+
}
|
|
212
339
|
function isRecord(value) {
|
|
213
340
|
return typeof value === "object" && value !== null;
|
|
214
341
|
}
|
|
@@ -288,9 +415,6 @@ function parseSearchOptions(c, defaults, projectIdentity) {
|
|
|
288
415
|
limit: limitValue && limitValue > 0 ? Math.min(limitValue, 100) : 50
|
|
289
416
|
};
|
|
290
417
|
}
|
|
291
|
-
function filterSessionsByWindow(sessions, from, to) {
|
|
292
|
-
return filterSessionsByActivityWindow(sessions, from, to);
|
|
293
|
-
}
|
|
294
418
|
function filterSessionsByActivityWindow(sessions, from, to) {
|
|
295
419
|
if (from == null && to == null) return sessions;
|
|
296
420
|
return sessions.filter((session) => {
|
|
@@ -382,19 +506,20 @@ function handleGetScanStatus(c, scanSource) {
|
|
|
382
506
|
}
|
|
383
507
|
function handleGetAgents(c, scanSource, defaults = {}) {
|
|
384
508
|
const scanResult = scanSource.getSnapshot();
|
|
385
|
-
const
|
|
509
|
+
const from = parseDateParam(c.req.query("from"), defaults.from);
|
|
510
|
+
const to = parseDateParam(c.req.query("to"), defaults.to);
|
|
386
511
|
const counts = Object.fromEntries(
|
|
387
512
|
Object.entries(scanResult.byAgent).map(([agentName, sessions]) => [
|
|
388
513
|
agentName,
|
|
389
|
-
|
|
514
|
+
filterSessionsByActivityWindow(sessions, from, to).length
|
|
390
515
|
])
|
|
391
516
|
);
|
|
392
|
-
|
|
393
|
-
return c.json(info);
|
|
517
|
+
return c.json(getAgentInfoMap(counts));
|
|
394
518
|
}
|
|
395
519
|
function handleGetProjects(c, scanSource, defaults = {}) {
|
|
396
520
|
const scanResult = scanSource.getSnapshot();
|
|
397
|
-
const
|
|
521
|
+
const from = parseDateParam(c.req.query("from"), defaults.from);
|
|
522
|
+
const to = parseDateParam(c.req.query("to"), defaults.to);
|
|
398
523
|
const sessions = filterSessionsByActivityWindow(scanResult.sessions, from, to);
|
|
399
524
|
return c.json({
|
|
400
525
|
projects: attachProjectMetrics(listCachedProjectGroups(sessions), sessions)
|
|
@@ -433,10 +558,18 @@ function handleGetSessions(c, scanSource, defaults = {}) {
|
|
|
433
558
|
if (tag) {
|
|
434
559
|
sessions = sessions.filter((s) => s.smart_tags?.includes(tag));
|
|
435
560
|
}
|
|
561
|
+
const aliases = loadSessionAliasMap();
|
|
436
562
|
if (q) {
|
|
437
|
-
sessions = sessions.filter((
|
|
563
|
+
sessions = sessions.filter((session) => {
|
|
564
|
+
const alias = aliases.get(getSessionAliasKey(getSessionAgentKey(session), session.id));
|
|
565
|
+
return session.title.toLowerCase().includes(q) || alias?.toLowerCase().includes(q);
|
|
566
|
+
});
|
|
438
567
|
}
|
|
439
|
-
return c.json({
|
|
568
|
+
return c.json({
|
|
569
|
+
sessions: sessions.map(
|
|
570
|
+
(session) => withDisplayTitle(session, getSessionAgentKey(session), aliases)
|
|
571
|
+
)
|
|
572
|
+
});
|
|
440
573
|
}
|
|
441
574
|
function handleSearchSessions(c, scanSource, defaults = {}) {
|
|
442
575
|
const query = c.req.query("q")?.trim() ?? "";
|
|
@@ -449,8 +582,17 @@ function handleSearchSessions(c, scanSource, defaults = {}) {
|
|
|
449
582
|
return c.json({ error: "projectKind and projectKey must form a valid project identity" }, 400);
|
|
450
583
|
}
|
|
451
584
|
const searchOptions = parseSearchOptions(c, defaults, projectIdentity);
|
|
452
|
-
const
|
|
453
|
-
|
|
585
|
+
const aliases = loadSessionAliasMap();
|
|
586
|
+
const results = executeSessionSearch(query, searchOptions, scanResult).map((result) => ({
|
|
587
|
+
...result,
|
|
588
|
+
session: withDisplayTitle(result.session, result.agentName, aliases)
|
|
589
|
+
}));
|
|
590
|
+
const aliasResults = findAliasSearchResults(query, searchOptions, scanResult, aliases);
|
|
591
|
+
const deduped = /* @__PURE__ */ new Map();
|
|
592
|
+
for (const result of [...aliasResults, ...results]) {
|
|
593
|
+
deduped.set(`${result.agentName}\0${result.session.id}`, result);
|
|
594
|
+
}
|
|
595
|
+
return c.json({ results: [...deduped.values()].slice(0, searchOptions.limit ?? 50) });
|
|
454
596
|
}
|
|
455
597
|
function parseFileActivityKind(value) {
|
|
456
598
|
if (value === "read" || value === "edit" || value === "write" || value === "delete") {
|
|
@@ -479,6 +621,7 @@ function handleGetFileActivity(c, defaults = {}) {
|
|
|
479
621
|
if (projectIdentity === null) {
|
|
480
622
|
return c.json({ error: "projectKind and projectKey must form a valid project identity" }, 400);
|
|
481
623
|
}
|
|
624
|
+
const aliases = loadSessionAliasMap();
|
|
482
625
|
return c.json({
|
|
483
626
|
activity: listFileActivity({
|
|
484
627
|
agent: optionalQueryValue(c.req.query("agent")),
|
|
@@ -492,7 +635,7 @@ function handleGetFileActivity(c, defaults = {}) {
|
|
|
492
635
|
from: parseDateParam(c.req.query("from"), defaults.from),
|
|
493
636
|
to: parseDateParam(c.req.query("to"), defaults.to),
|
|
494
637
|
limit
|
|
495
|
-
})
|
|
638
|
+
}).map((activity) => withFileActivityDisplayTitle(activity, aliases))
|
|
496
639
|
});
|
|
497
640
|
}
|
|
498
641
|
async function handleGetSessionData(c, scanSource) {
|
|
@@ -539,8 +682,9 @@ async function handleGetSessionData(c, scanSource) {
|
|
|
539
682
|
tag_duration_ms: Math.round(tagDuration),
|
|
540
683
|
duration_ms: Math.round(performance.now() - startedAt)
|
|
541
684
|
});
|
|
685
|
+
const aliases = loadSessionAliasMap();
|
|
542
686
|
return c.json({
|
|
543
|
-
...data,
|
|
687
|
+
...withDisplayTitle(data, agentName, aliases),
|
|
544
688
|
project_identity: projectIdentity,
|
|
545
689
|
smart_tags: smartTags,
|
|
546
690
|
smart_tags_source_updated_at: getSmartTagSourceTimestamp(data),
|
|
@@ -569,9 +713,13 @@ async function handlePostClientLog(c) {
|
|
|
569
713
|
}
|
|
570
714
|
function handleGetBookmarks(c) {
|
|
571
715
|
try {
|
|
572
|
-
|
|
716
|
+
const aliases = loadSessionAliasMap();
|
|
717
|
+
return c.json({
|
|
718
|
+
bookmarks: listBookmarks().map((bookmark) => withBookmarkDisplayTitle(bookmark, aliases)),
|
|
719
|
+
storageAvailable: true
|
|
720
|
+
});
|
|
573
721
|
} catch (error) {
|
|
574
|
-
if (error instanceof
|
|
722
|
+
if (error instanceof StateStorageUnavailableError) {
|
|
575
723
|
return c.json({ bookmarks: [], storageAvailable: false });
|
|
576
724
|
}
|
|
577
725
|
throw error;
|
|
@@ -585,7 +733,7 @@ async function handlePutBookmark(c) {
|
|
|
585
733
|
try {
|
|
586
734
|
return c.json({ bookmark: upsertBookmark(payload), storageAvailable: true });
|
|
587
735
|
} catch (error) {
|
|
588
|
-
if (error instanceof
|
|
736
|
+
if (error instanceof StateStorageUnavailableError) {
|
|
589
737
|
return c.json({ error: "Bookmark storage is unavailable" }, 503);
|
|
590
738
|
}
|
|
591
739
|
throw error;
|
|
@@ -603,7 +751,7 @@ async function handleImportBookmarks(c) {
|
|
|
603
751
|
try {
|
|
604
752
|
return c.json({ bookmarks: importBookmarks(bookmarks), storageAvailable: true });
|
|
605
753
|
} catch (error) {
|
|
606
|
-
if (error instanceof
|
|
754
|
+
if (error instanceof StateStorageUnavailableError) {
|
|
607
755
|
return c.json({ error: "Bookmark storage is unavailable" }, 503);
|
|
608
756
|
}
|
|
609
757
|
throw error;
|
|
@@ -619,36 +767,46 @@ function handleDeleteBookmark(c) {
|
|
|
619
767
|
deleteBookmark(agentKey, sessionId);
|
|
620
768
|
return c.json({ ok: true, storageAvailable: true });
|
|
621
769
|
} catch (error) {
|
|
622
|
-
if (error instanceof
|
|
770
|
+
if (error instanceof StateStorageUnavailableError) {
|
|
623
771
|
return c.json({ error: "Bookmark storage is unavailable" }, 503);
|
|
624
772
|
}
|
|
625
773
|
throw error;
|
|
626
774
|
}
|
|
627
775
|
}
|
|
628
|
-
function
|
|
629
|
-
const
|
|
630
|
-
const
|
|
631
|
-
const
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
776
|
+
async function handlePutSessionAlias(c) {
|
|
777
|
+
const agentKey = c.req.param("agent");
|
|
778
|
+
const sessionId = c.req.param("id");
|
|
779
|
+
const payload = await c.req.json().catch(() => null);
|
|
780
|
+
if (!agentKey || !sessionId || typeof payload?.alias !== "string") {
|
|
781
|
+
return c.json({ error: "Invalid session alias payload" }, 400);
|
|
782
|
+
}
|
|
783
|
+
try {
|
|
784
|
+
return c.json({ alias: upsertSessionAlias(agentKey, sessionId, payload.alias) });
|
|
785
|
+
} catch (error) {
|
|
786
|
+
if (error instanceof TypeError) {
|
|
787
|
+
return c.json({ error: "Session alias must be non-empty and at most 160 characters" }, 400);
|
|
788
|
+
}
|
|
789
|
+
if (isStateStorageUnavailable(error)) {
|
|
790
|
+
return c.json({ error: "Session alias storage is unavailable" }, 503);
|
|
791
|
+
}
|
|
792
|
+
throw error;
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
function handleDeleteSessionAlias(c) {
|
|
796
|
+
const agentKey = c.req.param("agent");
|
|
797
|
+
const sessionId = c.req.param("id");
|
|
798
|
+
if (!agentKey || !sessionId) {
|
|
799
|
+
return c.json({ error: "Missing session alias identifier" }, 400);
|
|
800
|
+
}
|
|
801
|
+
try {
|
|
802
|
+
deleteSessionAlias(agentKey, sessionId);
|
|
803
|
+
return c.json({ ok: true });
|
|
804
|
+
} catch (error) {
|
|
805
|
+
if (isStateStorageUnavailable(error)) {
|
|
806
|
+
return c.json({ error: "Session alias storage is unavailable" }, 503);
|
|
807
|
+
}
|
|
808
|
+
throw error;
|
|
650
809
|
}
|
|
651
|
-
return { from: fromTs, to: toTs, days };
|
|
652
810
|
}
|
|
653
811
|
function handleGetDashboard(c, scanSource, defaults = {}) {
|
|
654
812
|
const scanResult = scanSource.getSnapshot();
|
|
@@ -659,12 +817,15 @@ function handleGetDashboard(c, scanSource, defaults = {}) {
|
|
|
659
817
|
if (projectIdentity === null) {
|
|
660
818
|
return c.json({ error: "projectKind and projectKey must form a valid project identity" }, 400);
|
|
661
819
|
}
|
|
662
|
-
const { from, to, days } =
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
820
|
+
const { from, to, days } = resolveTimeWindow({
|
|
821
|
+
mode: "dashboard",
|
|
822
|
+
query: {
|
|
823
|
+
days: c.req.query("days"),
|
|
824
|
+
from: c.req.query("from"),
|
|
825
|
+
to: c.req.query("to")
|
|
826
|
+
},
|
|
827
|
+
defaults
|
|
828
|
+
});
|
|
668
829
|
const scope = {
|
|
669
830
|
agent: optionalQueryValue(c.req.query("agent"))?.toLowerCase(),
|
|
670
831
|
projectKind: projectIdentity?.kind,
|
|
@@ -691,11 +852,20 @@ function handleGetDashboard(c, scanSource, defaults = {}) {
|
|
|
691
852
|
}),
|
|
692
853
|
window: { from, to, days }
|
|
693
854
|
};
|
|
694
|
-
|
|
855
|
+
const aliases = loadSessionAliasMap();
|
|
856
|
+
return c.json({
|
|
857
|
+
...data,
|
|
858
|
+
recentSessions: data.recentSessions.map(
|
|
859
|
+
(session) => withDisplayTitle(session, session.agentName, aliases)
|
|
860
|
+
),
|
|
861
|
+
recentFileActivities: data.recentFileActivities.map(
|
|
862
|
+
(activity) => withFileActivityDisplayTitle(activity, aliases)
|
|
863
|
+
)
|
|
864
|
+
});
|
|
695
865
|
}
|
|
696
866
|
|
|
697
867
|
// src/api/routes.ts
|
|
698
|
-
function createSseResponse(
|
|
868
|
+
function createSseResponse(eventSource, signal) {
|
|
699
869
|
const encoder = new TextEncoder();
|
|
700
870
|
let cancelStream = () => {
|
|
701
871
|
};
|
|
@@ -712,11 +882,11 @@ function createSseResponse(store, signal) {
|
|
|
712
882
|
`));
|
|
713
883
|
};
|
|
714
884
|
write("connected", { timestamp: Date.now() });
|
|
715
|
-
write("scan-status",
|
|
716
|
-
const unsubscribeSessions =
|
|
885
|
+
write("scan-status", eventSource.getScanStatus());
|
|
886
|
+
const unsubscribeSessions = eventSource.subscribe((event) => {
|
|
717
887
|
write(event.type, event);
|
|
718
888
|
});
|
|
719
|
-
const unsubscribeScanStatus =
|
|
889
|
+
const unsubscribeScanStatus = eventSource.subscribeScanStatus((event) => {
|
|
720
890
|
write(event.type, event);
|
|
721
891
|
});
|
|
722
892
|
const heartbeat = setInterval(() => {
|
|
@@ -753,7 +923,7 @@ function createSseResponse(store, signal) {
|
|
|
753
923
|
}
|
|
754
924
|
);
|
|
755
925
|
}
|
|
756
|
-
function createApiRoutes(scanSource,
|
|
926
|
+
function createApiRoutes(scanSource, eventSource, options = {}) {
|
|
757
927
|
const api = new Hono();
|
|
758
928
|
const listDefaults = {
|
|
759
929
|
from: options.defaultSessionFrom,
|
|
@@ -761,8 +931,8 @@ function createApiRoutes(scanSource, store, options = {}) {
|
|
|
761
931
|
days: options.defaultSessionDays
|
|
762
932
|
};
|
|
763
933
|
api.get("/config", (c) => handleGetConfig(c, listDefaults));
|
|
764
|
-
if (
|
|
765
|
-
api.get("/status", (c) => handleGetScanStatus(c,
|
|
934
|
+
if (eventSource) {
|
|
935
|
+
api.get("/status", (c) => handleGetScanStatus(c, eventSource));
|
|
766
936
|
}
|
|
767
937
|
api.get("/agents", (c) => handleGetAgents(c, scanSource, listDefaults));
|
|
768
938
|
api.get("/projects", (c) => handleGetProjects(c, scanSource, listDefaults));
|
|
@@ -775,9 +945,11 @@ function createApiRoutes(scanSource, store, options = {}) {
|
|
|
775
945
|
api.put("/bookmarks", (c) => handlePutBookmark(c));
|
|
776
946
|
api.post("/bookmarks/import", (c) => handleImportBookmarks(c));
|
|
777
947
|
api.delete("/bookmarks/:agent/:id", (c) => handleDeleteBookmark(c));
|
|
948
|
+
api.put("/session-aliases/:agent/:id", (c) => handlePutSessionAlias(c));
|
|
949
|
+
api.delete("/session-aliases/:agent/:id", (c) => handleDeleteSessionAlias(c));
|
|
778
950
|
api.post("/logs", (c) => handlePostClientLog(c));
|
|
779
|
-
if (
|
|
780
|
-
api.get("/events", (c) => createSseResponse(
|
|
951
|
+
if (eventSource) {
|
|
952
|
+
api.get("/events", (c) => createSseResponse(eventSource, c.req.raw.signal));
|
|
781
953
|
}
|
|
782
954
|
return api;
|
|
783
955
|
}
|
|
@@ -906,14 +1078,7 @@ async function createServer(port, store, options = {}) {
|
|
|
906
1078
|
defaultSessionTo: options.defaultSessionTo,
|
|
907
1079
|
defaultSessionDays: options.defaultSessionDays
|
|
908
1080
|
};
|
|
909
|
-
app.route(
|
|
910
|
-
"/api",
|
|
911
|
-
createApiRoutes(
|
|
912
|
-
store,
|
|
913
|
-
"subscribe" in store ? store : void 0,
|
|
914
|
-
routeOptions
|
|
915
|
-
)
|
|
916
|
-
);
|
|
1081
|
+
app.route("/api", createApiRoutes(store, store, routeOptions));
|
|
917
1082
|
const webDistPath = findWebDistPath();
|
|
918
1083
|
if (webDistPath) {
|
|
919
1084
|
app.use("/*", serveStatic({ root: webDistPath }));
|
|
@@ -935,9 +1100,7 @@ async function createServer(port, store, options = {}) {
|
|
|
935
1100
|
if (isAddressInUse(error) && offset < attempts - 1) {
|
|
936
1101
|
continue;
|
|
937
1102
|
}
|
|
938
|
-
|
|
939
|
-
await store.shutdown();
|
|
940
|
-
}
|
|
1103
|
+
await store.shutdown();
|
|
941
1104
|
if (isAddressInUse(error) && attempts > 1) {
|
|
942
1105
|
throw new Error(
|
|
943
1106
|
`\u7AEF\u53E3 ${port}-${port + attempts - 1} \u5747\u5DF2\u88AB\u5360\u7528\uFF0C\u8BF7\u5173\u95ED\u73B0\u6709\u8FDB\u7A0B\u6216\u6539\u7528 --port \u6307\u5B9A\u5176\u4ED6\u7AEF\u53E3\u3002`
|
|
@@ -971,9 +1134,7 @@ async function createServer(port, store, options = {}) {
|
|
|
971
1134
|
}
|
|
972
1135
|
server.close(() => resolve4());
|
|
973
1136
|
});
|
|
974
|
-
|
|
975
|
-
await store.shutdown();
|
|
976
|
-
}
|
|
1137
|
+
await store.shutdown();
|
|
977
1138
|
}
|
|
978
1139
|
};
|
|
979
1140
|
}
|
|
@@ -981,7 +1142,6 @@ async function createServer(port, store, options = {}) {
|
|
|
981
1142
|
// src/live-scan.ts
|
|
982
1143
|
import { existsSync as existsSync5 } from "fs";
|
|
983
1144
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
984
|
-
import { Worker as Worker2 } from "worker_threads";
|
|
985
1145
|
|
|
986
1146
|
// src/search-index-job-runner.ts
|
|
987
1147
|
import { existsSync as existsSync3 } from "fs";
|
|
@@ -1274,7 +1434,7 @@ var ScanStatusModel = class {
|
|
|
1274
1434
|
agentStatuses: {},
|
|
1275
1435
|
totalAgents: 0,
|
|
1276
1436
|
updatedAt: Date.now(),
|
|
1277
|
-
backfill: { active: false, pendingAgents: [], completedAgents: [] }
|
|
1437
|
+
backfill: { active: false, pendingAgents: [], completedAgents: [], failedAgents: [] }
|
|
1278
1438
|
};
|
|
1279
1439
|
snapshot() {
|
|
1280
1440
|
return {
|
|
@@ -1292,7 +1452,8 @@ var ScanStatusModel = class {
|
|
|
1292
1452
|
backfill: {
|
|
1293
1453
|
...this.status.backfill,
|
|
1294
1454
|
pendingAgents: [...this.status.backfill.pendingAgents],
|
|
1295
|
-
completedAgents: [...this.status.backfill.completedAgents]
|
|
1455
|
+
completedAgents: [...this.status.backfill.completedAgents],
|
|
1456
|
+
failedAgents: [...this.status.backfill.failedAgents]
|
|
1296
1457
|
}
|
|
1297
1458
|
};
|
|
1298
1459
|
}
|
|
@@ -1443,79 +1604,180 @@ var ScanStatusModel = class {
|
|
|
1443
1604
|
}
|
|
1444
1605
|
};
|
|
1445
1606
|
|
|
1446
|
-
// src/
|
|
1447
|
-
var
|
|
1448
|
-
|
|
1449
|
-
currentAgent;
|
|
1450
|
-
completedAgents = [];
|
|
1451
|
-
get isRunning() {
|
|
1452
|
-
return this.currentAgent != null;
|
|
1453
|
-
}
|
|
1454
|
-
enqueue(agentName) {
|
|
1455
|
-
if (this.currentAgent === agentName || this.queue.includes(agentName)) return null;
|
|
1456
|
-
this.queue.push(agentName);
|
|
1457
|
-
return this.snapshot();
|
|
1458
|
-
}
|
|
1459
|
-
take() {
|
|
1460
|
-
if (this.currentAgent) return null;
|
|
1461
|
-
const agentName = this.queue.shift();
|
|
1462
|
-
if (!agentName) return null;
|
|
1463
|
-
this.currentAgent = agentName;
|
|
1464
|
-
return { agentName, status: this.snapshot() };
|
|
1465
|
-
}
|
|
1466
|
-
complete(agentName) {
|
|
1467
|
-
if (this.currentAgent === agentName) this.currentAgent = void 0;
|
|
1468
|
-
if (!this.completedAgents.includes(agentName)) this.completedAgents.push(agentName);
|
|
1469
|
-
return this.snapshot();
|
|
1470
|
-
}
|
|
1471
|
-
clear() {
|
|
1472
|
-
this.queue.length = 0;
|
|
1473
|
-
this.currentAgent = void 0;
|
|
1474
|
-
}
|
|
1475
|
-
snapshot() {
|
|
1476
|
-
return {
|
|
1477
|
-
active: this.currentAgent != null || this.queue.length > 0,
|
|
1478
|
-
pendingAgents: [...this.queue],
|
|
1479
|
-
currentAgent: this.currentAgent,
|
|
1480
|
-
completedAgents: [...this.completedAgents]
|
|
1481
|
-
};
|
|
1482
|
-
}
|
|
1483
|
-
};
|
|
1484
|
-
|
|
1485
|
-
// src/refresh-coordinator.ts
|
|
1607
|
+
// src/agent-sync-engine.ts
|
|
1608
|
+
var REFRESH_DEBOUNCE_MS = 200;
|
|
1609
|
+
var EMPTY_AGENT_REFRESH_DEBOUNCE_MS = 3e4;
|
|
1486
1610
|
var PENDING_REFRESH_DELAY_MS = 100;
|
|
1487
1611
|
var MAX_ADAPTIVE_REFRESH_DELAY_MS = 3e4;
|
|
1488
1612
|
var ADAPTIVE_REFRESH_DELAY_MULTIPLIER = 4;
|
|
1489
|
-
var
|
|
1490
|
-
|
|
1613
|
+
var SEARCH_INDEX_BULK_PENDING_PATH_THRESHOLD = 100;
|
|
1614
|
+
var BACKFILL_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
1615
|
+
function buildRefreshDiff(agentName, previousSessions, nextSessions, candidateChangedIds = []) {
|
|
1616
|
+
const { changes, removedSessionIds, counts } = computeSessionDiff(
|
|
1617
|
+
previousSessions,
|
|
1618
|
+
nextSessions,
|
|
1619
|
+
candidateChangedIds,
|
|
1620
|
+
sessionSignature
|
|
1621
|
+
);
|
|
1622
|
+
if (counts.new === 0 && counts.updated === 0 && counts.removed === 0) {
|
|
1623
|
+
return { event: null, changedSessions: changes, removedSessionIds };
|
|
1624
|
+
}
|
|
1625
|
+
return {
|
|
1626
|
+
changedSessions: changes,
|
|
1627
|
+
removedSessionIds,
|
|
1628
|
+
event: {
|
|
1629
|
+
type: "sessions-updated",
|
|
1630
|
+
changedAgents: [agentName],
|
|
1631
|
+
newSessions: counts.new,
|
|
1632
|
+
updatedSessions: counts.updated,
|
|
1633
|
+
removedSessions: counts.removed,
|
|
1634
|
+
totalSessions: nextSessions.length,
|
|
1635
|
+
timestamp: Date.now(),
|
|
1636
|
+
changedSessionHeads: changes.map(({ session }) => ({ agentName, session })),
|
|
1637
|
+
removedSessionRefs: removedSessionIds.map((sessionId) => ({ agentName, sessionId }))
|
|
1638
|
+
}
|
|
1639
|
+
};
|
|
1640
|
+
}
|
|
1641
|
+
function restoreAgentCacheMeta(agent, cached) {
|
|
1642
|
+
agent.setSessionMetaMap(new Map(Object.entries(cached.meta)));
|
|
1643
|
+
}
|
|
1644
|
+
var AgentSyncEngine = class {
|
|
1645
|
+
constructor(options) {
|
|
1646
|
+
this.options = options;
|
|
1647
|
+
}
|
|
1648
|
+
options;
|
|
1649
|
+
refreshStates = /* @__PURE__ */ new Map();
|
|
1491
1650
|
operationGenerations = /* @__PURE__ */ new Map();
|
|
1492
1651
|
operationTails = /* @__PURE__ */ new Map();
|
|
1652
|
+
backfillQueue = [];
|
|
1653
|
+
currentBackfillAgent;
|
|
1654
|
+
completedBackfillAgents = [];
|
|
1655
|
+
failedBackfillAgents = [];
|
|
1656
|
+
sessionsChangedListeners = /* @__PURE__ */ new Set();
|
|
1657
|
+
statusChangedListeners = /* @__PURE__ */ new Set();
|
|
1658
|
+
scanStatus = new ScanStatusModel();
|
|
1659
|
+
searchIndexJobs = new SearchIndexJobRunner();
|
|
1660
|
+
backgroundRefreshTimer = null;
|
|
1493
1661
|
isShuttingDown = false;
|
|
1494
|
-
|
|
1495
|
-
|
|
1662
|
+
initialize(cacheTimestamps = {}) {
|
|
1663
|
+
for (const agent of this.options.snapshot().agents) {
|
|
1664
|
+
this.state(agent.name).lastRefreshAt = cacheTimestamps[agent.name] ?? Date.now();
|
|
1665
|
+
}
|
|
1496
1666
|
}
|
|
1497
|
-
|
|
1498
|
-
return
|
|
1667
|
+
status() {
|
|
1668
|
+
return this.scanStatus.snapshot();
|
|
1499
1669
|
}
|
|
1500
|
-
|
|
1501
|
-
this.
|
|
1670
|
+
subscribeSessionsChanged(listener) {
|
|
1671
|
+
this.sessionsChangedListeners.add(listener);
|
|
1672
|
+
return () => this.sessionsChangedListeners.delete(listener);
|
|
1502
1673
|
}
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1674
|
+
subscribeStatusChanged(listener) {
|
|
1675
|
+
this.statusChangedListeners.add(listener);
|
|
1676
|
+
return () => this.statusChangedListeners.delete(listener);
|
|
1677
|
+
}
|
|
1678
|
+
async syncInitialIndex() {
|
|
1679
|
+
await this.searchIndexJobs.enqueue(
|
|
1680
|
+
"scan.initial",
|
|
1681
|
+
this.buildFullSearchIndexJobs("scan.initial")
|
|
1682
|
+
);
|
|
1683
|
+
}
|
|
1684
|
+
handleAgentsChanged(agentNames) {
|
|
1685
|
+
const snapshot = this.options.snapshot();
|
|
1686
|
+
for (const agentName of agentNames) {
|
|
1687
|
+
this.state(agentName).pendingPathCount += 1;
|
|
1688
|
+
const delayMs = (snapshot.byAgent[agentName]?.length ?? 0) === 0 ? EMPTY_AGENT_REFRESH_DEBOUNCE_MS : REFRESH_DEBOUNCE_MS;
|
|
1689
|
+
this.scheduleRefresh(agentName, delayMs);
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
startBackgroundRefresh() {
|
|
1693
|
+
if (this.backgroundRefreshTimer) return;
|
|
1694
|
+
const agentNames = this.options.snapshot().agents.map((agent) => agent.name);
|
|
1695
|
+
this.startScanBatch(agentNames, "scanning");
|
|
1696
|
+
this.backgroundRefreshTimer = setTimeout(() => {
|
|
1697
|
+
this.backgroundRefreshTimer = null;
|
|
1698
|
+
for (const agentName of agentNames) this.scheduleRefresh(agentName, 0);
|
|
1699
|
+
if (agentNames.length === 0) this.finishScanBatch();
|
|
1700
|
+
}, 0);
|
|
1701
|
+
}
|
|
1702
|
+
async refresh(agentName) {
|
|
1703
|
+
await this.runCoalescedRefresh(agentName);
|
|
1704
|
+
}
|
|
1705
|
+
async shutdown() {
|
|
1706
|
+
this.isShuttingDown = true;
|
|
1707
|
+
const activeOperations = {
|
|
1708
|
+
agent_operations: this.operationTails.size,
|
|
1709
|
+
refreshes: [...this.refreshStates.values()].filter((state) => state.isRunning).length,
|
|
1710
|
+
backfill_running: this.currentBackfillAgent != null || void 0,
|
|
1711
|
+
scan_workers: this.options.workerRunner.activeCount
|
|
1712
|
+
};
|
|
1713
|
+
if (activeOperations.agent_operations > 0 || activeOperations.scan_workers > 0) {
|
|
1714
|
+
appLogger.warn("scan.shutdown.active_operations", activeOperations);
|
|
1715
|
+
}
|
|
1716
|
+
for (const state of this.refreshStates.values()) {
|
|
1717
|
+
if (!state.timer) continue;
|
|
1718
|
+
clearTimeout(state.timer);
|
|
1719
|
+
state.timer = null;
|
|
1720
|
+
state.timerDeadline = 0;
|
|
1721
|
+
}
|
|
1722
|
+
if (this.backgroundRefreshTimer) {
|
|
1723
|
+
clearTimeout(this.backgroundRefreshTimer);
|
|
1724
|
+
this.backgroundRefreshTimer = null;
|
|
1725
|
+
}
|
|
1726
|
+
this.backfillQueue.length = 0;
|
|
1727
|
+
this.currentBackfillAgent = void 0;
|
|
1728
|
+
const searchIndexSnapshot = this.searchIndexJobs.snapshot();
|
|
1729
|
+
appLogger.info("search_index.shutdown.started", {
|
|
1730
|
+
active_batch_id: searchIndexSnapshot.activeBatchId,
|
|
1731
|
+
pending_batches: searchIndexSnapshot.pendingBatches
|
|
1732
|
+
});
|
|
1733
|
+
await this.searchIndexJobs.shutdown();
|
|
1734
|
+
await this.options.workerRunner.shutdown();
|
|
1735
|
+
await Promise.allSettled(this.operationTails.values());
|
|
1736
|
+
const stoppedSearchIndexSnapshot = this.searchIndexJobs.snapshot();
|
|
1737
|
+
appLogger.info("search_index.shutdown.completed", {
|
|
1738
|
+
active_batch_id: searchIndexSnapshot.activeBatchId,
|
|
1739
|
+
pending_batches: stoppedSearchIndexSnapshot.pendingBatches
|
|
1740
|
+
});
|
|
1741
|
+
}
|
|
1742
|
+
startScanBatch(agentNames, phase) {
|
|
1743
|
+
const snapshot = this.options.snapshot();
|
|
1744
|
+
const sessionCounts = Object.fromEntries(
|
|
1745
|
+
agentNames.map((agentName) => [agentName, snapshot.byAgent[agentName]?.length ?? 0])
|
|
1746
|
+
);
|
|
1747
|
+
this.publishStatus(this.scanStatus.startBatch(agentNames, phase, sessionCounts));
|
|
1748
|
+
}
|
|
1749
|
+
setScanPhase(phase) {
|
|
1750
|
+
this.publishStatus(this.scanStatus.setPhase(phase));
|
|
1751
|
+
}
|
|
1752
|
+
beginAgentScan(agentName) {
|
|
1753
|
+
const snapshot = this.options.snapshot();
|
|
1754
|
+
if (!this.scanStatus.snapshot().active) this.startScanBatch([agentName], "scanning");
|
|
1755
|
+
this.publishStatus(
|
|
1756
|
+
this.scanStatus.beginAgent(agentName, snapshot.byAgent[agentName]?.length ?? 0)
|
|
1757
|
+
);
|
|
1758
|
+
}
|
|
1759
|
+
updateAgentScanProgress(agentName, progress) {
|
|
1760
|
+
this.publishStatus(this.scanStatus.updateAgent(agentName, progress));
|
|
1761
|
+
}
|
|
1762
|
+
finishAgentScan(agentName) {
|
|
1763
|
+
const count = this.options.snapshot().byAgent[agentName]?.length;
|
|
1764
|
+
this.publishStatus(this.scanStatus.finishAgent(agentName, count));
|
|
1765
|
+
}
|
|
1766
|
+
finishScanBatch() {
|
|
1767
|
+
this.publishStatus(this.scanStatus.finishBatch());
|
|
1508
1768
|
}
|
|
1509
|
-
|
|
1510
|
-
|
|
1769
|
+
publishBackfillStatus() {
|
|
1770
|
+
this.publishStatus(this.scanStatus.updateBackfill(this.backfillStatus()));
|
|
1511
1771
|
}
|
|
1512
|
-
|
|
1513
|
-
this.
|
|
1772
|
+
publishStatus(event) {
|
|
1773
|
+
if (!event || this.isShuttingDown) return;
|
|
1774
|
+
for (const listener of this.statusChangedListeners) listener(event);
|
|
1514
1775
|
}
|
|
1515
|
-
|
|
1516
|
-
this.
|
|
1776
|
+
emitSessionsChanged(change) {
|
|
1777
|
+
if (this.isShuttingDown) return;
|
|
1778
|
+
for (const listener of this.sessionsChangedListeners) listener(change);
|
|
1517
1779
|
}
|
|
1518
|
-
|
|
1780
|
+
scheduleRefresh(agentName, delayMs) {
|
|
1519
1781
|
if (this.isShuttingDown) return;
|
|
1520
1782
|
const state = this.state(agentName);
|
|
1521
1783
|
const adaptiveDelayMs = Math.min(
|
|
@@ -1532,10 +1794,10 @@ var RefreshCoordinator = class {
|
|
|
1532
1794
|
state.timerDeadline = deadline;
|
|
1533
1795
|
state.timer = setTimeout(() => {
|
|
1534
1796
|
state.timer = null;
|
|
1535
|
-
void
|
|
1797
|
+
void this.runCoalescedRefresh(agentName);
|
|
1536
1798
|
}, effectiveDelayMs);
|
|
1537
1799
|
}
|
|
1538
|
-
async
|
|
1800
|
+
async runCoalescedRefresh(agentName) {
|
|
1539
1801
|
const state = this.state(agentName);
|
|
1540
1802
|
if (state.isRunning) {
|
|
1541
1803
|
appLogger.debug("scan.refresh.pending", { agent: agentName });
|
|
@@ -1544,91 +1806,423 @@ var RefreshCoordinator = class {
|
|
|
1544
1806
|
}
|
|
1545
1807
|
state.isRunning = true;
|
|
1546
1808
|
try {
|
|
1547
|
-
await this.serialize(agentName, "refresh",
|
|
1809
|
+
await this.serialize(agentName, "refresh", () => this.performRefresh(agentName));
|
|
1548
1810
|
} finally {
|
|
1549
1811
|
state.isRunning = false;
|
|
1550
1812
|
if (state.hasPendingRerun && !this.isShuttingDown) {
|
|
1551
1813
|
state.hasPendingRerun = false;
|
|
1552
|
-
this.
|
|
1553
|
-
agentName,
|
|
1554
|
-
PENDING_REFRESH_DELAY_MS,
|
|
1555
|
-
() => this.runRefresh(agentName, operation)
|
|
1556
|
-
);
|
|
1814
|
+
this.scheduleRefresh(agentName, PENDING_REFRESH_DELAY_MS);
|
|
1557
1815
|
}
|
|
1558
1816
|
}
|
|
1559
1817
|
}
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
}
|
|
1573
|
-
});
|
|
1574
|
-
const tail = run.then(
|
|
1575
|
-
() => void 0,
|
|
1576
|
-
() => void 0
|
|
1577
|
-
);
|
|
1578
|
-
this.operationTails.set(agentName, tail);
|
|
1579
|
-
void tail.finally(() => {
|
|
1580
|
-
if (this.operationTails.get(agentName) === tail) this.operationTails.delete(agentName);
|
|
1581
|
-
});
|
|
1582
|
-
return run;
|
|
1583
|
-
}
|
|
1584
|
-
async shutdown() {
|
|
1585
|
-
this.isShuttingDown = true;
|
|
1586
|
-
for (const state of this.states.values()) {
|
|
1587
|
-
if (!state.timer) continue;
|
|
1588
|
-
clearTimeout(state.timer);
|
|
1589
|
-
state.timer = null;
|
|
1590
|
-
state.timerDeadline = 0;
|
|
1818
|
+
async performRefresh(agentName) {
|
|
1819
|
+
this.beginAgentScan(agentName);
|
|
1820
|
+
try {
|
|
1821
|
+
return await this.runRefresh(agentName);
|
|
1822
|
+
} catch (error) {
|
|
1823
|
+
appLogger.error("scan.refresh.error", { agent: agentName, error });
|
|
1824
|
+
console.error(`[${agentName}] Session refresh failed:`, error);
|
|
1825
|
+
return "failed";
|
|
1826
|
+
} finally {
|
|
1827
|
+
this.finishAgentScan(agentName);
|
|
1828
|
+
const agent = this.findAgent(agentName);
|
|
1829
|
+
if (agent && this.needsBackfill(agent)) this.enqueueBackfill(agentName);
|
|
1591
1830
|
}
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
const
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
const
|
|
1611
|
-
const
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1831
|
+
}
|
|
1832
|
+
async runRefresh(agentName) {
|
|
1833
|
+
const startedAt = performance.now();
|
|
1834
|
+
const state = this.state(agentName);
|
|
1835
|
+
const pendingPathCount = state.pendingPathCount;
|
|
1836
|
+
state.pendingPathCount = 0;
|
|
1837
|
+
const agent = this.findAgent(agentName);
|
|
1838
|
+
if (!agent) {
|
|
1839
|
+
appLogger.warn("scan.refresh.missing_agent", { agent: agentName });
|
|
1840
|
+
return "skipped";
|
|
1841
|
+
}
|
|
1842
|
+
const previousSessions = this.options.snapshot().byAgent[agentName] ?? [];
|
|
1843
|
+
const cached = loadCachedSessions(agentName);
|
|
1844
|
+
const refreshBaseline = cached?.sessions ?? previousSessions;
|
|
1845
|
+
const cacheTimestamp = cached?.timestamp ?? state.lastRefreshAt;
|
|
1846
|
+
if (cached) restoreAgentCacheMeta(agent, cached);
|
|
1847
|
+
const isInitialized = isAgentCacheInitialized(agentName);
|
|
1848
|
+
const availabilityStartedAt = performance.now();
|
|
1849
|
+
const isAvailable = agent.isAvailable();
|
|
1850
|
+
const availabilityDuration = performance.now() - availabilityStartedAt;
|
|
1851
|
+
let strategyResult;
|
|
1852
|
+
if (!isAvailable) {
|
|
1853
|
+
strategyResult = this.refreshUnavailableAgent(agentName);
|
|
1854
|
+
} else if (!isInitialized) {
|
|
1855
|
+
strategyResult = await this.initializeAgent(agent, previousSessions);
|
|
1856
|
+
} else if (cached && agent instanceof FileSystemSessionSource) {
|
|
1857
|
+
strategyResult = await this.syncAgentSources(agent, cached, startedAt);
|
|
1858
|
+
} else if (refreshBaseline.length > 0) {
|
|
1859
|
+
strategyResult = await this.refreshChangedAgent(
|
|
1860
|
+
agent,
|
|
1861
|
+
refreshBaseline,
|
|
1862
|
+
cacheTimestamp,
|
|
1863
|
+
startedAt
|
|
1864
|
+
);
|
|
1865
|
+
} else {
|
|
1866
|
+
strategyResult = await this.scanAgentFully(agent, previousSessions);
|
|
1867
|
+
}
|
|
1868
|
+
if (strategyResult.status === "unchanged") return "unchanged";
|
|
1869
|
+
const nextSessions = attachMissingProjectIdentities(strategyResult.nextSessions);
|
|
1870
|
+
const diffStartedAt = performance.now();
|
|
1871
|
+
const diff = buildRefreshDiff(
|
|
1872
|
+
agentName,
|
|
1873
|
+
previousSessions,
|
|
1874
|
+
nextSessions,
|
|
1875
|
+
strategyResult.preciseChangedIds ?? []
|
|
1876
|
+
);
|
|
1877
|
+
const diffDuration = performance.now() - diffStartedAt;
|
|
1878
|
+
const searchIndexOptions = pendingPathCount >= SEARCH_INDEX_BULK_PENDING_PATH_THRESHOLD ? { isBulk: true } : void 0;
|
|
1879
|
+
const persistentChanges = strategyResult.persistenceDiff?.changedSessions ?? diff.changedSessions;
|
|
1880
|
+
const persistentRemovedSessionIds = strategyResult.persistenceDiff?.removedSessionIds ?? diff.removedSessionIds;
|
|
1881
|
+
const changedSessionIds = strategyResult.usedIncrementalScan ? new Set(persistentChanges.map(({ session }) => session.id)) : void 0;
|
|
1882
|
+
const persistStartedAt = performance.now();
|
|
1883
|
+
const persistentJob = strategyResult.usedIncrementalScan ? {
|
|
1884
|
+
kind: "changes",
|
|
1885
|
+
context: "scan.refresh",
|
|
1886
|
+
agentName,
|
|
1887
|
+
changes: persistentChanges,
|
|
1888
|
+
removedSessionIds: persistentRemovedSessionIds,
|
|
1889
|
+
meta: buildAgentCacheMeta(agent, changedSessionIds),
|
|
1890
|
+
...searchIndexOptions ? { searchIndexOptions } : {}
|
|
1891
|
+
} : strategyResult.fullScanSessions ? {
|
|
1892
|
+
kind: "full",
|
|
1893
|
+
context: "scan.refresh",
|
|
1894
|
+
agentName,
|
|
1895
|
+
sessions: strategyResult.fullScanSessions,
|
|
1896
|
+
meta: buildAgentCacheMeta(agent),
|
|
1897
|
+
saveCache: true,
|
|
1898
|
+
...searchIndexOptions ? { searchIndexOptions } : {}
|
|
1899
|
+
} : null;
|
|
1900
|
+
if (persistentJob) {
|
|
1901
|
+
const persist = this.searchIndexJobs.enqueue("scan.refresh", [persistentJob]);
|
|
1902
|
+
if (!isInitialized && persistentJob.kind === "full") {
|
|
1903
|
+
await persist;
|
|
1904
|
+
} else {
|
|
1905
|
+
void persist.catch((error) => {
|
|
1906
|
+
appLogger.error("scan.refresh.persist.error", { agent: agentName, error });
|
|
1907
|
+
console.error(`[${agentName}] Session persistence failed:`, error);
|
|
1908
|
+
});
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
const persistDuration = performance.now() - persistStartedAt;
|
|
1912
|
+
logSearchIndexSync("scan.refresh", null, { pending_paths: pendingPathCount });
|
|
1913
|
+
this.emitSessionsChanged({ agentName, sessions: nextSessions, event: diff.event });
|
|
1914
|
+
const totalDurationMs = performance.now() - startedAt;
|
|
1915
|
+
state.lastRefreshDurationMs = totalDurationMs;
|
|
1916
|
+
appLogger.info("scan.refresh.done", {
|
|
1917
|
+
agent: agentName,
|
|
1918
|
+
duration_ms: Math.round(totalDurationMs),
|
|
1919
|
+
sessions: nextSessions.length,
|
|
1920
|
+
new_sessions: diff.event?.newSessions ?? 0,
|
|
1921
|
+
updated_sessions: diff.event?.updatedSessions ?? 0,
|
|
1922
|
+
removed_sessions: diff.event?.removedSessions ?? 0,
|
|
1923
|
+
pending_paths: pendingPathCount,
|
|
1924
|
+
availability_ms: Math.round(availabilityDuration),
|
|
1925
|
+
check_ms: Math.round(strategyResult.checkDuration),
|
|
1926
|
+
scan_ms: Math.round(strategyResult.scanDuration),
|
|
1927
|
+
diff_ms: Math.round(diffDuration),
|
|
1928
|
+
persist_ms: Math.round(persistDuration),
|
|
1929
|
+
search_index_ms: 0,
|
|
1930
|
+
persistent_index_worker_job: persistentJob?.kind,
|
|
1931
|
+
persistent_index_skipped: !persistentJob || void 0
|
|
1932
|
+
});
|
|
1933
|
+
return "committed";
|
|
1934
|
+
}
|
|
1935
|
+
refreshUnavailableAgent(agentName) {
|
|
1936
|
+
this.state(agentName).lastRefreshAt = Date.now();
|
|
1937
|
+
return this.refreshStrategyResult([]);
|
|
1938
|
+
}
|
|
1939
|
+
async initializeAgent(agent, previousSessions) {
|
|
1940
|
+
this.setScanPhase("initializing");
|
|
1941
|
+
const scanStartedAt = performance.now();
|
|
1942
|
+
const result = await this.runWorker(agent, previousSessions, null, this.startupScanOptions());
|
|
1943
|
+
agent.setSessionMetaMap(new Map(Object.entries(result.meta)));
|
|
1944
|
+
const sessions = attachMissingProjectIdentities(result.sessions);
|
|
1945
|
+
this.state(agent.name).lastRefreshAt = Date.now();
|
|
1946
|
+
return this.refreshStrategyResult(sessions, {
|
|
1947
|
+
fullScanSessions: sessions,
|
|
1948
|
+
scanDuration: performance.now() - scanStartedAt
|
|
1949
|
+
});
|
|
1950
|
+
}
|
|
1951
|
+
async syncAgentSources(agent, cached, refreshStartedAt) {
|
|
1952
|
+
const scanStartedAt = performance.now();
|
|
1953
|
+
const result = await this.runWorker(agent, cached.sessions, null, this.startupScanOptions(), {
|
|
1954
|
+
sourceSync: true,
|
|
1955
|
+
meta: cached.meta
|
|
1956
|
+
});
|
|
1957
|
+
agent.setSessionMetaMap(new Map(Object.entries(result.meta)));
|
|
1958
|
+
const sessions = attachMissingProjectIdentities(result.sessions);
|
|
1959
|
+
const preciseChangedIds = result.changedIds ?? [];
|
|
1960
|
+
const persistenceDiff = buildRefreshDiff(
|
|
1961
|
+
agent.name,
|
|
1962
|
+
cached.sessions,
|
|
1963
|
+
sessions,
|
|
1964
|
+
preciseChangedIds
|
|
1965
|
+
);
|
|
1966
|
+
this.state(agent.name).lastRefreshAt = Date.now();
|
|
1967
|
+
if (preciseChangedIds.length === 0) this.logUnchangedRefresh(agent.name, refreshStartedAt);
|
|
1968
|
+
return this.refreshStrategyResult(sessions, {
|
|
1969
|
+
preciseChangedIds,
|
|
1970
|
+
usedIncrementalScan: true,
|
|
1971
|
+
persistenceDiff,
|
|
1972
|
+
scanDuration: performance.now() - scanStartedAt
|
|
1973
|
+
});
|
|
1974
|
+
}
|
|
1975
|
+
async refreshChangedAgent(agent, baseline, cacheTimestamp, refreshStartedAt) {
|
|
1976
|
+
const checkStartedAt = performance.now();
|
|
1977
|
+
const checkResult = await Promise.resolve(agent.checkForChanges(cacheTimestamp, baseline));
|
|
1978
|
+
const checkDuration = performance.now() - checkStartedAt;
|
|
1979
|
+
this.state(agent.name).lastRefreshAt = checkResult.timestamp;
|
|
1980
|
+
if (!checkResult.hasChanges) {
|
|
1981
|
+
this.logUnchangedRefresh(agent.name, refreshStartedAt);
|
|
1982
|
+
return this.refreshStrategyResult(baseline, { status: "unchanged", checkDuration });
|
|
1983
|
+
}
|
|
1984
|
+
const preciseChangedIds = checkResult.changedIds ?? null;
|
|
1985
|
+
const scanStartedAt = performance.now();
|
|
1986
|
+
const sessions = attachMissingProjectIdentities(
|
|
1987
|
+
await Promise.resolve(agent.incrementalScan(baseline, checkResult.changedIds ?? []))
|
|
1988
|
+
);
|
|
1989
|
+
return this.refreshStrategyResult(sessions, {
|
|
1990
|
+
preciseChangedIds,
|
|
1991
|
+
usedIncrementalScan: Array.isArray(checkResult.changedIds),
|
|
1992
|
+
persistenceDiff: buildRefreshDiff(agent.name, baseline, sessions, preciseChangedIds ?? []),
|
|
1993
|
+
checkDuration,
|
|
1994
|
+
scanDuration: performance.now() - scanStartedAt
|
|
1995
|
+
});
|
|
1996
|
+
}
|
|
1997
|
+
async scanAgentFully(agent, previousSessions) {
|
|
1998
|
+
const scanStartedAt = performance.now();
|
|
1999
|
+
const result = await this.runWorker(agent, previousSessions, null, {});
|
|
2000
|
+
agent.setSessionMetaMap(new Map(Object.entries(result.meta)));
|
|
2001
|
+
const sessions = attachMissingProjectIdentities(result.sessions);
|
|
2002
|
+
this.state(agent.name).lastRefreshAt = Date.now();
|
|
2003
|
+
return this.refreshStrategyResult(sessions, {
|
|
2004
|
+
fullScanSessions: sessions,
|
|
2005
|
+
scanDuration: performance.now() - scanStartedAt
|
|
2006
|
+
});
|
|
2007
|
+
}
|
|
2008
|
+
refreshStrategyResult(nextSessions, overrides = {}) {
|
|
2009
|
+
return {
|
|
2010
|
+
status: "continue",
|
|
2011
|
+
nextSessions,
|
|
2012
|
+
fullScanSessions: null,
|
|
2013
|
+
preciseChangedIds: null,
|
|
2014
|
+
usedIncrementalScan: false,
|
|
2015
|
+
persistenceDiff: null,
|
|
2016
|
+
checkDuration: 0,
|
|
2017
|
+
scanDuration: 0,
|
|
2018
|
+
...overrides
|
|
2019
|
+
};
|
|
2020
|
+
}
|
|
2021
|
+
runWorker(agent, previousSessions, changedIds, scanOptions, workerOptions = {}) {
|
|
2022
|
+
return this.options.workerRunner.run(agent.name, {
|
|
2023
|
+
previousSessions,
|
|
2024
|
+
changedIds,
|
|
2025
|
+
scanOptions,
|
|
2026
|
+
sourceSync: workerOptions.sourceSync,
|
|
2027
|
+
meta: workerOptions.meta ?? buildAgentCacheMeta(agent),
|
|
2028
|
+
onProgress: (progress) => this.updateAgentScanProgress(agent.name, progress)
|
|
2029
|
+
});
|
|
2030
|
+
}
|
|
2031
|
+
needsBackfill(agent) {
|
|
2032
|
+
const startupScanOptions = this.startupScanOptions();
|
|
2033
|
+
if (startupScanOptions.from == null && startupScanOptions.to == null) return false;
|
|
2034
|
+
if (!agent.isAvailable()) return false;
|
|
2035
|
+
const lastSyncAt = getAgentLastFullSyncAt(agent.name);
|
|
2036
|
+
return lastSyncAt == null || Date.now() - lastSyncAt > BACKFILL_INTERVAL_MS;
|
|
2037
|
+
}
|
|
2038
|
+
enqueueBackfill(agentName) {
|
|
2039
|
+
if (this.isShuttingDown || this.currentBackfillAgent === agentName || this.backfillQueue.includes(agentName)) {
|
|
2040
|
+
return;
|
|
2041
|
+
}
|
|
2042
|
+
this.backfillQueue.push(agentName);
|
|
2043
|
+
this.publishBackfillStatus();
|
|
2044
|
+
this.pumpBackfillQueue();
|
|
2045
|
+
}
|
|
2046
|
+
pumpBackfillQueue() {
|
|
2047
|
+
if (this.isShuttingDown || this.currentBackfillAgent) return;
|
|
2048
|
+
const agentName = this.backfillQueue.shift();
|
|
2049
|
+
if (!agentName) return;
|
|
2050
|
+
this.currentBackfillAgent = agentName;
|
|
2051
|
+
this.publishBackfillStatus();
|
|
2052
|
+
void this.serialize(agentName, "backfill", () => this.performBackfill(agentName)).then(
|
|
2053
|
+
(result) => {
|
|
2054
|
+
if (this.isShuttingDown) return;
|
|
2055
|
+
this.currentBackfillAgent = void 0;
|
|
2056
|
+
if (result === "committed") {
|
|
2057
|
+
if (!this.completedBackfillAgents.includes(agentName)) {
|
|
2058
|
+
this.completedBackfillAgents.push(agentName);
|
|
2059
|
+
}
|
|
2060
|
+
this.failedBackfillAgents = this.failedBackfillAgents.filter(
|
|
2061
|
+
(failedAgent) => failedAgent !== agentName
|
|
2062
|
+
);
|
|
2063
|
+
} else if (!this.failedBackfillAgents.includes(agentName)) {
|
|
2064
|
+
this.failedBackfillAgents.push(agentName);
|
|
2065
|
+
}
|
|
2066
|
+
this.publishBackfillStatus();
|
|
2067
|
+
this.pumpBackfillQueue();
|
|
2068
|
+
}
|
|
2069
|
+
);
|
|
2070
|
+
}
|
|
2071
|
+
async performBackfill(agentName) {
|
|
2072
|
+
const startedAt = performance.now();
|
|
2073
|
+
const agent = this.findAgent(agentName);
|
|
2074
|
+
if (!agent || !agent.isAvailable()) return "skipped";
|
|
2075
|
+
const snapshot = this.options.snapshot();
|
|
2076
|
+
const cached = loadCachedSessions(agentName);
|
|
2077
|
+
const baseline = cached?.sessions ?? snapshot.byAgent[agentName] ?? [];
|
|
2078
|
+
const meta = cached?.meta ?? buildAgentCacheMeta(agent);
|
|
2079
|
+
if (cached) restoreAgentCacheMeta(agent, cached);
|
|
2080
|
+
try {
|
|
2081
|
+
const result = await this.runWorker(
|
|
2082
|
+
agent,
|
|
2083
|
+
baseline,
|
|
2084
|
+
null,
|
|
2085
|
+
{},
|
|
2086
|
+
{
|
|
2087
|
+
sourceSync: agent instanceof FileSystemSessionSource,
|
|
2088
|
+
meta
|
|
2089
|
+
}
|
|
2090
|
+
);
|
|
2091
|
+
agent.setSessionMetaMap(new Map(Object.entries(result.meta)));
|
|
2092
|
+
const fullSessions = attachMissingProjectIdentities(result.sessions);
|
|
2093
|
+
const diff = buildRefreshDiff(
|
|
2094
|
+
agentName,
|
|
2095
|
+
snapshot.byAgent[agentName] ?? [],
|
|
2096
|
+
fullSessions,
|
|
2097
|
+
result.changedIds ?? []
|
|
2098
|
+
);
|
|
2099
|
+
await this.searchIndexJobs.enqueue("scan.backfill", [
|
|
2100
|
+
{
|
|
2101
|
+
kind: "full",
|
|
2102
|
+
context: "scan.backfill",
|
|
2103
|
+
agentName,
|
|
2104
|
+
sessions: fullSessions,
|
|
2105
|
+
meta: buildAgentCacheMeta(agent),
|
|
2106
|
+
saveCache: true
|
|
2107
|
+
}
|
|
2108
|
+
]);
|
|
2109
|
+
markAgentFullSyncCompleted(agentName);
|
|
2110
|
+
this.emitSessionsChanged({ agentName, sessions: fullSessions, event: diff.event });
|
|
2111
|
+
appLogger.info("scan.backfill.done", {
|
|
2112
|
+
agent: agentName,
|
|
2113
|
+
duration_ms: Math.round(performance.now() - startedAt),
|
|
2114
|
+
sessions: fullSessions.length,
|
|
2115
|
+
changed: result.changedIds?.length ?? 0
|
|
2116
|
+
});
|
|
2117
|
+
return "committed";
|
|
2118
|
+
} catch (error) {
|
|
2119
|
+
appLogger.error("scan.backfill.error", { agent: agentName, error });
|
|
2120
|
+
console.error(`[${agentName}] Backfill failed:`, error);
|
|
2121
|
+
return "failed";
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
serialize(agentName, kind, operation) {
|
|
2125
|
+
const previous = this.operationTails.get(agentName) ?? Promise.resolve();
|
|
2126
|
+
const run = previous.then(async () => {
|
|
2127
|
+
if (this.isShuttingDown) return "skipped";
|
|
2128
|
+
const lifecycle = this.beginOperation(agentName, kind);
|
|
2129
|
+
try {
|
|
2130
|
+
const result = await operation();
|
|
2131
|
+
this.completeOperation(lifecycle, result);
|
|
2132
|
+
return result;
|
|
2133
|
+
} catch (error) {
|
|
2134
|
+
this.completeOperation(lifecycle, "failed");
|
|
2135
|
+
throw error;
|
|
2136
|
+
}
|
|
2137
|
+
});
|
|
2138
|
+
const tail = run.then(
|
|
2139
|
+
() => void 0,
|
|
2140
|
+
() => void 0
|
|
2141
|
+
);
|
|
2142
|
+
this.operationTails.set(agentName, tail);
|
|
2143
|
+
void tail.finally(() => {
|
|
2144
|
+
if (this.operationTails.get(agentName) === tail) this.operationTails.delete(agentName);
|
|
2145
|
+
});
|
|
2146
|
+
return run;
|
|
2147
|
+
}
|
|
2148
|
+
state(agentName) {
|
|
2149
|
+
const existing = this.refreshStates.get(agentName);
|
|
2150
|
+
if (existing) return existing;
|
|
2151
|
+
const state = {
|
|
2152
|
+
timer: null,
|
|
2153
|
+
timerDeadline: 0,
|
|
2154
|
+
isRunning: false,
|
|
2155
|
+
hasPendingRerun: false,
|
|
2156
|
+
lastRefreshAt: 0,
|
|
2157
|
+
lastRefreshDurationMs: 0,
|
|
2158
|
+
pendingPathCount: 0
|
|
2159
|
+
};
|
|
2160
|
+
this.refreshStates.set(agentName, state);
|
|
2161
|
+
return state;
|
|
2162
|
+
}
|
|
2163
|
+
beginOperation(agentName, kind) {
|
|
2164
|
+
const generation = (this.operationGenerations.get(agentName) ?? 0) + 1;
|
|
2165
|
+
const startedAt = Date.now();
|
|
2166
|
+
this.operationGenerations.set(agentName, generation);
|
|
2167
|
+
appLogger.info("scan.agent_operation.started", {
|
|
2168
|
+
agent: agentName,
|
|
2169
|
+
operation: kind,
|
|
2170
|
+
generation,
|
|
2171
|
+
started_at: startedAt
|
|
2172
|
+
});
|
|
2173
|
+
return { agentName, kind, generation, startedAt };
|
|
2174
|
+
}
|
|
2175
|
+
completeOperation(lifecycle, result) {
|
|
2176
|
+
const completedAt = Date.now();
|
|
2177
|
+
appLogger.info("scan.agent_operation.completed", {
|
|
2178
|
+
agent: lifecycle.agentName,
|
|
2179
|
+
operation: lifecycle.kind,
|
|
2180
|
+
generation: lifecycle.generation,
|
|
2181
|
+
started_at: lifecycle.startedAt,
|
|
2182
|
+
completed_at: completedAt,
|
|
2183
|
+
duration_ms: completedAt - lifecycle.startedAt,
|
|
2184
|
+
result
|
|
2185
|
+
});
|
|
2186
|
+
}
|
|
2187
|
+
backfillStatus() {
|
|
2188
|
+
return {
|
|
2189
|
+
active: this.currentBackfillAgent != null || this.backfillQueue.length > 0,
|
|
2190
|
+
pendingAgents: [...this.backfillQueue],
|
|
2191
|
+
currentAgent: this.currentBackfillAgent,
|
|
2192
|
+
completedAgents: [...this.completedBackfillAgents],
|
|
2193
|
+
failedAgents: [...this.failedBackfillAgents]
|
|
2194
|
+
};
|
|
2195
|
+
}
|
|
2196
|
+
buildFullSearchIndexJobs(context) {
|
|
2197
|
+
const snapshot = this.options.snapshot();
|
|
2198
|
+
return snapshot.agents.map((agent) => {
|
|
2199
|
+
const cached = loadCachedSessions(agent.name);
|
|
2200
|
+
return cached ? {
|
|
2201
|
+
kind: "full",
|
|
2202
|
+
context,
|
|
2203
|
+
agentName: agent.name,
|
|
2204
|
+
sessions: cached.sessions,
|
|
2205
|
+
meta: cached.meta
|
|
2206
|
+
} : {
|
|
2207
|
+
kind: "full",
|
|
2208
|
+
context,
|
|
2209
|
+
agentName: agent.name,
|
|
2210
|
+
sessions: snapshot.byAgent[agent.name] ?? [],
|
|
2211
|
+
meta: buildAgentCacheMeta(agent)
|
|
2212
|
+
};
|
|
2213
|
+
});
|
|
2214
|
+
}
|
|
2215
|
+
findAgent(agentName) {
|
|
2216
|
+
return this.options.snapshot().agents.find((agent) => agent.name === agentName);
|
|
2217
|
+
}
|
|
2218
|
+
startupScanOptions() {
|
|
2219
|
+
return this.options.startupScanOptions ?? {};
|
|
2220
|
+
}
|
|
2221
|
+
logUnchangedRefresh(agentName, startedAt) {
|
|
2222
|
+
appLogger.debug("scan.refresh.unchanged", {
|
|
2223
|
+
agent: agentName,
|
|
2224
|
+
duration_ms: Math.round(performance.now() - startedAt)
|
|
2225
|
+
});
|
|
1632
2226
|
}
|
|
1633
2227
|
};
|
|
1634
2228
|
|
|
@@ -1950,41 +2544,78 @@ var SessionWatcher = class {
|
|
|
1950
2544
|
}
|
|
1951
2545
|
};
|
|
1952
2546
|
|
|
1953
|
-
// src/
|
|
1954
|
-
|
|
1955
|
-
var
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
var BACKFILL_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
1959
|
-
function buildRefreshDiff(agentName, previousSessions, nextSessions, candidateChangedIds = []) {
|
|
1960
|
-
const { changes, removedSessionIds, counts } = computeSessionDiff(
|
|
1961
|
-
previousSessions,
|
|
1962
|
-
nextSessions,
|
|
1963
|
-
candidateChangedIds,
|
|
1964
|
-
sessionSignature
|
|
1965
|
-
);
|
|
1966
|
-
if (counts.new === 0 && counts.updated === 0 && counts.removed === 0) {
|
|
1967
|
-
return { event: null, changedSessions: changes, removedSessionIds };
|
|
2547
|
+
// src/worker-runner.ts
|
|
2548
|
+
import { Worker as Worker2 } from "worker_threads";
|
|
2549
|
+
var ThreadWorkerRunner = class {
|
|
2550
|
+
constructor(workerUrl) {
|
|
2551
|
+
this.workerUrl = workerUrl;
|
|
1968
2552
|
}
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
}
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
2553
|
+
workerUrl;
|
|
2554
|
+
workers = /* @__PURE__ */ new Set();
|
|
2555
|
+
get activeCount() {
|
|
2556
|
+
return this.workers.size;
|
|
2557
|
+
}
|
|
2558
|
+
run(agentName, payload) {
|
|
2559
|
+
return new Promise((resolve4, reject) => {
|
|
2560
|
+
const worker = new Worker2(this.workerUrl, {
|
|
2561
|
+
workerData: {
|
|
2562
|
+
agentName,
|
|
2563
|
+
previousSessions: payload.previousSessions,
|
|
2564
|
+
changedIds: payload.changedIds,
|
|
2565
|
+
sourceSync: payload.sourceSync,
|
|
2566
|
+
scanOptions: payload.scanOptions,
|
|
2567
|
+
meta: payload.meta
|
|
2568
|
+
}
|
|
2569
|
+
});
|
|
2570
|
+
worker.unref();
|
|
2571
|
+
this.workers.add(worker);
|
|
2572
|
+
let settled = false;
|
|
2573
|
+
const finish = (callback, terminate = true) => {
|
|
2574
|
+
if (settled) return;
|
|
2575
|
+
settled = true;
|
|
2576
|
+
this.workers.delete(worker);
|
|
2577
|
+
if (terminate) void worker.terminate();
|
|
2578
|
+
callback();
|
|
2579
|
+
};
|
|
2580
|
+
worker.on("message", (message) => {
|
|
2581
|
+
if (message.type === "progress") {
|
|
2582
|
+
payload.onProgress?.(message.progress);
|
|
2583
|
+
return;
|
|
2584
|
+
}
|
|
2585
|
+
if (message.type === "done") {
|
|
2586
|
+
finish(
|
|
2587
|
+
() => resolve4({
|
|
2588
|
+
sessions: message.sessions,
|
|
2589
|
+
meta: message.meta,
|
|
2590
|
+
changedIds: message.changedIds
|
|
2591
|
+
})
|
|
2592
|
+
);
|
|
2593
|
+
return;
|
|
2594
|
+
}
|
|
2595
|
+
finish(() => reject(new Error(message.error)));
|
|
2596
|
+
});
|
|
2597
|
+
worker.once("error", (error) => {
|
|
2598
|
+
finish(() => reject(error));
|
|
2599
|
+
});
|
|
2600
|
+
worker.once("exit", (code) => {
|
|
2601
|
+
if (settled) return;
|
|
2602
|
+
appLogger.warn("scan.refresh_worker.exit_before_done", { agent: agentName, code });
|
|
2603
|
+
finish(
|
|
2604
|
+
() => reject(new Error(`Scan refresh worker exited before completing (code ${code})`)),
|
|
2605
|
+
false
|
|
2606
|
+
);
|
|
2607
|
+
});
|
|
2608
|
+
});
|
|
2609
|
+
}
|
|
2610
|
+
async shutdown() {
|
|
2611
|
+
const workers = [...this.workers];
|
|
2612
|
+
await Promise.allSettled(workers.map((worker) => worker.terminate()));
|
|
2613
|
+
this.workers.clear();
|
|
2614
|
+
}
|
|
2615
|
+
};
|
|
2616
|
+
|
|
2617
|
+
// src/live-scan.ts
|
|
2618
|
+
var NEW_SESSION_EVENT_WINDOW_MS = 250;
|
|
1988
2619
|
function mergeEvents(previous, next) {
|
|
1989
2620
|
const changedSessionHeads = /* @__PURE__ */ new Map();
|
|
1990
2621
|
const removedSessionRefs = /* @__PURE__ */ new Map();
|
|
@@ -2016,138 +2647,99 @@ function mergeEvents(previous, next) {
|
|
|
2016
2647
|
};
|
|
2017
2648
|
}
|
|
2018
2649
|
var LiveScanStore = class {
|
|
2019
|
-
constructor(watchEnabled = true, scanOptions = {}, startupScanOptions = {}, storeOptions = {}) {
|
|
2020
|
-
this.watchEnabled = watchEnabled;
|
|
2021
|
-
this.scanOptions = scanOptions;
|
|
2022
|
-
this.startupScanOptions = startupScanOptions;
|
|
2023
|
-
this.storeOptions = storeOptions;
|
|
2024
|
-
}
|
|
2025
2650
|
watchEnabled;
|
|
2026
2651
|
scanOptions;
|
|
2027
2652
|
startupScanOptions;
|
|
2028
|
-
|
|
2653
|
+
deferInitialRefresh;
|
|
2654
|
+
syncEngine;
|
|
2029
2655
|
agents = [];
|
|
2030
2656
|
byAgent = {};
|
|
2031
2657
|
sessions = [];
|
|
2032
2658
|
listeners = /* @__PURE__ */ new Set();
|
|
2033
|
-
scanStatusListeners = /* @__PURE__ */ new Set();
|
|
2034
|
-
scanStatus = new ScanStatusModel();
|
|
2035
|
-
backfills = new BackfillCoordinator();
|
|
2036
|
-
refreshes = new RefreshCoordinator();
|
|
2037
2659
|
watcher = null;
|
|
2038
2660
|
pendingEvent = null;
|
|
2039
2661
|
pendingEventTimer = null;
|
|
2040
|
-
backgroundRefreshTimer = null;
|
|
2041
|
-
scanRefreshWorkers = /* @__PURE__ */ new Set();
|
|
2042
|
-
searchIndexJobs = new SearchIndexJobRunner();
|
|
2043
2662
|
shutdownPromise = null;
|
|
2044
2663
|
shuttingDown = false;
|
|
2664
|
+
constructor(options = {}) {
|
|
2665
|
+
this.watchEnabled = options.watchEnabled ?? true;
|
|
2666
|
+
this.scanOptions = options.scanOptions ?? {};
|
|
2667
|
+
this.startupScanOptions = options.startupScanOptions ?? {};
|
|
2668
|
+
this.deferInitialRefresh = options.deferInitialRefresh === true;
|
|
2669
|
+
const workerRunner = options.workerRunner ?? new ThreadWorkerRunner(new URL("./scan-refresh-worker.js", import.meta.url));
|
|
2670
|
+
this.syncEngine = new AgentSyncEngine({
|
|
2671
|
+
snapshot: () => this.getSnapshot(),
|
|
2672
|
+
startupScanOptions: this.startupScanOptions,
|
|
2673
|
+
workerRunner
|
|
2674
|
+
});
|
|
2675
|
+
this.syncEngine.subscribeSessionsChanged((change) => this.applySessionsChanged(change));
|
|
2676
|
+
}
|
|
2045
2677
|
async initialize() {
|
|
2046
2678
|
const startedAt = performance.now();
|
|
2047
|
-
const deferInitialRefresh = this.storeOptions.deferInitialRefresh === true;
|
|
2048
2679
|
appLogger.info("scan.initial.start", {
|
|
2049
2680
|
watch_enabled: this.watchEnabled,
|
|
2050
2681
|
agents: this.scanOptions.agents,
|
|
2051
2682
|
use_cache: this.scanOptions.useCache ?? true,
|
|
2052
2683
|
startup_from: this.startupScanOptions.from,
|
|
2053
2684
|
startup_to: this.startupScanOptions.to,
|
|
2054
|
-
deferred: deferInitialRefresh || void 0
|
|
2685
|
+
deferred: this.deferInitialRefresh || void 0
|
|
2055
2686
|
});
|
|
2056
2687
|
const initialResult = await scanSessions({
|
|
2057
2688
|
...this.scanOptions,
|
|
2058
|
-
...deferInitialRefresh ? this.startupScanOptions : {},
|
|
2059
2689
|
useCache: this.scanOptions.useCache ?? true,
|
|
2060
2690
|
smartRefresh: false,
|
|
2061
|
-
cacheOnly: deferInitialRefresh,
|
|
2062
|
-
writeCache: deferInitialRefresh ? false : this.scanOptions.writeCache,
|
|
2691
|
+
cacheOnly: this.deferInitialRefresh,
|
|
2692
|
+
writeCache: this.deferInitialRefresh ? false : this.scanOptions.writeCache,
|
|
2063
2693
|
smartTagWorkerUrl: this.getSmartTagWorkerUrl() ?? void 0,
|
|
2064
|
-
includeSmartTags: deferInitialRefresh ? false : void 0
|
|
2694
|
+
includeSmartTags: this.deferInitialRefresh ? false : void 0
|
|
2065
2695
|
});
|
|
2066
2696
|
this.applyScanResult(initialResult);
|
|
2697
|
+
this.syncEngine.initialize(initialResult.cacheTimestamps);
|
|
2067
2698
|
const indexStartedAt = performance.now();
|
|
2068
|
-
if (!deferInitialRefresh)
|
|
2069
|
-
await this.searchIndexJobs.enqueue(
|
|
2070
|
-
"scan.initial",
|
|
2071
|
-
this.buildFullSearchIndexJobs("scan.initial")
|
|
2072
|
-
);
|
|
2073
|
-
}
|
|
2699
|
+
if (!this.deferInitialRefresh) await this.syncEngine.syncInitialIndex();
|
|
2074
2700
|
const indexDuration = performance.now() - indexStartedAt;
|
|
2075
2701
|
appLogger.info("scan.initial.done", {
|
|
2076
2702
|
duration_ms: Math.round(performance.now() - startedAt),
|
|
2077
|
-
index_ms: deferInitialRefresh ? void 0 : Math.round(indexDuration),
|
|
2078
|
-
deferred: deferInitialRefresh || void 0,
|
|
2703
|
+
index_ms: this.deferInitialRefresh ? void 0 : Math.round(indexDuration),
|
|
2704
|
+
deferred: this.deferInitialRefresh || void 0,
|
|
2079
2705
|
sessions: this.sessions.length,
|
|
2080
2706
|
agents: Object.fromEntries(
|
|
2081
2707
|
Object.entries(this.byAgent).map(([key, value]) => [key, value.length])
|
|
2082
2708
|
),
|
|
2083
2709
|
agent_timings: initialResult.timings ? Object.fromEntries(
|
|
2084
|
-
Object.entries(initialResult.timings).map(([name,
|
|
2710
|
+
Object.entries(initialResult.timings).map(([name, timing]) => [
|
|
2085
2711
|
name,
|
|
2086
2712
|
{
|
|
2087
|
-
total_ms: Math.round(
|
|
2088
|
-
cache_load_ms:
|
|
2089
|
-
check_changes_ms:
|
|
2090
|
-
scan_ms:
|
|
2091
|
-
identity_ms:
|
|
2092
|
-
tags_ms:
|
|
2713
|
+
total_ms: Math.round(timing.total),
|
|
2714
|
+
cache_load_ms: timing.cacheLoad != null ? Math.round(timing.cacheLoad) : void 0,
|
|
2715
|
+
check_changes_ms: timing.checkChanges != null ? Math.round(timing.checkChanges) : void 0,
|
|
2716
|
+
scan_ms: timing.scan != null ? Math.round(timing.scan) : void 0,
|
|
2717
|
+
identity_ms: timing.identity != null ? Math.round(timing.identity) : void 0,
|
|
2718
|
+
tags_ms: timing.tags != null ? Math.round(timing.tags) : void 0
|
|
2093
2719
|
}
|
|
2094
2720
|
])
|
|
2095
2721
|
) : void 0
|
|
2096
2722
|
});
|
|
2097
|
-
if (this.watchEnabled)
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
this.refreshes.recordChangedPaths(agentName);
|
|
2102
|
-
const delayMs = (this.byAgent[agentName]?.length ?? 0) === 0 ? EMPTY_AGENT_REFRESH_DEBOUNCE_MS : REFRESH_DEBOUNCE_MS;
|
|
2103
|
-
this.scheduleRefresh(agentName, delayMs);
|
|
2104
|
-
}
|
|
2105
|
-
});
|
|
2106
|
-
this.watcher.start(this.agents.map((agent) => agent.name));
|
|
2107
|
-
}
|
|
2723
|
+
if (!this.watchEnabled) return;
|
|
2724
|
+
this.watcher = new SessionWatcher();
|
|
2725
|
+
this.watcher.onAgentsChanged((agentNames) => this.syncEngine.handleAgentsChanged(agentNames));
|
|
2726
|
+
this.watcher.start(this.agents.map((agent) => agent.name));
|
|
2108
2727
|
}
|
|
2109
2728
|
startBackgroundRefresh() {
|
|
2110
|
-
|
|
2111
|
-
return;
|
|
2112
|
-
}
|
|
2113
|
-
const agentNames = this.agents.map((agent) => agent.name);
|
|
2114
|
-
this.startScanBatch(agentNames, "scanning");
|
|
2115
|
-
this.backgroundRefreshTimer = setTimeout(() => {
|
|
2116
|
-
this.backgroundRefreshTimer = null;
|
|
2117
|
-
for (const agentName of agentNames) {
|
|
2118
|
-
this.scheduleRefresh(agentName, 0);
|
|
2119
|
-
}
|
|
2120
|
-
if (agentNames.length === 0) {
|
|
2121
|
-
this.finishScanBatch();
|
|
2122
|
-
}
|
|
2123
|
-
for (const agent of this.agents) {
|
|
2124
|
-
if (this.needsBackfill(agent)) {
|
|
2125
|
-
this.enqueueBackfill(agent.name);
|
|
2126
|
-
}
|
|
2127
|
-
}
|
|
2128
|
-
}, 0);
|
|
2729
|
+
this.syncEngine.startBackgroundRefresh();
|
|
2129
2730
|
}
|
|
2130
2731
|
getSnapshot() {
|
|
2131
|
-
return {
|
|
2132
|
-
sessions: this.sessions,
|
|
2133
|
-
byAgent: this.byAgent,
|
|
2134
|
-
agents: this.agents
|
|
2135
|
-
};
|
|
2732
|
+
return { sessions: this.sessions, byAgent: this.byAgent, agents: this.agents };
|
|
2136
2733
|
}
|
|
2137
2734
|
getScanStatus() {
|
|
2138
|
-
return this.
|
|
2735
|
+
return this.syncEngine.status();
|
|
2139
2736
|
}
|
|
2140
2737
|
subscribe(listener) {
|
|
2141
2738
|
this.listeners.add(listener);
|
|
2142
|
-
return () =>
|
|
2143
|
-
this.listeners.delete(listener);
|
|
2144
|
-
};
|
|
2739
|
+
return () => this.listeners.delete(listener);
|
|
2145
2740
|
}
|
|
2146
2741
|
subscribeScanStatus(listener) {
|
|
2147
|
-
this.
|
|
2148
|
-
return () => {
|
|
2149
|
-
this.scanStatusListeners.delete(listener);
|
|
2150
|
-
};
|
|
2742
|
+
return this.syncEngine.subscribeStatusChanged(listener);
|
|
2151
2743
|
}
|
|
2152
2744
|
shutdown() {
|
|
2153
2745
|
this.shutdownPromise ??= this.performShutdown();
|
|
@@ -2155,43 +2747,23 @@ var LiveScanStore = class {
|
|
|
2155
2747
|
}
|
|
2156
2748
|
async performShutdown() {
|
|
2157
2749
|
this.shuttingDown = true;
|
|
2158
|
-
const activeOperations = {
|
|
2159
|
-
agent_operations: this.refreshes.activeOperationCount,
|
|
2160
|
-
refreshes: this.refreshes.activeRefreshCount,
|
|
2161
|
-
backfill_running: this.backfills.isRunning || void 0,
|
|
2162
|
-
scan_workers: this.scanRefreshWorkers.size
|
|
2163
|
-
};
|
|
2164
|
-
if (activeOperations.agent_operations > 0 || activeOperations.scan_workers > 0) {
|
|
2165
|
-
appLogger.warn("scan.shutdown.active_operations", activeOperations);
|
|
2166
|
-
}
|
|
2167
|
-
const searchIndexSnapshot = this.searchIndexJobs.snapshot();
|
|
2168
|
-
appLogger.info("search_index.shutdown.started", {
|
|
2169
|
-
active_batch_id: searchIndexSnapshot.activeBatchId,
|
|
2170
|
-
pending_batches: searchIndexSnapshot.pendingBatches
|
|
2171
|
-
});
|
|
2172
2750
|
if (this.pendingEventTimer) {
|
|
2173
2751
|
clearTimeout(this.pendingEventTimer);
|
|
2174
2752
|
this.pendingEventTimer = null;
|
|
2175
2753
|
}
|
|
2176
|
-
|
|
2177
|
-
clearTimeout(this.backgroundRefreshTimer);
|
|
2178
|
-
this.backgroundRefreshTimer = null;
|
|
2179
|
-
}
|
|
2180
|
-
this.backfills.clear();
|
|
2181
|
-
await this.searchIndexJobs.shutdown();
|
|
2182
|
-
const scanWorkers = [...this.scanRefreshWorkers];
|
|
2183
|
-
await Promise.allSettled(scanWorkers.map((scanWorker) => scanWorker.terminate()));
|
|
2184
|
-
await this.refreshes.shutdown();
|
|
2754
|
+
await this.syncEngine.shutdown();
|
|
2185
2755
|
this.pendingEvent = null;
|
|
2186
2756
|
if (this.watcher) {
|
|
2187
2757
|
await this.watcher.dispose();
|
|
2188
2758
|
this.watcher = null;
|
|
2189
2759
|
}
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2760
|
+
}
|
|
2761
|
+
applySessionsChanged(change) {
|
|
2762
|
+
this.byAgent[change.agentName] = sortSessions(change.sessions);
|
|
2763
|
+
this.rebuildSessions();
|
|
2764
|
+
if (!change.event) return;
|
|
2765
|
+
change.event.totalSessions = this.sessions.length;
|
|
2766
|
+
this.emit(change.event);
|
|
2195
2767
|
}
|
|
2196
2768
|
emit(event) {
|
|
2197
2769
|
if (this.shuttingDown) return;
|
|
@@ -2202,147 +2774,16 @@ var LiveScanStore = class {
|
|
|
2202
2774
|
this.emitNow(event);
|
|
2203
2775
|
}
|
|
2204
2776
|
emitNow(event) {
|
|
2205
|
-
for (const listener of this.listeners)
|
|
2206
|
-
listener(event);
|
|
2207
|
-
}
|
|
2208
|
-
}
|
|
2209
|
-
startScanBatch(agentNames, phase) {
|
|
2210
|
-
const sessionCounts = Object.fromEntries(
|
|
2211
|
-
agentNames.map((agentName) => [agentName, this.byAgent[agentName]?.length ?? 0])
|
|
2212
|
-
);
|
|
2213
|
-
this.publishScanStatus(this.scanStatus.startBatch(agentNames, phase, sessionCounts));
|
|
2214
|
-
}
|
|
2215
|
-
setScanPhase(phase) {
|
|
2216
|
-
this.publishScanStatus(this.scanStatus.setPhase(phase));
|
|
2217
|
-
}
|
|
2218
|
-
beginAgentScan(agentName) {
|
|
2219
|
-
if (!this.scanStatus.snapshot().active) this.startScanBatch([agentName], "scanning");
|
|
2220
|
-
this.publishScanStatus(
|
|
2221
|
-
this.scanStatus.beginAgent(agentName, this.byAgent[agentName]?.length ?? 0)
|
|
2222
|
-
);
|
|
2223
|
-
}
|
|
2224
|
-
updateAgentScanProgress(agentName, progress) {
|
|
2225
|
-
this.publishScanStatus(this.scanStatus.updateAgent(agentName, progress));
|
|
2226
|
-
}
|
|
2227
|
-
finishAgentScan(agentName) {
|
|
2228
|
-
this.publishScanStatus(this.scanStatus.finishAgent(agentName, this.byAgent[agentName]?.length));
|
|
2229
|
-
}
|
|
2230
|
-
finishScanBatch() {
|
|
2231
|
-
this.publishScanStatus(this.scanStatus.finishBatch());
|
|
2232
|
-
}
|
|
2233
|
-
updateBackfillStatus(patch) {
|
|
2234
|
-
this.publishScanStatus(this.scanStatus.updateBackfill(patch));
|
|
2235
|
-
}
|
|
2236
|
-
publishScanStatus(event) {
|
|
2237
|
-
if (!event || this.shuttingDown) return;
|
|
2238
|
-
for (const listener of this.scanStatusListeners) listener(event);
|
|
2239
|
-
}
|
|
2240
|
-
/**
|
|
2241
|
-
* Only FileSystemSessionSource agents pay the O(history) enumeration cost this
|
|
2242
|
-
* guards against: database agents already do a cheap single-file mtime check.
|
|
2243
|
-
* With no startup window configured, the regular refresh path already walks
|
|
2244
|
-
* full history, so backfill would be redundant.
|
|
2245
|
-
*/
|
|
2246
|
-
needsBackfill(agent) {
|
|
2247
|
-
if (this.startupScanOptions.from == null && this.startupScanOptions.to == null) return false;
|
|
2248
|
-
if (!(agent instanceof FileSystemSessionSource)) return false;
|
|
2249
|
-
if (!agent.isAvailable()) return false;
|
|
2250
|
-
const lastSyncAt = getAgentLastFullSyncAt(agent.name);
|
|
2251
|
-
return lastSyncAt == null || Date.now() - lastSyncAt > BACKFILL_INTERVAL_MS;
|
|
2252
|
-
}
|
|
2253
|
-
enqueueBackfill(agentName) {
|
|
2254
|
-
if (this.shuttingDown) return;
|
|
2255
|
-
const status = this.backfills.enqueue(agentName);
|
|
2256
|
-
if (!status) return;
|
|
2257
|
-
this.updateBackfillStatus(status);
|
|
2258
|
-
this.pumpBackfillQueue();
|
|
2259
|
-
}
|
|
2260
|
-
pumpBackfillQueue() {
|
|
2261
|
-
if (this.shuttingDown) return;
|
|
2262
|
-
const work = this.backfills.take();
|
|
2263
|
-
if (!work) return;
|
|
2264
|
-
this.updateBackfillStatus(work.status);
|
|
2265
|
-
void this.runBackfill(work.agentName).finally(() => {
|
|
2266
|
-
if (this.shuttingDown) return;
|
|
2267
|
-
this.updateBackfillStatus(this.backfills.complete(work.agentName));
|
|
2268
|
-
this.pumpBackfillQueue();
|
|
2269
|
-
});
|
|
2270
|
-
}
|
|
2271
|
-
/** Unbounded per-source sync to reconcile the full session history, not just the display window. */
|
|
2272
|
-
async runBackfill(agentName) {
|
|
2273
|
-
await this.refreshes.serialize(agentName, "backfill", () => this.performBackfill(agentName));
|
|
2274
|
-
}
|
|
2275
|
-
async performBackfill(agentName) {
|
|
2276
|
-
const startedAt = performance.now();
|
|
2277
|
-
const agent = this.agents.find((item) => item.name === agentName);
|
|
2278
|
-
if (!agent || !(agent instanceof FileSystemSessionSource) || !agent.isAvailable()) {
|
|
2279
|
-
return "skipped";
|
|
2280
|
-
}
|
|
2281
|
-
const cached = loadCachedSessions(agentName);
|
|
2282
|
-
const baseline = cached?.sessions ?? this.byAgent[agentName] ?? [];
|
|
2283
|
-
const meta = cached?.meta ?? buildAgentCacheMeta(agent);
|
|
2284
|
-
if (cached) {
|
|
2285
|
-
restoreAgentCacheMeta(agent, cached.meta);
|
|
2286
|
-
}
|
|
2287
|
-
try {
|
|
2288
|
-
const result = await this.scanAgentInWorker(
|
|
2289
|
-
agent,
|
|
2290
|
-
baseline,
|
|
2291
|
-
null,
|
|
2292
|
-
{},
|
|
2293
|
-
{ sourceSync: true, meta }
|
|
2294
|
-
);
|
|
2295
|
-
agent.setSessionMetaMap(new Map(Object.entries(result.meta)));
|
|
2296
|
-
const fullSessions = attachMissingProjectIdentities(result.sessions);
|
|
2297
|
-
const filtered = this.applyFilters(fullSessions);
|
|
2298
|
-
const diff = buildRefreshDiff(
|
|
2299
|
-
agentName,
|
|
2300
|
-
this.byAgent[agentName] ?? [],
|
|
2301
|
-
filtered,
|
|
2302
|
-
result.changedIds ?? []
|
|
2303
|
-
);
|
|
2304
|
-
this.byAgent[agentName] = sortSessions(filtered);
|
|
2305
|
-
this.rebuildSessions();
|
|
2306
|
-
await this.searchIndexJobs.enqueue("scan.backfill", [
|
|
2307
|
-
{
|
|
2308
|
-
kind: "full",
|
|
2309
|
-
context: "scan.backfill",
|
|
2310
|
-
agentName,
|
|
2311
|
-
sessions: fullSessions,
|
|
2312
|
-
meta: buildAgentCacheMeta(agent),
|
|
2313
|
-
saveCache: true
|
|
2314
|
-
}
|
|
2315
|
-
]);
|
|
2316
|
-
markAgentFullSyncCompleted(agentName);
|
|
2317
|
-
if (diff.event) {
|
|
2318
|
-
diff.event.totalSessions = this.sessions.length;
|
|
2319
|
-
this.emit(diff.event);
|
|
2320
|
-
}
|
|
2321
|
-
appLogger.info("scan.backfill.done", {
|
|
2322
|
-
agent: agentName,
|
|
2323
|
-
duration_ms: Math.round(performance.now() - startedAt),
|
|
2324
|
-
sessions: fullSessions.length,
|
|
2325
|
-
changed: result.changedIds?.length ?? 0
|
|
2326
|
-
});
|
|
2327
|
-
return "committed";
|
|
2328
|
-
} catch (error) {
|
|
2329
|
-
appLogger.error("scan.backfill.error", { agent: agentName, error });
|
|
2330
|
-
console.error(`[${agentName}] Backfill failed:`, error);
|
|
2331
|
-
return "failed";
|
|
2332
|
-
}
|
|
2777
|
+
for (const listener of this.listeners) listener(event);
|
|
2333
2778
|
}
|
|
2334
2779
|
queueEvent(event) {
|
|
2335
2780
|
this.pendingEvent = this.pendingEvent ? mergeEvents(this.pendingEvent, event) : event;
|
|
2336
|
-
if (this.pendingEventTimer)
|
|
2337
|
-
return;
|
|
2338
|
-
}
|
|
2781
|
+
if (this.pendingEventTimer) return;
|
|
2339
2782
|
this.pendingEventTimer = setTimeout(() => {
|
|
2340
2783
|
const pending = this.pendingEvent;
|
|
2341
2784
|
this.pendingEvent = null;
|
|
2342
2785
|
this.pendingEventTimer = null;
|
|
2343
|
-
if (pending)
|
|
2344
|
-
this.emitNow(pending);
|
|
2345
|
-
}
|
|
2786
|
+
if (pending) this.emitNow(pending);
|
|
2346
2787
|
}, NEW_SESSION_EVENT_WINDOW_MS);
|
|
2347
2788
|
}
|
|
2348
2789
|
rebuildSessions() {
|
|
@@ -2350,369 +2791,28 @@ var LiveScanStore = class {
|
|
|
2350
2791
|
}
|
|
2351
2792
|
getSmartTagWorkerUrl() {
|
|
2352
2793
|
const workerUrl = new URL("./smart-tag-worker.js", import.meta.url);
|
|
2353
|
-
if (workerUrl.protocol === "file:" && !existsSync5(fileURLToPath3(workerUrl)))
|
|
2354
|
-
return null;
|
|
2355
|
-
}
|
|
2794
|
+
if (workerUrl.protocol === "file:" && !existsSync5(fileURLToPath3(workerUrl))) return null;
|
|
2356
2795
|
return workerUrl;
|
|
2357
2796
|
}
|
|
2358
|
-
getScanRefreshWorkerUrl() {
|
|
2359
|
-
return new URL("./scan-refresh-worker.js", import.meta.url);
|
|
2360
|
-
}
|
|
2361
|
-
scanAgentInWorker(agent, previousSessions, changedIds, scanOptions, workerOptions = {}) {
|
|
2362
|
-
const workerUrl = this.getScanRefreshWorkerUrl();
|
|
2363
|
-
return new Promise((resolve4, reject) => {
|
|
2364
|
-
const worker = new Worker2(workerUrl, {
|
|
2365
|
-
workerData: {
|
|
2366
|
-
agentName: agent.name,
|
|
2367
|
-
previousSessions,
|
|
2368
|
-
changedIds,
|
|
2369
|
-
sourceSync: workerOptions.sourceSync,
|
|
2370
|
-
scanOptions,
|
|
2371
|
-
meta: workerOptions.meta ?? buildAgentCacheMeta(agent)
|
|
2372
|
-
}
|
|
2373
|
-
});
|
|
2374
|
-
worker.unref();
|
|
2375
|
-
this.scanRefreshWorkers.add(worker);
|
|
2376
|
-
let settled = false;
|
|
2377
|
-
const finish = (callback, terminate = true) => {
|
|
2378
|
-
if (settled) return;
|
|
2379
|
-
settled = true;
|
|
2380
|
-
this.scanRefreshWorkers.delete(worker);
|
|
2381
|
-
if (terminate) void worker.terminate();
|
|
2382
|
-
callback();
|
|
2383
|
-
};
|
|
2384
|
-
worker.on("message", (message) => {
|
|
2385
|
-
if (message.type === "progress") {
|
|
2386
|
-
this.updateAgentScanProgress(agent.name, message.progress);
|
|
2387
|
-
return;
|
|
2388
|
-
}
|
|
2389
|
-
if (message.type === "done") {
|
|
2390
|
-
finish(
|
|
2391
|
-
() => resolve4({
|
|
2392
|
-
sessions: message.sessions,
|
|
2393
|
-
meta: message.meta,
|
|
2394
|
-
changedIds: message.changedIds
|
|
2395
|
-
})
|
|
2396
|
-
);
|
|
2397
|
-
return;
|
|
2398
|
-
}
|
|
2399
|
-
finish(() => reject(new Error(message.error)));
|
|
2400
|
-
});
|
|
2401
|
-
worker.once("error", (error) => {
|
|
2402
|
-
finish(() => reject(error));
|
|
2403
|
-
});
|
|
2404
|
-
worker.once("exit", (code) => {
|
|
2405
|
-
if (!settled) {
|
|
2406
|
-
appLogger.warn("scan.refresh_worker.exit_before_done", {
|
|
2407
|
-
agent: agent.name,
|
|
2408
|
-
code
|
|
2409
|
-
});
|
|
2410
|
-
finish(
|
|
2411
|
-
() => reject(new Error(`Scan refresh worker exited before completing (code ${code})`)),
|
|
2412
|
-
false
|
|
2413
|
-
);
|
|
2414
|
-
}
|
|
2415
|
-
});
|
|
2416
|
-
});
|
|
2417
|
-
}
|
|
2418
|
-
buildFullSearchIndexJobs(context) {
|
|
2419
|
-
return this.agents.map((agent) => {
|
|
2420
|
-
const cached = loadCachedSessions(agent.name);
|
|
2421
|
-
if (cached) {
|
|
2422
|
-
return {
|
|
2423
|
-
kind: "full",
|
|
2424
|
-
context,
|
|
2425
|
-
agentName: agent.name,
|
|
2426
|
-
sessions: cached.sessions,
|
|
2427
|
-
meta: cached.meta
|
|
2428
|
-
};
|
|
2429
|
-
}
|
|
2430
|
-
return {
|
|
2431
|
-
kind: "full",
|
|
2432
|
-
context,
|
|
2433
|
-
agentName: agent.name,
|
|
2434
|
-
sessions: this.byAgent[agent.name] ?? [],
|
|
2435
|
-
meta: buildAgentCacheMeta(agent)
|
|
2436
|
-
};
|
|
2437
|
-
});
|
|
2438
|
-
}
|
|
2439
2797
|
applyScanResult(result) {
|
|
2440
|
-
const knownAgents = createRegisteredAgents();
|
|
2441
2798
|
const agentMap = /* @__PURE__ */ new Map();
|
|
2442
2799
|
const allowedAgents = this.getAllowedAgents();
|
|
2443
|
-
for (const agent of result.agents)
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
for (const agent of knownAgents) {
|
|
2447
|
-
if (!agentMap.has(agent.name)) {
|
|
2448
|
-
agentMap.set(agent.name, agent);
|
|
2449
|
-
}
|
|
2450
|
-
}
|
|
2451
|
-
this.agents = [...agentMap.values()].filter((agent) => {
|
|
2452
|
-
if (!allowedAgents) {
|
|
2453
|
-
return true;
|
|
2454
|
-
}
|
|
2455
|
-
return allowedAgents.has(agent.name.toLowerCase());
|
|
2456
|
-
});
|
|
2457
|
-
this.byAgent = {};
|
|
2458
|
-
for (const agent of this.agents) {
|
|
2459
|
-
this.byAgent[agent.name] = sortSessions(result.byAgent[agent.name] ?? []);
|
|
2460
|
-
this.refreshes.setLastRefreshAt(
|
|
2461
|
-
agent.name,
|
|
2462
|
-
result.cacheTimestamps?.[agent.name] ?? Date.now()
|
|
2463
|
-
);
|
|
2800
|
+
for (const agent of result.agents) agentMap.set(agent.name, agent);
|
|
2801
|
+
for (const agent of createRegisteredAgents()) {
|
|
2802
|
+
if (!agentMap.has(agent.name)) agentMap.set(agent.name, agent);
|
|
2464
2803
|
}
|
|
2804
|
+
this.agents = [...agentMap.values()].filter(
|
|
2805
|
+
(agent) => !allowedAgents || allowedAgents.has(agent.name.toLowerCase())
|
|
2806
|
+
);
|
|
2807
|
+
this.byAgent = Object.fromEntries(
|
|
2808
|
+
this.agents.map((agent) => [agent.name, sortSessions(result.byAgent[agent.name] ?? [])])
|
|
2809
|
+
);
|
|
2465
2810
|
this.rebuildSessions();
|
|
2466
2811
|
}
|
|
2467
2812
|
getAllowedAgents() {
|
|
2468
|
-
if (!this.scanOptions.agents?.length)
|
|
2469
|
-
return null;
|
|
2470
|
-
}
|
|
2813
|
+
if (!this.scanOptions.agents?.length) return null;
|
|
2471
2814
|
return new Set(this.scanOptions.agents.map((agent) => agent.toLowerCase()));
|
|
2472
2815
|
}
|
|
2473
|
-
applyFilters(sessions) {
|
|
2474
|
-
return filterSessions(sessions, { ...this.scanOptions, ...this.startupScanOptions });
|
|
2475
|
-
}
|
|
2476
|
-
async refreshInitialIndex() {
|
|
2477
|
-
const startedAt = performance.now();
|
|
2478
|
-
const context = "scan.initial.background";
|
|
2479
|
-
try {
|
|
2480
|
-
await this.searchIndexJobs.enqueue(context, this.buildFullSearchIndexJobs(context));
|
|
2481
|
-
appLogger.info(`${context}.complete`, {
|
|
2482
|
-
duration_ms: Math.round(performance.now() - startedAt),
|
|
2483
|
-
sessions: this.sessions.length
|
|
2484
|
-
});
|
|
2485
|
-
} catch (error) {
|
|
2486
|
-
if (this.shuttingDown) {
|
|
2487
|
-
return;
|
|
2488
|
-
}
|
|
2489
|
-
appLogger.error(`${context}.error`, { error });
|
|
2490
|
-
console.error("[search] Background index sync failed:", error);
|
|
2491
|
-
}
|
|
2492
|
-
}
|
|
2493
|
-
/**
|
|
2494
|
-
* Throttles rather than debounces: a pending timer only gets replaced by a
|
|
2495
|
-
* request with an earlier deadline. Plain debounce (reset on every call)
|
|
2496
|
-
* would let a steady stream of events — each arriving before the adaptive
|
|
2497
|
-
* backoff elapses — push the deadline out forever and starve the refresh.
|
|
2498
|
-
*/
|
|
2499
|
-
scheduleRefresh(agentName, delayMs = REFRESH_DEBOUNCE_MS) {
|
|
2500
|
-
this.refreshes.schedule(agentName, delayMs, () => this.refreshAgent(agentName));
|
|
2501
|
-
}
|
|
2502
|
-
async refreshAgent(agentName) {
|
|
2503
|
-
await this.refreshes.runRefresh(agentName, async () => {
|
|
2504
|
-
this.beginAgentScan(agentName);
|
|
2505
|
-
try {
|
|
2506
|
-
return await this.runRefresh(agentName);
|
|
2507
|
-
} catch (error) {
|
|
2508
|
-
appLogger.error("scan.refresh.error", { agent: agentName, error });
|
|
2509
|
-
console.error(`[${agentName}] Session refresh failed:`, error);
|
|
2510
|
-
return "failed";
|
|
2511
|
-
} finally {
|
|
2512
|
-
this.finishAgentScan(agentName);
|
|
2513
|
-
}
|
|
2514
|
-
});
|
|
2515
|
-
}
|
|
2516
|
-
async runRefresh(agentName) {
|
|
2517
|
-
const startedAt = performance.now();
|
|
2518
|
-
const pendingPathCount = this.refreshes.takePendingPathCount(agentName);
|
|
2519
|
-
const agent = this.agents.find((item) => item.name === agentName);
|
|
2520
|
-
if (!agent) {
|
|
2521
|
-
appLogger.warn("scan.refresh.missing_agent", { agent: agentName });
|
|
2522
|
-
return "skipped";
|
|
2523
|
-
}
|
|
2524
|
-
const previousSessions = this.byAgent[agentName] ?? [];
|
|
2525
|
-
const cached = loadCachedSessions(agentName);
|
|
2526
|
-
const refreshBaseline = cached?.sessions ?? previousSessions;
|
|
2527
|
-
const cacheTimestamp = cached?.timestamp ?? this.refreshes.lastRefreshAt(agentName);
|
|
2528
|
-
if (cached) {
|
|
2529
|
-
restoreAgentCacheMeta(agent, cached.meta);
|
|
2530
|
-
}
|
|
2531
|
-
const isInitialized = isAgentCacheInitialized(agentName);
|
|
2532
|
-
let nextSessions = previousSessions;
|
|
2533
|
-
let fullScanSessions = null;
|
|
2534
|
-
let preciseChangedIds = null;
|
|
2535
|
-
let usedIncrementalScan = false;
|
|
2536
|
-
let persistenceDiff = null;
|
|
2537
|
-
let availabilityDuration = 0;
|
|
2538
|
-
let checkDuration = 0;
|
|
2539
|
-
let scanDuration = 0;
|
|
2540
|
-
let filterDuration = 0;
|
|
2541
|
-
let diffDuration = 0;
|
|
2542
|
-
let persistDuration = 0;
|
|
2543
|
-
let searchIndexDuration = 0;
|
|
2544
|
-
let persistentJobKind;
|
|
2545
|
-
const availabilityStartedAt = performance.now();
|
|
2546
|
-
const isAvailable = agent.isAvailable();
|
|
2547
|
-
availabilityDuration = performance.now() - availabilityStartedAt;
|
|
2548
|
-
if (!isAvailable) {
|
|
2549
|
-
nextSessions = [];
|
|
2550
|
-
this.refreshes.setLastRefreshAt(agentName, Date.now());
|
|
2551
|
-
} else if (!isInitialized) {
|
|
2552
|
-
this.setScanPhase("initializing");
|
|
2553
|
-
const scanStartedAt = performance.now();
|
|
2554
|
-
const result = await this.scanAgentInWorker(
|
|
2555
|
-
agent,
|
|
2556
|
-
previousSessions,
|
|
2557
|
-
null,
|
|
2558
|
-
this.startupScanOptions
|
|
2559
|
-
);
|
|
2560
|
-
nextSessions = result.sessions;
|
|
2561
|
-
agent.setSessionMetaMap?.(new Map(Object.entries(result.meta)));
|
|
2562
|
-
fullScanSessions = attachMissingProjectIdentities(nextSessions);
|
|
2563
|
-
nextSessions = fullScanSessions;
|
|
2564
|
-
scanDuration = performance.now() - scanStartedAt;
|
|
2565
|
-
this.refreshes.setLastRefreshAt(agentName, Date.now());
|
|
2566
|
-
} else if (cached && agent instanceof FileSystemSessionSource) {
|
|
2567
|
-
const scanStartedAt = performance.now();
|
|
2568
|
-
const result = await this.scanAgentInWorker(
|
|
2569
|
-
agent,
|
|
2570
|
-
cached.sessions,
|
|
2571
|
-
null,
|
|
2572
|
-
this.startupScanOptions,
|
|
2573
|
-
{
|
|
2574
|
-
sourceSync: true,
|
|
2575
|
-
meta: cached.meta
|
|
2576
|
-
}
|
|
2577
|
-
);
|
|
2578
|
-
nextSessions = result.sessions;
|
|
2579
|
-
agent.setSessionMetaMap(new Map(Object.entries(result.meta)));
|
|
2580
|
-
preciseChangedIds = result.changedIds ?? [];
|
|
2581
|
-
usedIncrementalScan = true;
|
|
2582
|
-
persistenceDiff = buildRefreshDiff(
|
|
2583
|
-
agentName,
|
|
2584
|
-
cached.sessions,
|
|
2585
|
-
attachMissingProjectIdentities(nextSessions),
|
|
2586
|
-
preciseChangedIds
|
|
2587
|
-
);
|
|
2588
|
-
scanDuration = performance.now() - scanStartedAt;
|
|
2589
|
-
this.refreshes.setLastRefreshAt(agentName, Date.now());
|
|
2590
|
-
if (preciseChangedIds.length === 0) {
|
|
2591
|
-
appLogger.debug("scan.refresh.unchanged", {
|
|
2592
|
-
agent: agentName,
|
|
2593
|
-
duration_ms: Math.round(performance.now() - startedAt)
|
|
2594
|
-
});
|
|
2595
|
-
}
|
|
2596
|
-
} else if (refreshBaseline.length > 0) {
|
|
2597
|
-
const checkStartedAt = performance.now();
|
|
2598
|
-
const checkResult = await Promise.resolve(
|
|
2599
|
-
agent.checkForChanges(cacheTimestamp, refreshBaseline)
|
|
2600
|
-
);
|
|
2601
|
-
checkDuration = performance.now() - checkStartedAt;
|
|
2602
|
-
this.refreshes.setLastRefreshAt(agentName, checkResult.timestamp);
|
|
2603
|
-
if (!checkResult.hasChanges) {
|
|
2604
|
-
appLogger.debug("scan.refresh.unchanged", {
|
|
2605
|
-
agent: agentName,
|
|
2606
|
-
duration_ms: Math.round(performance.now() - startedAt)
|
|
2607
|
-
});
|
|
2608
|
-
return "unchanged";
|
|
2609
|
-
}
|
|
2610
|
-
preciseChangedIds = checkResult.changedIds ?? null;
|
|
2611
|
-
usedIncrementalScan = Array.isArray(checkResult.changedIds);
|
|
2612
|
-
const scanStartedAt = performance.now();
|
|
2613
|
-
nextSessions = await Promise.resolve(
|
|
2614
|
-
agent.incrementalScan(refreshBaseline, checkResult.changedIds ?? [])
|
|
2615
|
-
);
|
|
2616
|
-
const nextBaseline = attachMissingProjectIdentities(nextSessions);
|
|
2617
|
-
persistenceDiff = buildRefreshDiff(
|
|
2618
|
-
agentName,
|
|
2619
|
-
refreshBaseline,
|
|
2620
|
-
nextBaseline,
|
|
2621
|
-
preciseChangedIds ?? []
|
|
2622
|
-
);
|
|
2623
|
-
nextSessions = nextBaseline;
|
|
2624
|
-
scanDuration = performance.now() - scanStartedAt;
|
|
2625
|
-
} else {
|
|
2626
|
-
const scanStartedAt = performance.now();
|
|
2627
|
-
const result = await this.scanAgentInWorker(agent, previousSessions, null, {});
|
|
2628
|
-
nextSessions = result.sessions;
|
|
2629
|
-
agent.setSessionMetaMap(new Map(Object.entries(result.meta)));
|
|
2630
|
-
fullScanSessions = attachMissingProjectIdentities(nextSessions);
|
|
2631
|
-
nextSessions = fullScanSessions;
|
|
2632
|
-
scanDuration = performance.now() - scanStartedAt;
|
|
2633
|
-
this.refreshes.setLastRefreshAt(agentName, Date.now());
|
|
2634
|
-
}
|
|
2635
|
-
nextSessions = attachMissingProjectIdentities(nextSessions);
|
|
2636
|
-
const filterStartedAt = performance.now();
|
|
2637
|
-
nextSessions = this.applyFilters(nextSessions);
|
|
2638
|
-
filterDuration = performance.now() - filterStartedAt;
|
|
2639
|
-
const diffStartedAt = performance.now();
|
|
2640
|
-
const diff = buildRefreshDiff(
|
|
2641
|
-
agentName,
|
|
2642
|
-
previousSessions,
|
|
2643
|
-
nextSessions,
|
|
2644
|
-
preciseChangedIds ?? []
|
|
2645
|
-
);
|
|
2646
|
-
diffDuration = performance.now() - diffStartedAt;
|
|
2647
|
-
const searchIndexOptions = pendingPathCount >= SEARCH_INDEX_BULK_PENDING_PATH_THRESHOLD ? { isBulk: true } : void 0;
|
|
2648
|
-
const canPersistIncrementally = usedIncrementalScan;
|
|
2649
|
-
const persistentChanges = persistenceDiff?.changedSessions ?? diff.changedSessions;
|
|
2650
|
-
const persistentRemovedSessionIds = persistenceDiff?.removedSessionIds ?? diff.removedSessionIds;
|
|
2651
|
-
const changedSessionIds = canPersistIncrementally ? new Set(persistentChanges.map(({ session }) => session.id)) : void 0;
|
|
2652
|
-
const cacheMeta = buildAgentCacheMeta(agent, changedSessionIds);
|
|
2653
|
-
const persistStartedAt = performance.now();
|
|
2654
|
-
const persistentJob = canPersistIncrementally ? {
|
|
2655
|
-
kind: "changes",
|
|
2656
|
-
context: "scan.refresh",
|
|
2657
|
-
agentName,
|
|
2658
|
-
changes: persistentChanges,
|
|
2659
|
-
removedSessionIds: persistentRemovedSessionIds,
|
|
2660
|
-
meta: cacheMeta,
|
|
2661
|
-
...searchIndexOptions ? { searchIndexOptions } : {}
|
|
2662
|
-
} : fullScanSessions ? {
|
|
2663
|
-
kind: "full",
|
|
2664
|
-
context: "scan.refresh",
|
|
2665
|
-
agentName,
|
|
2666
|
-
sessions: fullScanSessions,
|
|
2667
|
-
meta: buildAgentCacheMeta(agent),
|
|
2668
|
-
saveCache: true,
|
|
2669
|
-
...searchIndexOptions ? { searchIndexOptions } : {}
|
|
2670
|
-
} : null;
|
|
2671
|
-
if (persistentJob) {
|
|
2672
|
-
persistentJobKind = persistentJob.kind;
|
|
2673
|
-
const persist = this.searchIndexJobs.enqueue("scan.refresh", [persistentJob]);
|
|
2674
|
-
if (!isInitialized && persistentJob.kind === "full") {
|
|
2675
|
-
await persist;
|
|
2676
|
-
} else {
|
|
2677
|
-
void persist.catch((error) => {
|
|
2678
|
-
appLogger.error("scan.refresh.persist.error", { agent: agentName, error });
|
|
2679
|
-
console.error(`[${agentName}] Session persistence failed:`, error);
|
|
2680
|
-
});
|
|
2681
|
-
}
|
|
2682
|
-
}
|
|
2683
|
-
persistDuration = performance.now() - persistStartedAt;
|
|
2684
|
-
const searchIndexStartedAt = performance.now();
|
|
2685
|
-
searchIndexDuration = performance.now() - searchIndexStartedAt;
|
|
2686
|
-
logSearchIndexSync("scan.refresh", null, { pending_paths: pendingPathCount });
|
|
2687
|
-
const event = diff.event;
|
|
2688
|
-
this.byAgent[agentName] = sortSessions(nextSessions);
|
|
2689
|
-
this.rebuildSessions();
|
|
2690
|
-
if (event) {
|
|
2691
|
-
event.totalSessions = this.sessions.length;
|
|
2692
|
-
this.emit(event);
|
|
2693
|
-
}
|
|
2694
|
-
const totalDurationMs = performance.now() - startedAt;
|
|
2695
|
-
this.refreshes.setLastRefreshDuration(agentName, totalDurationMs);
|
|
2696
|
-
appLogger.info("scan.refresh.done", {
|
|
2697
|
-
agent: agentName,
|
|
2698
|
-
duration_ms: Math.round(totalDurationMs),
|
|
2699
|
-
sessions: nextSessions.length,
|
|
2700
|
-
new_sessions: event?.newSessions ?? 0,
|
|
2701
|
-
updated_sessions: event?.updatedSessions ?? 0,
|
|
2702
|
-
removed_sessions: event?.removedSessions ?? 0,
|
|
2703
|
-
pending_paths: pendingPathCount,
|
|
2704
|
-
availability_ms: Math.round(availabilityDuration),
|
|
2705
|
-
check_ms: Math.round(checkDuration),
|
|
2706
|
-
scan_ms: Math.round(scanDuration),
|
|
2707
|
-
filter_ms: Math.round(filterDuration),
|
|
2708
|
-
diff_ms: Math.round(diffDuration),
|
|
2709
|
-
persist_ms: Math.round(persistDuration),
|
|
2710
|
-
search_index_ms: Math.round(searchIndexDuration),
|
|
2711
|
-
persistent_index_worker_job: persistentJobKind,
|
|
2712
|
-
persistent_index_skipped: !persistentJob || void 0
|
|
2713
|
-
});
|
|
2714
|
-
return "committed";
|
|
2715
|
-
}
|
|
2716
2816
|
};
|
|
2717
2817
|
|
|
2718
2818
|
// src/output.ts
|
|
@@ -2760,13 +2860,6 @@ function hasExplicitPortArg(argv) {
|
|
|
2760
2860
|
}
|
|
2761
2861
|
|
|
2762
2862
|
// src/index.ts
|
|
2763
|
-
function parseDateToTimestamp(dateStr) {
|
|
2764
|
-
const date = new Date(dateStr);
|
|
2765
|
-
if (Number.isNaN(date.getTime())) {
|
|
2766
|
-
throw new Error(`Invalid date: ${dateStr}`);
|
|
2767
|
-
}
|
|
2768
|
-
return date.getTime();
|
|
2769
|
-
}
|
|
2770
2863
|
function parseSessionUri(uri) {
|
|
2771
2864
|
const match = uri.match(/^([a-z]+):\/\/(.+)$/i);
|
|
2772
2865
|
if (!match) return null;
|
|
@@ -2892,7 +2985,7 @@ var main = defineCommand({
|
|
|
2892
2985
|
log_path: appLogger.getLogPath()
|
|
2893
2986
|
});
|
|
2894
2987
|
if (clearCache) {
|
|
2895
|
-
const { clearCache: clear } = await import("./dist-
|
|
2988
|
+
const { clearCache: clear } = await import("./dist-5356XOFP.js");
|
|
2896
2989
|
clear();
|
|
2897
2990
|
appLogger.info("cache.clear");
|
|
2898
2991
|
console.log("Cache cleared.");
|
|
@@ -2910,27 +3003,26 @@ var main = defineCommand({
|
|
|
2910
3003
|
if (cwdFilter === ".") {
|
|
2911
3004
|
cwdFilter = process.cwd();
|
|
2912
3005
|
}
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
}
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
listDefaultDays = 0;
|
|
2924
|
-
}
|
|
2925
|
-
}
|
|
2926
|
-
const listDefaultTo = args.to ? parseDateToTimestamp(args.to) : void 0;
|
|
3006
|
+
const {
|
|
3007
|
+
from: listDefaultFrom,
|
|
3008
|
+
to: listDefaultTo,
|
|
3009
|
+
days: listDefaultDays
|
|
3010
|
+
} = resolveTimeWindow({
|
|
3011
|
+
mode: "cli",
|
|
3012
|
+
from: args.from,
|
|
3013
|
+
to: args.to,
|
|
3014
|
+
days: args.days
|
|
3015
|
+
});
|
|
2927
3016
|
const scanOptions = {
|
|
2928
3017
|
agents: targetSession ? [targetSession.agent] : args.agent ? args.agent.split(",").map((a) => a.trim()) : void 0,
|
|
2929
3018
|
cwd: cwdFilter,
|
|
2930
3019
|
useCache
|
|
2931
3020
|
};
|
|
2932
3021
|
const startupScanOptions = targetSession || jsonOnly ? {} : { from: listDefaultFrom, to: listDefaultTo };
|
|
2933
|
-
const store = new LiveScanStore(
|
|
3022
|
+
const store = new LiveScanStore({
|
|
3023
|
+
watchEnabled: !jsonOnly,
|
|
3024
|
+
scanOptions,
|
|
3025
|
+
startupScanOptions,
|
|
2934
3026
|
deferInitialRefresh: !jsonOnly
|
|
2935
3027
|
});
|
|
2936
3028
|
await store.initialize();
|