codesesh 0.9.1 → 0.10.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,8 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  BookmarkStorageUnavailableError,
4
+ FileSystemSessionSource,
5
+ attachMissingProjectIdentities,
6
+ buildAgentCacheMeta,
7
+ buildDashboard,
4
8
  classifySessionTags,
5
9
  computeIdentity,
10
+ computeSessionDiff,
6
11
  createProjectScopeMatcher,
7
12
  createRegisteredAgents,
8
13
  deleteBookmark,
@@ -10,7 +15,10 @@ import {
10
15
  filterSessions,
11
16
  getAgentInfoMap,
12
17
  getCursorDataPath,
18
+ getSessionActivityTime,
19
+ getSessionAgentName,
13
20
  getSmartTagSourceTimestamp,
21
+ getTotalTokens,
14
22
  importBookmarks,
15
23
  isAgentCacheInitialized,
16
24
  listBookmarks,
@@ -28,8 +36,11 @@ import {
28
36
  scanSessions,
29
37
  searchFileActivitySessions,
30
38
  searchSessions,
39
+ sessionSignature,
40
+ sortSessions,
41
+ startOfLocalDay,
31
42
  upsertBookmark
32
- } from "./chunk-5UOLRSFH.js";
43
+ } from "./chunk-ZQMIB7QO.js";
33
44
 
34
45
  // src/index.ts
35
46
  import { defineCommand, runMain } from "citty";
@@ -195,7 +206,6 @@ function logSearchIndexSync(context, result, data = {}) {
195
206
  }
196
207
 
197
208
  // src/api/handlers.ts
198
- var DASHBOARD_RECENT_LIMIT = 10;
199
209
  function isRecord(value) {
200
210
  return typeof value === "object" && value !== null;
201
211
  }
@@ -219,15 +229,6 @@ function parseBookmarkPayload(value) {
219
229
  stats: value.stats
220
230
  };
221
231
  }
222
- function getTotalTokens(stats) {
223
- return stats.total_tokens ?? stats.total_input_tokens + stats.total_output_tokens;
224
- }
225
- function getSessionAgentName(session) {
226
- return session.slug.split("/")[0]?.toLowerCase() || "unknown";
227
- }
228
- function getSessionActivityTime(session) {
229
- return session.time_updated ?? session.time_created;
230
- }
231
232
  function parseDateParam(value, fallback) {
232
233
  if (value == null) return fallback;
233
234
  const ts = new Date(value).getTime();
@@ -682,18 +683,6 @@ function handleDeleteBookmark(c) {
682
683
  throw error;
683
684
  }
684
685
  }
