codesesh 0.6.1 → 0.7.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 +5 -5
- package/dist/{chunk-SQYHWMQV.js → chunk-JTHZVP6G.js} +1561 -601
- package/dist/chunk-JTHZVP6G.js.map +1 -0
- package/dist/{dist-NT4CH6KD.js → dist-FQAQSHHO.js} +18 -2
- package/dist/index.js +882 -219
- package/dist/index.js.map +1 -1
- package/dist/scan-refresh-worker.js +50 -0
- package/dist/scan-refresh-worker.js.map +1 -0
- package/dist/search-index-worker.js +46 -16
- package/dist/search-index-worker.js.map +1 -1
- package/dist/smart-tag-worker.js +33 -0
- package/dist/smart-tag-worker.js.map +1 -0
- package/dist/web/assets/index-Dy__ZJD8.js +108 -0
- package/dist/web/assets/index-xfiMbcgm.css +2 -0
- package/dist/web/index.html +189 -3
- package/package.json +1 -1
- package/dist/chunk-SQYHWMQV.js.map +0 -1
- package/dist/web/assets/index-D9lwrpQG.css +0 -2
- package/dist/web/assets/index-DZaQy6OQ.js +0 -106
- /package/dist/{dist-NT4CH6KD.js.map → dist-FQAQSHHO.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
BookmarkStorageUnavailableError,
|
|
4
4
|
classifySessionTags,
|
|
5
5
|
computeIdentity,
|
|
6
|
+
createProjectScopeMatcher,
|
|
6
7
|
createRegisteredAgents,
|
|
7
8
|
deleteBookmark,
|
|
8
9
|
extractSessionFileActivity,
|
|
@@ -11,21 +12,24 @@ import {
|
|
|
11
12
|
getCursorDataPath,
|
|
12
13
|
getSmartTagSourceTimestamp,
|
|
13
14
|
importBookmarks,
|
|
15
|
+
isAgentCacheInitialized,
|
|
14
16
|
listBookmarks,
|
|
15
17
|
listCachedProjectGroups,
|
|
16
18
|
listFileActivity,
|
|
19
|
+
listSessionFileActivity,
|
|
20
|
+
loadCachedSessionData,
|
|
21
|
+
loadCachedSessions,
|
|
22
|
+
matchesProjectScope,
|
|
17
23
|
parseSearchQuery,
|
|
18
24
|
perf,
|
|
19
25
|
realFs,
|
|
20
26
|
refreshPricingCache,
|
|
21
27
|
resolveProviderRoots,
|
|
22
|
-
saveCachedSessions,
|
|
23
28
|
scanSessions,
|
|
24
29
|
searchFileActivitySessions,
|
|
25
30
|
searchSessions,
|
|
26
|
-
syncSessionSearchIndex,
|
|
27
31
|
upsertBookmark
|
|
28
|
-
} from "./chunk-
|
|
32
|
+
} from "./chunk-JTHZVP6G.js";
|
|
29
33
|
|
|
30
34
|
// src/index.ts
|
|
31
35
|
import { defineCommand, runMain } from "citty";
|
|
@@ -191,6 +195,7 @@ function logSearchIndexSync(context, result, data = {}) {
|
|
|
191
195
|
}
|
|
192
196
|
|
|
193
197
|
// src/api/handlers.ts
|
|
198
|
+
var DASHBOARD_RECENT_LIMIT = 10;
|
|
194
199
|
function isRecord(value) {
|
|
195
200
|
return typeof value === "object" && value !== null;
|
|
196
201
|
}
|
|
@@ -290,12 +295,6 @@ function filterSessionsByActivityWindow(sessions, from, to) {
|
|
|
290
295
|
return true;
|
|
291
296
|
});
|
|
292
297
|
}
|
|
293
|
-
function matchesProjectScope(session, cwd) {
|
|
294
|
-
if (!session.directory) return false;
|
|
295
|
-
const identity = computeIdentity(cwd, realFs);
|
|
296
|
-
if (session.project_identity?.key === identity.key) return true;
|
|
297
|
-
return session.directory.toLowerCase().includes(cwd.toLowerCase());
|
|
298
|
-
}
|
|
299
298
|
function sanitizeClientLogData(value) {
|
|
300
299
|
if (!isRecord(value)) return {};
|
|
301
300
|
return Object.fromEntries(
|
|
@@ -406,22 +405,9 @@ function attachProjectMetrics(projects, sessions) {
|
|
|
406
405
|
};
|
|
407
406
|
});
|
|
408
407
|
}
|
|
409
|
-
function
|
|
410
|
-
if (scope.agent && getSessionAgentName(session) !== scope.agent) return false;
|
|
411
|
-
if (scope.projectKey) {
|
|
412
|
-
const identity = session.project_identity;
|
|
413
|
-
if (!identity || identity.key !== scope.projectKey) return false;
|
|
414
|
-
if (scope.projectKind && identity.kind !== scope.projectKind) return false;
|
|
415
|
-
}
|
|
416
|
-
return true;
|
|
417
|
-
}
|
|
418
|
-
function filterSessionsByDashboardScope(sessions, scope) {
|
|
419
|
-
if (!scope.agent && !scope.projectKey) return sessions;
|
|
420
|
-
return sessions.filter((session) => matchesDashboardScope(session, scope));
|
|
421
|
-
}
|
|
422
|
-
function matchesRecentSearchFilters(session, options) {
|
|
408
|
+
function matchesRecentSearchFilters(session, options, projectScope) {
|
|
423
409
|
if (options.projectKey && session.project_identity?.key !== options.projectKey) return false;
|
|
424
|
-
if (
|
|
410
|
+
if (projectScope && !matchesProjectScope(session, projectScope)) return false;
|
|
425
411
|
if (options.project) {
|
|
426
412
|
const projectNeedle = options.project.toLowerCase();
|
|
427
413
|
const projectText = [
|
|
@@ -438,9 +424,10 @@ function matchesRecentSearchFilters(session, options) {
|
|
|
438
424
|
return true;
|
|
439
425
|
}
|
|
440
426
|
function recentSearchSessions(scanResult, options) {
|
|
427
|
+
const projectScope = options.cwd ? createProjectScopeMatcher(options.cwd) : null;
|
|
441
428
|
const entries = options.agent ? [[options.agent, scanResult.byAgent[options.agent] ?? []]] : Object.entries(scanResult.byAgent);
|
|
442
429
|
return entries.flatMap(
|
|
443
|
-
([agentName, sessions]) => filterSessionsByActivityWindow(sessions, options.from, options.to).filter((session) => matchesRecentSearchFilters(session, options)).map((session) => ({ agentName, session }))
|
|
430
|
+
([agentName, sessions]) => filterSessionsByActivityWindow(sessions, options.from, options.to).filter((session) => matchesRecentSearchFilters(session, options, projectScope)).map((session) => ({ agentName, session }))
|
|
444
431
|
).toSorted(
|
|
445
432
|
(a, b) => (b.session.time_updated ?? b.session.time_created) - (a.session.time_updated ?? a.session.time_created)
|
|
446
433
|
).slice(0, options.limit).map(({ agentName, session }) => ({
|
|
@@ -459,6 +446,9 @@ function handleGetConfig(c, defaults) {
|
|
|
459
446
|
}
|
|
460
447
|
});
|
|
461
448
|
}
|
|
449
|
+
function handleGetScanStatus(c, scanSource) {
|
|
450
|
+
return c.json(scanSource.getScanStatus());
|
|
451
|
+
}
|
|
462
452
|
function handleGetAgents(c, scanSource, defaults = {}) {
|
|
463
453
|
const scanResult = scanSource.getSnapshot();
|
|
464
454
|
const { from, to } = defaults;
|
|
@@ -497,7 +487,8 @@ function handleGetSessions(c, scanSource, defaults = {}) {
|
|
|
497
487
|
if (projectKey) {
|
|
498
488
|
sessions = sessions.filter((s) => s.project_identity?.key === projectKey);
|
|
499
489
|
} else if (cwd) {
|
|
500
|
-
|
|
490
|
+
const projectScope = createProjectScopeMatcher(cwd);
|
|
491
|
+
sessions = sessions.filter((s) => matchesProjectScope(s, projectScope));
|
|
501
492
|
}
|
|
502
493
|
sessions = filterSessionsByActivityWindow(sessions, from, to);
|
|
503
494
|
if (tag) {
|
|
@@ -577,14 +568,26 @@ async function handleGetSessionData(c, scanSource) {
|
|
|
577
568
|
return c.json({ error: `Unknown agent: ${agentName}` }, 404);
|
|
578
569
|
}
|
|
579
570
|
try {
|
|
571
|
+
const head = scanResult.byAgent[agentName]?.find((item) => item.id === sessionId);
|
|
580
572
|
const loadStartedAt = performance.now();
|
|
581
|
-
const
|
|
573
|
+
const cachedData = loadCachedSessionData(agentName, sessionId);
|
|
574
|
+
const cachedMessageCount = cachedData?.stats.message_count ?? 0;
|
|
575
|
+
const cacheHasExpectedMessages = cachedData !== null && (cachedData.messages.length > 0 || cachedMessageCount === 0);
|
|
576
|
+
const data = cacheHasExpectedMessages ? cachedData : head ? agent.getSessionData(sessionId) : null;
|
|
582
577
|
const loadDuration = performance.now() - loadStartedAt;
|
|
578
|
+
if (!data) {
|
|
579
|
+
appLogger.warn("api.session_data.cache_miss", {
|
|
580
|
+
agent: agentName,
|
|
581
|
+
session_id: sessionId,
|
|
582
|
+
duration_ms: Math.round(performance.now() - startedAt)
|
|
583
|
+
});
|
|
584
|
+
return c.json({ error: "Session cache not ready" }, 404);
|
|
585
|
+
}
|
|
583
586
|
const tagStartedAt = performance.now();
|
|
584
|
-
const smartTags = classifySessionTags(data);
|
|
587
|
+
const smartTags = data.smart_tags ?? classifySessionTags(data);
|
|
585
588
|
const tagDuration = performance.now() - tagStartedAt;
|
|
586
|
-
const head = scanResult.byAgent[agentName]?.find((item) => item.id === sessionId);
|
|
587
589
|
const projectIdentity = data.project_identity ?? head?.project_identity ?? computeIdentity(data.directory, realFs);
|
|
590
|
+
const fileActivity = data.file_activity ?? (cacheHasExpectedMessages && cachedData ? listSessionFileActivity(agentName, sessionId) : extractSessionFileActivity(agentName, sessionId, projectIdentity.key, data.messages));
|
|
588
591
|
appLogger.info("api.session_data", {
|
|
589
592
|
agent: agentName,
|
|
590
593
|
session_id: sessionId,
|
|
@@ -598,12 +601,7 @@ async function handleGetSessionData(c, scanSource) {
|
|
|
598
601
|
project_identity: projectIdentity,
|
|
599
602
|
smart_tags: smartTags,
|
|
600
603
|
smart_tags_source_updated_at: getSmartTagSourceTimestamp(data),
|
|
601
|
-
file_activity:
|
|
602
|
-
agentName,
|
|
603
|
-
sessionId,
|
|
604
|
-
projectIdentity.key,
|
|
605
|
-
data.messages
|
|
606
|
-
)
|
|
604
|
+
file_activity: fileActivity
|
|
607
605
|
});
|
|
608
606
|
} catch (err) {
|
|
609
607
|
const message = err instanceof Error ? err.message : "Failed to load session";
|
|
@@ -699,13 +697,17 @@ function startOfLocalDay(ts) {
|
|
|
699
697
|
function resolveDashboardWindow(defaults, queryDays, queryFrom, queryTo) {
|
|
700
698
|
const now = Date.now();
|
|
701
699
|
const toTs = parseDateParam(queryTo, defaults.to) ?? now;
|
|
702
|
-
const
|
|
700
|
+
const hasQueryDays = queryDays != null && queryDays.trim() !== "";
|
|
701
|
+
const parsedDays = hasQueryDays ? parseInt(queryDays, 10) : NaN;
|
|
703
702
|
let days = Number.isFinite(parsedDays) && parsedDays > 0 ? parsedDays : defaults.days;
|
|
704
703
|
const fromFromQuery = parseDateParam(queryFrom, void 0);
|
|
705
704
|
let fromTs;
|
|
706
705
|
if (fromFromQuery != null) {
|
|
707
706
|
fromTs = fromFromQuery;
|
|
708
707
|
days ??= Math.max(1, Math.ceil((toTs - fromTs) / 864e5));
|
|
708
|
+
} else if (parsedDays === 0 || !hasQueryDays && defaults.days === 0) {
|
|
709
|
+
days = 0;
|
|
710
|
+
return { to: toTs, days };
|
|
709
711
|
} else if (defaults.from != null) {
|
|
710
712
|
fromTs = defaults.from;
|
|
711
713
|
days ??= Math.max(1, Math.ceil((toTs - fromTs) / 864e5));
|
|
@@ -730,79 +732,81 @@ function handleGetDashboard(c, scanSource, defaults = {}) {
|
|
|
730
732
|
projectKind: optionalQueryValue(c.req.query("projectKind")),
|
|
731
733
|
projectKey: optionalQueryValue(c.req.query("projectKey"))
|
|
732
734
|
};
|
|
733
|
-
const
|
|
734
|
-
const
|
|
735
|
-
const
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
name,
|
|
742
|
-
filterSessionsByActivityWindow(sessions, from, to).length
|
|
743
|
-
])
|
|
744
|
-
)
|
|
745
|
-
);
|
|
735
|
+
const agentMetrics = /* @__PURE__ */ new Map();
|
|
736
|
+
const agentMetricKeyByName = /* @__PURE__ */ new Map();
|
|
737
|
+
for (const name of Object.keys(scanResult.byAgent)) {
|
|
738
|
+
if (scope.agent && name.toLowerCase() !== scope.agent) continue;
|
|
739
|
+
agentMetrics.set(name, { sessions: 0, messages: 0, tokens: 0 });
|
|
740
|
+
agentMetricKeyByName.set(name.toLowerCase(), name);
|
|
741
|
+
}
|
|
742
|
+
const agentInfo = getAgentInfoMap({});
|
|
746
743
|
const agentInfoMap = new Map(agentInfo.map((a) => [a.name, a]));
|
|
744
|
+
let totalSessions = 0;
|
|
747
745
|
let totalMessages = 0;
|
|
748
746
|
let totalTokens = 0;
|
|
749
747
|
let totalCost = 0;
|
|
750
748
|
let hasEstimatedCost = false;
|
|
751
749
|
let latestActivity = 0;
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
totalTokens += getTotalTokens(session.stats);
|
|
755
|
-
totalCost += session.stats.total_cost ?? 0;
|
|
756
|
-
if (session.stats.cost_source === "estimated") hasEstimatedCost = true;
|
|
757
|
-
const activity = getSessionActivityTime(session);
|
|
758
|
-
if (activity > latestActivity) latestActivity = activity;
|
|
759
|
-
}
|
|
760
|
-
const perAgent = Object.entries(scopedByAgent).map(([name, sessions]) => {
|
|
761
|
-
const info = agentInfoMap.get(name);
|
|
762
|
-
const agentWindowed = filterSessionsByActivityWindow(sessions, from, to);
|
|
763
|
-
let messages = 0;
|
|
764
|
-
let tokens = 0;
|
|
765
|
-
for (const s of agentWindowed) {
|
|
766
|
-
messages += s.stats.message_count;
|
|
767
|
-
tokens += getTotalTokens(s.stats);
|
|
768
|
-
}
|
|
769
|
-
return {
|
|
770
|
-
name,
|
|
771
|
-
displayName: info?.displayName ?? name,
|
|
772
|
-
icon: info?.icon ?? "",
|
|
773
|
-
sessions: agentWindowed.length,
|
|
774
|
-
messages,
|
|
775
|
-
tokens
|
|
776
|
-
};
|
|
777
|
-
}).filter((item) => item.sessions > 0).sort((a, b) => b.sessions - a.sessions);
|
|
750
|
+
const recentCandidates = [];
|
|
751
|
+
const modelAgg = /* @__PURE__ */ new Map();
|
|
778
752
|
const dailyMap = /* @__PURE__ */ new Map();
|
|
779
753
|
const dailyTokenMap = /* @__PURE__ */ new Map();
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
754
|
+
if (from != null) {
|
|
755
|
+
const bucketStart = startOfLocalDay(from);
|
|
756
|
+
const bucketDays = Math.floor((startOfLocalDay(to) - bucketStart) / 864e5) + 1;
|
|
757
|
+
for (let i = 0; i < bucketDays; i += 1) {
|
|
758
|
+
const ts = bucketStart + i * 864e5;
|
|
759
|
+
const key = toLocalDateKey(ts);
|
|
760
|
+
dailyMap.set(key, { date: key, sessions: 0, messages: 0 });
|
|
761
|
+
dailyTokenMap.set(key, { date: key, input: 0, output: 0, cache_read: 0, cache_create: 0 });
|
|
762
|
+
}
|
|
787
763
|
}
|
|
788
|
-
const
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
}
|
|
796
|
-
const tokenBucket = dailyTokenMap.get(key);
|
|
797
|
-
if (tokenBucket) {
|
|
798
|
-
const cacheRead = session.stats.total_cache_read_tokens ?? 0;
|
|
799
|
-
const cacheCreate = session.stats.total_cache_create_tokens ?? 0;
|
|
800
|
-
const pureInput = session.stats.total_input_tokens - cacheRead - cacheCreate;
|
|
801
|
-
tokenBucket.input += Math.max(0, pureInput);
|
|
802
|
-
tokenBucket.output += session.stats.total_output_tokens;
|
|
803
|
-
tokenBucket.cache_read += cacheRead;
|
|
804
|
-
tokenBucket.cache_create += cacheCreate;
|
|
764
|
+
for (const session of scanResult.sessions) {
|
|
765
|
+
const agentName = getSessionAgentName(session);
|
|
766
|
+
if (scope.agent && agentName !== scope.agent) continue;
|
|
767
|
+
if (scope.projectKey) {
|
|
768
|
+
const identity = session.project_identity;
|
|
769
|
+
if (!identity || identity.key !== scope.projectKey) continue;
|
|
770
|
+
if (scope.projectKind && identity.kind !== scope.projectKind) continue;
|
|
805
771
|
}
|
|
772
|
+
const activity = getSessionActivityTime(session);
|
|
773
|
+
if (from != null && activity < from) continue;
|
|
774
|
+
if (activity > to) continue;
|
|
775
|
+
const messageCount = session.stats.message_count;
|
|
776
|
+
const sessionTokens = getTotalTokens(session.stats);
|
|
777
|
+
totalSessions += 1;
|
|
778
|
+
totalMessages += messageCount;
|
|
779
|
+
totalTokens += sessionTokens;
|
|
780
|
+
totalCost += session.stats.total_cost ?? 0;
|
|
781
|
+
if (session.stats.cost_source === "estimated") hasEstimatedCost = true;
|
|
782
|
+
if (activity > latestActivity) latestActivity = activity;
|
|
783
|
+
const metricKey = agentMetricKeyByName.get(agentName);
|
|
784
|
+
if (metricKey) {
|
|
785
|
+
const metric = agentMetrics.get(metricKey);
|
|
786
|
+
metric.sessions += 1;
|
|
787
|
+
metric.messages += messageCount;
|
|
788
|
+
metric.tokens += sessionTokens;
|
|
789
|
+
}
|
|
790
|
+
const key = toLocalDateKey(activity);
|
|
791
|
+
let bucket = dailyMap.get(key);
|
|
792
|
+
if (!bucket) {
|
|
793
|
+
bucket = { date: key, sessions: 0, messages: 0 };
|
|
794
|
+
dailyMap.set(key, bucket);
|
|
795
|
+
}
|
|
796
|
+
bucket.sessions += 1;
|
|
797
|
+
bucket.messages += messageCount;
|
|
798
|
+
let tokenBucket = dailyTokenMap.get(key);
|
|
799
|
+
if (!tokenBucket) {
|
|
800
|
+
tokenBucket = { date: key, input: 0, output: 0, cache_read: 0, cache_create: 0 };
|
|
801
|
+
dailyTokenMap.set(key, tokenBucket);
|
|
802
|
+
}
|
|
803
|
+
const cacheRead = session.stats.total_cache_read_tokens ?? 0;
|
|
804
|
+
const cacheCreate = session.stats.total_cache_create_tokens ?? 0;
|
|
805
|
+
const pureInput = session.stats.total_input_tokens - cacheRead - cacheCreate;
|
|
806
|
+
tokenBucket.input += Math.max(0, pureInput);
|
|
807
|
+
tokenBucket.output += session.stats.total_output_tokens;
|
|
808
|
+
tokenBucket.cache_read += cacheRead;
|
|
809
|
+
tokenBucket.cache_create += cacheCreate;
|
|
806
810
|
if (session.model_usage) {
|
|
807
811
|
for (const [model, tokens] of Object.entries(session.model_usage)) {
|
|
808
812
|
const entry = modelAgg.get(model);
|
|
@@ -814,17 +818,41 @@ function handleGetDashboard(c, scanSource, defaults = {}) {
|
|
|
814
818
|
}
|
|
815
819
|
}
|
|
816
820
|
}
|
|
821
|
+
let recentIndex = recentCandidates.length;
|
|
822
|
+
for (let i = 0; i < recentCandidates.length; i += 1) {
|
|
823
|
+
if (activity > recentCandidates[i].activity) {
|
|
824
|
+
recentIndex = i;
|
|
825
|
+
break;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
if (recentIndex < DASHBOARD_RECENT_LIMIT) {
|
|
829
|
+
recentCandidates.splice(recentIndex, 0, { session, activity });
|
|
830
|
+
if (recentCandidates.length > DASHBOARD_RECENT_LIMIT) recentCandidates.pop();
|
|
831
|
+
}
|
|
817
832
|
}
|
|
818
|
-
const
|
|
819
|
-
|
|
833
|
+
const perAgent = [...agentMetrics.entries()].map(([name, metrics]) => {
|
|
834
|
+
const info = agentInfoMap.get(name);
|
|
835
|
+
return {
|
|
836
|
+
name,
|
|
837
|
+
displayName: info?.displayName ?? name,
|
|
838
|
+
icon: info?.icon ?? "",
|
|
839
|
+
sessions: metrics.sessions,
|
|
840
|
+
messages: metrics.messages,
|
|
841
|
+
tokens: metrics.tokens
|
|
842
|
+
};
|
|
843
|
+
}).filter((item) => item.sessions > 0).sort((a, b) => b.sessions - a.sessions);
|
|
844
|
+
const dailyActivity = [...dailyMap.values()].sort((a, b) => a.date.localeCompare(b.date));
|
|
845
|
+
const dailyTokenActivity = [...dailyTokenMap.values()].sort(
|
|
846
|
+
(a, b) => a.date.localeCompare(b.date)
|
|
847
|
+
);
|
|
820
848
|
const modelDistribution = [...modelAgg.entries()].map(([model, { tokens, sessions: count }]) => ({ model, tokens, sessions: count })).sort((a, b) => b.tokens - a.tokens);
|
|
821
|
-
const recentSessions =
|
|
849
|
+
const recentSessions = recentCandidates.map(({ session }) => {
|
|
822
850
|
const agentKey = getSessionAgentName(session);
|
|
823
851
|
return { ...session, agentName: agentKey };
|
|
824
852
|
});
|
|
825
853
|
const data = {
|
|
826
854
|
totals: {
|
|
827
|
-
sessions:
|
|
855
|
+
sessions: totalSessions,
|
|
828
856
|
messages: totalMessages,
|
|
829
857
|
tokens: totalTokens,
|
|
830
858
|
cost: totalCost,
|
|
@@ -862,7 +890,11 @@ function createSseResponse(store, signal) {
|
|
|
862
890
|
`));
|
|
863
891
|
};
|
|
864
892
|
write("connected", { timestamp: Date.now() });
|
|
865
|
-
|
|
893
|
+
write("scan-status", store.getScanStatus());
|
|
894
|
+
const unsubscribeSessions = store.subscribe((event) => {
|
|
895
|
+
write(event.type, event);
|
|
896
|
+
});
|
|
897
|
+
const unsubscribeScanStatus = store.subscribeScanStatus((event) => {
|
|
866
898
|
write(event.type, event);
|
|
867
899
|
});
|
|
868
900
|
const heartbeat = setInterval(() => {
|
|
@@ -870,7 +902,8 @@ function createSseResponse(store, signal) {
|
|
|
870
902
|
}, 15e3);
|
|
871
903
|
const close = () => {
|
|
872
904
|
clearInterval(heartbeat);
|
|
873
|
-
|
|
905
|
+
unsubscribeSessions();
|
|
906
|
+
unsubscribeScanStatus();
|
|
874
907
|
controller.close();
|
|
875
908
|
};
|
|
876
909
|
signal.addEventListener("abort", close, { once: true });
|
|
@@ -896,6 +929,9 @@ function createApiRoutes(scanSource, store, options = {}) {
|
|
|
896
929
|
days: options.defaultSessionDays
|
|
897
930
|
};
|
|
898
931
|
api.get("/config", (c) => handleGetConfig(c, listDefaults));
|
|
932
|
+
if (store) {
|
|
933
|
+
api.get("/status", (c) => handleGetScanStatus(c, store));
|
|
934
|
+
}
|
|
899
935
|
api.get("/agents", (c) => handleGetAgents(c, scanSource, listDefaults));
|
|
900
936
|
api.get("/projects", (c) => handleGetProjects(c, scanSource, listDefaults));
|
|
901
937
|
api.get("/sessions", (c) => handleGetSessions(c, scanSource, listDefaults));
|
|
@@ -947,6 +983,13 @@ function getServerStartupErrorMessage(error, port) {
|
|
|
947
983
|
}
|
|
948
984
|
return error instanceof Error ? error.message : `\u542F\u52A8\u670D\u52A1\u5668\u5931\u8D25: ${String(error)}`;
|
|
949
985
|
}
|
|
986
|
+
function isAddressInUse(error) {
|
|
987
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "EADDRINUSE";
|
|
988
|
+
}
|
|
989
|
+
function getListeningPort(server, fallback) {
|
|
990
|
+
const address = server.address();
|
|
991
|
+
return typeof address === "object" && address !== null ? address.port : fallback;
|
|
992
|
+
}
|
|
950
993
|
async function createServer(port, store, options = {}) {
|
|
951
994
|
const app = new Hono2();
|
|
952
995
|
app.use("*", async (c, next) => {
|
|
@@ -987,26 +1030,48 @@ async function createServer(port, store, options = {}) {
|
|
|
987
1030
|
app.use("/*", serveStatic({ root: webDistPath }));
|
|
988
1031
|
app.get("/*", serveStatic({ root: webDistPath, path: "index.html" }));
|
|
989
1032
|
}
|
|
990
|
-
const
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
server.
|
|
996
|
-
|
|
997
|
-
await
|
|
1033
|
+
const attempts = Math.max(1, options.portFallbackAttempts ?? 1);
|
|
1034
|
+
let server = null;
|
|
1035
|
+
let actualPort = port;
|
|
1036
|
+
for (let offset = 0; offset < attempts; offset += 1) {
|
|
1037
|
+
const candidatePort = port + offset;
|
|
1038
|
+
server = serve({ fetch: app.fetch, port: candidatePort });
|
|
1039
|
+
try {
|
|
1040
|
+
await waitForListening(server);
|
|
1041
|
+
actualPort = getListeningPort(server, candidatePort);
|
|
1042
|
+
break;
|
|
1043
|
+
} catch (error) {
|
|
1044
|
+
appLogger.error("server.listen.error", { port: candidatePort, error });
|
|
1045
|
+
server.close();
|
|
1046
|
+
if (isAddressInUse(error) && offset < attempts - 1) {
|
|
1047
|
+
continue;
|
|
1048
|
+
}
|
|
1049
|
+
if (store.shutdown) {
|
|
1050
|
+
await store.shutdown();
|
|
1051
|
+
}
|
|
1052
|
+
if (isAddressInUse(error) && attempts > 1) {
|
|
1053
|
+
throw new Error(
|
|
1054
|
+
`\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`
|
|
1055
|
+
);
|
|
1056
|
+
}
|
|
1057
|
+
throw new Error(getServerStartupErrorMessage(error, candidatePort));
|
|
998
1058
|
}
|
|
999
|
-
throw new Error(getServerStartupErrorMessage(error, port));
|
|
1000
1059
|
}
|
|
1001
|
-
const url = `http://localhost:${
|
|
1002
|
-
appLogger.info("server.listen", { port, url });
|
|
1060
|
+
const url = `http://localhost:${actualPort}`;
|
|
1061
|
+
appLogger.info("server.listen", { port: actualPort, requested_port: port, url });
|
|
1003
1062
|
return {
|
|
1004
1063
|
url,
|
|
1005
|
-
shutdown: () => {
|
|
1006
|
-
appLogger.info("server.shutdown", { port });
|
|
1007
|
-
|
|
1064
|
+
shutdown: async () => {
|
|
1065
|
+
appLogger.info("server.shutdown", { port: actualPort });
|
|
1066
|
+
await new Promise((resolve4) => {
|
|
1067
|
+
if (!server) {
|
|
1068
|
+
resolve4();
|
|
1069
|
+
return;
|
|
1070
|
+
}
|
|
1071
|
+
server.close(() => resolve4());
|
|
1072
|
+
});
|
|
1008
1073
|
if (store.shutdown) {
|
|
1009
|
-
|
|
1074
|
+
await store.shutdown();
|
|
1010
1075
|
}
|
|
1011
1076
|
}
|
|
1012
1077
|
};
|
|
@@ -1042,49 +1107,89 @@ function sessionSignature(session) {
|
|
|
1042
1107
|
session.stats.total_tokens ?? 0
|
|
1043
1108
|
]);
|
|
1044
1109
|
}
|
|
1045
|
-
function buildAgentCacheMeta(agent) {
|
|
1110
|
+
function buildAgentCacheMeta(agent, sessionIds) {
|
|
1046
1111
|
const metaMap = agent.getSessionMetaMap?.();
|
|
1047
1112
|
const meta = {};
|
|
1048
1113
|
if (!metaMap) return meta;
|
|
1049
1114
|
for (const [id, data] of metaMap.entries()) {
|
|
1115
|
+
if (sessionIds && !sessionIds.has(id)) continue;
|
|
1050
1116
|
meta[id] = { id, ...data };
|
|
1051
1117
|
}
|
|
1052
1118
|
return meta;
|
|
1053
1119
|
}
|
|
1054
|
-
function
|
|
1120
|
+
function attachMissingProjectIdentities(sessions) {
|
|
1121
|
+
const identities = /* @__PURE__ */ new Map();
|
|
1122
|
+
return sessions.map((session) => {
|
|
1123
|
+
if (session.project_identity) return session;
|
|
1124
|
+
const directory = session.directory || "";
|
|
1125
|
+
let identity = identities.get(directory);
|
|
1126
|
+
if (!identity) {
|
|
1127
|
+
identity = computeIdentity(directory, realFs);
|
|
1128
|
+
identities.set(directory, identity);
|
|
1129
|
+
}
|
|
1130
|
+
return { ...session, project_identity: identity };
|
|
1131
|
+
});
|
|
1132
|
+
}
|
|
1133
|
+
function buildRefreshDiff(agentName, previousSessions, nextSessions, candidateChangedIds = []) {
|
|
1055
1134
|
const previousMap = new Map(previousSessions.map((session) => [session.id, session]));
|
|
1056
1135
|
const nextMap = new Map(nextSessions.map((session) => [session.id, session]));
|
|
1136
|
+
const candidateChangedIdSet = new Set(candidateChangedIds);
|
|
1137
|
+
const changedSessions = [];
|
|
1138
|
+
const removedSessionIds = [];
|
|
1057
1139
|
let newSessions = 0;
|
|
1058
1140
|
let updatedSessions = 0;
|
|
1059
1141
|
let removedSessions = 0;
|
|
1060
|
-
|
|
1142
|
+
nextSessions.forEach((session, index) => {
|
|
1143
|
+
const id = session.id;
|
|
1061
1144
|
const previous = previousMap.get(id);
|
|
1062
1145
|
if (!previous) {
|
|
1063
1146
|
newSessions += 1;
|
|
1064
|
-
|
|
1147
|
+
changedSessions.push({ session, sortIndex: index });
|
|
1148
|
+
return;
|
|
1065
1149
|
}
|
|
1066
|
-
|
|
1150
|
+
const hasSignatureChange = sessionSignature(previous) !== sessionSignature(session);
|
|
1151
|
+
const hasContentChange = candidateChangedIdSet.has(id);
|
|
1152
|
+
if (hasSignatureChange || hasContentChange) {
|
|
1067
1153
|
updatedSessions += 1;
|
|
1068
1154
|
}
|
|
1069
|
-
|
|
1155
|
+
if (hasContentChange || hasSignatureChange) {
|
|
1156
|
+
changedSessions.push({ session, sortIndex: index });
|
|
1157
|
+
}
|
|
1158
|
+
});
|
|
1070
1159
|
for (const id of previousMap.keys()) {
|
|
1071
1160
|
if (!nextMap.has(id)) {
|
|
1072
1161
|
removedSessions += 1;
|
|
1162
|
+
removedSessionIds.push(id);
|
|
1073
1163
|
}
|
|
1074
1164
|
}
|
|
1075
1165
|
if (newSessions === 0 && updatedSessions === 0 && removedSessions === 0) {
|
|
1076
|
-
return null;
|
|
1166
|
+
return { event: null, changedSessions, removedSessionIds };
|
|
1077
1167
|
}
|
|
1078
1168
|
return {
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1169
|
+
changedSessions,
|
|
1170
|
+
removedSessionIds,
|
|
1171
|
+
event: {
|
|
1172
|
+
type: "sessions-updated",
|
|
1173
|
+
changedAgents: [agentName],
|
|
1174
|
+
newSessions,
|
|
1175
|
+
updatedSessions,
|
|
1176
|
+
removedSessions,
|
|
1177
|
+
totalSessions: nextSessions.length,
|
|
1178
|
+
timestamp: Date.now(),
|
|
1179
|
+
changedSessionHeads: changedSessions.map(({ session }) => ({ agentName, session })),
|
|
1180
|
+
removedSessionRefs: removedSessionIds.map((sessionId) => ({ agentName, sessionId }))
|
|
1181
|
+
}
|
|
1086
1182
|
};
|
|
1087
1183
|
}
|
|
1184
|
+
function restoreAgentCacheMeta(agent, meta) {
|
|
1185
|
+
agent.setSessionMetaMap?.(new Map(Object.entries(meta)));
|
|
1186
|
+
}
|
|
1187
|
+
function sourceFingerprintFromMeta(meta) {
|
|
1188
|
+
return typeof meta?.sourceFingerprint === "string" ? meta.sourceFingerprint : null;
|
|
1189
|
+
}
|
|
1190
|
+
function sourcePathFromMeta(meta) {
|
|
1191
|
+
return typeof meta?.sourcePath === "string" ? meta.sourcePath : null;
|
|
1192
|
+
}
|
|
1088
1193
|
function toAbsolutePath(path) {
|
|
1089
1194
|
return isAbsolute(path) ? path : resolve2(path);
|
|
1090
1195
|
}
|
|
@@ -1127,6 +1232,23 @@ function isRelatedPath(changedPath, targetPath) {
|
|
|
1127
1232
|
return isSameOrChildPath(targetPath, changedPath) || isSameOrChildPath(changedPath, targetPath);
|
|
1128
1233
|
}
|
|
1129
1234
|
function mergeEvents(previous, next) {
|
|
1235
|
+
const changedSessionHeads = /* @__PURE__ */ new Map();
|
|
1236
|
+
const removedSessionRefs = /* @__PURE__ */ new Map();
|
|
1237
|
+
const sessionKey = (agentName, sessionId) => `${agentName}\0${sessionId}`;
|
|
1238
|
+
const addChanged = (item) => {
|
|
1239
|
+
const key = sessionKey(item.agentName, item.session.id);
|
|
1240
|
+
removedSessionRefs.delete(key);
|
|
1241
|
+
changedSessionHeads.set(key, item);
|
|
1242
|
+
};
|
|
1243
|
+
const addRemoved = (item) => {
|
|
1244
|
+
const key = sessionKey(item.agentName, item.sessionId);
|
|
1245
|
+
changedSessionHeads.delete(key);
|
|
1246
|
+
removedSessionRefs.set(key, item);
|
|
1247
|
+
};
|
|
1248
|
+
for (const item of previous.changedSessionHeads) addChanged(item);
|
|
1249
|
+
for (const item of previous.removedSessionRefs) addRemoved(item);
|
|
1250
|
+
for (const item of next.changedSessionHeads) addChanged(item);
|
|
1251
|
+
for (const item of next.removedSessionRefs) addRemoved(item);
|
|
1130
1252
|
return {
|
|
1131
1253
|
type: "sessions-updated",
|
|
1132
1254
|
changedAgents: Array.from(/* @__PURE__ */ new Set([...previous.changedAgents, ...next.changedAgents])),
|
|
@@ -1134,7 +1256,9 @@ function mergeEvents(previous, next) {
|
|
|
1134
1256
|
updatedSessions: previous.updatedSessions + next.updatedSessions,
|
|
1135
1257
|
removedSessions: previous.removedSessions + next.removedSessions,
|
|
1136
1258
|
totalSessions: next.totalSessions,
|
|
1137
|
-
timestamp: next.timestamp
|
|
1259
|
+
timestamp: next.timestamp,
|
|
1260
|
+
changedSessionHeads: [...changedSessionHeads.values()],
|
|
1261
|
+
removedSessionRefs: [...removedSessionRefs.values()]
|
|
1138
1262
|
};
|
|
1139
1263
|
}
|
|
1140
1264
|
function mergeScopes(target, scopes) {
|
|
@@ -1163,7 +1287,10 @@ function resolveAgentWatchTargets(agentName) {
|
|
|
1163
1287
|
{ path: "data/claudecode" }
|
|
1164
1288
|
];
|
|
1165
1289
|
case "codex":
|
|
1166
|
-
return [
|
|
1290
|
+
return [
|
|
1291
|
+
{ path: join2(roots.codexRoot, "sessions") },
|
|
1292
|
+
{ path: join2(roots.codexRoot, "session_index.jsonl") }
|
|
1293
|
+
];
|
|
1167
1294
|
case "cursor":
|
|
1168
1295
|
return cursorDataPath ? [
|
|
1169
1296
|
{
|
|
@@ -1187,18 +1314,31 @@ function resolveAgentWatchTargets(agentName) {
|
|
|
1187
1314
|
}
|
|
1188
1315
|
}
|
|
1189
1316
|
var LiveScanStore = class {
|
|
1190
|
-
constructor(watchEnabled = true, scanOptions = {}, startupScanOptions = {}) {
|
|
1317
|
+
constructor(watchEnabled = true, scanOptions = {}, startupScanOptions = {}, storeOptions = {}) {
|
|
1191
1318
|
this.watchEnabled = watchEnabled;
|
|
1192
1319
|
this.scanOptions = scanOptions;
|
|
1193
1320
|
this.startupScanOptions = startupScanOptions;
|
|
1321
|
+
this.storeOptions = storeOptions;
|
|
1194
1322
|
}
|
|
1195
1323
|
watchEnabled;
|
|
1196
1324
|
scanOptions;
|
|
1197
1325
|
startupScanOptions;
|
|
1326
|
+
storeOptions;
|
|
1198
1327
|
agents = [];
|
|
1199
1328
|
byAgent = {};
|
|
1200
1329
|
sessions = [];
|
|
1201
1330
|
listeners = /* @__PURE__ */ new Set();
|
|
1331
|
+
scanStatusListeners = /* @__PURE__ */ new Set();
|
|
1332
|
+
scanStatus = {
|
|
1333
|
+
active: false,
|
|
1334
|
+
phase: "idle",
|
|
1335
|
+
pendingAgents: [],
|
|
1336
|
+
scanningAgents: [],
|
|
1337
|
+
completedAgents: [],
|
|
1338
|
+
agentStatuses: {},
|
|
1339
|
+
totalAgents: 0,
|
|
1340
|
+
updatedAt: Date.now()
|
|
1341
|
+
};
|
|
1202
1342
|
refreshTimers = /* @__PURE__ */ new Map();
|
|
1203
1343
|
refreshTimestamps = /* @__PURE__ */ new Map();
|
|
1204
1344
|
refreshInFlight = /* @__PURE__ */ new Set();
|
|
@@ -1209,41 +1349,82 @@ var LiveScanStore = class {
|
|
|
1209
1349
|
stablePaths = /* @__PURE__ */ new Map();
|
|
1210
1350
|
pendingEvent = null;
|
|
1211
1351
|
pendingEventTimer = null;
|
|
1212
|
-
|
|
1352
|
+
backgroundRefreshTimer = null;
|
|
1213
1353
|
searchIndexWorker = null;
|
|
1354
|
+
pendingSearchIndexJobs = [];
|
|
1355
|
+
shuttingDown = false;
|
|
1214
1356
|
async initialize() {
|
|
1215
1357
|
const startedAt = performance.now();
|
|
1358
|
+
const deferInitialRefresh = this.storeOptions.deferInitialRefresh === true;
|
|
1216
1359
|
appLogger.info("scan.initial.start", {
|
|
1217
1360
|
watch_enabled: this.watchEnabled,
|
|
1218
1361
|
agents: this.scanOptions.agents,
|
|
1219
1362
|
use_cache: this.scanOptions.useCache ?? true,
|
|
1220
1363
|
startup_from: this.startupScanOptions.from,
|
|
1221
|
-
startup_to: this.startupScanOptions.to
|
|
1364
|
+
startup_to: this.startupScanOptions.to,
|
|
1365
|
+
deferred: deferInitialRefresh || void 0
|
|
1222
1366
|
});
|
|
1223
1367
|
const initialResult = await scanSessions({
|
|
1224
1368
|
...this.scanOptions,
|
|
1225
|
-
...this.startupScanOptions,
|
|
1369
|
+
...deferInitialRefresh ? this.startupScanOptions : {},
|
|
1226
1370
|
useCache: this.scanOptions.useCache ?? true,
|
|
1227
1371
|
smartRefresh: false,
|
|
1228
|
-
|
|
1229
|
-
|
|
1372
|
+
cacheOnly: deferInitialRefresh,
|
|
1373
|
+
writeCache: deferInitialRefresh ? false : this.scanOptions.writeCache,
|
|
1374
|
+
smartTagWorkerUrl: this.getSmartTagWorkerUrl() ?? void 0,
|
|
1375
|
+
includeSmartTags: deferInitialRefresh ? false : void 0
|
|
1230
1376
|
});
|
|
1231
1377
|
this.applyScanResult(initialResult);
|
|
1378
|
+
const indexStartedAt = performance.now();
|
|
1379
|
+
if (!deferInitialRefresh) {
|
|
1380
|
+
await this.enqueueSearchIndexJobs(
|
|
1381
|
+
"scan.initial",
|
|
1382
|
+
this.buildFullSearchIndexJobs("scan.initial")
|
|
1383
|
+
);
|
|
1384
|
+
}
|
|
1385
|
+
const indexDuration = performance.now() - indexStartedAt;
|
|
1232
1386
|
appLogger.info("scan.initial.done", {
|
|
1233
1387
|
duration_ms: Math.round(performance.now() - startedAt),
|
|
1388
|
+
index_ms: deferInitialRefresh ? void 0 : Math.round(indexDuration),
|
|
1389
|
+
deferred: deferInitialRefresh || void 0,
|
|
1234
1390
|
sessions: this.sessions.length,
|
|
1235
1391
|
agents: Object.fromEntries(
|
|
1236
1392
|
Object.entries(this.byAgent).map(([key, value]) => [key, value.length])
|
|
1237
|
-
)
|
|
1393
|
+
),
|
|
1394
|
+
agent_timings: initialResult.timings ? Object.fromEntries(
|
|
1395
|
+
Object.entries(initialResult.timings).map(([name, t]) => [
|
|
1396
|
+
name,
|
|
1397
|
+
{
|
|
1398
|
+
total_ms: Math.round(t.total),
|
|
1399
|
+
cache_load_ms: t.cacheLoad != null ? Math.round(t.cacheLoad) : void 0,
|
|
1400
|
+
check_changes_ms: t.checkChanges != null ? Math.round(t.checkChanges) : void 0,
|
|
1401
|
+
scan_ms: t.scan != null ? Math.round(t.scan) : void 0,
|
|
1402
|
+
identity_ms: t.identity != null ? Math.round(t.identity) : void 0,
|
|
1403
|
+
tags_ms: t.tags != null ? Math.round(t.tags) : void 0
|
|
1404
|
+
}
|
|
1405
|
+
])
|
|
1406
|
+
) : void 0
|
|
1238
1407
|
});
|
|
1239
1408
|
if (this.watchEnabled) {
|
|
1240
1409
|
this.startWatching();
|
|
1241
|
-
this.initialSearchIndexTimer = setTimeout(() => {
|
|
1242
|
-
this.initialSearchIndexTimer = null;
|
|
1243
|
-
this.startSearchIndexWorker("scan.initial.background");
|
|
1244
|
-
}, 1e3);
|
|
1245
1410
|
}
|
|
1246
1411
|
}
|
|
1412
|
+
startBackgroundRefresh() {
|
|
1413
|
+
if (this.backgroundRefreshTimer) {
|
|
1414
|
+
return;
|
|
1415
|
+
}
|
|
1416
|
+
const agentNames = this.agents.map((agent) => agent.name);
|
|
1417
|
+
this.startScanBatch(agentNames, "scanning");
|
|
1418
|
+
this.backgroundRefreshTimer = setTimeout(() => {
|
|
1419
|
+
this.backgroundRefreshTimer = null;
|
|
1420
|
+
for (const agentName of agentNames) {
|
|
1421
|
+
this.scheduleRefresh(agentName, 0);
|
|
1422
|
+
}
|
|
1423
|
+
if (agentNames.length === 0) {
|
|
1424
|
+
this.finishScanBatch();
|
|
1425
|
+
}
|
|
1426
|
+
}, 0);
|
|
1427
|
+
}
|
|
1247
1428
|
getSnapshot() {
|
|
1248
1429
|
return {
|
|
1249
1430
|
sessions: this.sessions,
|
|
@@ -1251,13 +1432,35 @@ var LiveScanStore = class {
|
|
|
1251
1432
|
agents: this.agents
|
|
1252
1433
|
};
|
|
1253
1434
|
}
|
|
1435
|
+
getScanStatus() {
|
|
1436
|
+
return {
|
|
1437
|
+
type: "scan-status",
|
|
1438
|
+
...this.scanStatus,
|
|
1439
|
+
pendingAgents: [...this.scanStatus.pendingAgents],
|
|
1440
|
+
scanningAgents: [...this.scanStatus.scanningAgents],
|
|
1441
|
+
completedAgents: [...this.scanStatus.completedAgents],
|
|
1442
|
+
agentStatuses: Object.fromEntries(
|
|
1443
|
+
Object.entries(this.scanStatus.agentStatuses).map(([agentName, status]) => [
|
|
1444
|
+
agentName,
|
|
1445
|
+
{ ...status }
|
|
1446
|
+
])
|
|
1447
|
+
)
|
|
1448
|
+
};
|
|
1449
|
+
}
|
|
1254
1450
|
subscribe(listener) {
|
|
1255
1451
|
this.listeners.add(listener);
|
|
1256
1452
|
return () => {
|
|
1257
1453
|
this.listeners.delete(listener);
|
|
1258
1454
|
};
|
|
1259
1455
|
}
|
|
1456
|
+
subscribeScanStatus(listener) {
|
|
1457
|
+
this.scanStatusListeners.add(listener);
|
|
1458
|
+
return () => {
|
|
1459
|
+
this.scanStatusListeners.delete(listener);
|
|
1460
|
+
};
|
|
1461
|
+
}
|
|
1260
1462
|
async shutdown() {
|
|
1463
|
+
this.shuttingDown = true;
|
|
1261
1464
|
for (const timer of this.refreshTimers.values()) {
|
|
1262
1465
|
clearTimeout(timer);
|
|
1263
1466
|
}
|
|
@@ -1273,14 +1476,18 @@ var LiveScanStore = class {
|
|
|
1273
1476
|
clearTimeout(this.pendingEventTimer);
|
|
1274
1477
|
this.pendingEventTimer = null;
|
|
1275
1478
|
}
|
|
1276
|
-
if (this.
|
|
1277
|
-
clearTimeout(this.
|
|
1278
|
-
this.
|
|
1479
|
+
if (this.backgroundRefreshTimer) {
|
|
1480
|
+
clearTimeout(this.backgroundRefreshTimer);
|
|
1481
|
+
this.backgroundRefreshTimer = null;
|
|
1279
1482
|
}
|
|
1280
1483
|
if (this.searchIndexWorker) {
|
|
1281
1484
|
await this.searchIndexWorker.terminate();
|
|
1282
1485
|
this.searchIndexWorker = null;
|
|
1283
1486
|
}
|
|
1487
|
+
for (const batch of this.pendingSearchIndexJobs) {
|
|
1488
|
+
batch.reject(new Error("Live scan store shut down"));
|
|
1489
|
+
}
|
|
1490
|
+
this.pendingSearchIndexJobs = [];
|
|
1284
1491
|
this.pendingEvent = null;
|
|
1285
1492
|
await Promise.all(this.watchers.map((watcher) => watcher.close()));
|
|
1286
1493
|
this.watchers = [];
|
|
@@ -1298,6 +1505,157 @@ var LiveScanStore = class {
|
|
|
1298
1505
|
listener(event);
|
|
1299
1506
|
}
|
|
1300
1507
|
}
|
|
1508
|
+
emitScanStatus() {
|
|
1509
|
+
const event = this.getScanStatus();
|
|
1510
|
+
for (const listener of this.scanStatusListeners) {
|
|
1511
|
+
listener(event);
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
updateScanStatus(next) {
|
|
1515
|
+
this.scanStatus = next;
|
|
1516
|
+
this.emitScanStatus();
|
|
1517
|
+
}
|
|
1518
|
+
startScanBatch(agentNames, phase) {
|
|
1519
|
+
const uniqueAgentNames = [...new Set(agentNames)];
|
|
1520
|
+
const now = Date.now();
|
|
1521
|
+
const agentStatuses = Object.fromEntries(
|
|
1522
|
+
uniqueAgentNames.map((agentName) => [
|
|
1523
|
+
agentName,
|
|
1524
|
+
{
|
|
1525
|
+
agentName,
|
|
1526
|
+
status: "pending",
|
|
1527
|
+
processed: 0,
|
|
1528
|
+
sessions: this.byAgent[agentName]?.length ?? 0,
|
|
1529
|
+
updatedAt: now
|
|
1530
|
+
}
|
|
1531
|
+
])
|
|
1532
|
+
);
|
|
1533
|
+
this.updateScanStatus({
|
|
1534
|
+
active: uniqueAgentNames.length > 0,
|
|
1535
|
+
phase: uniqueAgentNames.length > 0 ? phase : "idle",
|
|
1536
|
+
pendingAgents: uniqueAgentNames,
|
|
1537
|
+
scanningAgents: [],
|
|
1538
|
+
completedAgents: [],
|
|
1539
|
+
agentStatuses,
|
|
1540
|
+
totalAgents: uniqueAgentNames.length,
|
|
1541
|
+
startedAt: uniqueAgentNames.length > 0 ? now : void 0,
|
|
1542
|
+
updatedAt: now,
|
|
1543
|
+
completedAt: uniqueAgentNames.length > 0 ? void 0 : now
|
|
1544
|
+
});
|
|
1545
|
+
}
|
|
1546
|
+
setScanPhase(phase) {
|
|
1547
|
+
if (!this.scanStatus.active) return;
|
|
1548
|
+
this.updateScanStatus({
|
|
1549
|
+
...this.scanStatus,
|
|
1550
|
+
phase,
|
|
1551
|
+
updatedAt: Date.now()
|
|
1552
|
+
});
|
|
1553
|
+
}
|
|
1554
|
+
beginAgentScan(agentName) {
|
|
1555
|
+
if (!this.scanStatus.active) {
|
|
1556
|
+
this.startScanBatch([agentName], "scanning");
|
|
1557
|
+
}
|
|
1558
|
+
const pendingAgents = this.scanStatus.pendingAgents.filter((agent) => agent !== agentName);
|
|
1559
|
+
const scanningAgents = [.../* @__PURE__ */ new Set([...this.scanStatus.scanningAgents, agentName])];
|
|
1560
|
+
const completedAgents = this.scanStatus.completedAgents.filter((agent) => agent !== agentName);
|
|
1561
|
+
const existingStatus = this.scanStatus.agentStatuses[agentName];
|
|
1562
|
+
const agentStatuses = {
|
|
1563
|
+
...this.scanStatus.agentStatuses,
|
|
1564
|
+
[agentName]: {
|
|
1565
|
+
agentName,
|
|
1566
|
+
status: "scanning",
|
|
1567
|
+
total: existingStatus?.total,
|
|
1568
|
+
processed: existingStatus?.processed ?? 0,
|
|
1569
|
+
sessions: existingStatus?.sessions ?? this.byAgent[agentName]?.length ?? 0,
|
|
1570
|
+
startedAt: existingStatus?.startedAt ?? Date.now(),
|
|
1571
|
+
updatedAt: Date.now()
|
|
1572
|
+
}
|
|
1573
|
+
};
|
|
1574
|
+
this.updateScanStatus({
|
|
1575
|
+
...this.scanStatus,
|
|
1576
|
+
active: true,
|
|
1577
|
+
phase: this.scanStatus.phase === "initializing" ? "initializing" : "scanning",
|
|
1578
|
+
pendingAgents,
|
|
1579
|
+
scanningAgents,
|
|
1580
|
+
completedAgents,
|
|
1581
|
+
agentStatuses,
|
|
1582
|
+
totalAgents: Math.max(
|
|
1583
|
+
this.scanStatus.totalAgents,
|
|
1584
|
+
pendingAgents.length + scanningAgents.length
|
|
1585
|
+
),
|
|
1586
|
+
updatedAt: Date.now(),
|
|
1587
|
+
completedAt: void 0
|
|
1588
|
+
});
|
|
1589
|
+
}
|
|
1590
|
+
updateAgentScanProgress(agentName, progress) {
|
|
1591
|
+
const status = this.scanStatus.agentStatuses[agentName];
|
|
1592
|
+
if (!status || status.status !== "scanning") return;
|
|
1593
|
+
this.updateScanStatus({
|
|
1594
|
+
...this.scanStatus,
|
|
1595
|
+
agentStatuses: {
|
|
1596
|
+
...this.scanStatus.agentStatuses,
|
|
1597
|
+
[agentName]: {
|
|
1598
|
+
...status,
|
|
1599
|
+
total: progress.total ?? status.total,
|
|
1600
|
+
processed: progress.processed ?? status.processed,
|
|
1601
|
+
sessions: progress.sessions ?? status.sessions,
|
|
1602
|
+
updatedAt: Date.now()
|
|
1603
|
+
}
|
|
1604
|
+
},
|
|
1605
|
+
updatedAt: Date.now()
|
|
1606
|
+
});
|
|
1607
|
+
}
|
|
1608
|
+
finishAgentScan(agentName) {
|
|
1609
|
+
const pendingAgents = this.scanStatus.pendingAgents.filter((agent) => agent !== agentName);
|
|
1610
|
+
const scanningAgents = this.scanStatus.scanningAgents.filter((agent) => agent !== agentName);
|
|
1611
|
+
const completedAgents = [.../* @__PURE__ */ new Set([...this.scanStatus.completedAgents, agentName])];
|
|
1612
|
+
const active = pendingAgents.length > 0 || scanningAgents.length > 0;
|
|
1613
|
+
const now = Date.now();
|
|
1614
|
+
const previousStatus = this.scanStatus.agentStatuses[agentName];
|
|
1615
|
+
const sessions = this.byAgent[agentName]?.length ?? previousStatus?.sessions ?? 0;
|
|
1616
|
+
const total = previousStatus?.total ?? previousStatus?.processed;
|
|
1617
|
+
this.updateScanStatus({
|
|
1618
|
+
...this.scanStatus,
|
|
1619
|
+
active,
|
|
1620
|
+
phase: active ? "scanning" : "idle",
|
|
1621
|
+
pendingAgents,
|
|
1622
|
+
scanningAgents,
|
|
1623
|
+
completedAgents,
|
|
1624
|
+
agentStatuses: {
|
|
1625
|
+
...this.scanStatus.agentStatuses,
|
|
1626
|
+
[agentName]: {
|
|
1627
|
+
agentName,
|
|
1628
|
+
status: "complete",
|
|
1629
|
+
total,
|
|
1630
|
+
processed: total,
|
|
1631
|
+
sessions,
|
|
1632
|
+
startedAt: previousStatus?.startedAt,
|
|
1633
|
+
updatedAt: now,
|
|
1634
|
+
completedAt: now
|
|
1635
|
+
}
|
|
1636
|
+
},
|
|
1637
|
+
updatedAt: now,
|
|
1638
|
+
completedAt: active ? void 0 : now
|
|
1639
|
+
});
|
|
1640
|
+
}
|
|
1641
|
+
finishScanBatch() {
|
|
1642
|
+
const now = Date.now();
|
|
1643
|
+
this.updateScanStatus({
|
|
1644
|
+
...this.scanStatus,
|
|
1645
|
+
active: false,
|
|
1646
|
+
phase: "idle",
|
|
1647
|
+
pendingAgents: [],
|
|
1648
|
+
scanningAgents: [],
|
|
1649
|
+
agentStatuses: Object.fromEntries(
|
|
1650
|
+
Object.entries(this.scanStatus.agentStatuses).map(([agentName, status]) => [
|
|
1651
|
+
agentName,
|
|
1652
|
+
{ ...status, status: "complete", completedAt: status.completedAt ?? now, updatedAt: now }
|
|
1653
|
+
])
|
|
1654
|
+
),
|
|
1655
|
+
updatedAt: now,
|
|
1656
|
+
completedAt: now
|
|
1657
|
+
});
|
|
1658
|
+
}
|
|
1301
1659
|
queueEvent(event) {
|
|
1302
1660
|
this.pendingEvent = this.pendingEvent ? mergeEvents(this.pendingEvent, event) : event;
|
|
1303
1661
|
if (this.pendingEventTimer) {
|
|
@@ -1315,9 +1673,6 @@ var LiveScanStore = class {
|
|
|
1315
1673
|
rebuildSessions() {
|
|
1316
1674
|
this.sessions = sortSessions(Object.values(this.byAgent).flat());
|
|
1317
1675
|
}
|
|
1318
|
-
hasStartupWindow() {
|
|
1319
|
-
return this.startupScanOptions.from != null || this.startupScanOptions.to != null;
|
|
1320
|
-
}
|
|
1321
1676
|
getSearchIndexWorkerUrl() {
|
|
1322
1677
|
const workerUrl = new URL("./search-index-worker.js", import.meta.url);
|
|
1323
1678
|
if (workerUrl.protocol === "file:" && !existsSync3(fileURLToPath2(workerUrl))) {
|
|
@@ -1325,21 +1680,114 @@ var LiveScanStore = class {
|
|
|
1325
1680
|
}
|
|
1326
1681
|
return workerUrl;
|
|
1327
1682
|
}
|
|
1328
|
-
|
|
1329
|
-
|
|
1683
|
+
getSmartTagWorkerUrl() {
|
|
1684
|
+
const workerUrl = new URL("./smart-tag-worker.js", import.meta.url);
|
|
1685
|
+
if (workerUrl.protocol === "file:" && !existsSync3(fileURLToPath2(workerUrl))) {
|
|
1686
|
+
return null;
|
|
1687
|
+
}
|
|
1688
|
+
return workerUrl;
|
|
1689
|
+
}
|
|
1690
|
+
getScanRefreshWorkerUrl() {
|
|
1691
|
+
const workerUrl = new URL("./scan-refresh-worker.js", import.meta.url);
|
|
1692
|
+
if (workerUrl.protocol === "file:" && !existsSync3(fileURLToPath2(workerUrl))) {
|
|
1693
|
+
return null;
|
|
1694
|
+
}
|
|
1695
|
+
return workerUrl;
|
|
1696
|
+
}
|
|
1697
|
+
scanAgentInWorker(agent, previousSessions, changedIds, scanOptions) {
|
|
1698
|
+
const workerUrl = this.getScanRefreshWorkerUrl();
|
|
1699
|
+
if (!workerUrl) return null;
|
|
1700
|
+
return new Promise((resolve4, reject) => {
|
|
1701
|
+
const worker = new Worker(workerUrl, {
|
|
1702
|
+
workerData: {
|
|
1703
|
+
agentName: agent.name,
|
|
1704
|
+
previousSessions,
|
|
1705
|
+
changedIds,
|
|
1706
|
+
scanOptions,
|
|
1707
|
+
meta: buildAgentCacheMeta(agent)
|
|
1708
|
+
}
|
|
1709
|
+
});
|
|
1710
|
+
worker.unref();
|
|
1711
|
+
let settled = false;
|
|
1712
|
+
const finish = (callback) => {
|
|
1713
|
+
if (settled) return;
|
|
1714
|
+
settled = true;
|
|
1715
|
+
void worker.terminate();
|
|
1716
|
+
callback();
|
|
1717
|
+
};
|
|
1718
|
+
worker.on("message", (message) => {
|
|
1719
|
+
if (message.type === "progress") {
|
|
1720
|
+
this.updateAgentScanProgress(agent.name, message.progress);
|
|
1721
|
+
return;
|
|
1722
|
+
}
|
|
1723
|
+
if (message.type === "done") {
|
|
1724
|
+
finish(() => resolve4({ sessions: message.sessions, meta: message.meta }));
|
|
1725
|
+
return;
|
|
1726
|
+
}
|
|
1727
|
+
finish(() => reject(new Error(message.error)));
|
|
1728
|
+
});
|
|
1729
|
+
worker.once("error", (error) => {
|
|
1730
|
+
finish(() => reject(error));
|
|
1731
|
+
});
|
|
1732
|
+
worker.once("exit", (code) => {
|
|
1733
|
+
if (!settled && code !== 0) {
|
|
1734
|
+
finish(() => reject(new Error(`Scan refresh worker exited with code ${code}`)));
|
|
1735
|
+
}
|
|
1736
|
+
});
|
|
1737
|
+
});
|
|
1738
|
+
}
|
|
1739
|
+
buildFullSearchIndexJobs(context) {
|
|
1740
|
+
return this.agents.map((agent) => {
|
|
1741
|
+
const cached = loadCachedSessions(agent.name, { ignoreTtl: true });
|
|
1742
|
+
if (cached) {
|
|
1743
|
+
return {
|
|
1744
|
+
kind: "full",
|
|
1745
|
+
context,
|
|
1746
|
+
agentName: agent.name,
|
|
1747
|
+
sessions: cached.sessions,
|
|
1748
|
+
meta: cached.meta
|
|
1749
|
+
};
|
|
1750
|
+
}
|
|
1751
|
+
return {
|
|
1752
|
+
kind: "full",
|
|
1753
|
+
context,
|
|
1754
|
+
agentName: agent.name,
|
|
1755
|
+
sessions: this.byAgent[agent.name] ?? [],
|
|
1756
|
+
meta: buildAgentCacheMeta(agent)
|
|
1757
|
+
};
|
|
1758
|
+
});
|
|
1759
|
+
}
|
|
1760
|
+
enqueueSearchIndexJobs(context, jobs) {
|
|
1761
|
+
if (jobs.length === 0) return Promise.resolve();
|
|
1762
|
+
return new Promise((resolve4, reject) => {
|
|
1763
|
+
const batch = { context, jobs, resolve: resolve4, reject };
|
|
1764
|
+
if (this.searchIndexWorker) {
|
|
1765
|
+
this.pendingSearchIndexJobs.push(batch);
|
|
1766
|
+
appLogger.debug("search_index.worker_queued", {
|
|
1767
|
+
context,
|
|
1768
|
+
jobs: jobs.length,
|
|
1769
|
+
pending_jobs: this.pendingSearchIndexJobs.length
|
|
1770
|
+
});
|
|
1771
|
+
return;
|
|
1772
|
+
}
|
|
1773
|
+
this.startSearchIndexJobBatch(batch);
|
|
1774
|
+
});
|
|
1775
|
+
}
|
|
1776
|
+
startSearchIndexJobBatch(batch) {
|
|
1330
1777
|
const workerUrl = this.getSearchIndexWorkerUrl();
|
|
1331
1778
|
if (!workerUrl) {
|
|
1332
|
-
appLogger.warn("search_index.worker_missing", { context });
|
|
1779
|
+
appLogger.warn("search_index.worker_missing", { context: batch.context });
|
|
1780
|
+
batch.resolve();
|
|
1333
1781
|
return;
|
|
1334
1782
|
}
|
|
1783
|
+
let settled = false;
|
|
1335
1784
|
const worker = new Worker(workerUrl, {
|
|
1336
1785
|
workerData: {
|
|
1337
|
-
context,
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
)
|
|
1786
|
+
context: batch.context,
|
|
1787
|
+
jobs: batch.jobs,
|
|
1788
|
+
agentNames: [],
|
|
1789
|
+
sessionsByAgent: {},
|
|
1790
|
+
metaByAgent: {}
|
|
1343
1791
|
}
|
|
1344
1792
|
});
|
|
1345
1793
|
worker.unref();
|
|
@@ -1352,15 +1800,29 @@ var LiveScanStore = class {
|
|
|
1352
1800
|
duration_ms: Math.round(message.durationMs),
|
|
1353
1801
|
sessions: message.sessions
|
|
1354
1802
|
});
|
|
1803
|
+
settled = true;
|
|
1804
|
+
batch.resolve();
|
|
1355
1805
|
}
|
|
1356
1806
|
});
|
|
1357
1807
|
worker.on("error", (error) => {
|
|
1358
|
-
appLogger.error("search_index.worker_error", { context, error });
|
|
1808
|
+
appLogger.error("search_index.worker_error", { context: batch.context, error });
|
|
1809
|
+
if (!settled) {
|
|
1810
|
+
settled = true;
|
|
1811
|
+
batch.reject(error);
|
|
1812
|
+
}
|
|
1359
1813
|
});
|
|
1360
1814
|
worker.on("exit", (code) => {
|
|
1361
1815
|
this.searchIndexWorker = null;
|
|
1362
1816
|
if (code !== 0) {
|
|
1363
|
-
appLogger.warn("search_index.worker_exit", { context, code });
|
|
1817
|
+
appLogger.warn("search_index.worker_exit", { context: batch.context, code });
|
|
1818
|
+
if (!settled) {
|
|
1819
|
+
settled = true;
|
|
1820
|
+
batch.reject(new Error(`Search index worker exited with code ${code}`));
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
if (this.pendingSearchIndexJobs.length > 0) {
|
|
1824
|
+
const pendingBatch = this.pendingSearchIndexJobs.shift();
|
|
1825
|
+
this.startSearchIndexJobBatch(pendingBatch);
|
|
1364
1826
|
}
|
|
1365
1827
|
});
|
|
1366
1828
|
}
|
|
@@ -1385,7 +1847,7 @@ var LiveScanStore = class {
|
|
|
1385
1847
|
this.byAgent = {};
|
|
1386
1848
|
for (const agent of this.agents) {
|
|
1387
1849
|
this.byAgent[agent.name] = sortSessions(result.byAgent[agent.name] ?? []);
|
|
1388
|
-
this.refreshTimestamps.set(agent.name, Date.now());
|
|
1850
|
+
this.refreshTimestamps.set(agent.name, result.cacheTimestamps?.[agent.name] ?? Date.now());
|
|
1389
1851
|
}
|
|
1390
1852
|
this.rebuildSessions();
|
|
1391
1853
|
}
|
|
@@ -1398,6 +1860,59 @@ var LiveScanStore = class {
|
|
|
1398
1860
|
applyFilters(sessions) {
|
|
1399
1861
|
return filterSessions(sessions, { ...this.scanOptions, ...this.startupScanOptions });
|
|
1400
1862
|
}
|
|
1863
|
+
canSyncSources(agent) {
|
|
1864
|
+
return Boolean(agent.listSessionSources && agent.scanSessionSource);
|
|
1865
|
+
}
|
|
1866
|
+
syncAgentSources(agent, cachedSessions, cachedMeta) {
|
|
1867
|
+
const sessionMap = new Map(cachedSessions.map((session) => [session.id, session]));
|
|
1868
|
+
const sourceRefs = agent.listSessionSources();
|
|
1869
|
+
const currentIds = new Set(sourceRefs.map((source) => source.sessionId));
|
|
1870
|
+
const changedIds = /* @__PURE__ */ new Set();
|
|
1871
|
+
for (const source of sourceRefs) {
|
|
1872
|
+
const cachedSession = sessionMap.get(source.sessionId);
|
|
1873
|
+
const cached = cachedMeta[source.sessionId];
|
|
1874
|
+
const sameSource = sourcePathFromMeta(cached) === source.sourcePath;
|
|
1875
|
+
const sameFingerprint = sourceFingerprintFromMeta(cached) === source.fingerprint;
|
|
1876
|
+
if (cachedSession && sameSource && sameFingerprint) continue;
|
|
1877
|
+
const next = agent.scanSessionSource(source.sourcePath);
|
|
1878
|
+
changedIds.add(source.sessionId);
|
|
1879
|
+
if (next) {
|
|
1880
|
+
sessionMap.set(next.id, next);
|
|
1881
|
+
} else {
|
|
1882
|
+
sessionMap.delete(source.sessionId);
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
for (const session of cachedSessions) {
|
|
1886
|
+
if (!currentIds.has(session.id)) {
|
|
1887
|
+
sessionMap.delete(session.id);
|
|
1888
|
+
changedIds.add(session.id);
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
const sessions = attachMissingProjectIdentities([...sessionMap.values()]);
|
|
1892
|
+
const persistenceDiff = buildRefreshDiff(agent.name, cachedSessions, sessions, [...changedIds]);
|
|
1893
|
+
return {
|
|
1894
|
+
sessions,
|
|
1895
|
+
changedIds: [...changedIds],
|
|
1896
|
+
persistenceDiff
|
|
1897
|
+
};
|
|
1898
|
+
}
|
|
1899
|
+
async refreshInitialIndex() {
|
|
1900
|
+
const startedAt = performance.now();
|
|
1901
|
+
const context = "scan.initial.background";
|
|
1902
|
+
try {
|
|
1903
|
+
await this.enqueueSearchIndexJobs(context, this.buildFullSearchIndexJobs(context));
|
|
1904
|
+
appLogger.info(`${context}.complete`, {
|
|
1905
|
+
duration_ms: Math.round(performance.now() - startedAt),
|
|
1906
|
+
sessions: this.sessions.length
|
|
1907
|
+
});
|
|
1908
|
+
} catch (error) {
|
|
1909
|
+
if (this.shuttingDown) {
|
|
1910
|
+
return;
|
|
1911
|
+
}
|
|
1912
|
+
appLogger.error(`${context}.error`, { error });
|
|
1913
|
+
console.error("[search] Background index sync failed:", error);
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1401
1916
|
startWatching() {
|
|
1402
1917
|
const scopesByRoot = /* @__PURE__ */ new Map();
|
|
1403
1918
|
for (const agent of this.agents) {
|
|
@@ -1606,6 +2121,7 @@ var LiveScanStore = class {
|
|
|
1606
2121
|
return;
|
|
1607
2122
|
}
|
|
1608
2123
|
this.refreshInFlight.add(agentName);
|
|
2124
|
+
this.beginAgentScan(agentName);
|
|
1609
2125
|
try {
|
|
1610
2126
|
await this.runRefresh(agentName);
|
|
1611
2127
|
} catch (error) {
|
|
@@ -1613,6 +2129,7 @@ var LiveScanStore = class {
|
|
|
1613
2129
|
console.error(`[${agentName}] Session refresh failed:`, error);
|
|
1614
2130
|
} finally {
|
|
1615
2131
|
this.refreshInFlight.delete(agentName);
|
|
2132
|
+
this.finishAgentScan(agentName);
|
|
1616
2133
|
if (this.pendingRefreshes.delete(agentName)) {
|
|
1617
2134
|
this.scheduleRefresh(agentName, PENDING_REFRESH_DELAY_MS);
|
|
1618
2135
|
}
|
|
@@ -1628,14 +2145,72 @@ var LiveScanStore = class {
|
|
|
1628
2145
|
return;
|
|
1629
2146
|
}
|
|
1630
2147
|
const previousSessions = this.byAgent[agentName] ?? [];
|
|
2148
|
+
const cached = loadCachedSessions(agentName, { ignoreTtl: true });
|
|
2149
|
+
const refreshBaseline = cached?.sessions ?? previousSessions;
|
|
2150
|
+
const cacheTimestamp = cached?.timestamp ?? this.refreshTimestamps.get(agentName) ?? 0;
|
|
2151
|
+
if (cached) {
|
|
2152
|
+
restoreAgentCacheMeta(agent, cached.meta);
|
|
2153
|
+
}
|
|
2154
|
+
const isInitialized = isAgentCacheInitialized(agentName);
|
|
1631
2155
|
let nextSessions = previousSessions;
|
|
1632
|
-
|
|
2156
|
+
let fullScanSessions = null;
|
|
2157
|
+
let preciseChangedIds = null;
|
|
2158
|
+
let usedIncrementalScan = false;
|
|
2159
|
+
let persistenceDiff = null;
|
|
2160
|
+
let availabilityDuration = 0;
|
|
2161
|
+
let checkDuration = 0;
|
|
2162
|
+
let scanDuration = 0;
|
|
2163
|
+
let filterDuration = 0;
|
|
2164
|
+
let diffDuration = 0;
|
|
2165
|
+
let persistDuration = 0;
|
|
2166
|
+
let searchIndexDuration = 0;
|
|
2167
|
+
let persistentJobKind;
|
|
2168
|
+
const availabilityStartedAt = performance.now();
|
|
2169
|
+
const isAvailable = agent.isAvailable();
|
|
2170
|
+
availabilityDuration = performance.now() - availabilityStartedAt;
|
|
2171
|
+
if (!isAvailable) {
|
|
1633
2172
|
nextSessions = [];
|
|
1634
2173
|
this.refreshTimestamps.set(agentName, Date.now());
|
|
1635
|
-
} else if (
|
|
2174
|
+
} else if (!isInitialized) {
|
|
2175
|
+
this.setScanPhase("initializing");
|
|
2176
|
+
const scanStartedAt = performance.now();
|
|
2177
|
+
const workerResult = this.scanAgentInWorker(agent, previousSessions, null, {});
|
|
2178
|
+
if (workerResult) {
|
|
2179
|
+
const result = await workerResult;
|
|
2180
|
+
nextSessions = result.sessions;
|
|
2181
|
+
agent.setSessionMetaMap?.(new Map(Object.entries(result.meta)));
|
|
2182
|
+
} else {
|
|
2183
|
+
nextSessions = await Promise.resolve(
|
|
2184
|
+
agent.scan({
|
|
2185
|
+
onProgress: (progress) => this.updateAgentScanProgress(agentName, progress)
|
|
2186
|
+
})
|
|
2187
|
+
);
|
|
2188
|
+
}
|
|
2189
|
+
fullScanSessions = attachMissingProjectIdentities(nextSessions);
|
|
2190
|
+
nextSessions = fullScanSessions;
|
|
2191
|
+
scanDuration = performance.now() - scanStartedAt;
|
|
2192
|
+
this.refreshTimestamps.set(agentName, Date.now());
|
|
2193
|
+
} else if (cached && this.canSyncSources(agent)) {
|
|
2194
|
+
const scanStartedAt = performance.now();
|
|
2195
|
+
const result = this.syncAgentSources(agent, cached.sessions, cached.meta);
|
|
2196
|
+
nextSessions = result.sessions;
|
|
2197
|
+
preciseChangedIds = result.changedIds;
|
|
2198
|
+
usedIncrementalScan = true;
|
|
2199
|
+
persistenceDiff = result.persistenceDiff;
|
|
2200
|
+
scanDuration = performance.now() - scanStartedAt;
|
|
2201
|
+
this.refreshTimestamps.set(agentName, Date.now());
|
|
2202
|
+
if (result.changedIds.length === 0) {
|
|
2203
|
+
appLogger.debug("scan.refresh.unchanged", {
|
|
2204
|
+
agent: agentName,
|
|
2205
|
+
duration_ms: Math.round(performance.now() - startedAt)
|
|
2206
|
+
});
|
|
2207
|
+
}
|
|
2208
|
+
} else if (refreshBaseline.length > 0 && agent.checkForChanges && agent.incrementalScan) {
|
|
2209
|
+
const checkStartedAt = performance.now();
|
|
1636
2210
|
const checkResult = await Promise.resolve(
|
|
1637
|
-
agent.checkForChanges(
|
|
2211
|
+
agent.checkForChanges(cacheTimestamp, refreshBaseline)
|
|
1638
2212
|
);
|
|
2213
|
+
checkDuration = performance.now() - checkStartedAt;
|
|
1639
2214
|
this.refreshTimestamps.set(agentName, checkResult.timestamp);
|
|
1640
2215
|
if (!checkResult.hasChanges) {
|
|
1641
2216
|
appLogger.debug("scan.refresh.unchanged", {
|
|
@@ -1644,30 +2219,93 @@ var LiveScanStore = class {
|
|
|
1644
2219
|
});
|
|
1645
2220
|
return;
|
|
1646
2221
|
}
|
|
2222
|
+
preciseChangedIds = checkResult.changedIds ?? null;
|
|
2223
|
+
usedIncrementalScan = Array.isArray(checkResult.changedIds);
|
|
2224
|
+
const scanStartedAt = performance.now();
|
|
1647
2225
|
nextSessions = await Promise.resolve(
|
|
1648
|
-
agent.incrementalScan(
|
|
2226
|
+
agent.incrementalScan(refreshBaseline, checkResult.changedIds ?? [])
|
|
2227
|
+
);
|
|
2228
|
+
const nextBaseline = attachMissingProjectIdentities(nextSessions);
|
|
2229
|
+
persistenceDiff = buildRefreshDiff(
|
|
2230
|
+
agentName,
|
|
2231
|
+
refreshBaseline,
|
|
2232
|
+
nextBaseline,
|
|
2233
|
+
preciseChangedIds ?? []
|
|
1649
2234
|
);
|
|
2235
|
+
nextSessions = nextBaseline;
|
|
2236
|
+
scanDuration = performance.now() - scanStartedAt;
|
|
1650
2237
|
} else {
|
|
1651
|
-
|
|
2238
|
+
const scanStartedAt = performance.now();
|
|
2239
|
+
const workerResult = this.scanAgentInWorker(agent, previousSessions, null, {});
|
|
2240
|
+
if (workerResult) {
|
|
2241
|
+
const result = await workerResult;
|
|
2242
|
+
nextSessions = result.sessions;
|
|
2243
|
+
agent.setSessionMetaMap?.(new Map(Object.entries(result.meta)));
|
|
2244
|
+
} else {
|
|
2245
|
+
nextSessions = await Promise.resolve(
|
|
2246
|
+
agent.scan({
|
|
2247
|
+
onProgress: (progress) => this.updateAgentScanProgress(agentName, progress)
|
|
2248
|
+
})
|
|
2249
|
+
);
|
|
2250
|
+
}
|
|
2251
|
+
fullScanSessions = attachMissingProjectIdentities(nextSessions);
|
|
2252
|
+
nextSessions = fullScanSessions;
|
|
2253
|
+
scanDuration = performance.now() - scanStartedAt;
|
|
1652
2254
|
this.refreshTimestamps.set(agentName, Date.now());
|
|
1653
2255
|
}
|
|
2256
|
+
nextSessions = attachMissingProjectIdentities(nextSessions);
|
|
2257
|
+
const filterStartedAt = performance.now();
|
|
1654
2258
|
nextSessions = this.applyFilters(nextSessions);
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
const searchIndexOptions = pendingPathCount >= SEARCH_INDEX_BULK_PENDING_PATH_THRESHOLD ? { isBulk: true } : void 0;
|
|
1659
|
-
const syncResult = searchIndexOptions ? syncSessionSearchIndex(
|
|
1660
|
-
agentName,
|
|
1661
|
-
nextSessions,
|
|
1662
|
-
(sessionId) => agent.getSessionData(sessionId),
|
|
1663
|
-
searchIndexOptions
|
|
1664
|
-
) : syncSessionSearchIndex(
|
|
2259
|
+
filterDuration = performance.now() - filterStartedAt;
|
|
2260
|
+
const diffStartedAt = performance.now();
|
|
2261
|
+
const diff = buildRefreshDiff(
|
|
1665
2262
|
agentName,
|
|
2263
|
+
previousSessions,
|
|
1666
2264
|
nextSessions,
|
|
1667
|
-
|
|
2265
|
+
preciseChangedIds ?? []
|
|
1668
2266
|
);
|
|
1669
|
-
|
|
1670
|
-
const
|
|
2267
|
+
diffDuration = performance.now() - diffStartedAt;
|
|
2268
|
+
const searchIndexOptions = pendingPathCount >= SEARCH_INDEX_BULK_PENDING_PATH_THRESHOLD ? { isBulk: true } : void 0;
|
|
2269
|
+
const canPersistIncrementally = usedIncrementalScan;
|
|
2270
|
+
const persistentChanges = persistenceDiff?.changedSessions ?? diff.changedSessions;
|
|
2271
|
+
const persistentRemovedSessionIds = persistenceDiff?.removedSessionIds ?? diff.removedSessionIds;
|
|
2272
|
+
const changedSessionIds = canPersistIncrementally ? new Set(persistentChanges.map(({ session }) => session.id)) : void 0;
|
|
2273
|
+
const cacheMeta = buildAgentCacheMeta(agent, changedSessionIds);
|
|
2274
|
+
const persistStartedAt = performance.now();
|
|
2275
|
+
const persistentJob = canPersistIncrementally ? {
|
|
2276
|
+
kind: "changes",
|
|
2277
|
+
context: "scan.refresh",
|
|
2278
|
+
agentName,
|
|
2279
|
+
changes: persistentChanges,
|
|
2280
|
+
removedSessionIds: persistentRemovedSessionIds,
|
|
2281
|
+
meta: cacheMeta,
|
|
2282
|
+
...searchIndexOptions ? { searchIndexOptions } : {}
|
|
2283
|
+
} : fullScanSessions ? {
|
|
2284
|
+
kind: "full",
|
|
2285
|
+
context: "scan.refresh",
|
|
2286
|
+
agentName,
|
|
2287
|
+
sessions: fullScanSessions,
|
|
2288
|
+
meta: buildAgentCacheMeta(agent),
|
|
2289
|
+
saveCache: true,
|
|
2290
|
+
...searchIndexOptions ? { searchIndexOptions } : {}
|
|
2291
|
+
} : null;
|
|
2292
|
+
if (persistentJob) {
|
|
2293
|
+
persistentJobKind = persistentJob.kind;
|
|
2294
|
+
const persist = this.enqueueSearchIndexJobs("scan.refresh", [persistentJob]);
|
|
2295
|
+
if (!isInitialized && persistentJob.kind === "full") {
|
|
2296
|
+
await persist;
|
|
2297
|
+
} else {
|
|
2298
|
+
void persist.catch((error) => {
|
|
2299
|
+
appLogger.error("scan.refresh.persist.error", { agent: agentName, error });
|
|
2300
|
+
console.error(`[${agentName}] Session persistence failed:`, error);
|
|
2301
|
+
});
|
|
2302
|
+
}
|
|
2303
|
+
}
|
|
2304
|
+
persistDuration = performance.now() - persistStartedAt;
|
|
2305
|
+
const searchIndexStartedAt = performance.now();
|
|
2306
|
+
searchIndexDuration = performance.now() - searchIndexStartedAt;
|
|
2307
|
+
logSearchIndexSync("scan.refresh", null, { pending_paths: pendingPathCount });
|
|
2308
|
+
const event = diff.event;
|
|
1671
2309
|
this.byAgent[agentName] = sortSessions(nextSessions);
|
|
1672
2310
|
this.rebuildSessions();
|
|
1673
2311
|
if (event) {
|
|
@@ -1682,8 +2320,15 @@ var LiveScanStore = class {
|
|
|
1682
2320
|
updated_sessions: event?.updatedSessions ?? 0,
|
|
1683
2321
|
removed_sessions: event?.removedSessions ?? 0,
|
|
1684
2322
|
pending_paths: pendingPathCount,
|
|
1685
|
-
|
|
1686
|
-
|
|
2323
|
+
availability_ms: Math.round(availabilityDuration),
|
|
2324
|
+
check_ms: Math.round(checkDuration),
|
|
2325
|
+
scan_ms: Math.round(scanDuration),
|
|
2326
|
+
filter_ms: Math.round(filterDuration),
|
|
2327
|
+
diff_ms: Math.round(diffDuration),
|
|
2328
|
+
persist_ms: Math.round(persistDuration),
|
|
2329
|
+
search_index_ms: Math.round(searchIndexDuration),
|
|
2330
|
+
persistent_index_worker_job: persistentJobKind,
|
|
2331
|
+
persistent_index_skipped: !persistentJob || void 0
|
|
1687
2332
|
});
|
|
1688
2333
|
}
|
|
1689
2334
|
};
|
|
@@ -1700,42 +2345,36 @@ var pkg = JSON.parse(readFileSync(resolve3(__dirname, "../package.json"), "utf-8
|
|
|
1700
2345
|
var VERSION = pkg.version;
|
|
1701
2346
|
|
|
1702
2347
|
// src/output.ts
|
|
1703
|
-
function printScanResults(agents
|
|
2348
|
+
function printScanResults(agents) {
|
|
1704
2349
|
consola.log("");
|
|
1705
2350
|
consola.box({
|
|
1706
2351
|
title: "CodeSesh",
|
|
1707
|
-
message: `v${VERSION} \u2022
|
|
2352
|
+
message: `v${VERSION} \u2022 local session browser`,
|
|
1708
2353
|
style: {
|
|
1709
2354
|
padding: 1,
|
|
1710
2355
|
borderColor: "cyan"
|
|
1711
2356
|
}
|
|
1712
2357
|
});
|
|
1713
2358
|
consola.log("");
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
const count = sessions?.length ?? 0;
|
|
1719
|
-
if (count > 0) {
|
|
1720
|
-
availableCount++;
|
|
1721
|
-
rows.push(` ${green("\u2714")} ${pad(agent.displayName)} ${dim(`${count} sessions`)}`);
|
|
1722
|
-
} else {
|
|
1723
|
-
rows.push(` ${dim("\u2716")} ${pad(agent.displayName)} ${dim("not found")}`);
|
|
1724
|
-
}
|
|
1725
|
-
}
|
|
1726
|
-
consola.log(rows.join("\n"));
|
|
1727
|
-
consola.log("");
|
|
1728
|
-
consola.info(`Active: ${availableCount}/${agents.length} agents`);
|
|
2359
|
+
consola.info(
|
|
2360
|
+
`Indexing ${agents.map((agent) => agent.displayName).join(", ")} sessions in the background.`
|
|
2361
|
+
);
|
|
2362
|
+
consola.info("The Web UI will update automatically as sessions are discovered.");
|
|
1729
2363
|
consola.log("");
|
|
1730
2364
|
}
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
2365
|
+
|
|
2366
|
+
// src/ports.ts
|
|
2367
|
+
var DEFAULT_PORT = 4521;
|
|
2368
|
+
var DEFAULT_PORT_FALLBACK_ATTEMPTS = 20;
|
|
2369
|
+
function parsePort(value) {
|
|
2370
|
+
const port = parseInt(value ?? "", 10);
|
|
2371
|
+
return Number.isNaN(port) ? DEFAULT_PORT : port;
|
|
2372
|
+
}
|
|
2373
|
+
function hasExplicitPortArg(argv) {
|
|
2374
|
+
return argv.some((arg, index) => {
|
|
2375
|
+
if (arg === "--port" || arg === "-p") return index < argv.length - 1;
|
|
2376
|
+
return arg.startsWith("--port=") || /^-p\d+$/.test(arg);
|
|
2377
|
+
});
|
|
1739
2378
|
}
|
|
1740
2379
|
|
|
1741
2380
|
// src/index.ts
|
|
@@ -1762,7 +2401,7 @@ var main = defineCommand({
|
|
|
1762
2401
|
type: "string",
|
|
1763
2402
|
alias: "p",
|
|
1764
2403
|
description: "HTTP server port",
|
|
1765
|
-
default:
|
|
2404
|
+
default: String(DEFAULT_PORT)
|
|
1766
2405
|
},
|
|
1767
2406
|
agent: {
|
|
1768
2407
|
type: "string",
|
|
@@ -1821,7 +2460,8 @@ var main = defineCommand({
|
|
|
1821
2460
|
},
|
|
1822
2461
|
async run({ args }) {
|
|
1823
2462
|
const startedAt = performance.now();
|
|
1824
|
-
const port =
|
|
2463
|
+
const port = parsePort(args.port);
|
|
2464
|
+
const explicitPort = hasExplicitPortArg(process.argv.slice(2));
|
|
1825
2465
|
const noOpen = args.noOpen;
|
|
1826
2466
|
const jsonOnly = args.json;
|
|
1827
2467
|
const trace = args.trace;
|
|
@@ -1840,7 +2480,7 @@ var main = defineCommand({
|
|
|
1840
2480
|
log_path: appLogger.getLogPath()
|
|
1841
2481
|
});
|
|
1842
2482
|
if (clearCache) {
|
|
1843
|
-
const { clearCache: clear } = await import("./dist-
|
|
2483
|
+
const { clearCache: clear } = await import("./dist-FQAQSHHO.js");
|
|
1844
2484
|
clear();
|
|
1845
2485
|
appLogger.info("cache.clear");
|
|
1846
2486
|
console.log("Cache cleared.");
|
|
@@ -1867,6 +2507,8 @@ var main = defineCommand({
|
|
|
1867
2507
|
if (!Number.isNaN(days) && days > 0) {
|
|
1868
2508
|
listDefaultFrom = Date.now() - days * 24 * 60 * 60 * 1e3;
|
|
1869
2509
|
listDefaultDays = days;
|
|
2510
|
+
} else if (days === 0) {
|
|
2511
|
+
listDefaultDays = 0;
|
|
1870
2512
|
}
|
|
1871
2513
|
}
|
|
1872
2514
|
const listDefaultTo = args.to ? parseDateToTimestamp(args.to) : void 0;
|
|
@@ -1876,7 +2518,9 @@ var main = defineCommand({
|
|
|
1876
2518
|
useCache
|
|
1877
2519
|
};
|
|
1878
2520
|
const startupScanOptions = targetSession || jsonOnly ? {} : { from: listDefaultFrom, to: listDefaultTo };
|
|
1879
|
-
const store = new LiveScanStore(!jsonOnly, scanOptions, startupScanOptions
|
|
2521
|
+
const store = new LiveScanStore(!jsonOnly, scanOptions, startupScanOptions, {
|
|
2522
|
+
deferInitialRefresh: !jsonOnly
|
|
2523
|
+
});
|
|
1880
2524
|
await store.initialize();
|
|
1881
2525
|
const result = store.getSnapshot();
|
|
1882
2526
|
appLogger.info("cli.scan_ready", {
|
|
@@ -1918,18 +2562,37 @@ var main = defineCommand({
|
|
|
1918
2562
|
return;
|
|
1919
2563
|
}
|
|
1920
2564
|
const agents = createRegisteredAgents();
|
|
1921
|
-
printScanResults(agents
|
|
1922
|
-
let
|
|
2565
|
+
printScanResults(agents);
|
|
2566
|
+
let app;
|
|
1923
2567
|
try {
|
|
1924
|
-
|
|
2568
|
+
app = await createServer(port, store, {
|
|
1925
2569
|
defaultSessionFrom: listDefaultFrom,
|
|
1926
2570
|
defaultSessionTo: listDefaultTo,
|
|
1927
|
-
defaultSessionDays: listDefaultDays
|
|
1928
|
-
|
|
2571
|
+
defaultSessionDays: listDefaultDays,
|
|
2572
|
+
portFallbackAttempts: explicitPort ? 1 : DEFAULT_PORT_FALLBACK_ATTEMPTS
|
|
2573
|
+
});
|
|
1929
2574
|
} catch (error) {
|
|
1930
2575
|
console.error(getServerStartupErrorMessage(error, port));
|
|
1931
2576
|
process.exit(1);
|
|
1932
2577
|
}
|
|
2578
|
+
const { url } = app;
|
|
2579
|
+
if (!jsonOnly) {
|
|
2580
|
+
store.startBackgroundRefresh();
|
|
2581
|
+
}
|
|
2582
|
+
let shuttingDown = false;
|
|
2583
|
+
const shutdown = async (signal) => {
|
|
2584
|
+
if (shuttingDown) return;
|
|
2585
|
+
shuttingDown = true;
|
|
2586
|
+
appLogger.info("cli.shutdown", { signal });
|
|
2587
|
+
await app.shutdown();
|
|
2588
|
+
process.exit(0);
|
|
2589
|
+
};
|
|
2590
|
+
process.once("SIGINT", (signal) => {
|
|
2591
|
+
void shutdown(signal);
|
|
2592
|
+
});
|
|
2593
|
+
process.once("SIGTERM", (signal) => {
|
|
2594
|
+
void shutdown(signal);
|
|
2595
|
+
});
|
|
1933
2596
|
console.log(` ${url}`);
|
|
1934
2597
|
console.log("");
|
|
1935
2598
|
appLogger.info("cli.ready", {
|