codesesh 0.6.0 → 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/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-SQYHWMQV.js";
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 matchesDashboardScope(session, scope) {
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 (options.cwd && !matchesProjectScope(session, options.cwd)) return false;
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
- sessions = sessions.filter((s) => matchesProjectScope(s, cwd));
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 data = agent.getSessionData(sessionId);
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: extractSessionFileActivity(
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 parsedDays = queryDays ? parseInt(queryDays, 10) : NaN;
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 scopedSessions = filterSessionsByDashboardScope(scanResult.sessions, scope);
734
- const windowed = filterSessionsByActivityWindow(scopedSessions, from, to);
735
- const scopedByAgent = Object.fromEntries(
736
- Object.entries(scanResult.byAgent).filter(([name]) => !scope.agent || name.toLowerCase() === scope.agent).map(([name, sessions]) => [name, filterSessionsByDashboardScope(sessions, scope)])
737
- );
738
- const agentInfo = getAgentInfoMap(
739
- Object.fromEntries(
740
- Object.entries(scopedByAgent).map(([name, sessions]) => [
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
- for (const session of windowed) {
753
- totalMessages += session.stats.message_count;
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
- const bucketStart = startOfLocalDay(from);
781
- const bucketDays = Math.floor((startOfLocalDay(to) - bucketStart) / 864e5) + 1;
782
- for (let i = 0; i < bucketDays; i += 1) {
783
- const ts = bucketStart + i * 864e5;
784
- const key = toLocalDateKey(ts);
785
- dailyMap.set(key, { date: key, sessions: 0, messages: 0 });
786
- dailyTokenMap.set(key, { date: key, input: 0, output: 0, cache_read: 0, cache_create: 0 });
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 modelAgg = /* @__PURE__ */ new Map();
789
- for (const session of windowed) {
790
- const key = toLocalDateKey(getSessionActivityTime(session));
791
- const bucket = dailyMap.get(key);
792
- if (bucket) {
793
- bucket.sessions += 1;
794
- bucket.messages += session.stats.message_count;
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 dailyActivity = [...dailyMap.values()];
819
- const dailyTokenActivity = [...dailyTokenMap.values()];
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 = [...windowed].sort((a, b) => getSessionActivityTime(b) - getSessionActivityTime(a)).slice(0, 10).map((session) => {
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: windowed.length,
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
- const unsubscribe = store.subscribe((event) => {
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
- unsubscribe();
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 server = serve({ fetch: app.fetch, port });
991
- try {
992
- await waitForListening(server);
993
- } catch (error) {
994
- appLogger.error("server.listen.error", { port, error });
995
- server.close();
996
- if (store.shutdown) {
997
- await store.shutdown();
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:${port}`;
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
- server.close();
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
- void store.shutdown();
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 buildUpdateEvent(agentName, previousSessions, nextSessions) {
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
- for (const [id, session] of nextMap.entries()) {
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
- continue;
1147
+ changedSessions.push({ session, sortIndex: index });
1148
+ return;
1065
1149
  }
1066
- if (sessionSignature(previous) !== sessionSignature(session)) {
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
- type: "sessions-updated",
1080
- changedAgents: [agentName],
1081
- newSessions,
1082
- updatedSessions,
1083
- removedSessions,
1084
- totalSessions: nextSessions.length,
1085
- timestamp: Date.now()
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 [{ root: roots.codexRoot, path: join2(roots.codexRoot, "sessions") }];
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
- initialSearchIndexTimer = null;
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
- writeCache: this.startupScanOptions.from != null || this.startupScanOptions.to != null ? false : void 0,
1229
- includeSmartTags: this.startupScanOptions.from != null || this.startupScanOptions.to != null ? false : void 0
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.initialSearchIndexTimer) {
1277
- clearTimeout(this.initialSearchIndexTimer);
1278
- this.initialSearchIndexTimer = null;
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
- startSearchIndexWorker(context) {
1329
- if (this.searchIndexWorker) return;
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
- agentNames: this.agents.map((agent) => agent.name),
1339
- sessionsByAgent: this.byAgent,
1340
- metaByAgent: Object.fromEntries(
1341
- this.agents.map((agent) => [agent.name, buildAgentCacheMeta(agent)])
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
- if (!agent.isAvailable()) {
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 (previousSessions.length > 0 && agent.checkForChanges && agent.incrementalScan) {
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(this.refreshTimestamps.get(agentName) ?? 0, previousSessions)
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(previousSessions, checkResult.changedIds ?? [])
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
- nextSessions = await Promise.resolve(agent.scan(this.startupScanOptions));
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
- if (!this.hasStartupWindow()) {
1656
- saveCachedSessions(agentName, nextSessions, buildAgentCacheMeta(agent));
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
- (sessionId) => agent.getSessionData(sessionId)
2265
+ preciseChangedIds ?? []
1668
2266
  );
1669
- logSearchIndexSync("scan.refresh", syncResult, { pending_paths: pendingPathCount });
1670
- const event = buildUpdateEvent(agentName, previousSessions, nextSessions);
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
- search_index_mode: syncResult?.mode,
1686
- search_index_rebuild_duration_ms: syncResult?.rebuildDurationMs == null ? void 0 : Math.round(syncResult.rebuildDurationMs)
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, result) {
2348
+ function printScanResults(agents) {
1704
2349
  consola.log("");
1705
2350
  consola.box({
1706
2351
  title: "CodeSesh",
1707
- message: `v${VERSION} \u2022 ${result.sessions.length} sessions discovered`,
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
- const rows = [];
1715
- let availableCount = 0;
1716
- for (const agent of agents) {
1717
- const sessions = result.byAgent[agent.name];
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
- function pad(text, length = 16) {
1732
- return text.padEnd(length);
1733
- }
1734
- function green(text) {
1735
- return `\x1B[32m${text}\x1B[0m`;
1736
- }
1737
- function dim(text) {
1738
- return `\x1B[2m${text}\x1B[0m`;
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: "4321"
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 = parseInt(args.port, 10) || 4321;
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-NT4CH6KD.js");
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, result);
1922
- let url;
2565
+ printScanResults(agents);
2566
+ let app;
1923
2567
  try {
1924
- ({ url } = await createServer(port, store, {
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", {