codesesh 0.13.0 → 0.15.0

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