685
- function toLocalDateKey(ts) {
686
- const d = new Date(ts);
687
- const year = d.getFullYear();
688
- const month = `${d.getMonth() + 1}`.padStart(2, "0");
689
- const day = `${d.getDate()}`.padStart(2, "0");
690
- return `${year}-${month}-${day}`;
691
- }
692
- function startOfLocalDay(ts) {
693
- const d = new Date(ts);
694
- d.setHours(0, 0, 0, 0);
695
- return d.getTime();
696
- }
697
686
  function resolveDashboardWindow(defaults, queryDays, queryFrom, queryTo) {
698
687
  const now = Date.now();
699
688
  const toTs = parseDateParam(queryTo, defaults.to) ?? now;
@@ -732,138 +721,17 @@ function handleGetDashboard(c, scanSource, defaults = {}) {
732
721
  projectKind: optionalQueryValue(c.req.query("projectKind")),
733
722
  projectKey: optionalQueryValue(c.req.query("projectKey"))
734
723
  };
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
724
  const agentInfo = getAgentInfoMap({});
743
725
  const agentInfoMap = new Map(agentInfo.map((a) => [a.name, a]));
744
- let totalSessions = 0;
745
- let totalMessages = 0;
746
- let totalTokens = 0;
747
- let totalCost = 0;
748
- let hasEstimatedCost = false;
749
- let latestActivity = 0;
750
- const recentCandidates = [];
751
- const modelAgg = /* @__PURE__ */ new Map();
752
- const dailyMap = /* @__PURE__ */ new Map();
753
- const dailyTokenMap = /* @__PURE__ */ new Map();
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
- }
763
- }
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;
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;
810
- if (session.model_usage) {
811
- for (const [model, tokens] of Object.entries(session.model_usage)) {
812
- const entry = modelAgg.get(model);
813
- if (entry) {
814
- entry.tokens += tokens;
815
- entry.sessions += 1;
816
- } else {
817
- modelAgg.set(model, { tokens, sessions: 1 });
818
- }
819
- }
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
- }
832
- }
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
- );
848
- const modelDistribution = [...modelAgg.entries()].map(([model, { tokens, sessions: count }]) => ({ model, tokens, sessions: count })).sort((a, b) => b.tokens - a.tokens);
849
- const recentSessions = recentCandidates.map(({ session }) => {
850
- const agentKey = getSessionAgentName(session);
851
- return { ...session, agentName: agentKey };
726
+ const aggregate = buildDashboard(scanResult.sessions, {
727
+ byAgentNames: Object.keys(scanResult.byAgent),
728
+ scope,
729
+ from,
730
+ to,
731
+ agentInfoMap
852
732
  });
853
733
  const data = {
854
- totals: {
855
- sessions: totalSessions,
856
- messages: totalMessages,
857
- tokens: totalTokens,
858
- cost: totalCost,
859
- cost_source: totalCost > 0 ? hasEstimatedCost ? "estimated" : "recorded" : void 0,
860
- latestActivity: latestActivity || void 0
861
- },
862
- perAgent,
863
- dailyActivity,
864
- dailyTokenActivity,
865
- modelDistribution,
866
- recentSessions,
734
+ ...aggregate,
867
735
  recentFileActivities: listFileActivity({
868
736
  agent: scope.agent,
869
737
  projectKey: scope.projectKey,
@@ -1078,112 +946,15 @@ async function createServer(port, store, options = {}) {
1078
946
  }
1079
947
 
1080
948
  // src/live-scan.ts
1081
- import { existsSync as existsSync3, readdirSync as readdirSync2, statSync as statSync2, watch } from "fs";
1082
- import { dirname as dirname2, isAbsolute, join as join2, relative, resolve as resolve2 } from "path";
949
+ import { existsSync as existsSync4 } from "fs";
1083
950
  import { fileURLToPath as fileURLToPath2 } from "url";
1084
951
  import { Worker } from "worker_threads";
1085
- var REFRESH_DEBOUNCE_MS = 200;
1086
- var EMPTY_AGENT_REFRESH_DEBOUNCE_MS = 3e4;
1087
- var PENDING_REFRESH_DELAY_MS = 100;
952
+
953
+ // src/session-watcher.ts
954
+ import { existsSync as existsSync3, readdirSync as readdirSync2, statSync as statSync2, watch } from "fs";
955
+ import { dirname as dirname2, isAbsolute, join as join2, relative, resolve as resolve2 } from "path";
1088
956
  var WRITE_STABILITY_THRESHOLD_MS = 250;
1089
957
  var WRITE_STABILITY_POLL_MS = 100;
1090
- var NEW_SESSION_EVENT_WINDOW_MS = 250;
1091
- var SEARCH_INDEX_BULK_PENDING_PATH_THRESHOLD = 100;
1092
- function sortSessions(sessions) {
1093
- return [...sessions].sort(
1094
- (a, b) => (b.time_updated ?? b.time_created) - (a.time_updated ?? a.time_created)
1095
- );
1096
- }
1097
- function sessionSignature(session) {
1098
- return JSON.stringify([
1099
- session.title,
1100
- session.directory,
1101
- session.time_created,
1102
- session.time_updated ?? session.time_created,
1103
- session.stats.message_count,
1104
- session.stats.total_input_tokens,
1105
- session.stats.total_output_tokens,
1106
- session.stats.total_cost,
1107
- session.stats.total_tokens ?? 0
1108
- ]);
1109
- }
1110
- function buildAgentCacheMeta(agent, sessionIds) {
1111
- const metaMap = agent.getSessionMetaMap?.();
1112
- const meta = {};
1113
- if (!metaMap) return meta;
1114
- for (const [id, data] of metaMap.entries()) {
1115
- if (sessionIds && !sessionIds.has(id)) continue;
1116
- meta[id] = { id, ...data };
1117
- }
1118
- return meta;
1119
- }
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 = []) {
1134
- const previousMap = new Map(previousSessions.map((session) => [session.id, session]));
1135
- const nextMap = new Map(nextSessions.map((session) => [session.id, session]));
1136
- const candidateChangedIdSet = new Set(candidateChangedIds);
1137
- const changedSessions = [];
1138
- const removedSessionIds = [];
1139
- let newSessions = 0;
1140
- let updatedSessions = 0;
1141
- let removedSessions = 0;
1142
- nextSessions.forEach((session, index) => {
1143
- const id = session.id;
1144
- const previous = previousMap.get(id);
1145
- if (!previous) {
1146
- newSessions += 1;
1147
- changedSessions.push({ session, sortIndex: index });
1148
- return;
1149
- }
1150
- const hasSignatureChange = sessionSignature(previous) !== sessionSignature(session);
1151
- const hasContentChange = candidateChangedIdSet.has(id);
1152
- if (hasSignatureChange || hasContentChange) {
1153
- updatedSessions += 1;
1154
- }
1155
- if (hasContentChange || hasSignatureChange) {
1156
- changedSessions.push({ session, sortIndex: index });
1157
- }
1158
- });
1159
- for (const id of previousMap.keys()) {
1160
- if (!nextMap.has(id)) {
1161
- removedSessions += 1;
1162
- removedSessionIds.push(id);
1163
- }
1164
- }
1165
- if (newSessions === 0 && updatedSessions === 0 && removedSessions === 0) {
1166
- return { event: null, changedSessions, removedSessionIds };
1167
- }
1168
- return {
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
- }
1182
- };
1183
- }
1184
- function restoreAgentCacheMeta(agent, meta) {
1185
- agent.setSessionMetaMap?.(new Map(Object.entries(meta)));
1186
- }
1187
958
  function toAbsolutePath(path) {
1188
959
  return isAbsolute(path) ? path : resolve2(path);
1189
960
  }
@@ -1225,36 +996,6 @@ function isSameOrChildPath(parentPath, childPath) {
1225
996
  function isRelatedPath(changedPath, targetPath) {
1226
997
  return isSameOrChildPath(targetPath, changedPath) || isSameOrChildPath(changedPath, targetPath);
1227
998
  }
1228
- function mergeEvents(previous, next) {
1229
- const changedSessionHeads = /* @__PURE__ */ new Map();
1230
- const removedSessionRefs = /* @__PURE__ */ new Map();
1231
- const sessionKey = (agentName, sessionId) => `${agentName}\0${sessionId}`;
1232
- const addChanged = (item) => {
1233
- const key = sessionKey(item.agentName, item.session.id);
1234
- removedSessionRefs.delete(key);
1235
- changedSessionHeads.set(key, item);
1236
- };
1237
- const addRemoved = (item) => {
1238
- const key = sessionKey(item.agentName, item.sessionId);
1239
- changedSessionHeads.delete(key);
1240
- removedSessionRefs.set(key, item);
1241
- };
1242
- for (const item of previous.changedSessionHeads) addChanged(item);
1243
- for (const item of previous.removedSessionRefs) addRemoved(item);
1244
- for (const item of next.changedSessionHeads) addChanged(item);
1245
- for (const item of next.removedSessionRefs) addRemoved(item);
1246
- return {
1247
- type: "sessions-updated",
1248
- changedAgents: Array.from(/* @__PURE__ */ new Set([...previous.changedAgents, ...next.changedAgents])),
1249
- newSessions: previous.newSessions + next.newSessions,
1250
- updatedSessions: previous.updatedSessions + next.updatedSessions,
1251
- removedSessions: previous.removedSessions + next.removedSessions,
1252
- totalSessions: next.totalSessions,
1253
- timestamp: next.timestamp,
1254
- changedSessionHeads: [...changedSessionHeads.values()],
1255
- removedSessionRefs: [...removedSessionRefs.values()]
1256
- };
1257
- }
1258
999
  function mergeScopes(target, scopes) {
1259
1000
  for (const scope of scopes) {
1260
1001
  if (!target.some(
@@ -1312,165 +1053,446 @@ function resolveAgentWatchTargets(agentName) {
1312
1053
  return [];
1313
1054
  }
1314
1055
  }
1315
- var LiveScanStore = class {
1316
- constructor(watchEnabled = true, scanOptions = {}, startupScanOptions = {}, storeOptions = {}) {
1317
- this.watchEnabled = watchEnabled;
1318
- this.scanOptions = scanOptions;
1319
- this.startupScanOptions = startupScanOptions;
1320
- this.storeOptions = storeOptions;
1321
- }
1322
- watchEnabled;
1323
- scanOptions;
1324
- startupScanOptions;
1325
- storeOptions;
1326
- agents = [];
1327
- byAgent = {};
1328
- sessions = [];
1329
- listeners = /* @__PURE__ */ new Set();
1330
- scanStatusListeners = /* @__PURE__ */ new Set();
1331
- scanStatus = {
1332
- active: false,
1333
- phase: "idle",
1334
- pendingAgents: [],
1335
- scanningAgents: [],
1336
- completedAgents: [],
1337
- agentStatuses: {},
1338
- totalAgents: 0,
1339
- updatedAt: Date.now()
1340
- };
1341
- refreshTimers = /* @__PURE__ */ new Map();
1342
- refreshTimestamps = /* @__PURE__ */ new Map();
1343
- refreshInFlight = /* @__PURE__ */ new Set();
1344
- pendingRefreshes = /* @__PURE__ */ new Set();
1345
- pendingRefreshPathCounts = /* @__PURE__ */ new Map();
1056
+ var SessionWatcher = class {
1346
1057
  watchers = [];
1347
1058
  fallbackWatchScopes = /* @__PURE__ */ new Map();
1348
1059
  stablePaths = /* @__PURE__ */ new Map();
1349
- pendingEvent = null;
1350
- pendingEventTimer = null;
1351
- backgroundRefreshTimer = null;
1352
- searchIndexWorker = null;
1353
- pendingSearchIndexJobs = [];
1354
- shuttingDown = false;
1355
- async initialize() {
1356
- const startedAt = performance.now();
1357
- const deferInitialRefresh = this.storeOptions.deferInitialRefresh === true;
1358
- appLogger.info("scan.initial.start", {
1359
- watch_enabled: this.watchEnabled,
1360
- agents: this.scanOptions.agents,
1361
- use_cache: this.scanOptions.useCache ?? true,
1362
- startup_from: this.startupScanOptions.from,
1363
- startup_to: this.startupScanOptions.to,
1364
- deferred: deferInitialRefresh || void 0
1365
- });
1366
- const initialResult = await scanSessions({
1367
- ...this.scanOptions,
1368
- ...deferInitialRefresh ? this.startupScanOptions : {},
1369
- useCache: this.scanOptions.useCache ?? true,
1370
- smartRefresh: false,
1371
- cacheOnly: deferInitialRefresh,
1372
- writeCache: deferInitialRefresh ? false : this.scanOptions.writeCache,
1373
- smartTagWorkerUrl: this.getSmartTagWorkerUrl() ?? void 0,
1374
- includeSmartTags: deferInitialRefresh ? false : void 0
1375
- });
1376
- this.applyScanResult(initialResult);
1377
- const indexStartedAt = performance.now();
1378
- if (!deferInitialRefresh) {
1379
- await this.enqueueSearchIndexJobs(
1380
- "scan.initial",
1381
- this.buildFullSearchIndexJobs("scan.initial")
1382
- );
1383
- }
1384
- const indexDuration = performance.now() - indexStartedAt;
1385
- appLogger.info("scan.initial.done", {
1386
- duration_ms: Math.round(performance.now() - startedAt),
1387
- index_ms: deferInitialRefresh ? void 0 : Math.round(indexDuration),
1388
- deferred: deferInitialRefresh || void 0,
1389
- sessions: this.sessions.length,
1390
- agents: Object.fromEntries(
1391
- Object.entries(this.byAgent).map(([key, value]) => [key, value.length])
1392
- ),
1393
- agent_timings: initialResult.timings ? Object.fromEntries(
1394
- Object.entries(initialResult.timings).map(([name, t]) => [
1395
- name,
1396
- {
1397
- total_ms: Math.round(t.total),
1398
- cache_load_ms: t.cacheLoad != null ? Math.round(t.cacheLoad) : void 0,
1399
- check_changes_ms: t.checkChanges != null ? Math.round(t.checkChanges) : void 0,
1400
- scan_ms: t.scan != null ? Math.round(t.scan) : void 0,
1401
- identity_ms: t.identity != null ? Math.round(t.identity) : void 0,
1402
- tags_ms: t.tags != null ? Math.round(t.tags) : void 0
1403
- }
1404
- ])
1405
- ) : void 0
1406
- });
1407
- if (this.watchEnabled) {
1408
- this.startWatching();
1409
- }
1410
- }
1411
- startBackgroundRefresh() {
1412
- if (this.backgroundRefreshTimer) {
1413
- return;
1414
- }
1415
- const agentNames = this.agents.map((agent) => agent.name);
1416
- this.startScanBatch(agentNames, "scanning");
1417
- this.backgroundRefreshTimer = setTimeout(() => {
1418
- this.backgroundRefreshTimer = null;
1419
- for (const agentName of agentNames) {
1420
- this.scheduleRefresh(agentName, 0);
1421
- }
1422
- if (agentNames.length === 0) {
1423
- this.finishScanBatch();
1424
- }
1425
- }, 0);
1426
- }
1427
- getSnapshot() {
1428
- return {
1429
- sessions: this.sessions,
1430
- byAgent: this.byAgent,
1431
- agents: this.agents
1432
- };
1433
- }
1434
- getScanStatus() {
1435
- return {
1436
- type: "scan-status",
1437
- ...this.scanStatus,
1438
- pendingAgents: [...this.scanStatus.pendingAgents],
1439
- scanningAgents: [...this.scanStatus.scanningAgents],
1440
- completedAgents: [...this.scanStatus.completedAgents],
1441
- agentStatuses: Object.fromEntries(
1442
- Object.entries(this.scanStatus.agentStatuses).map(([agentName, status]) => [
1443
- agentName,
1444
- { ...status }
1445
- ])
1446
- )
1447
- };
1448
- }
1449
- subscribe(listener) {
1450
- this.listeners.add(listener);
1451
- return () => {
1452
- this.listeners.delete(listener);
1453
- };
1454
- }
1455
- subscribeScanStatus(listener) {
1456
- this.scanStatusListeners.add(listener);
1060
+ listeners = /* @__PURE__ */ new Set();
1061
+ /** Register a listener fired (after write-stability polling) with the changed agent set. */
1062
+ onAgentsChanged(cb) {
1063
+ this.listeners.add(cb);
1457
1064
  return () => {
1458
- this.scanStatusListeners.delete(listener);
1065
+ this.listeners.delete(cb);
1459
1066
  };
1460
1067
  }
1461
- async shutdown() {
1462
- this.shuttingDown = true;
1463
- for (const timer of this.refreshTimers.values()) {
1464
- clearTimeout(timer);
1465
- }
1466
- this.refreshTimers.clear();
1467
- this.pendingRefreshPathCounts.clear();
1468
- for (const state of this.stablePaths.values()) {
1469
- if (state.timer) {
1470
- clearTimeout(state.timer);
1068
+ /** Begin watching the given agent names' data directories. */
1069
+ start(agentNames) {
1070
+ const scopesByRoot = /* @__PURE__ */ new Map();
1071
+ for (const agentName of agentNames) {
1072
+ const watchTargets = resolveAgentWatchTargets(agentName);
1073
+ if (watchTargets.length === 0) {
1074
+ appLogger.debug("watch.skip", { agent: agentName });
1075
+ continue;
1076
+ }
1077
+ for (const target of watchTargets) {
1078
+ const watchRootPath = closestWatchablePath(target.root ?? target.path);
1079
+ if (!watchRootPath) continue;
1080
+ let rootPath;
1081
+ try {
1082
+ rootPath = getWatchRoot(watchRootPath);
1083
+ } catch (error) {
1084
+ this.reportWatchError("watch.resolve.error", { path: watchRootPath, error });
1085
+ continue;
1086
+ }
1087
+ const targetPath = toAbsolutePath(target.path);
1088
+ const scopes = scopesByRoot.get(rootPath) ?? [];
1089
+ if (!scopes.some((scope) => scope.agentName === agentName && scope.targetPath === targetPath)) {
1090
+ scopes.push({ agentName, targetPath });
1091
+ }
1092
+ scopesByRoot.set(rootPath, scopes);
1471
1093
  }
1472
1094
  }
1473
- this.stablePaths.clear();
1095
+ for (const [rootPath, scopes] of scopesByRoot.entries()) {
1096
+ const agents = Array.from(new Set(scopes.map((scope) => scope.agentName)));
1097
+ appLogger.info("watch.start", {
1098
+ root: rootPath,
1099
+ agents,
1100
+ targets: scopes.map((scope) => ({
1101
+ agent: scope.agentName,
1102
+ path: scope.targetPath
1103
+ }))
1104
+ });
1105
+ if (isRecursiveWatchSupported()) {
1106
+ const started = this.watchDirectory(rootPath, scopes, true);
1107
+ if (started) {
1108
+ continue;
1109
+ }
1110
+ }
1111
+ this.watchDirectoryTree(rootPath, scopes);
1112
+ }
1113
+ }
1114
+ /** Stop all watchers and clear pending stability polls. */
1115
+ async dispose() {
1116
+ for (const state of this.stablePaths.values()) {
1117
+ if (state.timer) {
1118
+ clearTimeout(state.timer);
1119
+ }
1120
+ }
1121
+ this.stablePaths.clear();
1122
+ await Promise.all(this.watchers.map((watcher) => watcher.close()));
1123
+ this.watchers = [];
1124
+ this.fallbackWatchScopes.clear();
1125
+ this.listeners.clear();
1126
+ }
1127
+ watchDirectory(path, scopes, recursive) {
1128
+ try {
1129
+ const watcher = watch(path, { recursive }, (eventType, filename) => {
1130
+ queueMicrotask(() => {
1131
+ try {
1132
+ const activeScopes = recursive ? scopes : this.fallbackWatchScopes.get(path) ?? scopes;
1133
+ this.handleWatchEvent(path, activeScopes, eventType, filename);
1134
+ if (!recursive) {
1135
+ this.watchNewDirectories(path, filename, activeScopes);
1136
+ }
1137
+ } catch (error) {
1138
+ this.reportWatchError("watch.event.error", { path, recursive, error });
1139
+ }
1140
+ });
1141
+ });
1142
+ watcher.on("error", (error) => {
1143
+ this.reportWatchError("watch.error", { path, recursive, error });
1144
+ });
1145
+ this.watchers.push(watcher);
1146
+ return true;
1147
+ } catch (error) {
1148
+ if (recursive && isRecursiveWatchUnavailable(error)) {
1149
+ appLogger.warn("watch.recursive_unavailable", { path, error });
1150
+ return false;
1151
+ }
1152
+ this.reportWatchError("watch.start.error", { path, recursive, error });
1153
+ return false;
1154
+ }
1155
+ }
1156
+ watchDirectoryTree(rootPath, scopes) {
1157
+ const pending = [rootPath];
1158
+ while (pending.length > 0) {
1159
+ const dirPath = pending.pop();
1160
+ this.watchFallbackDirectory(dirPath, scopes);
1161
+ try {
1162
+ for (const entry of readdirSync2(dirPath, { withFileTypes: true })) {
1163
+ if (entry.isDirectory()) {
1164
+ pending.push(join2(dirPath, entry.name));
1165
+ }
1166
+ }
1167
+ } catch (error) {
1168
+ this.reportWatchError("watch.scan.error", { path: dirPath, error });
1169
+ }
1170
+ }
1171
+ }
1172
+ watchFallbackDirectory(path, scopes) {
1173
+ const existingScopes = this.fallbackWatchScopes.get(path);
1174
+ if (existingScopes) {
1175
+ mergeScopes(existingScopes, scopes);
1176
+ return;
1177
+ }
1178
+ const storedScopes = [...scopes];
1179
+ this.fallbackWatchScopes.set(path, storedScopes);
1180
+ if (!this.watchDirectory(path, storedScopes, false)) {
1181
+ this.fallbackWatchScopes.delete(path);
1182
+ }
1183
+ }
1184
+ watchNewDirectories(watchPath, filename, scopes) {
1185
+ const path = resolveWatchEventPath(watchPath, filename);
1186
+ try {
1187
+ if (statSync2(path).isDirectory()) {
1188
+ this.watchDirectoryTree(path, scopes);
1189
+ }
1190
+ } catch {
1191
+ }
1192
+ }
1193
+ handleWatchEvent(watchPath, scopes, eventType, filename) {
1194
+ const changedPath = resolveWatchEventPath(watchPath, filename);
1195
+ const agentNames = new Set(
1196
+ scopes.filter((scope) => isRelatedPath(changedPath, scope.targetPath)).map((scope) => scope.agentName)
1197
+ );
1198
+ if (agentNames.size === 0) {
1199
+ return;
1200
+ }
1201
+ appLogger.debug("watch.event", {
1202
+ event: eventType,
1203
+ path: changedPath,
1204
+ agents: Array.from(agentNames)
1205
+ });
1206
+ this.waitForStablePath(changedPath, agentNames);
1207
+ }
1208
+ waitForStablePath(path, agentNames) {
1209
+ const existing = this.stablePaths.get(path);
1210
+ if (existing) {
1211
+ for (const agentName of agentNames) {
1212
+ existing.agentNames.add(agentName);
1213
+ }
1214
+ return;
1215
+ }
1216
+ const state = {
1217
+ path,
1218
+ agentNames: new Set(agentNames),
1219
+ lastMtimeMs: null,
1220
+ lastSize: null,
1221
+ stableSince: Date.now(),
1222
+ timer: null
1223
+ };
1224
+ this.stablePaths.set(path, state);
1225
+ this.pollStablePath(path);
1226
+ }
1227
+ pollStablePath(path) {
1228
+ const state = this.stablePaths.get(path);
1229
+ if (!state) {
1230
+ return;
1231
+ }
1232
+ let size;
1233
+ let mtimeMs;
1234
+ try {
1235
+ const stat = statSync2(path);
1236
+ size = stat.size;
1237
+ mtimeMs = stat.mtimeMs;
1238
+ } catch {
1239
+ this.stablePaths.delete(path);
1240
+ this.emitAgentsChanged(state.agentNames);
1241
+ return;
1242
+ }
1243
+ const now = Date.now();
1244
+ const unchanged = state.lastSize === size && state.lastMtimeMs === mtimeMs;
1245
+ if (!unchanged) {
1246
+ state.lastSize = size;
1247
+ state.lastMtimeMs = mtimeMs;
1248
+ state.stableSince = now;
1249
+ }
1250
+ if (unchanged && now - state.stableSince >= WRITE_STABILITY_THRESHOLD_MS) {
1251
+ this.stablePaths.delete(path);
1252
+ this.emitAgentsChanged(state.agentNames);
1253
+ return;
1254
+ }
1255
+ state.timer = setTimeout(() => this.pollStablePath(path), WRITE_STABILITY_POLL_MS);
1256
+ }
1257
+ emitAgentsChanged(agentNames) {
1258
+ if (agentNames.size === 0) return;
1259
+ for (const listener of this.listeners) {
1260
+ listener(new Set(agentNames));
1261
+ }
1262
+ }
1263
+ reportWatchError(event, data) {
1264
+ appLogger.error(event, data);
1265
+ console.error("[watch] File watcher failed:", data.error);
1266
+ }
1267
+ };
1268
+
1269
+ // src/live-scan.ts
1270
+ var REFRESH_DEBOUNCE_MS = 200;
1271
+ var EMPTY_AGENT_REFRESH_DEBOUNCE_MS = 3e4;
1272
+ var PENDING_REFRESH_DELAY_MS = 100;
1273
+ var NEW_SESSION_EVENT_WINDOW_MS = 250;
1274
+ var SEARCH_INDEX_BULK_PENDING_PATH_THRESHOLD = 100;
1275
+ function buildRefreshDiff(agentName, previousSessions, nextSessions, candidateChangedIds = []) {
1276
+ const { changes, removedSessionIds, counts } = computeSessionDiff(
1277
+ previousSessions,
1278
+ nextSessions,
1279
+ candidateChangedIds,
1280
+ sessionSignature
1281
+ );
1282
+ if (counts.new === 0 && counts.updated === 0 && counts.removed === 0) {
1283
+ return { event: null, changedSessions: changes, removedSessionIds };
1284
+ }
1285
+ return {
1286
+ changedSessions: changes,
1287
+ removedSessionIds,
1288
+ event: {
1289
+ type: "sessions-updated",
1290
+ changedAgents: [agentName],
1291
+ newSessions: counts.new,
1292
+ updatedSessions: counts.updated,
1293
+ removedSessions: counts.removed,
1294
+ totalSessions: nextSessions.length,
1295
+ timestamp: Date.now(),
1296
+ changedSessionHeads: changes.map(({ session }) => ({ agentName, session })),
1297
+ removedSessionRefs: removedSessionIds.map((sessionId) => ({ agentName, sessionId }))
1298
+ }
1299
+ };
1300
+ }
1301
+ function restoreAgentCacheMeta(agent, meta) {
1302
+ agent.setSessionMetaMap(new Map(Object.entries(meta)));
1303
+ }
1304
+ function mergeEvents(previous, next) {
1305
+ const changedSessionHeads = /* @__PURE__ */ new Map();
1306
+ const removedSessionRefs = /* @__PURE__ */ new Map();
1307
+ const sessionKey = (agentName, sessionId) => `${agentName}\0${sessionId}`;
1308
+ const addChanged = (item) => {
1309
+ const key = sessionKey(item.agentName, item.session.id);
1310
+ removedSessionRefs.delete(key);
1311
+ changedSessionHeads.set(key, item);
1312
+ };
1313
+ const addRemoved = (item) => {
1314
+ const key = sessionKey(item.agentName, item.sessionId);
1315
+ changedSessionHeads.delete(key);
1316
+ removedSessionRefs.set(key, item);
1317
+ };
1318
+ for (const item of previous.changedSessionHeads) addChanged(item);
1319
+ for (const item of previous.removedSessionRefs) addRemoved(item);
1320
+ for (const item of next.changedSessionHeads) addChanged(item);
1321
+ for (const item of next.removedSessionRefs) addRemoved(item);
1322
+ return {
1323
+ type: "sessions-updated",
1324
+ changedAgents: Array.from(/* @__PURE__ */ new Set([...previous.changedAgents, ...next.changedAgents])),
1325
+ newSessions: previous.newSessions + next.newSessions,
1326
+ updatedSessions: previous.updatedSessions + next.updatedSessions,
1327
+ removedSessions: previous.removedSessions + next.removedSessions,
1328
+ totalSessions: next.totalSessions,
1329
+ timestamp: next.timestamp,
1330
+ changedSessionHeads: [...changedSessionHeads.values()],
1331
+ removedSessionRefs: [...removedSessionRefs.values()]
1332
+ };
1333
+ }
1334
+ var LiveScanStore = class {
1335
+ constructor(watchEnabled = true, scanOptions = {}, startupScanOptions = {}, storeOptions = {}) {
1336
+ this.watchEnabled = watchEnabled;
1337
+ this.scanOptions = scanOptions;
1338
+ this.startupScanOptions = startupScanOptions;
1339
+ this.storeOptions = storeOptions;
1340
+ }
1341
+ watchEnabled;
1342
+ scanOptions;
1343
+ startupScanOptions;
1344
+ storeOptions;
1345
+ agents = [];
1346
+ byAgent = {};
1347
+ sessions = [];
1348
+ listeners = /* @__PURE__ */ new Set();
1349
+ scanStatusListeners = /* @__PURE__ */ new Set();
1350
+ scanStatus = {
1351
+ active: false,
1352
+ phase: "idle",
1353
+ pendingAgents: [],
1354
+ scanningAgents: [],
1355
+ completedAgents: [],
1356
+ agentStatuses: {},
1357
+ totalAgents: 0,
1358
+ updatedAt: Date.now()
1359
+ };
1360
+ refreshTimers = /* @__PURE__ */ new Map();
1361
+ refreshTimestamps = /* @__PURE__ */ new Map();
1362
+ refreshInFlight = /* @__PURE__ */ new Set();
1363
+ pendingRefreshes = /* @__PURE__ */ new Set();
1364
+ pendingRefreshPathCounts = /* @__PURE__ */ new Map();
1365
+ watcher = null;
1366
+ pendingEvent = null;
1367
+ pendingEventTimer = null;
1368
+ backgroundRefreshTimer = null;
1369
+ searchIndexWorker = null;
1370
+ pendingSearchIndexJobs = [];
1371
+ shuttingDown = false;
1372
+ async initialize() {
1373
+ const startedAt = performance.now();
1374
+ const deferInitialRefresh = this.storeOptions.deferInitialRefresh === true;
1375
+ appLogger.info("scan.initial.start", {
1376
+ watch_enabled: this.watchEnabled,
1377
+ agents: this.scanOptions.agents,
1378
+ use_cache: this.scanOptions.useCache ?? true,
1379
+ startup_from: this.startupScanOptions.from,
1380
+ startup_to: this.startupScanOptions.to,
1381
+ deferred: deferInitialRefresh || void 0
1382
+ });
1383
+ const initialResult = await scanSessions({
1384
+ ...this.scanOptions,
1385
+ ...deferInitialRefresh ? this.startupScanOptions : {},
1386
+ useCache: this.scanOptions.useCache ?? true,
1387
+ smartRefresh: false,
1388
+ cacheOnly: deferInitialRefresh,
1389
+ writeCache: deferInitialRefresh ? false : this.scanOptions.writeCache,
1390
+ smartTagWorkerUrl: this.getSmartTagWorkerUrl() ?? void 0,
1391
+ includeSmartTags: deferInitialRefresh ? false : void 0
1392
+ });
1393
+ this.applyScanResult(initialResult);
1394
+ const indexStartedAt = performance.now();
1395
+ if (!deferInitialRefresh) {
1396
+ await this.enqueueSearchIndexJobs(
1397
+ "scan.initial",
1398
+ this.buildFullSearchIndexJobs("scan.initial")
1399
+ );
1400
+ }
1401
+ const indexDuration = performance.now() - indexStartedAt;
1402
+ appLogger.info("scan.initial.done", {
1403
+ duration_ms: Math.round(performance.now() - startedAt),
1404
+ index_ms: deferInitialRefresh ? void 0 : Math.round(indexDuration),
1405
+ deferred: deferInitialRefresh || void 0,
1406
+ sessions: this.sessions.length,
1407
+ agents: Object.fromEntries(
1408
+ Object.entries(this.byAgent).map(([key, value]) => [key, value.length])
1409
+ ),
1410
+ agent_timings: initialResult.timings ? Object.fromEntries(
1411
+ Object.entries(initialResult.timings).map(([name, t]) => [
1412
+ name,
1413
+ {
1414
+ total_ms: Math.round(t.total),
1415
+ cache_load_ms: t.cacheLoad != null ? Math.round(t.cacheLoad) : void 0,
1416
+ check_changes_ms: t.checkChanges != null ? Math.round(t.checkChanges) : void 0,
1417
+ scan_ms: t.scan != null ? Math.round(t.scan) : void 0,
1418
+ identity_ms: t.identity != null ? Math.round(t.identity) : void 0,
1419
+ tags_ms: t.tags != null ? Math.round(t.tags) : void 0
1420
+ }
1421
+ ])
1422
+ ) : void 0
1423
+ });
1424
+ if (this.watchEnabled) {
1425
+ this.watcher = new SessionWatcher();
1426
+ this.watcher.onAgentsChanged((agentNames) => {
1427
+ for (const agentName of agentNames) {
1428
+ this.pendingRefreshPathCounts.set(
1429
+ agentName,
1430
+ (this.pendingRefreshPathCounts.get(agentName) ?? 0) + 1
1431
+ );
1432
+ const delayMs = (this.byAgent[agentName]?.length ?? 0) === 0 ? EMPTY_AGENT_REFRESH_DEBOUNCE_MS : REFRESH_DEBOUNCE_MS;
1433
+ this.scheduleRefresh(agentName, delayMs);
1434
+ }
1435
+ });
1436
+ this.watcher.start(this.agents.map((agent) => agent.name));
1437
+ }
1438
+ }
1439
+ startBackgroundRefresh() {
1440
+ if (this.backgroundRefreshTimer) {
1441
+ return;
1442
+ }
1443
+ const agentNames = this.agents.map((agent) => agent.name);
1444
+ this.startScanBatch(agentNames, "scanning");
1445
+ this.backgroundRefreshTimer = setTimeout(() => {
1446
+ this.backgroundRefreshTimer = null;
1447
+ for (const agentName of agentNames) {
1448
+ this.scheduleRefresh(agentName, 0);
1449
+ }
1450
+ if (agentNames.length === 0) {
1451
+ this.finishScanBatch();
1452
+ }
1453
+ }, 0);
1454
+ }
1455
+ getSnapshot() {
1456
+ return {
1457
+ sessions: this.sessions,
1458
+ byAgent: this.byAgent,
1459
+ agents: this.agents
1460
+ };
1461
+ }
1462
+ getScanStatus() {
1463
+ return {
1464
+ type: "scan-status",
1465
+ ...this.scanStatus,
1466
+ pendingAgents: [...this.scanStatus.pendingAgents],
1467
+ scanningAgents: [...this.scanStatus.scanningAgents],
1468
+ completedAgents: [...this.scanStatus.completedAgents],
1469
+ agentStatuses: Object.fromEntries(
1470
+ Object.entries(this.scanStatus.agentStatuses).map(([agentName, status]) => [
1471
+ agentName,
1472
+ { ...status }
1473
+ ])
1474
+ )
1475
+ };
1476
+ }
1477
+ subscribe(listener) {
1478
+ this.listeners.add(listener);
1479
+ return () => {
1480
+ this.listeners.delete(listener);
1481
+ };
1482
+ }
1483
+ subscribeScanStatus(listener) {
1484
+ this.scanStatusListeners.add(listener);
1485
+ return () => {
1486
+ this.scanStatusListeners.delete(listener);
1487
+ };
1488
+ }
1489
+ async shutdown() {
1490
+ this.shuttingDown = true;
1491
+ for (const timer of this.refreshTimers.values()) {
1492
+ clearTimeout(timer);
1493
+ }
1494
+ this.refreshTimers.clear();
1495
+ this.pendingRefreshPathCounts.clear();
1474
1496
  if (this.pendingEventTimer) {
1475
1497
  clearTimeout(this.pendingEventTimer);
1476
1498
  this.pendingEventTimer = null;
@@ -1488,9 +1510,10 @@ var LiveScanStore = class {
1488
1510
  }
1489
1511
  this.pendingSearchIndexJobs = [];
1490
1512
  this.pendingEvent = null;
1491
- await Promise.all(this.watchers.map((watcher) => watcher.close()));
1492
- this.watchers = [];
1493
- this.fallbackWatchScopes.clear();
1513
+ if (this.watcher) {
1514
+ await this.watcher.dispose();
1515
+ this.watcher = null;
1516
+ }
1494
1517
  }
1495
1518
  emit(event) {
1496
1519
  if (this.pendingEvent || event.newSessions > 0) {
@@ -1674,14 +1697,14 @@ var LiveScanStore = class {
1674
1697
  }
1675
1698
  getSearchIndexWorkerUrl() {
1676
1699
  const workerUrl = new URL("./search-index-worker.js", import.meta.url);
1677
- if (workerUrl.protocol === "file:" && !existsSync3(fileURLToPath2(workerUrl))) {
1700
+ if (workerUrl.protocol === "file:" && !existsSync4(fileURLToPath2(workerUrl))) {
1678
1701
  return null;
1679
1702
  }
1680
1703
  return workerUrl;
1681
1704
  }
1682
1705
  getSmartTagWorkerUrl() {
1683
1706
  const workerUrl = new URL("./smart-tag-worker.js", import.meta.url);
1684
- if (workerUrl.protocol === "file:" && !existsSync3(fileURLToPath2(workerUrl))) {
1707
+ if (workerUrl.protocol === "file:" && !existsSync4(fileURLToPath2(workerUrl))) {
1685
1708
  return null;
1686
1709
  }
1687
1710
  return workerUrl;
@@ -1861,9 +1884,6 @@ var LiveScanStore = class {
1861
1884
  applyFilters(sessions) {
1862
1885
  return filterSessions(sessions, { ...this.scanOptions, ...this.startupScanOptions });
1863
1886
  }
1864
- canSyncSources(agent) {
1865
- return Boolean(agent.listSessionSources && agent.scanSessionSource);
1866
- }
1867
1887
  async refreshInitialIndex() {
1868
1888
  const startedAt = performance.now();
1869
1889
  const context = "scan.initial.background";
@@ -1881,195 +1901,6 @@ var LiveScanStore = class {
1881
1901
  console.error("[search] Background index sync failed:", error);
1882
1902
  }
1883
1903
  }
1884
- startWatching() {
1885
- const scopesByRoot = /* @__PURE__ */ new Map();
1886
- for (const agent of this.agents) {
1887
- const watchTargets = resolveAgentWatchTargets(agent.name);
1888
- if (watchTargets.length === 0) {
1889
- appLogger.debug("watch.skip", { agent: agent.name });
1890
- continue;
1891
- }
1892
- for (const target of watchTargets) {
1893
- const watchRootPath = closestWatchablePath(target.root ?? target.path);
1894
- if (!watchRootPath) continue;
1895
- let rootPath;
1896
- try {
1897
- rootPath = getWatchRoot(watchRootPath);
1898
- } catch (error) {
1899
- this.reportWatchError("watch.resolve.error", { path: watchRootPath, error });
1900
- continue;
1901
- }
1902
- const targetPath = toAbsolutePath(target.path);
1903
- const scopes = scopesByRoot.get(rootPath) ?? [];
1904
- if (!scopes.some((scope) => scope.agentName === agent.name && scope.targetPath === targetPath)) {
1905
- scopes.push({ agentName: agent.name, targetPath });
1906
- }
1907
- scopesByRoot.set(rootPath, scopes);
1908
- }
1909
- }
1910
- for (const [rootPath, scopes] of scopesByRoot.entries()) {
1911
- const agents = Array.from(new Set(scopes.map((scope) => scope.agentName)));
1912
- appLogger.info("watch.start", {
1913
- root: rootPath,
1914
- agents,
1915
- targets: scopes.map((scope) => ({
1916
- agent: scope.agentName,
1917
- path: scope.targetPath
1918
- }))
1919
- });
1920
- if (isRecursiveWatchSupported()) {
1921
- const started = this.watchDirectory(rootPath, scopes, true);
1922
- if (started) {
1923
- continue;
1924
- }
1925
- }
1926
- this.watchDirectoryTree(rootPath, scopes);
1927
- }
1928
- }
1929
- watchDirectory(path, scopes, recursive) {
1930
- try {
1931
- const watcher = watch(path, { recursive }, (eventType, filename) => {
1932
- queueMicrotask(() => {
1933
- try {
1934
- const activeScopes = recursive ? scopes : this.fallbackWatchScopes.get(path) ?? scopes;
1935
- this.handleWatchEvent(path, activeScopes, eventType, filename);
1936
- if (!recursive) {
1937
- this.watchNewDirectories(path, filename, activeScopes);
1938
- }
1939
- } catch (error) {
1940
- this.reportWatchError("watch.event.error", { path, recursive, error });
1941
- }
1942
- });
1943
- });
1944
- watcher.on("error", (error) => {
1945
- this.reportWatchError("watch.error", { path, recursive, error });
1946
- });
1947
- this.watchers.push(watcher);
1948
- return true;
1949
- } catch (error) {
1950
- if (recursive && isRecursiveWatchUnavailable(error)) {
1951
- appLogger.warn("watch.recursive_unavailable", { path, error });
1952
- return false;
1953
- }
1954
- this.reportWatchError("watch.start.error", { path, recursive, error });
1955
- return false;
1956
- }
1957
- }
1958
- watchDirectoryTree(rootPath, scopes) {
1959
- const pending = [rootPath];
1960
- while (pending.length > 0) {
1961
- const dirPath = pending.pop();
1962
- this.watchFallbackDirectory(dirPath, scopes);
1963
- try {
1964
- for (const entry of readdirSync2(dirPath, { withFileTypes: true })) {
1965
- if (entry.isDirectory()) {
1966
- pending.push(join2(dirPath, entry.name));
1967
- }
1968
- }
1969
- } catch (error) {
1970
- this.reportWatchError("watch.scan.error", { path: dirPath, error });
1971
- }
1972
- }
1973
- }
1974
- watchFallbackDirectory(path, scopes) {
1975
- const existingScopes = this.fallbackWatchScopes.get(path);
1976
- if (existingScopes) {
1977
- mergeScopes(existingScopes, scopes);
1978
- return;
1979
- }
1980
- const storedScopes = [...scopes];
1981
- this.fallbackWatchScopes.set(path, storedScopes);
1982
- if (!this.watchDirectory(path, storedScopes, false)) {
1983
- this.fallbackWatchScopes.delete(path);
1984
- }
1985
- }
1986
- watchNewDirectories(watchPath, filename, scopes) {
1987
- const path = resolveWatchEventPath(watchPath, filename);
1988
- try {
1989
- if (statSync2(path).isDirectory()) {
1990
- this.watchDirectoryTree(path, scopes);
1991
- }
1992
- } catch {
1993
- }
1994
- }
1995
- handleWatchEvent(watchPath, scopes, eventType, filename) {
1996
- const changedPath = resolveWatchEventPath(watchPath, filename);
1997
- const agentNames = new Set(
1998
- scopes.filter((scope) => isRelatedPath(changedPath, scope.targetPath)).map((scope) => scope.agentName)
1999
- );
2000
- if (agentNames.size === 0) {
2001
- return;
2002
- }
2003
- appLogger.debug("watch.event", {
2004
- event: eventType,
2005
- path: changedPath,
2006
- agents: Array.from(agentNames)
2007
- });
2008
- this.waitForStablePath(changedPath, agentNames);
2009
- }
2010
- waitForStablePath(path, agentNames) {
2011
- const existing = this.stablePaths.get(path);
2012
- if (existing) {
2013
- for (const agentName of agentNames) {
2014
- existing.agentNames.add(agentName);
2015
- }
2016
- return;
2017
- }
2018
- const state = {
2019
- path,
2020
- agentNames: new Set(agentNames),
2021
- lastMtimeMs: null,
2022
- lastSize: null,
2023
- stableSince: Date.now(),
2024
- timer: null
2025
- };
2026
- this.stablePaths.set(path, state);
2027
- this.pollStablePath(path);
2028
- }
2029
- pollStablePath(path) {
2030
- const state = this.stablePaths.get(path);
2031
- if (!state) {
2032
- return;
2033
- }
2034
- let size;
2035
- let mtimeMs;
2036
- try {
2037
- const stat = statSync2(path);
2038
- size = stat.size;
2039
- mtimeMs = stat.mtimeMs;
2040
- } catch {
2041
- this.stablePaths.delete(path);
2042
- this.scheduleRefreshForAgents(state.agentNames);
2043
- return;
2044
- }
2045
- const now = Date.now();
2046
- const unchanged = state.lastSize === size && state.lastMtimeMs === mtimeMs;
2047
- if (!unchanged) {
2048
- state.lastSize = size;
2049
- state.lastMtimeMs = mtimeMs;
2050
- state.stableSince = now;
2051
- }
2052
- if (unchanged && now - state.stableSince >= WRITE_STABILITY_THRESHOLD_MS) {
2053
- this.stablePaths.delete(path);
2054
- this.scheduleRefreshForAgents(state.agentNames);
2055
- return;
2056
- }
2057
- state.timer = setTimeout(() => this.pollStablePath(path), WRITE_STABILITY_POLL_MS);
2058
- }
2059
- scheduleRefreshForAgents(agentNames) {
2060
- for (const agentName of agentNames) {
2061
- this.pendingRefreshPathCounts.set(
2062
- agentName,
2063
- (this.pendingRefreshPathCounts.get(agentName) ?? 0) + 1
2064
- );
2065
- const delayMs = (this.byAgent[agentName]?.length ?? 0) === 0 ? EMPTY_AGENT_REFRESH_DEBOUNCE_MS : REFRESH_DEBOUNCE_MS;
2066
- this.scheduleRefresh(agentName, delayMs);
2067
- }
2068
- }
2069
- reportWatchError(event, data) {
2070
- appLogger.error(event, data);
2071
- console.error("[watch] File watcher failed:", data.error);
2072
- }
2073
1904
  scheduleRefresh(agentName, delayMs = REFRESH_DEBOUNCE_MS) {
2074
1905
  appLogger.debug("scan.refresh.schedule", { agent: agentName, delay_ms: delayMs });
2075
1906
  const existing = this.refreshTimers.get(agentName);
@@ -2149,7 +1980,7 @@ var LiveScanStore = class {
2149
1980
  nextSessions = fullScanSessions;
2150
1981
  scanDuration = performance.now() - scanStartedAt;
2151
1982
  this.refreshTimestamps.set(agentName, Date.now());
2152
- } else if (cached && this.canSyncSources(agent)) {
1983
+ } else if (cached && agent instanceof FileSystemSessionSource) {
2153
1984
  const scanStartedAt = performance.now();
2154
1985
  const result = await this.scanAgentInWorker(
2155
1986
  agent,
@@ -2162,7 +1993,7 @@ var LiveScanStore = class {
2162
1993
  }
2163
1994
  );
2164
1995
  nextSessions = result.sessions;
2165
- agent.setSessionMetaMap?.(new Map(Object.entries(result.meta)));
1996
+ agent.setSessionMetaMap(new Map(Object.entries(result.meta)));
2166
1997
  preciseChangedIds = result.changedIds ?? [];
2167
1998
  usedIncrementalScan = true;
2168
1999
  persistenceDiff = buildRefreshDiff(
@@ -2179,7 +2010,7 @@ var LiveScanStore = class {
2179
2010
  duration_ms: Math.round(performance.now() - startedAt)
2180
2011
  });
2181
2012
  }
2182
- } else if (refreshBaseline.length > 0 && agent.checkForChanges && agent.incrementalScan) {
2013
+ } else if (refreshBaseline.length > 0) {
2183
2014
  const checkStartedAt = performance.now();
2184
2015
  const checkResult = await Promise.resolve(
2185
2016
  agent.checkForChanges(cacheTimestamp, refreshBaseline)
@@ -2212,7 +2043,7 @@ var LiveScanStore = class {
2212
2043
  const scanStartedAt = performance.now();
2213
2044
  const result = await this.scanAgentInWorker(agent, previousSessions, null, {});
2214
2045
  nextSessions = result.sessions;
2215
- agent.setSessionMetaMap?.(new Map(Object.entries(result.meta)));
2046
+ agent.setSessionMetaMap(new Map(Object.entries(result.meta)));
2216
2047
  fullScanSessions = attachMissingProjectIdentities(nextSessions);
2217
2048
  nextSessions = fullScanSessions;
2218
2049
  scanDuration = performance.now() - scanStartedAt;
@@ -2445,7 +2276,7 @@ var main = defineCommand({
2445
2276
  log_path: appLogger.getLogPath()
2446
2277
  });
2447
2278
  if (clearCache) {
2448
- const { clearCache: clear } = await import("./dist-VEIV33FR.js");
2279
+ const { clearCache: clear } = await import("./dist-GZSUB4NY.js");
2449
2280
  clear();
2450
2281
  appLogger.info("cache.clear");
2451
2282
  console.log("Cache cleared.");